Skip to content

Commit

Permalink
[cmd] Enable db-manager CLI to support subcommands (#1122)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickmis authored Sep 24, 2024
1 parent b41ffd2 commit 8ee12cd
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 244 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ test-go-units:
test-go-units-crdb: cleanup-test-go-units-crdb
@docker run -d --name dss-crdb-for-testing -p 26257:26257 -p 8080:8080 cockroachdb/cockroach:v24.1.3 start-single-node --insecure > /dev/null
@until [ -n "`docker logs dss-crdb-for-testing | grep 'nodeID'`" ]; do echo "Waiting for CRDB to be ready"; sleep 3; done;
go run ./cmds/db-manager/main.go --schemas_dir ./build/db_schemas/rid --db_version latest --cockroach_host localhost
go run ./cmds/db-manager/main.go migrate --schemas_dir ./build/db_schemas/rid --db_version latest --cockroach_host localhost
go test -count=1 -v ./pkg/rid/store/cockroach --cockroach_host localhost --cockroach_port 26257 --cockroach_ssl_mode disable --cockroach_user root --cockroach_db_name rid
go test -count=1 -v ./pkg/rid/application --cockroach_host localhost --cockroach_port 26257 --cockroach_ssl_mode disable --cockroach_user root --cockroach_db_name rid
@docker stop dss-crdb-for-testing > /dev/null
Expand Down
4 changes: 2 additions & 2 deletions build/deploy/schema-manager.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ local schema_dir = '/db-schemas';
},
soloContainer:: base.Container('rid-schema-manager') {
image: metadata.schema_manager.image,
command: ['db-manager'],
command: ['db-manager', 'migrate'],
args_:: {
cockroach_host: 'cockroachdb-balanced.' + metadata.namespace,
cockroach_port: metadata.cockroach.grpc_port,
Expand All @@ -42,7 +42,7 @@ local schema_dir = '/db-schemas';
},
soloContainer:: base.Container('scd-schema-manager') {
image: metadata.schema_manager.image,
command: ['db-manager'],
command: ['db-manager', 'migrate'],
args_:: {
cockroach_host: 'cockroachdb-balanced.' + metadata.namespace,
cockroach_port: metadata.cockroach.grpc_port,
Expand Down
4 changes: 2 additions & 2 deletions build/dev/haproxy_local_setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ docker run --rm --name rid-db-manager \
--link dss-crdb-cluster-for-testing:crdb \
--network dss_sandbox-default \
local-interuss-dss-image \
/usr/bin/db-manager \
/usr/bin/db-manager migrate \
--schemas_dir db-schemas/rid \
--db_version "latest" \
--cockroach_host crdb
Expand All @@ -139,7 +139,7 @@ docker run --rm --name scd-db-manager \
--link dss-crdb-cluster-for-testing:crdb \
--network dss_sandbox-default \
local-interuss-dss-image \
/usr/bin/db-manager \
/usr/bin/db-manager migrate \
--schemas_dir db-schemas/scd \
--db_version "latest" \
--cockroach_host crdb
Expand Down
2 changes: 1 addition & 1 deletion build/dev/startup/rid_bootstrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ else
sleep 3

echo "Bootstrapping RID DB..."
/usr/bin/db-manager \
/usr/bin/db-manager migrate \
--schemas_dir /db-schemas/rid \
--db_version "latest" \
--cockroach_host local-dss-crdb
Expand Down
2 changes: 1 addition & 1 deletion build/dev/startup/scd_bootstrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ else
sleep 3

echo "Bootstrapping SCD DB..."
/usr/bin/db-manager \
/usr/bin/db-manager migrate \
--schemas_dir /db-schemas/scd \
--db_version "latest" \
--cockroach_host local-dss-crdb
Expand Down
4 changes: 2 additions & 2 deletions cmds/core-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ docker container run -p 26257:26257 -p 8080:8080 --rm cockroachdb/cockroach:v24.
Once an initialized CockroachDB cluster is available, the necessary databases within the CRDB cluster must be created/configured properly. This can be accomplished with [migrate_local_db.sh](../../build/dev/migrate_local_db.sh), as documented in the [standalone instance documentation](../../build/dev/standalone_instance.md), when using the standard standalone development DSS instance, or it can be accomplished manually with commands similar to those below starting from the repo root folder:

```bash
go run ./cmds/db-manager \
go run ./cmds/db-manager migrate \
--schemas_dir ./build/db_schemas/rid \
--db_version latest \
--cockroach_host localhost
go run ./cmds/db-manager \
go run ./cmds/db-manager migrate \
--schemas_dir ./build/db_schemas/scd \
--db_version latest \
--cockroach_host localhost
Expand Down
247 changes: 13 additions & 234 deletions cmds/db-manager/main.go
Original file line number Diff line number Diff line change
@@ -1,250 +1,29 @@
// Script for Database bootstrap deployment and migration

package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/coreos/go-semver/semver"
"github.com/interuss/dss/pkg/cockroach"
"github.com/interuss/dss/pkg/cockroach/flags"
"github.com/interuss/stacktrace"
)

type MigrationStep struct {
version semver.Version
upToFile string
downFromFile string
}

var (
// Pattern to match files describing migration steps
migrationStepRegexp = "(upto|downfrom)-v(\\d+\\.\\d+\\.\\d+)-(.*)\\.sql"
"github.com/interuss/dss/cmds/db-manager/migration"
"github.com/spf13/cobra"
)

var (
path = flag.String("schemas_dir", "", "path to db migration files directory. the migrations found there will be applied to the database whose name matches the folder name.")
dbVersion = flag.String("db_version", "", "the db version to migrate to (ex: 1.0.0) or use \"latest\" to automatically upgrade to the latest version or leave blank to print the current version")
)

func main() {
// Read and validate schemas_dir input
flag.Parse()
if *path == "" {
log.Panic("Must specify schemas_dir path")
}
dbName := filepath.Base(*path)

// Enumerate schema versions
steps, err := enumerateMigrationSteps(path)
if err != nil {
log.Panicf("Failed to read schema version migration definitions: %v", err)
}
if len(steps) == 0 {
log.Panicf("No migration definitions found in schemas_dir=%s", *path)
}

// Determine target version
var targetVersion *semver.Version
if strings.ToLower(*dbVersion) == "latest" {
targetVersion = &steps[len(steps)-1].version
} else if strings.TrimSpace(*dbVersion) == "" {
// User just wants to print the current version
targetVersion = nil
} else {
targetVersion, err = semver.NewVersion(*dbVersion)
if err != nil {
log.Panicf("Failed to parse desired db_version: %v", err)
}
}

// Connect to database server
connectParameters := flags.ConnectParameters()
connectParameters.ApplicationName = "db-manager"
connectParameters.DBName = "postgres" // Use an initial database that is known to always be present
crdb, err := cockroach.Dial(context.Background(), connectParameters)
if err != nil {
log.Panicf("Failed to connect to database with %+v: %v", connectParameters, err)
}
defer func() {
crdb.Pool.Close()
}()

crdbVersion, err := crdb.GetServerVersion()
if err != nil {
log.Panicf("Unable to retrieve the version of the server %s:%d: %v", connectParameters.Host, connectParameters.Port, err)
}
log.Printf("CRDB server version: %s", crdbVersion)

// Make sure specified database exists
exists, err := doesDatabaseExist(crdb, dbName)
if err != nil {
log.Panicf("Failed to check whether database %s exists: %v", dbName, err)
}
if !exists && dbName == "rid" {
// In the special case of rid, the database was previously named defaultdb
log.Printf("Database %s does not exist; checking for older \"defaultdb\" database", dbName)
dbName = "defaultdb"
exists, err = doesDatabaseExist(crdb, dbName)
if err != nil {
log.Panicf("Failed to check whether old defaultdb database exists: %v", err)
}
}
if !exists {
log.Printf("Database %s does not exist; creating now", dbName)
createDB := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", dbName)
if _, err := crdb.Pool.Exec(context.Background(), createDB); err != nil {
log.Panicf("Failed to create new database %s: %v", dbName, err)
}
} else {
log.Printf("Database %s already exists; reading current state", dbName)
}

// Read current schema version of database
currentVersion, err := crdb.GetVersion(context.Background(), dbName)
if err != nil {
log.Panicf("Failed to get current database version for %s: %v", dbName, err)
}
log.Printf("Initial %s database schema version is %v, target is %v", dbName, currentVersion, targetVersion)
if targetVersion == nil {
return
}

// Compute index of current version
var currentStepIndex int = -1
for i, version := range steps {
if version.version == *currentVersion {
currentStepIndex = i
}
}

// Perform migration steps until current version matches target version
for !currentVersion.Equal(*targetVersion) {
// Compute which migration step to run next and how it will change the schema version
var newCurrentStepIndex int
var sqlFile string
var newVersion *semver.Version
if currentVersion.LessThan(*targetVersion) {
// Migrate up to next version
sqlFile = steps[currentStepIndex+1].upToFile
newVersion = &steps[currentStepIndex+1].version
newCurrentStepIndex = currentStepIndex + 1
} else {
// Migrate down from current version
sqlFile = steps[currentStepIndex].downFromFile
newCurrentStepIndex = currentStepIndex - 1
newVersion = &steps[newCurrentStepIndex].version
}
log.Printf("Running %s to migrate %v to %v", sqlFile, currentVersion, newVersion)

// Read migration SQL into string
fullFilePath := filepath.Join(*path, sqlFile)
rawMigrationSQL, err := os.ReadFile(fullFilePath)
if err != nil {
log.Panicf("Failed to load SQL content from %s: %v", fullFilePath, err)
}

// Ensure SQL session has implicit transactions disabled for CRDB versions 22.2+
sessionConfigurationSQL := ""
if crdbVersion.Compare(*semver.New("22.2.0")) >= 0 {
sessionConfigurationSQL = "SET enable_implicit_transaction_for_batch_statements = false;\n"
}

migrationSQL := sessionConfigurationSQL + fmt.Sprintf("USE %s;\n", dbName) + string(rawMigrationSQL)

// Execute migration step
if _, err := crdb.Pool.Exec(context.Background(), migrationSQL); err != nil {
log.Panicf("Failed to execute %s migration step %s: %v", dbName, fullFilePath, err)
}

// Update current state
if dbName == "defaultdb" && newVersion.String() == "4.0.0" && newCurrentStepIndex > currentStepIndex {
// RID database changes from `defaultdb` to `rid` when moving up to 4.0.0
dbName = "rid"
}
if dbName == "rid" && currentVersion.String() == "4.0.0" && newCurrentStepIndex < currentStepIndex {
// RID database changes from `rid` to `defaultdb` when moving down from 4.0.0
dbName = "defaultdb"
}
actualVersion, err := crdb.GetVersion(context.Background(), dbName)
if err != nil {
log.Panicf("Failed to get current database version for %s: %v", dbName, err)
}
if !actualVersion.Equal(*newVersion) {
log.Panicf("Migration %s should have migrated %s schema version %v to %v, but instead resulted in %v", fullFilePath, dbName, currentVersion, newVersion, currentVersion)
}
currentVersion = actualVersion
currentStepIndex = newCurrentStepIndex
}

log.Printf("Final %s version: %v", dbName, currentVersion)
}

func enumerateMigrationSteps(path *string) ([]MigrationStep, error) {
steps := make(map[semver.Version]MigrationStep)

// Identify files defining version migration steps
files, err := os.ReadDir(*path)
if err != nil {
return make([]MigrationStep, 0), stacktrace.Propagate(err, "Failed to read schema files directory")
}
r := regexp.MustCompile(migrationStepRegexp)
for _, file := range files {
if !file.IsDir() {
match := r.FindStringSubmatch(file.Name())
if len(match) > 0 {
v := *semver.New(match[2])
step := steps[v]
step.version = v
if match[1] == "upto" {
step.upToFile = file.Name()
} else if match[1] == "downfrom" {
step.downFromFile = file.Name()
} else {
return make([]MigrationStep, 0), fmt.Errorf("Unexpected migration step prefix: %s", match[1])
}
steps[v] = step
}
}
}

// Sort versions in ascending order
versions := make([]*semver.Version, len(steps))
i := 0
for k := range steps {
v := steps[k].version
versions[i] = &v
i++
}
semver.Sort(versions)

// Render sorted step list
result := make([]MigrationStep, len(versions)+1)
result[0].version = *semver.New("0.0.0")
for i := 0; i < len(versions); i++ {
result[i+1] = steps[*versions[i]]
DBManagerCmd = &cobra.Command{
Use: "db-manager",
Short: "DSS database management utility",
}
)

return result, nil
func init() {
DBManagerCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) // enable support for flags not yet migrated to using pflag (e.g. crdb flags)
DBManagerCmd.AddCommand(migration.MigrationCmd)
}

func doesDatabaseExist(crdb *cockroach.DB, database string) (bool, error) {
const checkDbQuery = `
SELECT EXISTS (
SELECT * FROM pg_database WHERE datname = $1
)`

var exists bool
if err := crdb.Pool.QueryRow(context.Background(), checkDbQuery, database).Scan(&exists); err != nil {
return false, err
func main() {
if err := DBManagerCmd.Execute(); err != nil {
log.Printf("failed to execute db-manager: %v", err)
os.Exit(1)
}

return exists, nil
}
Loading

0 comments on commit 8ee12cd

Please sign in to comment.