| package main |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import ( |
| "bytes" |
| "errors" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "github.com/apache/trafficcontrol/lib/go-log" |
| |
| "github.com/golang-migrate/migrate/v4" |
| "github.com/golang-migrate/migrate/v4/database" |
| _ "github.com/golang-migrate/migrate/v4/database/postgres" |
| "github.com/golang-migrate/migrate/v4/source" |
| _ "github.com/golang-migrate/migrate/v4/source/file" |
| "gopkg.in/yaml.v2" |
| ) |
| |
| type dbConfig struct { |
| Development envConfig `yaml:"development"` |
| Test envConfig `yaml:"test"` |
| Integration envConfig `yaml:"integration"` |
| Production envConfig `yaml:"production"` |
| } |
| |
| type envConfig struct { |
| Driver string `yaml:"driver"` |
| Open string `yaml:"open"` |
| } |
| |
| func (conf dbConfig) getEnvironmentConfig(env string) (envConfig, error) { |
| switch env { |
| case envDevelopment: |
| return conf.Development, nil |
| case envTest: |
| return conf.Test, nil |
| case envIntegration: |
| return conf.Integration, nil |
| case envProduction: |
| return conf.Production, nil |
| default: |
| return envConfig{}, errors.New("invalid environment: " + env) |
| } |
| } |
| |
| // the possible environments to use. |
| const ( |
| envDevelopment = "development" |
| envTest = "test" |
| envIntegration = "integration" |
| envProduction = "production" |
| ) |
| |
| // keys in the database config's "open" string value. |
| const ( |
| hostKey = "host" |
| portKey = "port" |
| userKey = "user" |
| passwordKey = "password" |
| dbNameKey = "dbname" |
| sslModeKey = "sslmode" |
| ) |
| |
| // available commands. |
| const ( |
| cmdCreateDB = "createdb" |
| cmdDropDB = "dropdb" |
| cmdCreateMigration = "create_migration" |
| cmdCreateUser = "create_user" |
| cmdDropUser = "drop_user" |
| cmdShowUsers = "show_users" |
| cmdReset = "reset" |
| cmdUpgrade = "upgrade" |
| cmdMigrate = "migrate" |
| cmdUp = "up" |
| cmdDown = "down" |
| cmdRedo = "redo" |
| // Deprecated: Migrate only tracks migration timestamp and dirty status, not a status for each migration. |
| // Use CmdDBVersion to check the migration timestamp and dirty status. |
| cmdStatus = "status" |
| cmdDBVersion = "dbversion" |
| cmdSeed = "seed" |
| cmdLoadSchema = "load_schema" |
| cmdPatch = "patch" |
| ) |
| |
| // Default file system paths for TODB files. |
| const ( |
| defaultDBDir = "db/" |
| defaultDBConfigPath = defaultDBDir + "dbconf.yml" |
| defaultDBMigrationsPath = defaultDBDir + "migrations" |
| defaultDBSeedsPath = defaultDBDir + "seeds.sql" |
| defaultDBSchemaPath = defaultDBDir + "create_tables.sql" |
| defaultDBPatchesPath = defaultDBDir + "patches.sql" |
| ) |
| |
| // Default file system paths for TV files. |
| const ( |
| defaultTrafficVaultDir = defaultDBDir + "trafficvault/" |
| defaultTrafficVaultDBConfigPath = defaultTrafficVaultDir + "dbconf.yml" |
| defaultTrafficVaultMigrationsPath = defaultTrafficVaultDir + "migrations" |
| defaultTrafficVaultSchemaPath = defaultTrafficVaultDir + "create_tables.sql" |
| ) |
| |
| // Default connection information. |
| const ( |
| defaultEnvironment = envDevelopment |
| defaultDBSuperUser = "postgres" |
| ) |
| |
| const ( |
| // 2021012200000000_max_request_header_bytes_default_zero.sql. |
| lastSquashedMigrationTimestamp uint = 2021012200000000 |
| // 2021012700000000_update_interfaces_multiple_routers.up.sql. |
| firstMigrationTimestamp uint = 2021012700000000 |
| ) |
| |
| // globals that are passed in via CLI flags and used in commands. |
| var ( |
| environment string |
| trafficVault bool |
| dbVersion uint |
| dbVersionDirty bool |
| ) |
| |
| // globals that are parsed out of DBConfigFile and used in commands. |
| var ( |
| connectionString string |
| dbDriver string |
| dbName string |
| dbSuperUser = defaultDBSuperUser |
| dbUser string |
| dbPassword string |
| hostIP string |
| hostPort string |
| sslMode string |
| migrateInstance *migrate.Migrate |
| migrationName string |
| ) |
| |
| // Actual TODB file paths. |
| var ( |
| dbConfigPath = defaultDBConfigPath |
| dbMigrationsDir = defaultDBMigrationsPath |
| dbSeedsPath = defaultDBSeedsPath |
| dbSchemaPath = defaultDBSchemaPath |
| dbPatchesPath = defaultDBPatchesPath |
| ) |
| |
| // Actual TV file paths. |
| var ( |
| trafficVaultDBConfigPath = defaultTrafficVaultDBConfigPath |
| trafficVaultMigrationsPath = defaultTrafficVaultMigrationsPath |
| trafficVaultSchemaPath = defaultTrafficVaultSchemaPath |
| ) |
| |
| func parseDBConfig() error { |
| var cfgPath string |
| if trafficVault { |
| cfgPath = trafficVaultDBConfigPath |
| } else { |
| cfgPath = dbConfigPath |
| } |
| confBytes, err := ioutil.ReadFile(cfgPath) |
| if err != nil { |
| return fmt.Errorf("reading DB conf '%s': %w", cfgPath, err) |
| } |
| |
| dbConfig := dbConfig{} |
| err = yaml.Unmarshal(confBytes, &dbConfig) |
| if err != nil { |
| return errors.New("unmarshalling DB conf yaml: " + err.Error()) |
| } |
| |
| envConfig, err := dbConfig.getEnvironmentConfig(environment) |
| if err != nil { |
| return errors.New("getting environment config: " + err.Error()) |
| } |
| |
| dbDriver = envConfig.Driver |
| // parse the 'open' string into a map |
| open := make(map[string]string) |
| pairs := strings.Split(envConfig.Open, " ") |
| for _, pair := range pairs { |
| if pair == "" { |
| continue |
| } |
| kv := strings.Split(pair, "=") |
| if len(kv) != 2 || kv[0] == "" || kv[1] == "" { |
| continue |
| } |
| open[kv[0]] = kv[1] |
| } |
| |
| ok := false |
| stringPointers := []*string{&hostIP, &hostPort, &dbUser, &dbPassword, &dbName, &sslMode} |
| keys := []string{hostKey, portKey, userKey, passwordKey, dbNameKey, sslModeKey} |
| for index, stringPointer := range stringPointers { |
| key := keys[index] |
| *stringPointer, ok = open[key] |
| if !ok { |
| return errors.New("unable to get '" + key + "' for environment '" + environment + "'") |
| } |
| } |
| |
| return nil |
| } |
| |
| func createDB() { |
| dbExistsCmd := exec.Command("psql", "-h", hostIP, "-U", dbSuperUser, "-p", hostPort, "-tAc", "SELECT 1 FROM pg_database WHERE datname='"+dbName+"'") |
| stderr := bytes.Buffer{} |
| dbExistsCmd.Stderr = &stderr |
| out, err := dbExistsCmd.Output() |
| // An error is returned if the database could not be found, which is to be expected. Don't exit on this error. |
| if err != nil { |
| fmt.Fprintln(os.Stderr, "unable to check if DB already exists: "+err.Error()+", stderr: "+stderr.String()) |
| } |
| if len(out) > 0 { |
| fmt.Println("Database " + dbName + " already exists") |
| return |
| } |
| createDBCmd := exec.Command("createdb", "-h", hostIP, "-p", hostPort, "-U", dbSuperUser, "-e", "--owner", dbUser, dbName) |
| out, err = createDBCmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't create db " + dbName + ": " + err.Error()) |
| } |
| } |
| |
| func dropDB() { |
| fmt.Println("Dropping database: " + dbName) |
| cmd := exec.Command("dropdb", "-h", hostIP, "-p", hostPort, "-U", dbSuperUser, "-e", "--if-exists", dbName) |
| out, err := cmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't drop db " + dbName + ": " + err.Error()) |
| } |
| } |
| |
| func createMigration() { |
| const apacheLicense2 = `/* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| ` |
| var err error |
| if err = os.MkdirAll(dbMigrationsDir, os.ModePerm); err != nil { |
| die("Creating migrations directory " + dbMigrationsDir + ": " + err.Error()) |
| } |
| migrationTime := time.Now() |
| formattedMigrationTime := migrationTime.Format("20060102150405") + fmt.Sprintf("%02d", migrationTime.Nanosecond()%100) |
| for _, direction := range []string{"up", "down"} { |
| var migrationFile *os.File |
| basename := fmt.Sprintf("%s_%s.%s.sql", formattedMigrationTime, migrationName, direction) |
| filename := filepath.Join(dbMigrationsDir, basename) |
| if migrationFile, err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644); err != nil { |
| die("Creating migration " + filename + ": " + err.Error()) |
| } |
| defer log.Close(migrationFile, "closing migration "+filename) |
| if _, err = migrationFile.Write([]byte(apacheLicense2)); err != nil { |
| die("Writing content to migration " + filename + ": " + err.Error()) |
| } |
| fmt.Println("Created migration ", filename) |
| } |
| } |
| |
| func createUser() { |
| fmt.Println("Creating user: " + dbUser) |
| userExistsCmd := exec.Command("psql", "-h", hostIP, "-U", dbSuperUser, "-p", hostPort, "-tAc", "SELECT 1 FROM pg_roles WHERE rolname='"+dbUser+"'") |
| stderr := bytes.Buffer{} |
| userExistsCmd.Stderr = &stderr |
| out, err := userExistsCmd.Output() |
| // An error is returned if the user could not be found, which is to be expected. Don't exit on this error. |
| if err != nil { |
| fmt.Fprintln(os.Stderr, "unable to check if user already exists: "+err.Error()+", stderr: "+stderr.String()) |
| } |
| if len(out) > 0 { |
| fmt.Println("User " + dbUser + " already exists") |
| return |
| } |
| createUserCmd := exec.Command("psql", "-h", hostIP, "-p", hostPort, "-U", dbSuperUser, "-etAc", "CREATE USER "+dbUser+" WITH LOGIN ENCRYPTED PASSWORD '"+dbPassword+"'") |
| out, err = createUserCmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't create user " + dbUser) |
| } |
| } |
| |
| func dropUser() { |
| cmd := exec.Command("dropuser", "-h", hostIP, "-p", hostPort, "-U", dbSuperUser, "-i", "-e", dbUser) |
| out, err := cmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't drop user " + dbUser) |
| } |
| } |
| |
| func showUsers() { |
| cmd := exec.Command("psql", "-h", hostIP, "-p", hostPort, "-U", dbSuperUser, "-ec", `\du`) |
| out, err := cmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't show users") |
| } |
| } |
| |
| func reset() { |
| createUser() |
| dropDB() |
| createDB() |
| loadSchema() |
| runMigrations() |
| } |
| |
| func upgrade() { |
| runMigrations() |
| if !trafficVault { |
| seed() |
| patch() |
| } |
| } |
| |
| func maybeMigrateFromGoose() bool { |
| var versionErr error |
| dbVersion, dbVersionDirty, versionErr = migrateInstance.Version() |
| if versionErr == nil { |
| return false |
| } |
| if !errors.Is(versionErr, migrate.ErrNilVersion) { |
| die("Error running migrate version: " + versionErr.Error()) |
| } |
| if err := migrateInstance.Steps(1); err != nil { |
| die("Error migrating to Migrate from Goose: " + err.Error()) |
| } |
| dbVersion, dbVersionDirty, _ = migrateInstance.Version() |
| return true |
| } |
| |
| // runFirstMigration is essentially Migrate.Migrate(FirstMigrationTimestamp) but without the obligatory Migrate.versionExists() call. |
| // If calling Migrate.versionExists() is made optional, runFirstMigration() can be replaced. |
| func runFirstMigration() error { |
| sourceDriver, sourceDriverErr := source.Open("file:" + dbMigrationsDir) |
| if sourceDriverErr != nil { |
| return fmt.Errorf("opening the migration source driver: " + sourceDriverErr.Error()) |
| } |
| dbDriver, dbDriverErr := database.Open(connectionString) |
| if dbDriverErr != nil { |
| return fmt.Errorf("opening the dbdriver: " + dbDriverErr.Error()) |
| } |
| firstMigration, firstMigrationName, migrationReadErr := sourceDriver.ReadUp(firstMigrationTimestamp) |
| if migrationReadErr != nil { |
| return fmt.Errorf("reading migration %s: %w", firstMigrationName, migrationReadErr) |
| } |
| if setDirtyVersionErr := dbDriver.SetVersion(int(firstMigrationTimestamp), true); setDirtyVersionErr != nil { |
| return fmt.Errorf("setting the dirty version: %w", setDirtyVersionErr) |
| } |
| if migrateErr := dbDriver.Run(firstMigration); migrateErr != nil { |
| return fmt.Errorf("running the migration: %w", migrateErr) |
| } |
| if setVersionErr := dbDriver.SetVersion(int(firstMigrationTimestamp), false); setVersionErr != nil { |
| return fmt.Errorf("setting the version after successfully running the migration: %w", setVersionErr) |
| } |
| return nil |
| } |
| |
| func runMigrations() { |
| migratedFromGoose, migrationsShouldRun := initMigrate() |
| if !migrationsShouldRun { |
| return |
| } |
| if !trafficVault && dbVersion == lastSquashedMigrationTimestamp && !dbVersionDirty { |
| if migrateErr := runFirstMigration(); migrateErr != nil { |
| die(fmt.Sprintf("Error migrating from DB version %d to %d: %s", lastSquashedMigrationTimestamp, firstMigrationTimestamp, migrateErr.Error())) |
| } |
| } |
| if upErr := migrateInstance.Up(); errors.Is(upErr, migrate.ErrNoChange) { |
| if !migratedFromGoose { |
| fmt.Fprintln(os.Stderr, upErr.Error()) |
| } |
| } else if upErr != nil { |
| die("Error running migrate up: " + upErr.Error()) |
| } |
| } |
| |
| func runUp() { |
| runMigrations() |
| } |
| |
| func down() { |
| initMigrate() |
| if err := migrateInstance.Steps(-1); err != nil { |
| die("Error running migrate down: " + err.Error()) |
| } |
| } |
| |
| func redo() { |
| initMigrate() |
| if downErr := migrateInstance.Steps(-1); downErr != nil { |
| die("Error running migrate down 1 in 'redo': " + downErr.Error()) |
| } |
| if upErr := migrateInstance.Steps(1); upErr != nil { |
| die("Error running migrate up 1 in 'redo': " + upErr.Error()) |
| } |
| } |
| |
| // Deprecated: Migrate does not track migration status of past migrations. Use dbversion() to check the migration timestamp and dirty status. |
| func status() { |
| getDBVersion() |
| } |
| |
| func getDBVersion() { |
| initMigrate() |
| fmt.Printf("dbversion %d", dbVersion) |
| if dbVersionDirty { |
| fmt.Printf(" (dirty)") |
| } |
| fmt.Println() |
| } |
| |
| func seed() { |
| if trafficVault { |
| die("seed not supported for trafficvault environment") |
| } |
| fmt.Println("Seeding database w/ required data.") |
| seedsBytes, err := ioutil.ReadFile(dbSeedsPath) |
| if err != nil { |
| die("unable to read '" + dbSeedsPath + "': " + err.Error()) |
| } |
| cmd := exec.Command("psql", "-h", hostIP, "-p", hostPort, "-d", dbName, "-U", dbUser, "-e", "-v", "ON_ERROR_STOP=1") |
| cmd.Stdin = bytes.NewBuffer(seedsBytes) |
| cmd.Env = append(os.Environ(), "PGPASSWORD="+dbPassword) |
| out, err := cmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't patch database w/ required data") |
| } |
| } |
| |
| func loadSchema() { |
| fmt.Println("Creating database tables.") |
| schemaPath := dbSchemaPath |
| if trafficVault { |
| schemaPath = trafficVaultSchemaPath |
| } |
| schemaBytes, err := ioutil.ReadFile(schemaPath) |
| if err != nil { |
| die("unable to read '" + schemaPath + "': " + err.Error()) |
| } |
| cmd := exec.Command("psql", "-h", hostIP, "-p", hostPort, "-d", dbName, "-U", dbUser, "-e", "-v", "ON_ERROR_STOP=1") |
| cmd.Stdin = bytes.NewBuffer(schemaBytes) |
| cmd.Env = append(os.Environ(), "PGPASSWORD="+dbPassword) |
| out, err := cmd.CombinedOutput() |
| fmt.Println(string(out)) |
| if err != nil { |
| die("Can't create database tables") |
| } |
| } |
| |
| func patch() { |
| if trafficVault { |
| die("patch not supported for trafficvault environment") |
| } |
| fmt.Println("Patching database with required data fixes.") |
| patchesBytes, err := ioutil.ReadFile(dbPatchesPath) |
| if err != nil { |
| die("unable to read '" + dbPatchesPath + "': " + err.Error()) |
| } |
| cmd := exec.Command("psql", "-h", hostIP, "-p", hostPort, "-d", dbName, "-U", dbUser, "-e", "-v", "ON_ERROR_STOP=1") |
| cmd.Stdin = bytes.NewBuffer(patchesBytes) |
| cmd.Env = append(os.Environ(), "PGPASSWORD="+dbPassword) |
| out, err := cmd.CombinedOutput() |
| fmt.Printf(string(out)) |
| if err != nil { |
| die("Can't patch database w/ required data") |
| } |
| } |
| |
| func die(message string) { |
| fmt.Fprintln(os.Stderr, message) |
| os.Exit(1) |
| } |
| |
| func usage() string { |
| programName := os.Args[0] |
| var buff strings.Builder |
| buff.WriteString("Usage: ") |
| buff.WriteString(programName) |
| buff.WriteString(` [OPTION] OPERATION [ARGUMENT(S)] |
| |
| -c, --config CFG Provide a path to a database configuration file, |
| instead of using the default (./db/dbconf.yml or |
| ./db/trafficvault/dbconf.yml for Traffic Vault) |
| -e, --env ENV Use configuration for environment ENV (defined in |
| the database configuration file) |
| -h, --help Show usage information and exit |
| -m, --migrations-dir DIR Use DIR as the migrations directory, instead of the |
| default (./db/migrations/ or |
| ./db/trafficvault/migrations for Traffic Vault) |
| -p, --patches PATCH Provide a path to a set of database patch statements, |
| instead of using the default (./db/patches.sql) |
| -s, --schema SCHEMA Provide a path to a schema file, instead of using the |
| default (./db/create_tables.sql or |
| ./db/trafficvault/create_tables.sql for Traffic Vault) |
| -S, --seeds SEEDS Provide a path to a seeds statements file, instead of |
| using the default (./db/seeds.sql) |
| -v, --trafficvault Perform operations for Traffic Vault instead of the |
| Traffic Ops database |
| |
| OPERATION The operation to perform; one of the following: |
| |
| migrate - Execute migrate (without seeds or patches) on the database for the |
| current environment. |
| up - Alias for 'migrate' |
| down - Roll back a single migration from the current version. |
| createdb - Execute db 'createdb' the database for the current environment. |
| dropdb - Execute db 'dropdb' on the database for the current environment. |
| create_migration NAME |
| - Creates a pair of timestamped up/down migrations titled NAME. |
| create_user - Execute 'create_user' the user for the current environment |
| (traffic_ops). |
| dbversion - Prints the current migration timestamp |
| drop_user - Execute 'drop_user' the user for the current environment |
| (traffic_ops). |
| patch - Execute sql from db/patches.sql for loading post-migration data |
| patches (NOTE: not supported with --trafficvault option). |
| redo - Roll back the most recently applied migration, then run it again. |
| reset - Execute db 'dropdb', 'createdb', load_schema, migrate on the |
| database for the current environment. |
| seed - Execute sql from db/seeds.sql for loading static data (NOTE: not |
| supported with --trafficvault option). |
| show_users - Execute sql to show all of the user for the current environment. |
| status - Prints the current migration timestamp (Deprecated, status is now an |
| alias for dbversion and will be removed in a future Traffic |
| Control release). |
| upgrade - Execute migrate, seed, and patches on the database for the current |
| environment.`) |
| return buff.String() |
| } |
| |
| // collapses two options for 'name', using a default if given, stored into 'dest'. |
| // if the two option values conflict, the whole program dies and an error is printed to |
| // stderr. |
| func collapse(o1, o2, name, def string, dest *string) { |
| if o1 == "" { |
| if o2 == "" { |
| *dest = def |
| return |
| } |
| *dest = o2 |
| return |
| } |
| if o2 == "" { |
| *dest = o1 |
| return |
| } |
| if o1 != o2 { |
| die("conflicting definitions of '" + name + "' - must be specified only once\n" + usage()) |
| } |
| *dest = o1 |
| } |
| |
| func main() { |
| flag.Usage = func() { fmt.Fprintln(os.Stderr, usage()) } |
| |
| var shortCfg string |
| var longCfg string |
| flag.StringVar(&shortCfg, "c", "", "Provide a path to a database configuration file, instead of using the default (./db/dbconf.yml or ./db/trafficvault/dbconf.yml for Traffic Vault)") |
| flag.StringVar(&longCfg, "config", "", "Provide a path to a database configuration file, instead of using the default (./db/dbconf.yml or ./db/trafficvault/dbconf.yml for Traffic Vault)") |
| |
| var shortEnv string |
| var longEnv string |
| flag.StringVar(&shortEnv, "e", "", "Use configuration for environment ENV (defined in the database configuration file)") |
| flag.StringVar(&longEnv, "env", "", "Use configuration for environment ENV (defined in the database configuration file)") |
| |
| var shortMigrations string |
| var longMigrations string |
| flag.StringVar(&shortMigrations, "m", "", "Use DIR as the migrations directory, instead of the default (./db/migrations/ or ./db/trafficvault/migrations for Traffic Vault)") |
| flag.StringVar(&longMigrations, "migrations-dir", "", "Use DIR as the migrations directory, instead of the default (./db/migrations/ or ./db/trafficvault/migrations for Traffic Vault)") |
| |
| var shortPatches string |
| var longPatches string |
| flag.StringVar(&shortPatches, "p", "", "Provide a path to a set of database patch statements, instead of using the default (./db/patches.sql)") |
| flag.StringVar(&longPatches, "patches", "", "Provide a path to a set of database patch statements, instead of using the default (./db/patches.sql)") |
| |
| var shortSchema string |
| var longSchema string |
| flag.StringVar(&shortSchema, "s", "", "Provide a path to a schema file, instead of using the default (./db/create_tables.sql or ./db/trafficvault/create_tables.sql for Traffic Vault)") |
| flag.StringVar(&longSchema, "schema", "", "Provide a path to a schema file, instead of using the default (./db/create_tables.sql or ./db/trafficvault/create_tables.sql for Traffic Vault)") |
| |
| var shortSeeds string |
| var longSeeds string |
| flag.StringVar(&shortSeeds, "S", "", "Provide a path to a seeds statements file, instead of using the default (./db/seeds.sql)") |
| flag.StringVar(&longSeeds, "seeds", "", "Provide a path to a seeds statements file, instead of using the default (./db/seeds.sql)") |
| |
| flag.BoolVar(&trafficVault, "v", false, "Perform operations for Traffic Vault instead of the Traffic Ops database") |
| flag.BoolVar(&trafficVault, "trafficvault", false, "Perform operations for Traffic Vault instead of the Traffic Ops database") |
| flag.Parse() |
| |
| if trafficVault { |
| collapse(shortCfg, longCfg, "config", defaultTrafficVaultDBConfigPath, &trafficVaultDBConfigPath) |
| collapse(shortMigrations, longMigrations, "migrations-dir", defaultTrafficVaultMigrationsPath, &trafficVaultMigrationsPath) |
| collapse(shortSchema, longSchema, "schema", defaultTrafficVaultSchemaPath, &trafficVaultSchemaPath) |
| } else { |
| collapse(shortCfg, longCfg, "config", defaultDBConfigPath, &dbConfigPath) |
| collapse(shortMigrations, longMigrations, "migrations-dir", defaultDBMigrationsPath, &dbMigrationsDir) |
| collapse(shortSchema, longSchema, "schema", defaultDBSchemaPath, &dbSchemaPath) |
| } |
| collapse(shortEnv, longEnv, "environment", defaultEnvironment, &environment) |
| collapse(shortPatches, longPatches, "patches", defaultDBPatchesPath, &dbPatchesPath) |
| collapse(shortSeeds, longSeeds, "seeds", defaultDBSeedsPath, &dbSeedsPath) |
| |
| if flag.Arg(0) == cmdCreateMigration { |
| if len(flag.Args()) != 2 { |
| die(usage()) |
| } |
| migrationName = flag.Arg(1) |
| } else if len(flag.Args()) != 1 || flag.Arg(0) == "" { |
| die(usage()) |
| } |
| if environment == "" { |
| die(usage()) |
| } |
| if err := parseDBConfig(); err != nil { |
| die(err.Error()) |
| } |
| commands := make(map[string]func()) |
| |
| commands[cmdCreateDB] = createDB |
| commands[cmdDropDB] = dropDB |
| commands[cmdCreateMigration] = createMigration |
| commands[cmdCreateUser] = createUser |
| commands[cmdDropUser] = dropUser |
| commands[cmdShowUsers] = showUsers |
| commands[cmdReset] = reset |
| commands[cmdUpgrade] = upgrade |
| commands[cmdMigrate] = runMigrations |
| commands[cmdUp] = runUp |
| commands[cmdDown] = down |
| commands[cmdRedo] = redo |
| commands[cmdStatus] = status |
| commands[cmdDBVersion] = getDBVersion |
| commands[cmdSeed] = seed |
| commands[cmdLoadSchema] = loadSchema |
| commands[cmdPatch] = patch |
| |
| userCmd := flag.Arg(0) |
| if cmd, ok := commands[userCmd]; ok { |
| cmd() |
| } else { |
| die("invalid command: " + userCmd + "\n" + usage()) |
| } |
| } |
| |
| // initMigrate initializes Migrate and returns whether a migration from Goose to |
| // Migrate was performed during said initialization and whether or not there are |
| // any actual migrations to run. |
| // |
| // If the second return value is false, then the global "Migrate" variable IS |
| // NOT SET so the caller must skip all migrations. |
| func initMigrate() (bool, bool) { |
| var err error |
| connectionString = fmt.Sprintf("%s://%s:%s@%s:%s/%s?sslmode=%s", dbDriver, dbUser, dbPassword, hostIP, hostPort, dbName, sslMode) |
| if trafficVault { |
| migrateInstance, err = migrate.New("file:"+trafficVaultMigrationsPath, connectionString) |
| } else { |
| migrateInstance, err = migrate.New("file:"+dbMigrationsDir, connectionString) |
| } |
| if errors.Is(err, os.ErrNotExist) { |
| return false, false |
| } |
| if err != nil { |
| die("Starting Migrate: " + err.Error()) |
| } |
| migrateInstance.Log = &Log{} |
| return maybeMigrateFromGoose(), true |
| } |
| |
| // Log represents the logger |
| // https://github.com/golang-migrate/migrate/blob/v4.14.1/internal/cli/log.go#L9-L12 |
| type Log struct { |
| verbose bool |
| } |
| |
| // Printf prints out formatted string into a log |
| // https://github.com/golang-migrate/migrate/blob/v4.14.1/internal/cli/log.go#L14-L21 |
| func (l *Log) Printf(format string, v ...interface{}) { |
| if l.verbose { |
| fmt.Printf(format, v...) |
| } else { |
| fmt.Fprintf(os.Stderr, format, v...) |
| } |
| } |
| |
| // Println prints out args into a log |
| // https://github.com/golang-migrate/migrate/blob/v4.14.1/internal/cli/log.go#L23-L30 |
| func (l *Log) Println(args ...interface{}) { |
| if l.verbose { |
| fmt.Println(args...) |
| } else { |
| fmt.Fprintln(os.Stderr, args...) |
| } |
| } |
| |
| // Verbose shows if verbose print enabled |
| // https://github.com/golang-migrate/migrate/blob/v4.14.1/internal/cli/log.go#L32-L35 |
| func (l *Log) Verbose() bool { |
| return l.verbose |
| } |