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  }