Skip to content

Commit

Permalink
cmd: migrate command (#18)
Browse files Browse the repository at this point in the history
What
Adds the migrate up/down command.

Why
For applying the database migration. This is a required component for the CI/CD setup.
  • Loading branch information
daniel-burghardt authored Jun 10, 2024
1 parent 0c2d8f2 commit ef6aeda
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 69 deletions.
52 changes: 14 additions & 38 deletions cmd/ingest.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package cmd

import (
"fmt"
"go/types"

_ "github.com/lib/pq"
"github.com/spf13/cobra"
"github.com/stellar/go/network"
"github.com/stellar/go/support/config"
"github.com/stellar/go/support/log"
"github.com/stellar/wallet-backend/cmd/utils"
Expand All @@ -17,31 +17,9 @@ type ingestCmd struct{}
func (c *ingestCmd) Command() *cobra.Command {
cfg := ingest.Configs{}
cfgOpts := config.ConfigOptions{
{
Name: "database-url",
Usage: "Database connection URL.",
OptType: types.String,
ConfigKey: &cfg.DatabaseURL,
FlagDefault: "postgres://postgres@localhost:5432/wallet-backend?sslmode=disable",
Required: true,
},
{
Name: "network-passphrase",
Usage: "Stellar Network Passphrase to connect.",
OptType: types.String,
ConfigKey: &cfg.NetworkPassphrase,
FlagDefault: network.TestNetworkPassphrase,
Required: true,
},
{
Name: "log-level",
Usage: `The log level used in this project. Options: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", or "PANIC".`,
OptType: types.String,
FlagDefault: "TRACE",
ConfigKey: &cfg.LogLevel,
CustomSetValue: utils.SetConfigOptionLogLevel,
Required: false,
},
utils.DatabaseURLOption(&cfg.DatabaseURL),
utils.LogLevelOption(&cfg.LogLevel),
utils.NetworkPassphraseOption(&cfg.NetworkPassphrase),
{
Name: "captive-core-bin-path",
Usage: "Path to Captive Core's binary file.",
Expand Down Expand Up @@ -86,27 +64,25 @@ func (c *ingestCmd) Command() *cobra.Command {
}

cmd := &cobra.Command{
Use: "ingest",
Short: "Run Ingestion service",
PersistentPreRun: func(_ *cobra.Command, _ []string) {
cfgOpts.Require()
if err := cfgOpts.SetValues(); err != nil {
log.Fatalf("Error setting values of config options: %s", err.Error())
}
},
Run: func(_ *cobra.Command, _ []string) {
c.Run(cfg)
Use: "ingest",
Short: "Run Ingestion service",
PersistentPreRunE: utils.DefaultPersistentPreRunE(cfgOpts),
RunE: func(_ *cobra.Command, _ []string) error {
return c.Run(cfg)
},
}

if err := cfgOpts.Init(cmd); err != nil {
log.Fatalf("Error initializing a config option: %s", err.Error())
}

return cmd
}

func (c *ingestCmd) Run(cfg ingest.Configs) {
func (c *ingestCmd) Run(cfg ingest.Configs) error {
err := ingest.Ingest(cfg)
if err != nil {
log.Fatalf("Error running Ingest: %s", err.Error())
return fmt.Errorf("running ingest: %w", err)
}
return nil
}
103 changes: 103 additions & 0 deletions cmd/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cmd

import (
"context"
"fmt"
"strconv"

migrate "github.com/rubenv/sql-migrate"
"github.com/spf13/cobra"
"github.com/stellar/go/support/config"
"github.com/stellar/go/support/log"
"github.com/stellar/wallet-backend/cmd/utils"
"github.com/stellar/wallet-backend/internal/db"
)

type migrateCmd struct{}

func (c *migrateCmd) Command() *cobra.Command {
var databaseURL string
cfgOpts := config.ConfigOptions{
utils.DatabaseURLOption(&databaseURL),
}

migrateCmd := &cobra.Command{
Use: "migrate",
Short: "Schema migration helpers",
PersistentPreRunE: utils.DefaultPersistentPreRunE(cfgOpts),
}

migrateUpCmd := &cobra.Command{
Use: "up",
Short: "Migrates database up [count] migrations",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return c.RunMigrateUp(cmd.Context(), databaseURL, args)
},
}

migrateDownCmd := &cobra.Command{
Use: "down [count]",
Short: "Migrates database down [count] migrations",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return c.RunMigrateDown(cmd.Context(), databaseURL, args)
},
}

migrateCmd.AddCommand(migrateUpCmd)
migrateCmd.AddCommand(migrateDownCmd)

if err := cfgOpts.Init(migrateCmd); err != nil {
log.Fatalf("Error initializing a config option: %s", err.Error())
}

return migrateCmd
}

func (c *migrateCmd) RunMigrateUp(ctx context.Context, databaseURL string, args []string) error {
var count int
if len(args) > 0 {
var err error
count, err = strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid [count] argument: %s", args[0])
}
}
if err := executeMigrations(ctx, databaseURL, migrate.Up, count); err != nil {
return fmt.Errorf("executing migrate up: %w", err)
}
return nil
}

func (c *migrateCmd) RunMigrateDown(ctx context.Context, databaseURL string, args []string) error {
count, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid [count] argument: %s", args[0])
}
if err := executeMigrations(ctx, databaseURL, migrate.Down, count); err != nil {
return fmt.Errorf("executing migrate down: %v", err)
}
return nil
}

func executeMigrations(ctx context.Context, databaseURL string, direction migrate.MigrationDirection, count int) error {
numMigrationsRun, err := db.Migrate(ctx, databaseURL, direction, count)
if err != nil {
return fmt.Errorf("migrating database: %w", err)
}

if numMigrationsRun == 0 {
log.Ctx(ctx).Info("No migrations applied.")
} else {
log.Ctx(ctx).Infof("Successfully applied %d migrations %s.", numMigrationsRun, migrationDirectionStr(direction))
}
return nil
}

func migrationDirectionStr(direction migrate.MigrationDirection) string {
if direction == migrate.Up {
return "up"
}
return "down"
}
6 changes: 4 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "wallet-backend",
Short: "Wallet Backend Server",
Use: "wallet-backend",
Short: "Wallet Backend Server",
SilenceErrors: true,
Run: func(cmd *cobra.Command, args []string) {
err := cmd.Help()
if err != nil {
Expand All @@ -31,4 +32,5 @@ func init() {

rootCmd.AddCommand((&serveCmd{}).Command())
rootCmd.AddCommand((&ingestCmd{}).Command())
rootCmd.AddCommand((&migrateCmd{}).Command())
}
42 changes: 13 additions & 29 deletions cmd/serve.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"fmt"
"go/types"

_ "github.com/lib/pq"
Expand All @@ -16,6 +17,8 @@ type serveCmd struct{}
func (c *serveCmd) Command() *cobra.Command {
cfg := serve.Configs{}
cfgOpts := config.ConfigOptions{
utils.DatabaseURLOption(&cfg.DatabaseURL),
utils.LogLevelOption(&cfg.LogLevel),
{
Name: "port",
Usage: "Port to listen and serve on",
Expand All @@ -24,14 +27,6 @@ func (c *serveCmd) Command() *cobra.Command {
FlagDefault: 8000,
Required: false,
},
{
Name: "database-url",
Usage: "Database connection URL",
OptType: types.String,
ConfigKey: &cfg.DatabaseURL,
FlagDefault: "postgres://postgres@localhost:5432/wallet-backend?sslmode=disable",
Required: true,
},
{
Name: "server-base-url",
Usage: "The server base URL",
Expand All @@ -40,15 +35,6 @@ func (c *serveCmd) Command() *cobra.Command {
FlagDefault: "http://localhost:8000",
Required: true,
},
{
Name: "log-level",
Usage: `The log level used in this project. Options: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", or "PANIC".`,
OptType: types.String,
FlagDefault: "TRACE",
ConfigKey: &cfg.LogLevel,
CustomSetValue: utils.SetConfigOptionLogLevel,
Required: false,
},
{
Name: "wallet-signing-key",
Usage: "The public key of the Stellar account that signs the payloads when making HTTP Request to this server.",
Expand All @@ -59,27 +45,25 @@ func (c *serveCmd) Command() *cobra.Command {
},
}
cmd := &cobra.Command{
Use: "serve",
Short: "Run Wallet Backend server",
PersistentPreRun: func(_ *cobra.Command, _ []string) {
cfgOpts.Require()
if err := cfgOpts.SetValues(); err != nil {
log.Fatalf("Error setting values of config options: %s", err.Error())
}
},
Run: func(_ *cobra.Command, _ []string) {
c.Run(cfg)
Use: "serve",
Short: "Run Wallet Backend server",
PersistentPreRunE: utils.DefaultPersistentPreRunE(cfgOpts),
RunE: func(_ *cobra.Command, _ []string) error {
return c.Run(cfg)
},
}

if err := cfgOpts.Init(cmd); err != nil {
log.Fatalf("Error initializing a config option: %s", err.Error())
}

return cmd
}

func (c *serveCmd) Run(cfg serve.Configs) {
func (c *serveCmd) Run(cfg serve.Configs) error {
err := serve.Serve(cfg)
if err != nil {
log.Fatalf("Error running Serve: %s", err.Error())
return fmt.Errorf("running serve: %w", err)
}
return nil
}
43 changes: 43 additions & 0 deletions cmd/utils/global_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package utils

import (
"go/types"

"github.com/sirupsen/logrus"
"github.com/stellar/go/network"
"github.com/stellar/go/support/config"
)

func DatabaseURLOption(configKey *string) *config.ConfigOption {
return &config.ConfigOption{
Name: "database-url",
Usage: "Database connection URL.",
OptType: types.String,
ConfigKey: configKey,
FlagDefault: "postgres://postgres@localhost:5432/wallet-backend?sslmode=disable",
Required: true,
}
}

func LogLevelOption(configKey *logrus.Level) *config.ConfigOption {
return &config.ConfigOption{
Name: "log-level",
Usage: `The log level used in this project. Options: "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", or "PANIC".`,
OptType: types.String,
FlagDefault: "TRACE",
ConfigKey: configKey,
CustomSetValue: SetConfigOptionLogLevel,
Required: false,
}
}

func NetworkPassphraseOption(configKey *string) *config.ConfigOption {
return &config.ConfigOption{
Name: "network-passphrase",
Usage: "Stellar Network Passphrase to connect.",
OptType: types.String,
ConfigKey: configKey,
FlagDefault: network.TestNetworkPassphrase,
Required: true,
}
}
20 changes: 20 additions & 0 deletions cmd/utils/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package utils

import (
"fmt"

"github.com/spf13/cobra"
"github.com/stellar/go/support/config"
)

func DefaultPersistentPreRunE(cfgOpts config.ConfigOptions) func(_ *cobra.Command, _ []string) error {
return func(_ *cobra.Command, _ []string) error {
if err := cfgOpts.RequireE(); err != nil {
return fmt.Errorf("requiring values of config options: %w", err)
}
if err := cfgOpts.SetValues(); err != nil {
return fmt.Errorf("setting values of config options: %w", err)
}
return nil
}
}
5 changes: 5 additions & 0 deletions internal/db/dbtest/dbtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ func Open(t *testing.T) *dbtest.DB {

return db
}

func OpenWithoutMigrations(t *testing.T) *dbtest.DB {
db := dbtest.Postgres(t)
return db
}
25 changes: 25 additions & 0 deletions internal/db/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package db

import (
"context"
"fmt"
"net/http"

migrate "github.com/rubenv/sql-migrate"
"github.com/stellar/wallet-backend/internal/db/migrations"
)

func Migrate(ctx context.Context, databaseURL string, direction migrate.MigrationDirection, count int) (int, error) {
dbConnectionPool, err := OpenDBConnectionPool(databaseURL)
if err != nil {
return 0, fmt.Errorf("connecting to the database: %w", err)
}
defer dbConnectionPool.Close()

m := migrate.HttpFileSystemMigrationSource{FileSystem: http.FS(migrations.FS)}
db, err := dbConnectionPool.SqlDB(ctx)
if err != nil {
return 0, fmt.Errorf("fetching sql.DB: %w", err)
}
return migrate.ExecMax(db, dbConnectionPool.DriverName(), m, direction, count)
}
Loading

0 comments on commit ef6aeda

Please sign in to comment.