diff --git a/cmd/flamenco-manager-poc/main.go b/cmd/flamenco-manager-poc/main.go index 981c08e1..401e864a 100644 --- a/cmd/flamenco-manager-poc/main.go +++ b/cmd/flamenco-manager-poc/main.go @@ -22,6 +22,7 @@ package main import ( "context" + "flag" "net" "net/http" "time" @@ -43,11 +44,29 @@ import ( "gitlab.com/blender/flamenco-ng-poc/pkg/api" ) +var cliArgs struct { + version bool + initDB bool +} + func main() { output := zerolog.ConsoleWriter{Out: colorable.NewColorableStdout(), TimeFormat: time.RFC3339} log.Logger = log.Output(output) log.Info().Str("version", appinfo.ApplicationVersion).Msgf("starting %v", appinfo.ApplicationName) + parseCliArgs() + if cliArgs.version { + return + } + if cliArgs.initDB { + log.Info().Msg("creating databases") + err := persistence.InitialSetup() + if err != nil { + log.Fatal().Err(err).Msg("problem performing initial setup") + } + return + } + // Open the database. dbCtx, dbCtxCancel := context.WithTimeout(context.Background(), 5*time.Second) defer dbCtxCancel() @@ -113,3 +132,27 @@ func buildWebService(flamenco api.ServerInterface, persist api_impl.PersistenceS return e } + +func parseCliArgs() { + var quiet, debug, trace bool + + flag.BoolVar(&cliArgs.version, "version", false, "Shows the application version, then exits.") + flag.BoolVar(&cliArgs.initDB, "initdb", false, "Create the database; requires admin access to PostgreSQL.") + flag.BoolVar(&quiet, "quiet", false, "Only log warning-level and worse.") + flag.BoolVar(&debug, "debug", false, "Enable debug-level logging.") + flag.BoolVar(&trace, "trace", false, "Enable trace-level logging.") + flag.Parse() + + var logLevel zerolog.Level + switch { + case trace: + logLevel = zerolog.TraceLevel + case debug: + logLevel = zerolog.DebugLevel + case quiet: + logLevel = zerolog.WarnLevel + default: + logLevel = zerolog.InfoLevel + } + zerolog.SetGlobalLevel(logLevel) +} diff --git a/go.mod b/go.mod index 268cc059..4d2271a0 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/valyala/fasttemplate v1.2.1 // indirect golang.org/x/mod v0.4.2 // indirect golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect golang.org/x/tools v0.1.7 // indirect diff --git a/go.sum b/go.sum index b27ed44d..9f598a34 100644 --- a/go.sum +++ b/go.sum @@ -306,6 +306,8 @@ golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1U golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/manager/persistence/initialisation.go b/internal/manager/persistence/initialisation.go new file mode 100644 index 00000000..d2314918 --- /dev/null +++ b/internal/manager/persistence/initialisation.go @@ -0,0 +1,141 @@ +package persistence + +/* ***** BEGIN GPL LICENSE BLOCK ***** + * + * Original Code Copyright (C) 2022 Blender Foundation. + * + * This file is part of Flamenco. + * + * Flamenco is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * Flamenco is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * Flamenco. If not, see . + * + * ***** END GPL LICENSE BLOCK ***** */ + +import ( + "bufio" + "errors" + "fmt" + "os" + "runtime" + + "github.com/rs/zerolog/log" + "golang.org/x/term" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +var errInputTooLong = errors.New("input is too long") + +const adminDSN = "host=localhost user=postgres password=%s dbname=%s TimeZone=Europe/Amsterdam" + +// InitialSetup uses the `postgres` admin user to set up the database. +// TODO: distinguish between production and development setups. +func InitialSetup() error { + // Get the password of the 'postgres' user. + adminPass, err := readPassword() + if err != nil { + return fmt.Errorf("unable to read password: %w", err) + } + + // Connect to the 'postgres' database so we can create other databases. + db, err := connectDBAsAdmin(adminPass, "postgres") + if err != nil { + return fmt.Errorf("unable to connect to the database: %w", err) + } + + // TODO: get username / password / database name from some config file, user input, CLI args, whatevah. + // Has to be used by the regular Flamenco Manager runs as well, though. + username := "flamenco" + userPass := "flamenco" + // tx := db.Exec("CREATE USER flamenco PASSWORD ? NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN", userPass) + // if tx.Error != nil { + // return fmt.Errorf("unable to create database user '%s': %w", username, tx.Error) + // } + { + sqlDB, err := db.DB() + if err != nil { + panic(err) + } + _, err = sqlDB.Exec("CREATE USER flamenco WITH PASSWORD $1::string NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN", userPass) + if err != nil { + panic(err) + } + } + + // Create the databases. + tx := db.Debug().Exec("CREATE DATABASE flamenco OWNER ? ENCODING 'utf8'", username) + if tx.Error != nil { + return fmt.Errorf("unable to create database 'flamenco': %w", tx.Error) + } + tx = db.Exec("CREATE DATABASE flamenco-test OWNER ? ENCODING 'utf8'", username) + if tx.Error != nil { + return fmt.Errorf("unable to create database 'flamenco': %w", tx.Error) + } + + // Close the connection so we can reconnect. + sqlDB, err := db.DB() + if err != nil { + fmt.Printf("error closing the database connection, please report this issue: %v", err) + } else { + sqlDB.Close() + } + + // Allow 'flamenco' user to completely nuke and recreate the flamenco-test database, without needing 'CREATEDB' permission. + db, err = connectDBAsAdmin(adminPass, "flamenco-test") + if err != nil { + return fmt.Errorf("unable to reconnect to the database: %w", err) + } + tx = db.Exec("ALTER SCHEMA public OWNER TO ?", username) + if tx.Error != nil { + fmt.Printf("Unable to allow database user '%s' to reset the test database: %v\n", username, tx.Error) + fmt.Println("This is not an issue, unless you want to develop Flamenco yourself.") + } + + return nil +} + +func readPassword() (string, error) { + if pwFromEnv := os.Getenv("PSQL_ADMIN"); pwFromEnv != "" { + log.Info().Msg("getting password from PSQL_ADMIN environment variable") + return pwFromEnv, nil + } + + fmt.Print("PostgreSQL admin password: ") + + var ( + line []byte + err error + ) + + if runtime.GOOS == "windows" { + // term.ReadPassword() doesn't work reliably on Windows, especially when you + // use a MingW terminal (like Git Bash). See + // https://github.com/golang/go/issues/11914#issuecomment-613715787 for more + // info. + // + // The downside is that this echoes the password to the terminal. + buf := bufio.NewReader(os.Stdin) + line, _, err = buf.ReadLine() + } else { + fd := int(os.Stdin.Fd()) + line, err = term.ReadPassword(fd) + } + if err != nil { + return "", err + } + return string(line), nil +} + +func connectDBAsAdmin(password, database string) (*gorm.DB, error) { + dsn := fmt.Sprintf(adminDSN, password, database) + return gorm.Open(postgres.Open(dsn), &gorm.Config{}) +}