mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-11-15 11:28:22 +00:00
Postgres schema support
This commit is contained in:
1 parent
741c9c982f
commit
fc8b1fcde2
12 files changed
+117
-8
No files matched your search
@@ -37,11 +37,21 @@ func SetupDatabase(onConfigured func() error) func(http.ResponseWriter, *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// Sqlite lives in the data folder and needs nothing else
|
||||
if newCfg.GetDriver() == config.DatabaseSqlite {
|
||||
switch newCfg.GetDriver() {
|
||||
case config.DatabaseSqlite:
|
||||
// Sqlite lives in the data folder and needs nothing else
|
||||
newCfg = config.DBConfig{Driver: config.DatabaseSqlite}
|
||||
} else if newCfg.SSLMode == "" {
|
||||
newCfg.SSLMode = "disable"
|
||||
case config.DatabasePostgres:
|
||||
if newCfg.SSLMode == "" {
|
||||
newCfg.SSLMode = "disable"
|
||||
}
|
||||
newCfg.Schema = newCfg.GetSchema()
|
||||
default:
|
||||
if newCfg.SSLMode == "" {
|
||||
newCfg.SSLMode = "disable"
|
||||
}
|
||||
// Schemas are a postgres concept
|
||||
newCfg.Schema = ""
|
||||
}
|
||||
|
||||
if err := database.TestConnection(newCfg); err != nil {
|
||||
|
||||
@@ -50,6 +50,9 @@ func TestSetupRouter(t *testing.T) {
|
||||
// Schema validation
|
||||
assert.Equal(t, http.StatusBadRequest, post(failing, `{"driver":"oracle"}`).Code)
|
||||
|
||||
// Postgres schema must be a plain identifier
|
||||
assert.Equal(t, http.StatusBadRequest, post(failing, `{"driver":"postgres","schema":"public; drop table x"}`).Code)
|
||||
|
||||
// Postgres/mysql config missing fields
|
||||
assert.Equal(t, http.StatusBadRequest, post(failing, `{"driver":"postgres","host":"db"}`).Code)
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ func SetupDatabase() string {
|
||||
"sslmode": {
|
||||
"type": "string",
|
||||
"enum": ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"]
|
||||
},
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z_][A-Za-z0-9_$]{0,62}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,22 @@ func TestConnectURLs(t *testing.T) {
|
||||
"NPM_DB_NAME=npm",
|
||||
},
|
||||
want: want{
|
||||
gorm: "host=2.2.2.2 user=postgresuser password=pgpass dbname=npm port=9824 sslmode=strict TimeZone=UTC",
|
||||
gorm: "host=2.2.2.2 user=postgresuser password=pgpass dbname=npm port=9824 sslmode=strict search_path=public TimeZone=UTC",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "postgres-schema",
|
||||
envs: []string{
|
||||
"NPM_DB_DRIVER=postgres",
|
||||
"NPM_DB_HOST=2.2.2.2",
|
||||
"NPM_DB_PORT=5432",
|
||||
"NPM_DB_USERNAME=postgresuser",
|
||||
"NPM_DB_PASSWORD=pgpass",
|
||||
"NPM_DB_NAME=npm",
|
||||
"NPM_DB_SCHEMA=npm_app",
|
||||
},
|
||||
want: want{
|
||||
gorm: "host=2.2.2.2 user=postgresuser password=pgpass dbname=npm port=5432 sslmode=disable search_path=npm_app TimeZone=UTC",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"npm/internal/errors"
|
||||
@@ -13,8 +14,15 @@ const (
|
||||
DatabaseSqlite = "sqlite"
|
||||
DatabasePostgres = "postgres"
|
||||
DatabaseMysql = "mysql"
|
||||
|
||||
// DefaultPostgresSchema is used when no schema has been specified
|
||||
DefaultPostgresSchema = "public"
|
||||
)
|
||||
|
||||
// postgresSchemaRegex limits schema names to unquoted Postgres identifiers
|
||||
// (max 63 bytes) so they can be placed in the connection string safely
|
||||
var postgresSchemaRegex = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_$]{0,62}$`)
|
||||
|
||||
// DBConfig holds the database connection parameters. It is populated from
|
||||
// NPM_DB_* environment variables when NPM_DB_DRIVER is set, otherwise from
|
||||
// the db.conf file in the data folder written by the setup wizard.
|
||||
@@ -26,6 +34,7 @@ type DBConfig struct {
|
||||
Password string `json:"password" envconfig:"optional,default="`
|
||||
Name string `json:"name" envconfig:"optional,default="`
|
||||
SSLMode string `json:"sslmode" envconfig:"optional,default=disable"`
|
||||
Schema string `json:"schema,omitempty" envconfig:"optional,default=public"`
|
||||
}
|
||||
|
||||
// GetDriver returns the lowercase driver name
|
||||
@@ -33,6 +42,14 @@ func (d *DBConfig) GetDriver() string {
|
||||
return strings.ToLower(d.Driver)
|
||||
}
|
||||
|
||||
// GetSchema returns the postgres schema, falling back to the default
|
||||
func (d *DBConfig) GetSchema() string {
|
||||
if d.Schema == "" {
|
||||
return DefaultPostgresSchema
|
||||
}
|
||||
return d.Schema
|
||||
}
|
||||
|
||||
// IsConfigured returns true when a database driver has been chosen, either
|
||||
// by environment variables or by the setup wizard
|
||||
func (d *DBConfig) IsConfigured() bool {
|
||||
@@ -60,6 +77,9 @@ func (d *DBConfig) IsValid() (bool, error) {
|
||||
if d.Username == "" {
|
||||
errs = append(errs, eris.New("database username is empty"))
|
||||
}
|
||||
if d.GetDriver() == DatabasePostgres && !postgresSchemaRegex.MatchString(d.GetSchema()) {
|
||||
errs = append(errs, eris.Errorf("database schema %s is invalid", d.Schema))
|
||||
}
|
||||
default:
|
||||
errs = append(errs, eris.Errorf("database driver %s is not supported. Valid options are: %s, %s or %s", d.Driver, DatabaseSqlite, DatabasePostgres, DatabaseMysql))
|
||||
}
|
||||
@@ -73,13 +93,14 @@ func (d *DBConfig) GetGormConnectURL() string {
|
||||
case DatabaseSqlite:
|
||||
return fmt.Sprintf("%s/nginxproxymanager.db", Configuration.DataFolder)
|
||||
case DatabasePostgres:
|
||||
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=UTC",
|
||||
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s search_path=%s TimeZone=UTC",
|
||||
d.Host,
|
||||
d.Username,
|
||||
d.Password,
|
||||
d.Name,
|
||||
d.Port,
|
||||
d.SSLMode,
|
||||
d.GetSchema(),
|
||||
)
|
||||
case DatabaseMysql:
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestDBConfigFile(t *testing.T) {
|
||||
Password: "secret",
|
||||
Name: "npm",
|
||||
SSLMode: "require",
|
||||
Schema: "npm",
|
||||
}
|
||||
require.NoError(t, SaveDBConfig(cfg))
|
||||
|
||||
|
||||
@@ -67,7 +67,27 @@ func TestConnection(cfg config.DBConfig) error {
|
||||
// nolint: errcheck
|
||||
defer sqlDB.Close()
|
||||
|
||||
return sqlDB.Ping()
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Postgres will happily connect with a search_path that doesn't exist,
|
||||
// then fail to create any tables during migration
|
||||
if cfg.GetDriver() == config.DatabasePostgres {
|
||||
var exists bool
|
||||
row := sqlDB.QueryRow(
|
||||
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)",
|
||||
cfg.GetSchema(),
|
||||
)
|
||||
if err := row.Scan(&exists); err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return eris.Errorf("schema %s does not exist in database %s", cfg.GetSchema(), cfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes and discards the current instance, if any
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface DatabaseConfig {
|
||||
password?: string;
|
||||
name?: string;
|
||||
sslmode?: string;
|
||||
schema?: string;
|
||||
}
|
||||
|
||||
export async function setupDatabase(
|
||||
|
||||
@@ -365,6 +365,9 @@
|
||||
"setup-database.sqlite-description": {
|
||||
"defaultMessage": "SQLite speichert alles in einer einzigen Datei im Datenordner und benötigt keine weitere Konfiguration."
|
||||
},
|
||||
"setup-database.schema": {
|
||||
"defaultMessage": "Schema"
|
||||
},
|
||||
"setup-database.sslmode": {
|
||||
"defaultMessage": "SSL-Modus"
|
||||
},
|
||||
|
||||
@@ -641,6 +641,9 @@
|
||||
"setup-database.sqlite-description": {
|
||||
"defaultMessage": "SQLite stores everything in a single file in the data folder and requires no further configuration."
|
||||
},
|
||||
"setup-database.schema": {
|
||||
"defaultMessage": "Schema"
|
||||
},
|
||||
"setup-database.sslmode": {
|
||||
"defaultMessage": "SSL Mode"
|
||||
},
|
||||
|
||||
@@ -365,6 +365,9 @@
|
||||
"setup-database.sqlite-description": {
|
||||
"defaultMessage": "SQLite همه چیز را در یک فایل در پوشه داده ذخیره میکند و به پیکربندی بیشتری نیاز ندارد."
|
||||
},
|
||||
"setup-database.schema": {
|
||||
"defaultMessage": "اسکیما"
|
||||
},
|
||||
"setup-database.sslmode": {
|
||||
"defaultMessage": "حالت SSL"
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ interface Payload {
|
||||
password: string;
|
||||
name: string;
|
||||
sslmode: string;
|
||||
schema: string;
|
||||
}
|
||||
|
||||
const defaultPorts: Record<string, string> = {
|
||||
@@ -62,7 +63,7 @@ function SetupDatabase() {
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
...(values.driver === "postgres"
|
||||
? { sslmode: values.sslmode }
|
||||
? { sslmode: values.sslmode, schema: values.schema }
|
||||
: {}),
|
||||
};
|
||||
|
||||
@@ -128,6 +129,7 @@ function SetupDatabase() {
|
||||
password: "",
|
||||
name: "",
|
||||
sslmode: "disable",
|
||||
schema: "public",
|
||||
} as Payload
|
||||
}
|
||||
onSubmit={onSubmit}>
|
||||
@@ -276,6 +278,29 @@ function SetupDatabase() {
|
||||
</FormControl>
|
||||
)}
|
||||
</Field>
|
||||
{values.driver === "postgres" ? (
|
||||
<Field
|
||||
name="schema"
|
||||
validate={validateString(1, 63)}>
|
||||
{({ field, form }: any) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={
|
||||
form.errors.schema && form.touched.schema
|
||||
}>
|
||||
<FormLabel htmlFor="schema">
|
||||
{intl.formatMessage({
|
||||
id: "setup-database.schema",
|
||||
})}
|
||||
</FormLabel>
|
||||
<Input {...field} id="schema" />
|
||||
<FormErrorMessage>
|
||||
{form.errors.schema}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
</Field>
|
||||
) : null}
|
||||
{values.driver === "postgres" ? (
|
||||
<Field name="sslmode">
|
||||
{({ field }: any) => (
|
||||
|
||||
Reference in new issue
Block a user