github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/cmd/migrate.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/fatih/color" 9 "github.com/jzelinskie/cobrautil/v2" 10 "github.com/spf13/cobra" 11 12 crdbmigrations "github.com/authzed/spicedb/internal/datastore/crdb/migrations" 13 mysqlmigrations "github.com/authzed/spicedb/internal/datastore/mysql/migrations" 14 "github.com/authzed/spicedb/internal/datastore/postgres/migrations" 15 spannermigrations "github.com/authzed/spicedb/internal/datastore/spanner/migrations" 16 log "github.com/authzed/spicedb/internal/logging" 17 "github.com/authzed/spicedb/pkg/cmd/server" 18 "github.com/authzed/spicedb/pkg/cmd/termination" 19 "github.com/authzed/spicedb/pkg/datastore" 20 "github.com/authzed/spicedb/pkg/migrate" 21 ) 22 23 func RegisterMigrateFlags(cmd *cobra.Command) { 24 cmd.Flags().String("datastore-engine", "memory", fmt.Sprintf(`type of datastore to initialize (%s)`, datastore.EngineOptions())) 25 cmd.Flags().String("datastore-conn-uri", "", `connection string used by remote datastores (e.g. "postgres://postgres:password@localhost:5432/spicedb")`) 26 cmd.Flags().String("datastore-credentials-provider-name", "", fmt.Sprintf(`retrieve datastore credentials dynamically using (%s)`, datastore.CredentialsProviderOptions())) 27 cmd.Flags().String("datastore-spanner-credentials", "", "path to service account key credentials file with access to the cloud spanner instance (omit to use application default credentials)") 28 cmd.Flags().String("datastore-spanner-emulator-host", "", "URI of spanner emulator instance used for development and testing (e.g. localhost:9010)") 29 cmd.Flags().String("datastore-mysql-table-prefix", "", "prefix to add to the name of all mysql database tables") 30 cmd.Flags().Uint64("migration-backfill-batch-size", 1000, "number of items to migrate per iteration of a datastore backfill") 31 cmd.Flags().Duration("migration-timeout", 1*time.Hour, "defines a timeout for the execution of the migration, set to 1 hour by default") 32 } 33 34 func NewMigrateCommand(programName string) *cobra.Command { 35 return &cobra.Command{ 36 Use: "migrate [revision]", 37 Short: "execute datastore schema migrations", 38 Long: fmt.Sprintf("Executes datastore schema migrations for the datastore.\nThe special value \"%s\" can be used to migrate to the latest revision.", color.YellowString(migrate.Head)), 39 PreRunE: server.DefaultPreRunE(programName), 40 RunE: termination.PublishError(migrateRun), 41 Args: cobra.ExactArgs(1), 42 } 43 } 44 45 func migrateRun(cmd *cobra.Command, args []string) error { 46 datastoreEngine := cobrautil.MustGetStringExpanded(cmd, "datastore-engine") 47 dbURL := cobrautil.MustGetStringExpanded(cmd, "datastore-conn-uri") 48 timeout := cobrautil.MustGetDuration(cmd, "migration-timeout") 49 migrationBatachSize := cobrautil.MustGetUint64(cmd, "migration-backfill-batch-size") 50 51 if datastoreEngine == "cockroachdb" { 52 log.Ctx(cmd.Context()).Info().Msg("migrating cockroachdb datastore") 53 54 var err error 55 migrationDriver, err := crdbmigrations.NewCRDBDriver(dbURL) 56 if err != nil { 57 return fmt.Errorf("unable to create migration driver for %s: %w", datastoreEngine, err) 58 } 59 return runMigration(cmd.Context(), migrationDriver, crdbmigrations.CRDBMigrations, args[0], timeout, migrationBatachSize) 60 } else if datastoreEngine == "postgres" { 61 log.Ctx(cmd.Context()).Info().Msg("migrating postgres datastore") 62 63 var credentialsProvider datastore.CredentialsProvider 64 credentialsProviderName := cobrautil.MustGetString(cmd, "datastore-credentials-provider-name") 65 if credentialsProviderName != "" { 66 var err error 67 credentialsProvider, err = datastore.NewCredentialsProvider(cmd.Context(), credentialsProviderName) 68 if err != nil { 69 return err 70 } 71 } 72 73 migrationDriver, err := migrations.NewAlembicPostgresDriver(cmd.Context(), dbURL, credentialsProvider) 74 if err != nil { 75 return fmt.Errorf("unable to create migration driver for %s: %w", datastoreEngine, err) 76 } 77 return runMigration(cmd.Context(), migrationDriver, migrations.DatabaseMigrations, args[0], timeout, migrationBatachSize) 78 } else if datastoreEngine == "spanner" { 79 log.Ctx(cmd.Context()).Info().Msg("migrating spanner datastore") 80 81 credFile := cobrautil.MustGetStringExpanded(cmd, "datastore-spanner-credentials") 82 var err error 83 emulatorHost, err := cmd.Flags().GetString("datastore-spanner-emulator-host") 84 if err != nil { 85 log.Ctx(cmd.Context()).Fatal().Err(err).Msg("unable to get spanner emulator host") 86 } 87 migrationDriver, err := spannermigrations.NewSpannerDriver(cmd.Context(), dbURL, credFile, emulatorHost) 88 if err != nil { 89 return fmt.Errorf("unable to create migration driver for %s: %w", datastoreEngine, err) 90 } 91 return runMigration(cmd.Context(), migrationDriver, spannermigrations.SpannerMigrations, args[0], timeout, migrationBatachSize) 92 } else if datastoreEngine == "mysql" { 93 log.Ctx(cmd.Context()).Info().Msg("migrating mysql datastore") 94 95 var err error 96 tablePrefix, err := cmd.Flags().GetString("datastore-mysql-table-prefix") 97 if err != nil { 98 log.Ctx(cmd.Context()).Fatal().Msg(fmt.Sprintf("unable to get table prefix: %s", err)) 99 } 100 101 var credentialsProvider datastore.CredentialsProvider 102 credentialsProviderName := cobrautil.MustGetString(cmd, "datastore-credentials-provider-name") 103 if credentialsProviderName != "" { 104 var err error 105 credentialsProvider, err = datastore.NewCredentialsProvider(cmd.Context(), credentialsProviderName) 106 if err != nil { 107 return err 108 } 109 } 110 111 migrationDriver, err := mysqlmigrations.NewMySQLDriverFromDSN(dbURL, tablePrefix, credentialsProvider) 112 if err != nil { 113 return fmt.Errorf("unable to create migration driver for %s: %w", datastoreEngine, err) 114 } 115 return runMigration(cmd.Context(), migrationDriver, mysqlmigrations.Manager, args[0], timeout, migrationBatachSize) 116 } 117 118 return fmt.Errorf("cannot migrate datastore engine type: %s", datastoreEngine) 119 } 120 121 func runMigration[D migrate.Driver[C, T], C any, T any]( 122 ctx context.Context, 123 driver D, 124 manager *migrate.Manager[D, C, T], 125 targetRevision string, 126 timeout time.Duration, 127 backfillBatchSize uint64, 128 ) error { 129 log.Ctx(ctx).Info().Str("targetRevision", targetRevision).Msg("running migrations") 130 ctxWithBatch := context.WithValue(ctx, migrate.BackfillBatchSize, backfillBatchSize) 131 ctx, cancel := context.WithTimeout(ctxWithBatch, timeout) 132 defer cancel() 133 if err := manager.Run(ctx, driver, targetRevision, migrate.LiveRun); err != nil { 134 return fmt.Errorf("unable to migrate to `%s` revision: %w", targetRevision, err) 135 } 136 137 if err := driver.Close(ctx); err != nil { 138 return fmt.Errorf("unable to close migration driver: %w", err) 139 } 140 return nil 141 } 142 143 func RegisterHeadFlags(cmd *cobra.Command) { 144 cmd.Flags().String("datastore-engine", "postgres", fmt.Sprintf(`type of datastore to initialize (%s)`, datastore.EngineOptions())) 145 } 146 147 func NewHeadCommand(programName string) *cobra.Command { 148 return &cobra.Command{ 149 Use: "head", 150 Short: "compute the head database migration revision", 151 PreRunE: server.DefaultPreRunE(programName), 152 RunE: func(cmd *cobra.Command, args []string) error { 153 headRevision, err := HeadRevision(cobrautil.MustGetStringExpanded(cmd, "datastore-engine")) 154 if err != nil { 155 return fmt.Errorf("unable to compute head revision: %w", err) 156 } 157 fmt.Println(headRevision) 158 return nil 159 }, 160 Args: cobra.ExactArgs(0), 161 } 162 } 163 164 // HeadRevision returns the latest migration revision for a given engine 165 func HeadRevision(engine string) (string, error) { 166 switch engine { 167 case "cockroachdb": 168 return crdbmigrations.CRDBMigrations.HeadRevision() 169 case "postgres": 170 return migrations.DatabaseMigrations.HeadRevision() 171 case "mysql": 172 return mysqlmigrations.Manager.HeadRevision() 173 case "spanner": 174 return spannermigrations.SpannerMigrations.HeadRevision() 175 default: 176 return "", fmt.Errorf("cannot migrate datastore engine type: %s", engine) 177 } 178 }