diff --git a/cli/cli.go b/cli/cli.go index 7f3936d86..858bfe2de 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -9,6 +9,7 @@ import ( "github.com/harness/gitness/cli/operations/account" "github.com/harness/gitness/cli/operations/hooks" + "github.com/harness/gitness/cli/operations/migrate" "github.com/harness/gitness/cli/operations/user" "github.com/harness/gitness/cli/operations/users" "github.com/harness/gitness/cli/server" @@ -29,6 +30,8 @@ func Command() { args := getArguments() app := kingpin.New(application, description) + + migrate.Register(app) server.Register(app) user.Register(app) diff --git a/cli/operations/migrate/current.go b/cli/operations/migrate/current.go new file mode 100644 index 000000000..add6cb3bc --- /dev/null +++ b/cli/operations/migrate/current.go @@ -0,0 +1,49 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package migrate + +import ( + "context" + "fmt" + "time" + + "github.com/harness/gitness/internal/store/database/migrate" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type commandCurrent struct { + envfile string +} + +func (c *commandCurrent) run(*kingpin.ParseContext) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + db, err := getDB(ctx, c.envfile) + if err != nil { + return err + } + + version, err := migrate.Current(ctx, db) + if err != nil { + return err + } + + fmt.Println(version) + + return nil +} + +func registerCurrent(app *kingpin.CmdClause) { + c := &commandCurrent{} + + cmd := app.Command("current", "display the current version of the database"). + Action(c.run) + + cmd.Arg("envfile", "load the environment variable file"). + Default(""). + StringVar(&c.envfile) +} diff --git a/cli/operations/migrate/migrate.go b/cli/operations/migrate/migrate.go new file mode 100644 index 000000000..ca95a6544 --- /dev/null +++ b/cli/operations/migrate/migrate.go @@ -0,0 +1,40 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package migrate + +import ( + "context" + "fmt" + + "github.com/harness/gitness/cli/server" + "github.com/harness/gitness/internal/store/database" + + "github.com/jmoiron/sqlx" + "github.com/joho/godotenv" + "gopkg.in/alecthomas/kingpin.v2" +) + +func getDB(ctx context.Context, envfile string) (*sqlx.DB, error) { + _ = godotenv.Load(envfile) + + config, err := server.LoadConfig() + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + + db, err := database.Connect(ctx, config.Database.Driver, config.Database.Datasource) + if err != nil { + return nil, fmt.Errorf("failed to create database handle: %w", err) + } + + return db, nil +} + +// Register the server command. +func Register(app *kingpin.Application) { + cmd := app.Command("migrate", "database migration tool") + registerCurrent(cmd) + registerTo(cmd) +} diff --git a/cli/operations/migrate/to.go b/cli/operations/migrate/to.go new file mode 100644 index 000000000..63d9ec47e --- /dev/null +++ b/cli/operations/migrate/to.go @@ -0,0 +1,46 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package migrate + +import ( + "context" + "time" + + "github.com/harness/gitness/internal/store/database/migrate" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type commandTo struct { + envfile string + version string +} + +func (c *commandTo) run(k *kingpin.ParseContext) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + db, err := getDB(ctx, c.envfile) + if err != nil { + return err + } + + return migrate.To(ctx, db, c.version) +} + +func registerTo(app *kingpin.CmdClause) { + c := &commandTo{} + + cmd := app.Command("to", "migrates the database to the provided version"). + Action(c.run) + + cmd.Arg("version", "database version to migrate to"). + Required(). + StringVar(&c.version) + + cmd.Arg("envfile", "load the environment variable file"). + Default(""). + StringVar(&c.envfile) +} diff --git a/cli/server/server.go b/cli/server/server.go index 6f4a14fee..e545b919e 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -50,7 +50,7 @@ func (c *command) run(*kingpin.ParseContext) error { } // configure the log level - setupLogger(config) + SetupLogger(config) // add logger to context log := log.Logger.With().Logger() @@ -119,9 +119,8 @@ func (c *command) run(*kingpin.ParseContext) error { return err } -// helper function configures the global logger from -// the loaded configuration. -func setupLogger(config *types.Config) { +// SetupLogger configures the global logger from the loaded configuration. +func SetupLogger(config *types.Config) { // configure the log level switch { case config.Trace: diff --git a/internal/store/database/migrate/migrate.go b/internal/store/database/migrate/migrate.go index 2bb8b0b3a..e6e7ea35e 100644 --- a/internal/store/database/migrate/migrate.go +++ b/internal/store/database/migrate/migrate.go @@ -8,6 +8,7 @@ import ( "context" "database/sql" "embed" + "fmt" "io/fs" "github.com/jmoiron/sqlx" @@ -21,8 +22,59 @@ var postgres embed.FS //go:embed sqlite/*.sql var sqlite embed.FS +const tableName = "migrations" + // Migrate performs the database migration. func Migrate(ctx context.Context, db *sqlx.DB) error { + opts := getMigrator(db) + return migrate.New(opts).MigrateUp(ctx) +} + +// To performs the database migration to the specific version. +func To(ctx context.Context, db *sqlx.DB, version string) error { + opts := getMigrator(db) + return migrate.New(opts).MigrateTo(ctx, version) +} + +// Current returns the current version ID (the latest migration applied) of the database. +func Current(ctx context.Context, db *sqlx.DB) (string, error) { + var ( + query string + migrationTableCount int + ) + + switch db.DriverName() { + case "sqlite3": + query = ` + SELECT count(*) + FROM sqlite_master + WHERE name = ? and type = 'table'` + default: + query = ` + SELECT count(*) + FROM information_schema.tables + WHERE table_name = ? and table_schema = 'public'` + } + + if err := db.QueryRowContext(ctx, query, tableName).Scan(&migrationTableCount); err != nil { + return "", fmt.Errorf("failed to check migration table existence: %w", err) + } + + if migrationTableCount == 0 { + return "", nil + } + + var version string + + query = "select version from " + tableName + " limit 1" + if err := db.QueryRowContext(ctx, query).Scan(&version); err != nil { + return "", fmt.Errorf("failed to read current DB version from migration table: %w", err) + } + + return version, nil +} + +func getMigrator(db *sqlx.DB) migrate.Options { before := func(_ context.Context, _ *sql.Tx, version string) error { log.Trace().Str("version", version).Msg("migration started") return nil @@ -38,7 +90,7 @@ func Migrate(ctx context.Context, db *sqlx.DB) error { Before: before, DB: db.DB, FS: sqlite, - Table: "migrations", + Table: tableName, } switch db.DriverName() { @@ -51,5 +103,5 @@ func Migrate(ctx context.Context, db *sqlx.DB) error { opts.FS = folder } - return migrate.New(opts).MigrateUp(ctx) + return opts } diff --git a/internal/store/database/store.go b/internal/store/database/store.go index 6a0574666..a9c8f50fe 100644 --- a/internal/store/database/store.go +++ b/internal/store/database/store.go @@ -46,7 +46,17 @@ func Connect(ctx context.Context, driver string, datasource string) (*sqlx.DB, e return nil, fmt.Errorf("failed to ping the db: %w", err) } - if err = setupDatabase(ctx, dbx); err != nil { + return dbx, nil +} + +// ConnectAndMigrate creates the database handle and migrates the database. +func ConnectAndMigrate(ctx context.Context, driver string, datasource string) (*sqlx.DB, error) { + dbx, err := Connect(ctx, driver, datasource) + if err != nil { + return nil, err + } + + if err = migrateDatabase(ctx, dbx); err != nil { return nil, fmt.Errorf("failed to setup the db: %w", err) } @@ -111,6 +121,6 @@ func pingDatabase(ctx context.Context, db *sqlx.DB) error { // helper function to setup the database by performing automated // database migration steps. -func setupDatabase(ctx context.Context, db *sqlx.DB) error { +func migrateDatabase(ctx context.Context, db *sqlx.DB) error { return migrate.Migrate(ctx, db) } diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go index 977e90a1b..93ab08421 100644 --- a/internal/store/database/wire.go +++ b/internal/store/database/wire.go @@ -34,7 +34,7 @@ var WireSet = wire.NewSet( // ProvideDatabase provides a database connection. func ProvideDatabase(ctx context.Context, config *types.Config) (*sqlx.DB, error) { - return Connect( + return ConnectAndMigrate( ctx, config.Database.Driver, config.Database.Datasource,