diff --git a/internal/cmd/dataimports/cancel.go b/internal/cmd/dataimports/cancel.go deleted file mode 100644 index 8db4e2a8..00000000 --- a/internal/cmd/dataimports/cancel.go +++ /dev/null @@ -1,87 +0,0 @@ -package dataimports - -import ( - "fmt" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" - "github.com/spf13/cobra" -) - -func CancelDataImportCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - name string - force bool - } - - cancelRequest := &ps.CancelDataImportRequest{} - - cmd := &cobra.Command{ - Use: "cancel [database]", - Short: "cancel data import request", - Aliases: []string{"c"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - if flags.name == "" { - flags.name = args[0] - } - - cancelRequest.Organization = ch.Config.Organization - cancelRequest.Database = flags.name - - client, err := ch.Client() - if err != nil { - return err - } - - if !flags.force { - confirmationName := fmt.Sprintf("%s/%s", cancelRequest.Organization, cancelRequest.Database) - confirmError := ch.Printer.ConfirmCommand(confirmationName, "cancel import", "cancellation of import") - if confirmError != nil { - return confirmError - } - } - - getImportReq := &ps.GetImportStatusRequest{ - Organization: ch.Config.Organization, - Database: flags.name, - } - end := ch.Printer.PrintProgress(fmt.Sprintf("Getting current import status for PlanetScale database %s...", printer.BoldBlue(flags.name))) - defer end() - - dataImport, err := client.DataImports.GetDataImportStatus(ctx, getImportReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to cancel import into PlanetScale database %s", flags.name) - default: - return cmdutil.HandleError(err) - } - } - end() - - if dataImport.ImportState == ps.DataImportReady { - return fmt.Errorf("cannot cancel import into PlanetScale Database %s/%s because this import has completed", getImportReq.Organization, getImportReq.Database) - } - - end = ch.Printer.PrintProgress(fmt.Sprintf("Cancelling Data Import into PlanetScale database %s...", printer.BoldBlue(getImportReq.Organization+"/"+flags.name))) - defer end() - - err = client.DataImports.CancelDataImport(ctx, cancelRequest) - if err != nil { - return err - } - end() - ch.Printer.Printf("Data Import into PlanetScale database %v/%v has been cancelled", getImportReq.Organization, flags.name) - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.name, "name", "", "PlanetScale database importing data") - cmd.Flags().BoolVar(&flags.force, "force", false, "Cancel an import without confirmation") - cmd.MarkPersistentFlagRequired("name") - - return cmd -} diff --git a/internal/cmd/dataimports/cancel_test.go b/internal/cmd/dataimports/cancel_test.go deleted file mode 100644 index 29705941..00000000 --- a/internal/cmd/dataimports/cancel_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package dataimports - -import ( - "bytes" - "context" - "errors" - "fmt" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/planetscale/cli/internal/cmdutil" - "github.com/planetscale/cli/internal/config" - "github.com/planetscale/cli/internal/mock" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" -) - -func TestImports_Cancel_FailsIfNoImport(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return nil, errors.New("DataImport does not exist") - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeCancel := false - out, err := invokeCancel(org, db, c, shouldInvokeCancel, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, "DataImport does not exist") - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_Cancel_FailsIfImportCompleted(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportReady, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeCancel := false - out, err := invokeCancel(org, db, c, shouldInvokeCancel, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, "cannot cancel import into PlanetScale Database planetscale/employees because this import has completed") - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_Cancel_Success(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportSwitchTrafficPending, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - CancelDataImportFn: func(ctx context.Context, request *ps.CancelDataImportRequest) error { - return nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - "Cancelling Data Import into PlanetScale database planetscale/employees...\n", - "Data Import into PlanetScale database planetscale/employees has been cancelled", - } - shouldInvokeCancel := true - out, err := invokeCancel(org, db, c, shouldInvokeCancel, svc) - c.Assert(err, qt.IsNil) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func invokeCancel(org, dbName string, c *qt.C, shouldInvokeCancel bool, svc *mock.DataImportsService) (string, error) { - var buf bytes.Buffer - format := printer.Human - p := printer.NewPrinter(&format) - p.SetHumanOutput(&buf) - p.SetResourceOutput(&buf) - - ch := &cmdutil.Helper{ - Printer: p, - Config: &config.Config{ - Organization: org, - }, - Client: func() (*ps.Client, error) { - return &ps.Client{ - DataImports: svc, - }, nil - }, - } - - cmd := CancelDataImportCmd(ch) - - cmd.SetArgs([]string{ - "--name", dbName, - "--force", "true", - }) - cmd.SilenceUsage = true - err := cmd.Execute() - - c.Assert(svc.GetDataImportStatusFnInvoked, qt.IsTrue) - c.Assert(svc.CancelDataImportFnInvoked, qt.Equals, shouldInvokeCancel) - return buf.String(), err -} diff --git a/internal/cmd/dataimports/dataimports.go b/internal/cmd/dataimports/dataimports.go deleted file mode 100644 index 42b3691d..00000000 --- a/internal/cmd/dataimports/dataimports.go +++ /dev/null @@ -1,32 +0,0 @@ -package dataimports - -import ( - "github.com/planetscale/cli/internal/cmdutil" - "github.com/spf13/cobra" -) - -const deprecation = "import MySQL databases with \"pscale keyspace create-external\" and \"pscale branch vtctl move-tables\" instead. See https://planetscale.com/docs/vitess/imports/database-imports for more information." - -// DataImportsCmd handles data imports into PlanetScale. -func DataImportsCmd(ch *cmdutil.Helper) *cobra.Command { - cmd := &cobra.Command{ - Use: "data-imports ", - Short: "Create, list, and delete branch data imports", - Long: "Create, list, and delete branch data imports.\n\nThis command is only supported for Vitess databases.", - Deprecated: deprecation, - PersistentPreRunE: cmdutil.WarnDeprecated("data-imports", deprecation, cmdutil.CheckAuthentication(ch.Config)), - } - - cmd.PersistentFlags().StringVar(&ch.Config.Organization, "org", ch.Config.Organization, "The organization for the current user") - cmd.MarkPersistentFlagRequired("org") // nolint:errcheck - - cmd.AddCommand(LintExternalDataSourceCmd(ch)) - cmd.AddCommand(StartDataImportCmd(ch)) - cmd.AddCommand(DetachExternalDatabaseCmd(ch)) - cmd.AddCommand(MakePlanetScalePrimaryCmd(ch)) - cmd.AddCommand(MakePlanetScaleReplicaCmd(ch)) - cmd.AddCommand(GetDataImportCmd(ch)) - cmd.AddCommand(CancelDataImportCmd(ch)) - - return cmd -} diff --git a/internal/cmd/dataimports/detach.go b/internal/cmd/dataimports/detach.go deleted file mode 100644 index 77b2902e..00000000 --- a/internal/cmd/dataimports/detach.go +++ /dev/null @@ -1,81 +0,0 @@ -package dataimports - -import ( - "fmt" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/spf13/cobra" -) - -func DetachExternalDatabaseCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - name string - force bool - } - - detachExternalDatabaseReq := &ps.DetachExternalDatabaseRequest{} - - cmd := &cobra.Command{ - Use: "detach-external-database [options]", - Short: "detach external database that is used as a source for PlanetScale database", - Aliases: []string{"ded"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - detachExternalDatabaseReq.Organization = ch.Config.Organization - detachExternalDatabaseReq.Database = flags.name - - client, err := ch.Client() - if err != nil { - return err - } - - if !flags.force { - confirmationName := fmt.Sprintf("%s/%s", detachExternalDatabaseReq.Organization, detachExternalDatabaseReq.Database) - confirmError := ch.Printer.ConfirmCommand(confirmationName, "detach external database", "detaching external database") - if confirmError != nil { - return confirmError - } - } - - getImportReq := &ps.GetImportStatusRequest{ - Organization: ch.Config.Organization, - Database: flags.name, - } - - dataImport, err := client.DataImports.GetDataImportStatus(ctx, getImportReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to switch PlanetScale database %s to Primary", flags.name) - default: - return cmdutil.HandleError(err) - } - } - - if dataImport.ImportState != ps.DataImportSwitchTrafficCompleted { - return fmt.Errorf("cannot detach external database %s at %s because PlanetScale is not serving as a Primary", getImportReq.Organization, getImportReq.Database) - } - - dataImport, err = client.DataImports.DetachExternalDatabase(ctx, detachExternalDatabaseReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to detach external database for PlanetScale database %s", flags.name) - default: - return cmdutil.HandleError(err) - } - } - - PrintDataImport(ch.Printer, *dataImport) - - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.name, "name", "", "PlanetScale database importing data") - cmd.Flags().BoolVar(&flags.force, "force", false, "Make PlanetScale database Replica without confirmation") - cmd.MarkPersistentFlagRequired("name") - return cmd -} diff --git a/internal/cmd/dataimports/get.go b/internal/cmd/dataimports/get.go deleted file mode 100644 index 74625a72..00000000 --- a/internal/cmd/dataimports/get.go +++ /dev/null @@ -1,66 +0,0 @@ -package dataimports - -import ( - "fmt" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" - "github.com/spf13/cobra" -) - -func GetDataImportCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - name string - } - - getRequest := &ps.GetImportStatusRequest{} - - cmd := &cobra.Command{ - Use: "get [database]", - Short: "get the current state of a data import request into a PlanetScale database", - Aliases: []string{"g"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - getRequest.Organization = ch.Config.Organization - getRequest.Database = flags.name - - client, err := ch.Client() - if err != nil { - return err - } - - resp, err := client.DataImports.GetDataImportStatus(ctx, getRequest) - if err != nil { - return err - } - - PrintDataImport(ch.Printer, *resp) - - if resp.ImportState == ps.DataImportPreparingDataCopyFailed || - resp.ImportState == ps.DataImportCopyingDataFailed || - resp.ImportState == ps.DataImportSwitchTrafficError || - resp.ImportState == ps.DataImportReverseTrafficError || - resp.ImportState == ps.DataImportDetachExternalDatabaseError { - return fmt.Errorf("import from external database into PlanetScale failed with \n %s \n current state is %s", printer.BoldRed(resp.Errors), resp.ImportState) - } - - if resp.ImportState == ps.DataImportSwitchTrafficPending { - ch.Printer.Printf("all data and schema has been copied from the external database and your PlanetScale database %s is running in replica mode\n", printer.BoldGreen(flags.name)) - ch.Printer.Printf("you should now be able to switch your PlanetScale database %s into Primary mode using the \"make-primary\" command \n", printer.BoldGreen(flags.name)) - } - - if resp.ImportState == ps.DataImportSwitchTrafficCompleted { - ch.Printer.Printf("Your PlanetScale database %s is now running as a primary \n", printer.BoldGreen(flags.name)) - ch.Printer.Printf("if necessary, you can use the \"make-replica\" command to switch back to replica mode\n") - } - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.name, "name", "", "PlanetScale database importing data") - cmd.MarkPersistentFlagRequired("name") - - return cmd -} diff --git a/internal/cmd/dataimports/lint.go b/internal/cmd/dataimports/lint.go deleted file mode 100644 index 902ab3f0..00000000 --- a/internal/cmd/dataimports/lint.go +++ /dev/null @@ -1,113 +0,0 @@ -package dataimports - -import ( - "errors" - "fmt" - "strings" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" - "github.com/spf13/cobra" -) - -func LintExternalDataSourceCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - host string - username string - password string - database string - port int - sslMode string - sslCA string - sslKey string - sslCertificate string - sslServerName string - } - - testRequest := &ps.TestDataImportSourceRequest{} - - cmd := &cobra.Command{ - Use: "lint [options]", - Short: "lint external database for compatibility with PlanetScale", - Aliases: []string{"l"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - sslMode := cmdutil.ParseSSLMode(flags.sslMode) - testRequest.Organization = ch.Config.Organization - testRequest.Database = flags.database - testRequest.Connection = ps.DataImportSource{ - Database: flags.database, - UserName: flags.username, - Password: flags.password, - HostName: flags.host, - Port: flags.port, - SSLVerificationMode: sslMode, - SSLKey: flags.sslKey, - SSLCertificate: flags.sslCertificate, - SSLCA: flags.sslCA, - SSLServerName: flags.sslServerName, - } - - client, err := ch.Client() - if err != nil { - return err - } - - end := ch.Printer.PrintProgress(fmt.Sprintf("Testing Compatibility of database %s with user %s...", printer.BoldBlue(flags.database), printer.BoldBlue(flags.username))) - defer end() - - resp, err := client.DataImports.TestDataImportSource(ctx, testRequest) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to check compatibility of database %s, hosted at %s", flags.database, flags.host) - default: - return cmdutil.HandleError(err) - } - } - - if !resp.CanConnect && len(resp.ConnectError) > 0 { - return errors.New(resp.ConnectError) - } - - if len(resp.Errors) > 0 { - var sb strings.Builder - sb.WriteString(printer.Red("External database compatibility check failed.\n")) - sb.WriteString("Please fix the following errors and then try again:\n") - - for idx, compatError := range resp.Errors { - fmt.Fprintf(&sb, "%v. %s\n", (idx + 1), compatError.ErrorDescription) - } - - return errors.New(sb.String()) - } - end() - - ch.Printer.Printf("Database %s hosted at %s is compatible and can be imported into PlanetScale!\n", flags.database, flags.host) - if resp.SuggestedBillingPlan == ps.ScalerProPlan { - ch.Printer.Println("\nIf you choose to continue, the imported database will be on Scaler Pro with a PS-10.") - } - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.host, "host", "", "Host name of the external database.") - cmd.PersistentFlags().StringVar(&flags.database, "database", "", "Name of the external database") - cmd.PersistentFlags().StringVar(&flags.username, "username", "", "Username to connect to external database.") - cmd.PersistentFlags().StringVar(&flags.password, "password", "", "Password to connect to external database.") - cmd.PersistentFlags().StringVar(&flags.sslMode, "ssl-mode", "", "SSL verification mode, allowed values: disabled, preferred, required, verify_ca, verify_identity") - cmd.PersistentFlags().StringVar(&flags.sslServerName, "ssl-server-name", "", "SSL server name override") - cmd.PersistentFlags().StringVar(&flags.sslCA, "ssl-certificate-authority", "", "Provide the full CA certificate chain here") - cmd.PersistentFlags().StringVar(&flags.sslKey, "ssl-client-key", "", "Private key for the client certificate") - cmd.PersistentFlags().StringVar(&flags.sslCertificate, "ssl-client-certificate", "", "Client Certificate to authenticate PlanetScale with your database server") - cmd.PersistentFlags().IntVar(&flags.port, "port", 3306, "Port number to connect to external database") - - cmd.MarkPersistentFlagRequired("host") - cmd.MarkPersistentFlagRequired("database") - cmd.MarkPersistentFlagRequired("username") - cmd.MarkPersistentFlagRequired("password") - cmd.MarkPersistentFlagRequired("ssl-mode") - - return cmd -} diff --git a/internal/cmd/dataimports/lint_test.go b/internal/cmd/dataimports/lint_test.go deleted file mode 100644 index f0192bbf..00000000 --- a/internal/cmd/dataimports/lint_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package dataimports - -import ( - "bytes" - "context" - "fmt" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/planetscale/cli/internal/cmdutil" - "github.com/planetscale/cli/internal/config" - "github.com/planetscale/cli/internal/mock" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" -) - -func TestImports_LintDatabase_Success(t *testing.T) { - c := qt.New(t) - - org := "planetscale" - externalDataSource := ps.DataImportSource{ - Database: "aws-upstream-database", - Port: 3306, - HostName: "rds.amazonaws.com", - UserName: "aws-user", - Password: "aws-password", - SSLVerificationMode: ps.SSLModePreferred, - SSLMode: ps.SSLModePreferred.String(), - } - res := &ps.TestDataImportSourceResponse{ - CanConnect: true, - } - - out, err := invokeLintDatabase(&externalDataSource, org, c, res) - c.Assert(err, qt.IsNil) - expectedOut := []string{ - fmt.Sprintf("Testing Compatibility of database %s with user %s...\n", externalDataSource.Database, externalDataSource.UserName), - fmt.Sprintf("Database %s hosted at %s is compatible and can be imported into PlanetScale!\n", externalDataSource.Database, externalDataSource.HostName), - } - - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_LintDatabase_CannotConnect(t *testing.T) { - c := qt.New(t) - - org := "planetscale" - externalDataSource := ps.DataImportSource{ - Database: "aws-upstream-database", - Port: 3306, - HostName: "rds.amazonaws.com", - UserName: "aws-user", - Password: "aws-password", - SSLVerificationMode: ps.SSLModePreferred, - SSLMode: ps.SSLModePreferred.String(), - } - - res := &ps.TestDataImportSourceResponse{ - CanConnect: false, - ConnectError: "AWS RDS is down", - } - - out, err := invokeLintDatabase(&externalDataSource, org, c, res) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, res.ConnectError) - expectedOut := []string{ - fmt.Sprintf("Testing Compatibility of database %s with user %s...\n", externalDataSource.Database, externalDataSource.UserName), - } - - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_LintDatabase_SchemaIncompatible(t *testing.T) { - c := qt.New(t) - - org := "planetscale" - externalDataSource := ps.DataImportSource{ - Database: "aws-upstream-database", - Port: 3306, - HostName: "rds.amazonaws.com", - UserName: "aws-user", - Password: "aws-password", - SSLVerificationMode: ps.SSLModePreferred, - SSLMode: ps.SSLModePreferred.String(), - } - - res := &ps.TestDataImportSourceResponse{ - CanConnect: true, - Errors: []*ps.DataSourceIncompatibilityError{ - { - LintError: "NO_PRIMARY_KEY", - ErrorDescription: "Table \"employees\" has no primary key", - }, - { - LintError: "NO_PRIMARY_KEY", - ErrorDescription: "Table \"departments\" has no primary key", - }, - }, - } - - out, err := invokeLintDatabase(&externalDataSource, org, c, res) - c.Assert(err, qt.IsNotNil) - expectedError := `External database compatibility check failed. -Please fix the following errors and then try again: -1. Table "employees" has no primary key -2. Table "departments" has no primary key -` - c.Assert(err, qt.ErrorMatches, expectedError) - expectedOut := []string{ - fmt.Sprintf("Testing Compatibility of database %s with user %s...\n", externalDataSource.Database, externalDataSource.UserName), - } - - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func invokeLintDatabase(externalDataSource *ps.DataImportSource, org string, c *qt.C, response *ps.TestDataImportSourceResponse) (string, error) { - svc := &mock.DataImportsService{ - TestDataImportSourceFn: func(ctx context.Context, req *ps.TestDataImportSourceRequest) (*ps.TestDataImportSourceResponse, error) { - req.Connection.SSLMode = req.Connection.SSLVerificationMode.String() - c.Assert(req.Organization, qt.Equals, org) - c.Assert(req.Connection, qt.Equals, *externalDataSource) - return response, nil - }, - } - - var buf bytes.Buffer - format := printer.Human - p := printer.NewPrinter(&format) - p.SetHumanOutput(&buf) - p.SetResourceOutput(&buf) - - ch := &cmdutil.Helper{ - Printer: p, - Config: &config.Config{ - Organization: org, - }, - Client: func() (*ps.Client, error) { - return &ps.Client{ - DataImports: svc, - }, nil - }, - } - - cmd := LintExternalDataSourceCmd(ch) - - cmd.SetArgs([]string{ - "--database", externalDataSource.Database, - "--host", externalDataSource.HostName, - "--username", externalDataSource.UserName, - "--password", externalDataSource.Password, - "--ssl-mode", "preferred", - }) - cmd.SilenceUsage = true - err := cmd.Execute() - - c.Assert(svc.TestDataImportSourceFnInvoked, qt.IsTrue) - return buf.String(), err -} diff --git a/internal/cmd/dataimports/makeplanetscaleprimary.go b/internal/cmd/dataimports/makeplanetscaleprimary.go deleted file mode 100644 index 6f514d06..00000000 --- a/internal/cmd/dataimports/makeplanetscaleprimary.go +++ /dev/null @@ -1,99 +0,0 @@ -package dataimports - -import ( - "fmt" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" - "github.com/spf13/cobra" -) - -func MakePlanetScalePrimaryCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - name string - force bool - } - - makePrimaryReq := &ps.MakePlanetScalePrimaryRequest{} - - cmd := &cobra.Command{ - Use: "make-primary [options]", - Short: "mark PlanetScale's database as the Primary, and the external database as Replica", - Aliases: []string{"mp"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - makePrimaryReq.Organization = ch.Config.Organization - makePrimaryReq.Database = flags.name - - client, err := ch.Client() - if err != nil { - return err - } - - if !flags.force { - confirmationName := fmt.Sprintf("%s/%s", makePrimaryReq.Organization, makePrimaryReq.Database) - confirmError := ch.Printer.ConfirmCommand(confirmationName, "make primary", "promotion to primary") - if confirmError != nil { - return confirmError - } - } - - end := ch.Printer.PrintProgress(fmt.Sprintf("Getting current import status for PlanetScale database %s...", printer.BoldBlue(flags.name))) - defer end() - getImportReq := &ps.GetImportStatusRequest{ - Organization: ch.Config.Organization, - Database: flags.name, - } - - dataImport, err := client.DataImports.GetDataImportStatus(ctx, getImportReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("PlanetScale database %s is not importing data", flags.name) - default: - return cmdutil.HandleError(err) - } - } - - if dataImport.ImportState != ps.DataImportSwitchTrafficPending { - reason := "it is already switched to Primary" - switch dataImport.ImportState { - case ps.DataImportCopyingData, ps.DataImportPreparingDataCopy: - reason = "we are still copying data from upstream database" - case ps.DataImportCopyingDataFailed, ps.DataImportPreparingDataCopyFailed: - reason = "we are unable to copy data from upstream database" - case ps.DataImportReady: - reason = "this import has completed" - } - return fmt.Errorf("cannot make PlanetScale Database %s/%s Primary because %s", getImportReq.Organization, getImportReq.Database, reason) - } - end() - end = ch.Printer.PrintProgress(fmt.Sprintf("Switching PlanetScale database %s to Primary...", printer.BoldBlue(flags.name))) - defer end() - - dataImport, err = client.DataImports.MakePlanetScalePrimary(ctx, makePrimaryReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to switch PlanetScale database %s to Primary", flags.name) - default: - return cmdutil.HandleError(err) - } - } - end() - - ch.Printer.Printf("Successfully switch PlanetScale database %s to Primary.\n", printer.BoldBlue(flags.name)) - PrintDataImport(ch.Printer, *dataImport) - - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.name, "name", "", "PlanetScale database importing data") - cmd.Flags().BoolVar(&flags.force, "force", false, "Make PlanetScale database Primary without confirmation") - cmd.MarkPersistentFlagRequired("name") - - return cmd -} diff --git a/internal/cmd/dataimports/makeplanetscaleprimary_test.go b/internal/cmd/dataimports/makeplanetscaleprimary_test.go deleted file mode 100644 index 731742ee..00000000 --- a/internal/cmd/dataimports/makeplanetscaleprimary_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package dataimports - -import ( - "bytes" - "context" - "errors" - "fmt" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/planetscale/cli/internal/cmdutil" - "github.com/planetscale/cli/internal/config" - "github.com/planetscale/cli/internal/mock" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" -) - -func TestImports_MakePrimary_FailsIfNoImport(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return nil, errors.New("DataImport does not exist") - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakePrimary := false - out, err := invokeMakePrimary(org, db, c, shouldInvokeMakePrimary, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, "DataImport does not exist") - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakePrimary_FailsIfDataCopyInComplete(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportCopyingData, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakePrimary := false - out, err := invokeMakePrimary(org, db, c, shouldInvokeMakePrimary, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, fmt.Sprintf("cannot make PlanetScale Database %s/%s Primary because we are still copying data from upstream database", org, db)) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakePrimary_FailsIfAlreadyPrimary(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportSwitchTrafficCompleted, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakePrimary := false - out, err := invokeMakePrimary(org, db, c, shouldInvokeMakePrimary, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, fmt.Sprintf("cannot make PlanetScale Database %s/%s Primary because it is already switched to Primary", org, db)) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakePrimary_FailsIfComplete(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportReady, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakePrimary := false - out, err := invokeMakePrimary(org, db, c, shouldInvokeMakePrimary, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, fmt.Sprintf("cannot make PlanetScale Database %s/%s Primary because this import has completed", org, db)) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakePrimary_Success(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportSwitchTrafficPending, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - MakePlanetScalePrimaryFn: func(ctx context.Context, request *ps.MakePlanetScalePrimaryRequest) (*ps.DataImport, error) { - di.ImportState = ps.DataImportSwitchTrafficCompleted - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - "Switching PlanetScale database employees to Primary...\n", - "Successfully switch PlanetScale database employees to Primary.\n", - "1. Started Data Copy\n", - "2. Copied Data\n", - "3. Running as Replica\n", - "> 4. Running as Primary\n", - "5. Detached external database\n", - } - shouldInvokeMakePrimary := true - out, err := invokeMakePrimary(org, db, c, shouldInvokeMakePrimary, svc) - c.Assert(err, qt.IsNil) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func invokeMakePrimary(org, dbName string, c *qt.C, shouldInvokeMakePrimary bool, svc *mock.DataImportsService) (string, error) { - var buf bytes.Buffer - format := printer.Human - p := printer.NewPrinter(&format) - p.SetHumanOutput(&buf) - p.SetResourceOutput(&buf) - - ch := &cmdutil.Helper{ - Printer: p, - Config: &config.Config{ - Organization: org, - }, - Client: func() (*ps.Client, error) { - return &ps.Client{ - DataImports: svc, - }, nil - }, - } - - cmd := MakePlanetScalePrimaryCmd(ch) - - cmd.SetArgs([]string{ - "--name", dbName, - "--force", "true", - }) - cmd.SilenceUsage = true - err := cmd.Execute() - - c.Assert(svc.GetDataImportStatusFnInvoked, qt.IsTrue) - c.Assert(svc.MakePlanetScalePrimaryFnInvoked, qt.Equals, shouldInvokeMakePrimary) - return buf.String(), err -} diff --git a/internal/cmd/dataimports/makeplanetscalereplica.go b/internal/cmd/dataimports/makeplanetscalereplica.go deleted file mode 100644 index e56b5c17..00000000 --- a/internal/cmd/dataimports/makeplanetscalereplica.go +++ /dev/null @@ -1,91 +0,0 @@ -package dataimports - -import ( - "fmt" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" - "github.com/spf13/cobra" -) - -func MakePlanetScaleReplicaCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - name string - force bool - } - - makeReplicaReq := &ps.MakePlanetScaleReplicaRequest{} - - cmd := &cobra.Command{ - Use: "make-replica [options]", - Short: "mark PlanetScale's database as the Replica, and the external database as Primary", - Aliases: []string{"mr"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - makeReplicaReq.Organization = ch.Config.Organization - makeReplicaReq.Database = flags.name - - client, err := ch.Client() - if err != nil { - return err - } - - if !flags.force { - confirmationName := fmt.Sprintf("%s/%s", makeReplicaReq.Organization, makeReplicaReq.Database) - confirmError := ch.Printer.ConfirmCommand(confirmationName, "make replica", "demotion to replica") - if confirmError != nil { - return confirmError - } - } - - getImportReq := &ps.GetImportStatusRequest{ - Organization: ch.Config.Organization, - Database: flags.name, - } - end := ch.Printer.PrintProgress(fmt.Sprintf("Getting current import status for PlanetScale database %s...", printer.BoldBlue(flags.name))) - defer end() - dataImport, err := client.DataImports.GetDataImportStatus(ctx, getImportReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to switch PlanetScale database %s to Primary", flags.name) - default: - return cmdutil.HandleError(err) - } - } - end() - - if dataImport.ImportState == ps.DataImportReady { - return fmt.Errorf("cannot make PlanetScale Database %s/%s Replica because this import has completed", getImportReq.Organization, getImportReq.Database) - } - - if dataImport.ImportState != ps.DataImportSwitchTrafficCompleted { - return fmt.Errorf("cannot make PlanetScale Database %s/%s Replica because it is not serving as a Primary", getImportReq.Organization, getImportReq.Database) - } - - end = ch.Printer.PrintProgress(fmt.Sprintf("Switching PlanetScale database %s to Primary...", printer.BoldBlue(flags.name))) - defer end() - dataImport, err = client.DataImports.MakePlanetScaleReplica(ctx, makeReplicaReq) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to switch PlanetScale database %s to Replica", flags.name) - default: - return cmdutil.HandleError(err) - } - } - end() - ch.Printer.Printf("Successfully switch PlanetScale database %s to Replica.\n", printer.BoldBlue(flags.name)) - PrintDataImport(ch.Printer, *dataImport) - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.name, "name", "", "PlanetScale database importing data") - cmd.Flags().BoolVar(&flags.force, "force", false, "Make PlanetScale database Replica without confirmation") - cmd.MarkPersistentFlagRequired("name") - - return cmd -} diff --git a/internal/cmd/dataimports/makeplanetscalereplica_test.go b/internal/cmd/dataimports/makeplanetscalereplica_test.go deleted file mode 100644 index 20e3db0a..00000000 --- a/internal/cmd/dataimports/makeplanetscalereplica_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package dataimports - -import ( - "bytes" - "context" - "errors" - "fmt" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/planetscale/cli/internal/cmdutil" - "github.com/planetscale/cli/internal/config" - "github.com/planetscale/cli/internal/mock" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" -) - -func TestImports_MakeReplica_FailsIfNoImport(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return nil, errors.New("DataImport does not exist") - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakeReplica := false - out, err := invokeMakeReplica(org, db, c, shouldInvokeMakeReplica, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, "DataImport does not exist") - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakeReplica_FailsIfNotPrimary(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportCopyingData, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakeReplica := false - out, err := invokeMakeReplica(org, db, c, shouldInvokeMakeReplica, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, fmt.Sprintf("cannot make PlanetScale Database %s/%s Replica because it is not serving as a Primary", org, db)) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakeReplica_FailsIfAlreadyReplica(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportSwitchTrafficPending, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakeReplica := false - out, err := invokeMakeReplica(org, db, c, shouldInvokeMakeReplica, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, fmt.Sprintf("cannot make PlanetScale Database %s/%s Replica because it is not serving as a Primary", org, db)) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakeReplica_FailsIfComplete(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportReady, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - } - shouldInvokeMakeReplica := false - out, err := invokeMakeReplica(org, db, c, shouldInvokeMakeReplica, svc) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, fmt.Sprintf("cannot make PlanetScale Database %s/%s Replica because this import has completed", org, db)) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestImports_MakeReplica_Success(t *testing.T) { - c := qt.New(t) - org := "planetscale" - db := "employees" - di := &ps.DataImport{ - ImportState: ps.DataImportSwitchTrafficCompleted, - } - svc := &mock.DataImportsService{ - GetDataImportStatusFn: func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - return di, nil - }, - MakePlanetScaleReplicaFn: func(ctx context.Context, request *ps.MakePlanetScaleReplicaRequest) (*ps.DataImport, error) { - di.ImportState = ps.DataImportSwitchTrafficPending - return di, nil - }, - } - expectedOut := []string{ - fmt.Sprintf("Getting current import status for PlanetScale database %s...\n", db), - "Switching PlanetScale database employees to Primary...\n", - "Successfully switch PlanetScale database employees to Replica.\n", - "1. Started Data Copy\n", - "2. Copied Data\n", - "> 3. Running as Replica\n", - "4. Running as Primary\n", - "5. Detached external database\n", - } - shouldInvokeMakeReplica := true - out, err := invokeMakeReplica(org, db, c, shouldInvokeMakeReplica, svc) - c.Assert(err, qt.IsNil) - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func invokeMakeReplica(org, dbName string, c *qt.C, shouldInvokeMakeReplica bool, svc *mock.DataImportsService) (string, error) { - var buf bytes.Buffer - format := printer.Human - p := printer.NewPrinter(&format) - p.SetHumanOutput(&buf) - p.SetResourceOutput(&buf) - - ch := &cmdutil.Helper{ - Printer: p, - Config: &config.Config{ - Organization: org, - }, - Client: func() (*ps.Client, error) { - return &ps.Client{ - DataImports: svc, - }, nil - }, - } - - cmd := MakePlanetScaleReplicaCmd(ch) - - cmd.SetArgs([]string{ - "--name", dbName, - "--force", "true", - }) - cmd.SilenceUsage = true - err := cmd.Execute() - - c.Assert(svc.GetDataImportStatusFnInvoked, qt.IsTrue) - c.Assert(svc.MakePlanetScaleReplicaFnInvoked, qt.Equals, shouldInvokeMakeReplica) - return buf.String(), err -} diff --git a/internal/cmd/dataimports/printer.go b/internal/cmd/dataimports/printer.go deleted file mode 100644 index c4f036f1..00000000 --- a/internal/cmd/dataimports/printer.go +++ /dev/null @@ -1,124 +0,0 @@ -package dataimports - -import ( - "strings" - - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" -) - -func PrintDataImport(p *printer.Printer, di ps.DataImport) { - completedSteps := GetCompletedImportStates(p, di.ImportState) - if len(completedSteps) > 0 { - p.Println(completedSteps) - } - - inProgressStep, _ := GetCurrentImportState(di.ImportState) - if len(inProgressStep) > 0 { - p.Println(inProgressStep) - } - - pendingSteps := GetPendingImportStates(di.ImportState) - if len(pendingSteps) > 0 { - p.Println(pendingSteps) - } -} - -func GetCompletedImportStates(p *printer.Printer, state ps.DataImportState) string { - completedStates := []string{} - switch state { - case ps.DataImportCopyingData: - completedStates = append(completedStates, printer.BoldGreen("1. Started Data Copy")) - case ps.DataImportSwitchTrafficPending, ps.DataImportSwitchTrafficError: - completedStates = append(completedStates, printer.BoldGreen("1. Started Data Copy")) - completedStates = append(completedStates, printer.BoldGreen("2. Copied Data")) - case ps.DataImportSwitchTrafficCompleted: - completedStates = append(completedStates, printer.BoldGreen("1. Started Data Copy")) - completedStates = append(completedStates, printer.BoldGreen("2. Copied Data")) - completedStates = append(completedStates, printer.BoldGreen("3. Running as Replica")) - - case ps.DataImportReady: - completedStates = append(completedStates, printer.BoldGreen("1. Started Data Copy")) - completedStates = append(completedStates, printer.BoldGreen("2. Copied Data")) - completedStates = append(completedStates, printer.BoldGreen("3. Running as replica")) - completedStates = append(completedStates, printer.BoldGreen("4. Running as Primary")) - } - return strings.Join(completedStates, "\n") -} - -func GetCurrentImportState(d ps.DataImportState) (string, bool) { - switch d { - case ps.DataImportPreparingDataCopy: - return printer.BoldYellow("> 1. Starting Data Copy"), true - case ps.DataImportPreparingDataCopyFailed: - return printer.BoldRed("> 1. Cannot Start Data Copy"), false - case ps.DataImportCopyingData: - return printer.BoldYellow("> 2. Copying Data"), true - case ps.DataImportCopyingDataFailed: - return printer.BoldRed("> 2. Failed to Copy Data"), false - case ps.DataImportSwitchTrafficPending: - return printer.BoldYellow("> 3. Running as Replica"), true - case ps.DataImportSwitchTrafficRunning: - return printer.BoldYellow("> 3. Switching to Primary"), true - case ps.DataImportSwitchTrafficError: - return printer.BoldRed("> 3. Failed switching to Primary"), false - case ps.DataImportReverseTrafficRunning: - return printer.BoldYellow("3. Switching to replica"), true - case ps.DataImportSwitchTrafficCompleted: - return printer.BoldYellow("> 4. Running as Primary"), true - case ps.DataImportReverseTrafficError: - return printer.BoldRed("> 3. Failed switching to Primary"), false - case ps.DataImportDetachExternalDatabaseRunning: - return printer.BoldYellow("> 4. detaching external database"), true - case ps.DataImportDetachExternalDatabaseError: - return printer.BoldRed("> 4. failed to detach external database"), false - case ps.DataImportReady: - return printer.BoldGreen("> 5. Ready"), false - } - - panic("unhandled state " + d.String()) -} - -func GetPendingImportStates(state ps.DataImportState) string { - var pendingStates []string - switch state { - case ps.DataImportPreparingDataCopy: - pendingStates = append(pendingStates, printer.BoldBlack("2. Copied Data")) - pendingStates = append(pendingStates, printer.BoldBlack("3. Running as Replica")) - pendingStates = append(pendingStates, printer.BoldBlack("4. Running as Primary")) - pendingStates = append(pendingStates, printer.BoldBlack("5. Detached external database")) - case ps.DataImportCopyingData: - pendingStates = append(pendingStates, printer.BoldBlack("3. Running as Replica")) - pendingStates = append(pendingStates, printer.BoldBlack("4. Running as Primary")) - pendingStates = append(pendingStates, printer.BoldBlack("5. Detached external database")) - case ps.DataImportSwitchTrafficPending: - pendingStates = append(pendingStates, printer.BoldBlack("4. Running as Primary")) - pendingStates = append(pendingStates, printer.BoldBlack("5. Detached external database")) - case ps.DataImportSwitchTrafficCompleted: - pendingStates = append(pendingStates, printer.BoldBlack("5. Detached external database")) - } - return strings.Join(pendingStates, "\n") -} - -// -//func ImportProgress(state ps.DataImportState) string { -// preparingDataCopyState := color.New(color.FgBlack).Add(color.Bold).Sprint("1. Started Data Copy") -// dataCopyState := color.New(color.FgBlack).Add(color.Bold).Sprint("2. Copied Data") -// switchingTrafficState := color.New(color.FgBlack).Add(color.Bold).Sprint("3. Running as Replica") -// detachExternalDatabaseState := color.New(color.FgBlack).Add(color.Bold).Sprint("4. Running as Primary") -// readyState := color.New(color.FgBlack).Add(color.Bold).Sprint("5. Detached External Database, ready to use") -// // Preparing data copy > Data copying > Running as Replica > Running as Primary > Detach External database -// switch state { -// case ps.DataImportPreparingDataCopy: -// preparingDataCopyState = color.New(color.FgYellow).Add(color.Bold).Sprint(state.String()) -// case ps.DataImportSwitchTrafficPending: -// preparingDataCopyState = color.New(color.FgGreen).Add(color.Bold).Sprint("1. Started Data Copy") -// dataCopyState = color.New(color.FgGreen).Add(color.Bold).Sprint("2. Copied Data") -// switchingTrafficState = color.New(color.FgYellow).Add(color.Bold).Sprint("3. Running as Replica") -// case ps.DataImportPreparingDataCopyFailed: -// preparingDataCopyState = color.New(color.FgGreen).Add(color.Bold).Sprint("1. Cannot start data copy") -// } -// -// return strings.Join([]string{preparingDataCopyState, dataCopyState, switchingTrafficState, detachExternalDatabaseState, readyState}, "\n") -// -//} diff --git a/internal/cmd/dataimports/start.go b/internal/cmd/dataimports/start.go deleted file mode 100644 index 8777cd9b..00000000 --- a/internal/cmd/dataimports/start.go +++ /dev/null @@ -1,151 +0,0 @@ -package dataimports - -import ( - "errors" - "fmt" - "strings" - - "github.com/planetscale/cli/internal/cmdutil" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" - "github.com/spf13/cobra" -) - -func StartDataImportCmd(ch *cmdutil.Helper) *cobra.Command { - var flags struct { - name string - host string - region string - username string - password string - database string - port int - dryRun bool - sslMode string - sslCA string - sslKey string - sslCertificate string - sslServerName string - } - - startImportRequest := &ps.StartDataImportRequest{} - testRequest := &ps.TestDataImportSourceRequest{} - cmd := &cobra.Command{ - Use: "start [options]", - Short: "start importing data from an external database", - Aliases: []string{"s"}, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - sslMode := cmdutil.ParseSSLMode(flags.sslMode) - - dataSource := ps.DataImportSource{ - Database: flags.database, - UserName: flags.username, - Password: flags.password, - HostName: flags.host, - Port: flags.port, - SSLVerificationMode: sslMode, - SSLKey: flags.sslKey, - SSLCertificate: flags.sslCertificate, - SSLCA: flags.sslCA, - SSLServerName: flags.sslServerName, - } - startImportRequest.Organization = ch.Config.Organization - startImportRequest.Database = flags.name - startImportRequest.Connection = dataSource - startImportRequest.Region = flags.region - - testRequest.Organization = ch.Config.Organization - testRequest.Database = flags.database - testRequest.Connection = dataSource - - client, err := ch.Client() - if err != nil { - return err - } - ch.Printer.Println(fmt.Sprintf("Testing Compatibility of database %s with user %s...", printer.BoldBlue(flags.database), printer.BoldYellow(flags.username))) - - resp, err := client.DataImports.TestDataImportSource(ctx, testRequest) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to check compatibility of database %s, hosted at %s", flags.database, flags.host) - default: - return cmdutil.HandleError(err) - } - } - - if !resp.CanConnect && len(resp.ConnectError) > 0 { - return errors.New(resp.ConnectError) - } - - if len(resp.Errors) > 0 { - var sb strings.Builder - sb.WriteString(printer.Red("External database compatibility check failed. ")) - sb.WriteString("Fix the following errors and then try again:\n\n") - for _, compatError := range resp.Errors { - fmt.Fprintf(&sb, "• %s\n", compatError.ErrorDescription) - } - - return errors.New(sb.String()) - } - ch.Printer.Printf("Database %s is compatible and can be imported into PlanetScale database %s\n", printer.BoldBlue(flags.database), printer.BoldGreen(flags.name)) - if resp.SuggestedBillingPlan == ps.ScalerProPlan { - ch.Printer.Println("If you choose to continue, the imported database will be on Scaler Pro with a PS-10.") - } - if flags.dryRun { - ch.Printer.Println("Please run this command with --dry-run=false to start the import") - return nil - } - if resp.SuggestedBillingPlan == ps.ScalerProPlan { - confirmationName := "start" - confirmError := ch.Printer.ConfirmCommand(confirmationName, "import", "import into PlanetScale") - if confirmError != nil { - return confirmError - } - } - - startImportRequest.Plan = resp.SuggestedPlan - startImportRequest.MaxPoolSize = resp.MaxPoolSize - - ch.Printer.Println(fmt.Sprintf("starting import of schema and data from external database %s to PlanetScale database %s", printer.BoldBlue(flags.database), printer.BoldGreen(flags.name))) - dataImport, err := client.DataImports.StartDataImport(ctx, startImportRequest) - if err != nil { - switch cmdutil.ErrCode(err) { - case ps.ErrNotFound: - return fmt.Errorf("unable to check compatibility of database %s, hosted at %s", flags.database, flags.host) - default: - return cmdutil.HandleError(err) - } - } - - ch.Printer.Printf("database %s hosted at %s is being imported into PlanetScale database %s\n", flags.database, flags.host, flags.name) - PrintDataImport(ch.Printer, *dataImport) - - return nil - }, - } - - cmd.PersistentFlags().StringVar(&flags.name, "name", "", "Name of the PlanetScale database you are importing into.") - cmd.PersistentFlags().StringVar(&flags.region, "region", "", "Region of the PlanetScale database.") - cmd.PersistentFlags().StringVar(&flags.host, "host", "", "Host name of the external database.") - cmd.PersistentFlags().StringVar(&flags.database, "database", "", "Name of the external database") - cmd.PersistentFlags().StringVar(&flags.username, "username", "", "Username to connect to external database.") - cmd.PersistentFlags().StringVar(&flags.password, "password", "", "Password to connect to external database.") - cmd.PersistentFlags().IntVar(&flags.port, "port", 3306, "Port number to connect to external database") - cmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", true, "Only run compatibility check, do not start import") - cmd.PersistentFlags().StringVar(&flags.sslMode, "ssl-mode", "", "SSL verification mode, allowed values: disabled, preferred, required, verify_ca, verify_identity") - cmd.PersistentFlags().StringVar(&flags.sslServerName, "ssl-server-name", "", "SSL server name override") - cmd.PersistentFlags().StringVar(&flags.sslCA, "ssl-certificate-authority", "", "Provide the full CA certificate chain here") - cmd.PersistentFlags().StringVar(&flags.sslKey, "ssl-client-key", "", "Private key for the client certificate") - cmd.PersistentFlags().StringVar(&flags.sslKey, "ssl-client-certificate", "", "Client Certificate to authenticate PlanetScale with your database server") - - cmd.MarkPersistentFlagRequired("name") - cmd.MarkPersistentFlagRequired("host") - cmd.MarkPersistentFlagRequired("database") - cmd.MarkPersistentFlagRequired("username") - cmd.MarkPersistentFlagRequired("password") - cmd.MarkPersistentFlagRequired("ssl-mode") - - return cmd -} diff --git a/internal/cmd/dataimports/start_test.go b/internal/cmd/dataimports/start_test.go deleted file mode 100644 index 08ad70a9..00000000 --- a/internal/cmd/dataimports/start_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package dataimports - -import ( - "bytes" - "context" - "fmt" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/planetscale/cli/internal/cmdutil" - "github.com/planetscale/cli/internal/config" - "github.com/planetscale/cli/internal/mock" - ps "github.com/planetscale/cli/internal/planetscale" - "github.com/planetscale/cli/internal/printer" -) - -func TestStart_LintDatabase_Success(t *testing.T) { - c := qt.New(t) - - org := "planetscale" - externalDataSource := ps.DataImportSource{ - Database: "aws-upstream-database", - Port: 3306, - HostName: "rds.amazonaws.com", - UserName: "aws-user", - Password: "aws-password", - SSLVerificationMode: ps.SSLModePreferred, - SSLMode: ps.SSLModePreferred.String(), - } - res := &ps.TestDataImportSourceResponse{ - CanConnect: true, - } - - out, err := invokeStartDatabase(&externalDataSource, "ps-downstream-database", org, c, res) - c.Assert(err, qt.IsNil) - expectedOut := []string{ - fmt.Sprintf("Testing Compatibility of database %s with user %s...\n", externalDataSource.Database, externalDataSource.UserName), - fmt.Sprintf("Database %s is compatible and can be imported into PlanetScale database ps-downstream-database\n", externalDataSource.Database), - "Please run this command with --dry-run=false to start the import\n", - } - - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestStart_LintDatabase_CannotConnect(t *testing.T) { - c := qt.New(t) - - org := "planetscale" - externalDataSource := ps.DataImportSource{ - Database: "aws-upstream-database", - Port: 3306, - HostName: "rds.amazonaws.com", - UserName: "aws-user", - Password: "aws-password", - SSLVerificationMode: ps.SSLModePreferred, - SSLMode: ps.SSLModePreferred.String(), - } - - res := &ps.TestDataImportSourceResponse{ - CanConnect: false, - ConnectError: "AWS RDS is down", - } - - out, err := invokeStartDatabase(&externalDataSource, "ps-downstream-database", org, c, res) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, res.ConnectError) - expectedOut := []string{ - fmt.Sprintf("Testing Compatibility of database %s with user %s...\n", externalDataSource.Database, externalDataSource.UserName), - } - - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func TestStart_LintDatabase_SchemaIncompatible(t *testing.T) { - c := qt.New(t) - - org := "planetscale" - externalDataSource := ps.DataImportSource{ - Database: "aws-upstream-database", - Port: 3306, - HostName: "rds.amazonaws.com", - UserName: "aws-user", - Password: "aws-password", - SSLVerificationMode: ps.SSLModePreferred, - SSLMode: ps.SSLModePreferred.String(), - } - - res := &ps.TestDataImportSourceResponse{ - CanConnect: true, - Errors: []*ps.DataSourceIncompatibilityError{ - { - LintError: "NO_PRIMARY_KEY", - ErrorDescription: "Table \"employees\" has no primary key", - }, - { - LintError: "NO_PRIMARY_KEY", - ErrorDescription: "Table \"departments\" has no primary key", - }, - }, - } - - out, err := invokeStartDatabase(&externalDataSource, "ps-downstream-database", org, c, res) - c.Assert(err, qt.IsNotNil) - expectedError := `External database compatibility check failed. Fix the following errors and then try again: - -• Table "employees" has no primary key -• Table "departments" has no primary key -` - c.Assert(err, qt.ErrorMatches, expectedError) - expectedOut := []string{ - fmt.Sprintf("Testing Compatibility of database %s with user %s...\n", externalDataSource.Database, externalDataSource.UserName), - } - - c.Assert(out, qt.Equals, strings.Join(expectedOut, "")) -} - -func invokeStartDatabase(externalDataSource *ps.DataImportSource, psdbName, org string, c *qt.C, response *ps.TestDataImportSourceResponse) (string, error) { - svc := &mock.DataImportsService{ - TestDataImportSourceFn: func(ctx context.Context, req *ps.TestDataImportSourceRequest) (*ps.TestDataImportSourceResponse, error) { - req.Connection.SSLMode = req.Connection.SSLVerificationMode.String() - c.Assert(req.Organization, qt.Equals, org) - c.Assert(req.Connection, qt.Equals, *externalDataSource) - return response, nil - }, - } - - var buf bytes.Buffer - format := printer.Human - p := printer.NewPrinter(&format) - p.SetHumanOutput(&buf) - ch := &cmdutil.Helper{ - Printer: p, - Config: &config.Config{ - Organization: org, - }, - Client: func() (*ps.Client, error) { - return &ps.Client{ - DataImports: svc, - }, nil - }, - } - - cmd := StartDataImportCmd(ch) - - cmd.SetArgs([]string{ - "--name", psdbName, - "--database", externalDataSource.Database, - "--host", externalDataSource.HostName, - "--username", externalDataSource.UserName, - "--password", externalDataSource.Password, - "--ssl-mode", "preferred", - }) - cmd.SilenceUsage = true - err := cmd.Execute() - - c.Assert(svc.TestDataImportSourceFnInvoked, qt.IsTrue) - return buf.String(), err -} diff --git a/internal/cmd/inspect/checks.go b/internal/cmd/inspect/checks.go index 1eb899df..e8d4aadd 100644 --- a/internal/cmd/inspect/checks.go +++ b/internal/cmd/inspect/checks.go @@ -627,7 +627,7 @@ var checks = []check{ Name: "subscriptions", Short: "Per-table logical replication progress on this subscriber", EmptyMessage: "No subscriptions found on this database.", - MySQLHint: "Subscriptions are a PostgreSQL concept; for imports see: pscale data-imports get", + MySQLHint: "Subscriptions are a PostgreSQL concept; for imports see: pscale branch vtctl move-tables list", Postgres: &engineSQL{ SQL: ` SELECT diff --git a/internal/cmd/keyspace/create_external.go b/internal/cmd/keyspace/create_external.go index fb48aa15..313f57bc 100644 --- a/internal/cmd/keyspace/create_external.go +++ b/internal/cmd/keyspace/create_external.go @@ -38,11 +38,10 @@ func CreateExternalCmd(ch *cmdutil.Helper) *cobra.Command { Short: "Create an external keyspace on a branch", Long: `Create an external keyspace by connecting a branch to an existing MySQL database. -Connection flags follow pscale data-imports start. --source-database is the -remote MySQL database name, not the PlanetScale database. --cluster-size is -optional and selects the external tablet size; when omitted, PlanetScale -chooses a size from the source storage. Managed organizations should pass a -size from pscale size cluster list.`, +--source-database is the remote MySQL database name, not the PlanetScale +database. --cluster-size is optional and selects the external tablet size; +when omitted, PlanetScale chooses a size from the source storage. Managed +organizations should pass a size from pscale size cluster list.`, Args: cmdutil.RequiredArgs("database", "branch", "keyspace"), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 56d97e6a..b230e1b3 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -43,7 +43,6 @@ import ( "github.com/planetscale/cli/internal/cmd/branch" "github.com/planetscale/cli/internal/cmd/connect" "github.com/planetscale/cli/internal/cmd/database" - "github.com/planetscale/cli/internal/cmd/dataimports" "github.com/planetscale/cli/internal/cmd/deployrequest" "github.com/planetscale/cli/internal/cmd/importcmd" "github.com/planetscale/cli/internal/cmd/insights" @@ -367,10 +366,6 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer. connectCmd.GroupID = "vitess" rootCmd.AddCommand(connectCmd) - dataimportsCmd := dataimports.DataImportsCmd(ch) - dataimportsCmd.GroupID = "vitess" - rootCmd.AddCommand(dataimportsCmd) - deployRequestCmd := deployrequest.DeployRequestCmd(ch) deployRequestCmd.GroupID = "vitess" rootCmd.AddCommand(deployRequestCmd) diff --git a/internal/mock/imports.go b/internal/mock/imports.go deleted file mode 100644 index b41632e9..00000000 --- a/internal/mock/imports.go +++ /dev/null @@ -1,59 +0,0 @@ -package mock - -import ( - "context" - - ps "github.com/planetscale/cli/internal/planetscale" -) - -type DataImportsService struct { - TestDataImportSourceFn func(ctx context.Context, request *ps.TestDataImportSourceRequest) (*ps.TestDataImportSourceResponse, error) - TestDataImportSourceFnInvoked bool - StartDataImportFn func(ctx context.Context, request *ps.StartDataImportRequest) (*ps.DataImport, error) - StartDataImportFnInvoked bool - CancelDataImportFn func(ctx context.Context, request *ps.CancelDataImportRequest) error - CancelDataImportFnInvoked bool - GetDataImportStatusFn func(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) - GetDataImportStatusFnInvoked bool - MakePlanetScalePrimaryFn func(ctx context.Context, request *ps.MakePlanetScalePrimaryRequest) (*ps.DataImport, error) - MakePlanetScalePrimaryFnInvoked bool - MakePlanetScaleReplicaFn func(ctx context.Context, request *ps.MakePlanetScaleReplicaRequest) (*ps.DataImport, error) - MakePlanetScaleReplicaFnInvoked bool - DetachExternalDatabaseFn func(ctx context.Context, request *ps.DetachExternalDatabaseRequest) (*ps.DataImport, error) - DetachExternalDatabaseFnInvoked bool -} - -func (d *DataImportsService) TestDataImportSource(ctx context.Context, request *ps.TestDataImportSourceRequest) (*ps.TestDataImportSourceResponse, error) { - d.TestDataImportSourceFnInvoked = true - return d.TestDataImportSourceFn(ctx, request) -} - -func (d *DataImportsService) StartDataImport(ctx context.Context, request *ps.StartDataImportRequest) (*ps.DataImport, error) { - d.StartDataImportFnInvoked = true - return d.StartDataImportFn(ctx, request) -} - -func (d *DataImportsService) CancelDataImport(ctx context.Context, request *ps.CancelDataImportRequest) error { - d.CancelDataImportFnInvoked = true - return d.CancelDataImportFn(ctx, request) -} - -func (d *DataImportsService) GetDataImportStatus(ctx context.Context, request *ps.GetImportStatusRequest) (*ps.DataImport, error) { - d.GetDataImportStatusFnInvoked = true - return d.GetDataImportStatusFn(ctx, request) -} - -func (d *DataImportsService) MakePlanetScalePrimary(ctx context.Context, request *ps.MakePlanetScalePrimaryRequest) (*ps.DataImport, error) { - d.MakePlanetScalePrimaryFnInvoked = true - return d.MakePlanetScalePrimaryFn(ctx, request) -} - -func (d *DataImportsService) MakePlanetScaleReplica(ctx context.Context, request *ps.MakePlanetScaleReplicaRequest) (*ps.DataImport, error) { - d.MakePlanetScaleReplicaFnInvoked = true - return d.MakePlanetScaleReplicaFn(ctx, request) -} - -func (d *DataImportsService) DetachExternalDatabase(ctx context.Context, request *ps.DetachExternalDatabaseRequest) (*ps.DataImport, error) { - d.DetachExternalDatabaseFnInvoked = true - return d.DetachExternalDatabaseFn(ctx, request) -} diff --git a/internal/planetscale/client.go b/internal/planetscale/client.go index 1df943d2..a7e65d0a 100644 --- a/internal/planetscale/client.go +++ b/internal/planetscale/client.go @@ -58,7 +58,6 @@ type Client struct { D1ImportNotifications D1ImportNotificationsService DatabaseBranches DatabaseBranchesService Databases DatabasesService - DataImports DataImportsService DeployRequests DeployRequestsService Invoices InvoicesService Keyspaces KeyspacesService @@ -363,7 +362,6 @@ func NewClient(opts ...ClientOption) (*Client, error) { c.D1ImportNotifications = &d1ImportNotificationsService{client: c} c.DatabaseBranches = &databaseBranchesService{client: c} c.Databases = &databasesService{client: c} - c.DataImports = &dataImportsService{client: c} c.DeployRequests = &deployRequestsService{client: c} c.Invoices = &invoicesService{client: c} c.Keyspaces = &keyspacesService{client: c} diff --git a/internal/planetscale/imports.go b/internal/planetscale/imports.go deleted file mode 100644 index 71cbd869..00000000 --- a/internal/planetscale/imports.go +++ /dev/null @@ -1,361 +0,0 @@ -package planetscale - -import ( - "context" - "fmt" - "net/http" - "path" - "time" -) - -type BillingPlan int - -const ( - HobbyPlan BillingPlan = iota - ScalerProPlan -) - -func (bp BillingPlan) String() string { - switch bp { - case ScalerProPlan: - return "scaler_pro" - default: - return "developer" - } -} - -var planToBillingPlanMap = map[string]BillingPlan{ - "scaler_pro": ScalerProPlan, - "developer": HobbyPlan, -} - -type DataImportSource struct { - HostName string `json:"hostname"` - Database string `json:"schema_name"` - Port int `json:"port"` - SSLMode string `json:"ssl_mode"` - SSLVerificationMode ExternalDataSourceSSLVerificationMode - UserName string `json:"username"` - Password string `json:"password"` - SSLCA string `json:"ssl_ca"` - SSLCertificate string `json:"ssl_cert"` - SSLKey string `json:"ssl_key"` - SSLServerName string `json:"ssl_server_name"` -} - -type ExternalDataSourceSSLVerificationMode int - -const ( - SSLModeDisabled ExternalDataSourceSSLVerificationMode = iota - SSLModePreferred - SSLModeRequired - SSLModeVerifyCA - SSLModeVerifyIdentity -) - -func (sm ExternalDataSourceSSLVerificationMode) String() string { - switch sm { - case SSLModeDisabled: - return "disabled" - case SSLModePreferred: - return "preferred" - case SSLModeRequired: - return "required" - case SSLModeVerifyCA: - return "verify_ca" - default: - return "verify_identity" - } -} - -type DataImportState int - -const ( - DataImportPreparingDataCopy DataImportState = iota - DataImportPreparingDataCopyFailed - DataImportCopyingData - DataImportCopyingDataFailed - DataImportSwitchTrafficPending - DataImportSwitchTrafficRunning - DataImportSwitchTrafficCompleted - DataImportSwitchTrafficError - DataImportReverseTrafficRunning - DataImportReverseTrafficCompleted - DataImportReverseTrafficError - DataImportDetachExternalDatabaseRunning - DataImportDetachExternalDatabaseError - DataImportReady -) - -var stateToImportStateMap = map[string]DataImportState{ - "prepare_data_copy_pending": DataImportPreparingDataCopy, - "prepare_data_copy_error": DataImportPreparingDataCopyFailed, - "data_copy_pending": DataImportCopyingData, - "data_copy_error": DataImportCopyingDataFailed, - "switch_traffic_workflow_pending": DataImportSwitchTrafficPending, - "switch_traffic_workflow_running": DataImportSwitchTrafficRunning, - "switch_traffic_workflow_error": DataImportSwitchTrafficError, - "reverse_traffic_workflow_running": DataImportReverseTrafficRunning, - "reverse_traffic_workflow_error": DataImportReverseTrafficError, - "cleanup_workflow_pending": DataImportSwitchTrafficCompleted, - "cleanup_workflow_running": DataImportDetachExternalDatabaseRunning, - "cleanup_workflow_error": DataImportDetachExternalDatabaseError, - "ready": DataImportReady, -} - -var importStateToDescMap = map[DataImportState]string{ - DataImportPreparingDataCopy: "Preparing to copy data from external database", - DataImportPreparingDataCopyFailed: "Failed to copy data from external database", - DataImportCopyingData: "Copying data from external database", - DataImportCopyingDataFailed: "Failed to copy data from external database", - DataImportSwitchTrafficPending: "PlanetScale database is running in replica mode", - DataImportSwitchTrafficRunning: "Switching PlanetScale database to primary mode", - DataImportSwitchTrafficError: "Failed to switching PlanetScale database to primary mode", - DataImportReverseTrafficRunning: "Switching PlanetScale database to replica mode", - DataImportReverseTrafficError: "Failed to switching PlanetScale database to replica mode", - DataImportDetachExternalDatabaseRunning: "Detaching external database from PlanetScale database", - DataImportDetachExternalDatabaseError: "Failed to detach external database from PlanetScale database", - DataImportReady: "Import has completed and your PlanetScale Database is now ready", -} - -func (d DataImportState) String() string { - if val, ok := importStateToDescMap[d]; ok { - return val - } - - panic("unknown data import state") -} - -type DataImport struct { - ID string `json:"id"` - ImportState DataImportState - State string `json:"state"` - Errors string `json:"import_check_errors"` - StartedAt *time.Time `json:"started_at"` - FinishedAt *time.Time `json:"finished_at"` - DeletedAt *time.Time `json:"deleted_at"` - ExternalDataSource DataImportSource `json:"data_source"` -} - -func (di *DataImport) ParseState() { - if val, ok := stateToImportStateMap[di.State]; ok { - di.ImportState = val - return - } - - panic("unknown data import state " + di.State) -} - -type TestDataImportSourceRequest struct { - Organization string `json:"organization"` - Database string `json:"database_name"` - Connection DataImportSource `json:"connection"` -} - -// DataSourceIncompatibilityError represents an error that occurs when the -// source schema in an external database server is incompatible with PlanetScale. -type DataSourceIncompatibilityError struct { - LintError string `json:"lint_error"` - Keyspace string `json:"keyspace_name"` - Table string `json:"table_name"` - SubjectType string `json:"subject_type"` - ErrorDescription string `json:"error_description"` - DocsUrl string `json:"docs_url"` -} - -type UserShouldUpgradePlanError struct{} - -func (e UserShouldUpgradePlanError) Error() string { - return "Importing databases over 5GB requires a paid plan. Log in to app.planetscale.com to upgrade." -} - -type TestDataImportSourceResponse struct { - CanConnect bool `json:"can_connect"` - ShouldUpgradePlan bool `json:"should_upgrade"` - SuggestedPlan string `json:"suggested_plan"` - SuggestedBillingPlan BillingPlan - ConnectError string `json:"error"` - Errors []*DataSourceIncompatibilityError `json:"lint_errors"` - MaxPoolSize int `json:"max_pool_size"` -} - -type StartDataImportRequest struct { - Organization string `json:"organization"` - Database string `json:"database_name"` - Connection DataImportSource `json:"connection"` - Region string `json:"region"` - Plan string `json:"plan"` - MaxPoolSize int `json:"max_pool_size"` -} - -type MakePlanetScalePrimaryRequest struct { - Organization string - Database string -} - -type MakePlanetScaleReplicaRequest struct { - Organization string - Database string -} - -type DetachExternalDatabaseRequest struct { - Organization string - Database string -} - -type GetImportStatusRequest struct { - Organization string - Database string -} - -type CancelDataImportRequest struct { - Organization string - Database string -} - -// DataImportsService is an interface for communicating with the PlanetScale -// Data Imports API endpoint. -type DataImportsService interface { - // TestDataImportSource checks if the external database that we're importing will be supported - // by PlanetScale. It checks for ability to replicate binlogs, schema compatibility and other factors. - TestDataImportSource(ctx context.Context, request *TestDataImportSourceRequest) (*TestDataImportSourceResponse, error) - // StartDataImport spins up a downstream PlanetScale database in replica mode, with the - // external database as a Primary and starts copying data from external to PlanetScale. - StartDataImport(ctx context.Context, request *StartDataImportRequest) (*DataImport, error) - // CancelDataImport halts all replication and data copy from external to PlanetScale - // and deletes the PlanetScale database. - CancelDataImport(ctx context.Context, request *CancelDataImportRequest) error - // GetDataImportStatus gets the current status of a DataImport for a given database - // Fails if the database is not importing any data. - GetDataImportStatus(ctx context.Context, request *GetImportStatusRequest) (*DataImport, error) - // MakePlanetScalePrimary makes the downstream PlanetScale database a Primary and the external database a Replica. - MakePlanetScalePrimary(ctx context.Context, request *MakePlanetScalePrimaryRequest) (*DataImport, error) - // MakePlanetScaleReplica makes the downstream PlanetScale database a Replica and the external database a Primary. - MakePlanetScaleReplica(ctx context.Context, request *MakePlanetScaleReplicaRequest) (*DataImport, error) - // DetachExternalDatabase detaches the external database from PlanetScale after a data import has finished - // and PlanetScale is running as Primary. - DetachExternalDatabase(ctx context.Context, request *DetachExternalDatabaseRequest) (*DataImport, error) -} - -type dataImportsService struct { - client *Client -} - -// TestDataImportSource will check an external database for compatibility with PlanetScale -func (d *dataImportsService) TestDataImportSource(ctx context.Context, request *TestDataImportSourceRequest) (*TestDataImportSourceResponse, error) { - request.Connection.SSLMode = request.Connection.SSLVerificationMode.String() - pathStr := path.Join("/v1/organizations", request.Organization, "data-imports/test-connection") - req, err := d.client.newRequest(http.MethodPost, pathStr, request) - if err != nil { - return nil, fmt.Errorf("error creating http request: %w", err) - } - - resp := &TestDataImportSourceResponse{} - if err := d.client.do(ctx, req, &resp); err != nil { - return nil, err - } - - if resp.ShouldUpgradePlan { - return resp, UserShouldUpgradePlanError{} - } - - resp.SuggestedBillingPlan = planToBillingPlanMap[resp.SuggestedPlan] - return resp, nil -} - -func (d *dataImportsService) StartDataImport(ctx context.Context, request *StartDataImportRequest) (*DataImport, error) { - request.Connection.SSLMode = request.Connection.SSLVerificationMode.String() - pathStr := path.Join("/v1/organizations", request.Organization, "data-imports/new") - req, err := d.client.newRequest(http.MethodPost, pathStr, request) - if err != nil { - return nil, fmt.Errorf("error creating http request: %w", err) - } - - resp := &DataImport{} - if err := d.client.do(ctx, req, &resp); err != nil { - return nil, err - } - - return resp, nil -} - -func (d *dataImportsService) GetDataImportStatus(ctx context.Context, getReq *GetImportStatusRequest) (*DataImport, error) { - pathStr := dataImportAPIPath(getReq.Organization, getReq.Database) - req, err := d.client.newRequest(http.MethodGet, pathStr, nil) - if err != nil { - return nil, fmt.Errorf("error creating request for get database: %w", err) - } - - di := &DataImport{} - err = d.client.do(ctx, req, &di) - if err != nil { - return nil, err - } - - di.ParseState() - return di, nil -} - -func (d *dataImportsService) CancelDataImport(ctx context.Context, cancelReq *CancelDataImportRequest) error { - pathStr := path.Join(dataImportAPIPath(cancelReq.Organization, cancelReq.Database), "cancel") - req, err := d.client.newRequest(http.MethodPost, pathStr, nil) - if err != nil { - return fmt.Errorf("error creating http request: %w", err) - } - - if err := d.client.do(ctx, req, nil); err != nil { - return err - } - - return nil -} - -func (d *dataImportsService) MakePlanetScalePrimary(ctx context.Context, request *MakePlanetScalePrimaryRequest) (*DataImport, error) { - pathStr := path.Join(dataImportAPIPath(request.Organization, request.Database), "begin-switch-traffic") - req, err := d.client.newRequest(http.MethodPost, pathStr, nil) - if err != nil { - return nil, fmt.Errorf("error creating http request: %w", err) - } - - resp := &DataImport{} - if err := d.client.do(ctx, req, &resp); err != nil { - return nil, err - } - resp.ParseState() - return resp, nil -} - -func (d *dataImportsService) MakePlanetScaleReplica(ctx context.Context, request *MakePlanetScaleReplicaRequest) (*DataImport, error) { - pathStr := path.Join(dataImportAPIPath(request.Organization, request.Database), "begin-reverse-traffic") - req, err := d.client.newRequest(http.MethodPost, pathStr, nil) - if err != nil { - return nil, fmt.Errorf("error creating http request: %w", err) - } - - resp := &DataImport{} - if err := d.client.do(ctx, req, &resp); err != nil { - return nil, err - } - resp.ParseState() - return resp, nil -} - -func (d *dataImportsService) DetachExternalDatabase(ctx context.Context, request *DetachExternalDatabaseRequest) (*DataImport, error) { - pathStr := path.Join(dataImportAPIPath(request.Organization, request.Database), "detach-external-database") - req, err := d.client.newRequest(http.MethodPost, pathStr, nil) - if err != nil { - return nil, fmt.Errorf("error creating http request: %w", err) - } - - resp := &DataImport{} - if err := d.client.do(ctx, req, &resp); err != nil { - return nil, err - } - - resp.ParseState() - return resp, nil -} - -func dataImportAPIPath(organization, database string) string { - return path.Join("/v1/organizations", organization, "databases", database, "data-imports") -} diff --git a/internal/planetscale/imports_test.go b/internal/planetscale/imports_test.go deleted file mode 100644 index 93623eff..00000000 --- a/internal/planetscale/imports_test.go +++ /dev/null @@ -1,409 +0,0 @@ -package planetscale - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - qt "github.com/frankban/quicktest" -) - -var knownStates = map[string]DataImportState{ - "prepare_data_copy_pending": DataImportPreparingDataCopy, - "prepare_data_copy_error": DataImportPreparingDataCopyFailed, - "data_copy_pending": DataImportCopyingData, - "data_copy_error": DataImportCopyingDataFailed, - "switch_traffic_workflow_pending": DataImportSwitchTrafficPending, - "switch_traffic_workflow_running": DataImportSwitchTrafficRunning, - "switch_traffic_workflow_error": DataImportSwitchTrafficError, - "reverse_traffic_workflow_running": DataImportReverseTrafficRunning, - "cleanup_workflow_pending": DataImportSwitchTrafficCompleted, - "cleanup_workflow_running": DataImportDetachExternalDatabaseRunning, - "cleanup_workflow_error": DataImportDetachExternalDatabaseError, - "ready": DataImportReady, -} - -func TestImports_ParseState(t *testing.T) { - c := qt.New(t) - for state, importState := range knownStates { - t.Run(fmt.Sprintf("Can parse state : %s", state), func(t *testing.T) { - di := DataImport{ - State: state, - } - - di.ParseState() - - c.Assert(di.ImportState, qt.Equals, importState) - }) - } -} - -func TestImports_CanRunLintExternalDatabase_Success(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/data-imports/test-connection") - w.WriteHeader(200) - out := `{ "can_connect": true, "error": "", "lint_errors": [], "table_statuses": []}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - td := TestDataImportSourceRequest{ - Organization: org, - Database: db, - Connection: DataImportSource{}, - } - - results, err := client.DataImports.TestDataImportSource(ctx, &td) - c.Assert(err, qt.IsNil) - - c.Assert(true, qt.Equals, results.CanConnect) -} - -func TestImports_CanRunLintExternalDatabase_ConnectFailure(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/data-imports/test-connection") - w.WriteHeader(200) - out := `{ "can_connect": false, "error": "external database is down", "lint_errors": [], "table_statuses": []}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - td := TestDataImportSourceRequest{ - Organization: org, - Database: db, - Connection: DataImportSource{}, - } - - results, err := client.DataImports.TestDataImportSource(ctx, &td) - c.Assert(err, qt.IsNil) - - c.Assert(false, qt.Equals, results.CanConnect) - c.Assert("external database is down", qt.Equals, results.ConnectError) -} - -func TestImports_CanRunLintExternalDatabase_LintFailure(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/data-imports/test-connection") - - w.WriteHeader(200) - out := `{ -"can_connect": true, -"error": "", -"lint_errors": [{ - "lint_error": "NO_PRIMARY_KEY", - "table_name": "employees", - "error_description": "Table 'employees' has no primary key" -}], -"table_statuses": [] -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - td := TestDataImportSourceRequest{ - Organization: org, - Database: db, - Connection: DataImportSource{}, - } - - results, err := client.DataImports.TestDataImportSource(ctx, &td) - c.Assert(err, qt.IsNil) - - c.Assert(true, qt.Equals, results.CanConnect) - c.Assert("", qt.Equals, results.ConnectError) - c.Assert(1, qt.Equals, len(results.Errors)) - c.Assert([]*DataSourceIncompatibilityError{ - { - LintError: "NO_PRIMARY_KEY", - Table: "employees", - ErrorDescription: "Table 'employees' has no primary key", - }, - }, qt.DeepEquals, results.Errors) -} - -func TestImports_CanRunLintExternalDatabase_NeedsUpgrade(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/data-imports/test-connection") - w.WriteHeader(200) - out := `{ -"can_connect": true, -"error": "", -"should_upgrade": true, -"table_statuses": [] -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - td := TestDataImportSourceRequest{ - Organization: org, - Database: db, - Connection: DataImportSource{}, - } - - _, err = client.DataImports.TestDataImportSource(ctx, &td) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorIs, UserShouldUpgradePlanError{}) -} - -func TestImports_CanStartDataImport_Success(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/data-imports/new") - var startRequest StartDataImportRequest - err := json.NewDecoder(r.Body).Decode(&startRequest) - c.Assert(err, qt.IsNil) - c.Assert("us-west-2", qt.Equals, startRequest.Region) - - w.WriteHeader(200) - out := `{ -"id": "PUBLIC_ID", -"state": "prepare_data_copy_pending", -"import_check_errors": "", -"data_source": { - "hostname": "aws.rds.something.com", - "port": 25060, - "database": "employees" -} -}` - _, err = w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - - startReq := &StartDataImportRequest{ - Organization: org, - Database: db, - Region: "us-west-2", - } - di, err := client.DataImports.StartDataImport(ctx, startReq) - c.Assert(err, qt.IsNil) - c.Assert(di.ID, qt.Equals, "PUBLIC_ID") - c.Assert(di.Errors, qt.Equals, "") - c.Assert(di.ImportState, qt.Equals, DataImportPreparingDataCopy) -} - -func TestImports_CanGetDataImportStatus_Success(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/data-imports") - w.WriteHeader(200) - out := `{ - "id": "IMPORT_PUBLIC_ID", - "state": "switch_traffic_workflow_pending" -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - di, err := client.DataImports.GetDataImportStatus(ctx, &GetImportStatusRequest{ - Organization: org, - Database: db, - }) - c.Assert(err, qt.IsNil) - c.Assert(di.ID, qt.Equals, "IMPORT_PUBLIC_ID") - c.Assert(di.ImportState, qt.Equals, DataImportSwitchTrafficPending) -} - -func TestImports_CanGetDataImportStatus_NoImport(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/data-imports") - w.WriteHeader(400) - out := `{ -"code": "bad_request", -"message": "Data import has not been setup for this database." -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - di, err := client.DataImports.GetDataImportStatus(ctx, &GetImportStatusRequest{ - Organization: org, - Database: db, - }) - c.Assert(err, qt.IsNotNil) - c.Assert(err, qt.ErrorMatches, "Data import has not been setup for this database.") - c.Assert(di, qt.IsNil) -} - -func TestImports_CanCancelDataImport(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/data-imports/cancel") - w.WriteHeader(200) - out := `{ -"id": "PUBLIC_ID", -"state": "prepare_data_copy_pending", -"import_check_errors": "", -"data_source": { - "hostname": "aws.rds.something.com", - "port": "25060", - "database": "employees" -} -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - err = client.DataImports.CancelDataImport(ctx, &CancelDataImportRequest{ - Organization: org, - Database: db, - }) - c.Assert(err, qt.IsNil) -} - -func TestImports_CanMakePlanetScalePrimary(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/data-imports/begin-switch-traffic") - w.WriteHeader(200) - out := `{ -"id": "PUBLIC_ID", -"state": "cleanup_workflow_pending", -"import_check_errors": "", -"data_source": { - "hostname": "aws.rds.something.com", - "port": 25060, - "database": "employees" -} -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - - makePrimRequest := &MakePlanetScalePrimaryRequest{ - Organization: org, - Database: db, - } - di, err := client.DataImports.MakePlanetScalePrimary(ctx, makePrimRequest) - c.Assert(err, qt.IsNil) - c.Assert(di.ID, qt.Equals, "PUBLIC_ID") - c.Assert(di.Errors, qt.Equals, "") - c.Assert(di.ImportState, qt.Equals, DataImportSwitchTrafficCompleted) -} - -func TestImports_CanMakePlanetScaleReplica(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/data-imports/begin-reverse-traffic") - w.WriteHeader(200) - out := `{ -"id": "PUBLIC_ID", -"state": "switch_traffic_workflow_pending", -"import_check_errors": "", -"data_source": { - "hostname": "aws.rds.something.com", - "port": 25060, - "database": "employees" -} -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - - makeReplicaRequest := &MakePlanetScaleReplicaRequest{ - Organization: org, - Database: db, - } - di, err := client.DataImports.MakePlanetScaleReplica(ctx, makeReplicaRequest) - c.Assert(err, qt.IsNil) - c.Assert(di.ID, qt.Equals, "PUBLIC_ID") - c.Assert(di.Errors, qt.Equals, "") - c.Assert(di.ImportState, qt.Equals, DataImportSwitchTrafficPending) -} - -func TestImports_CanDetachExternalDatabase(t *testing.T) { - c := qt.New(t) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/my-org/databases/my-db/data-imports/detach-external-database") - w.WriteHeader(200) - out := `{ -"id": "PUBLIC_ID", -"state": "ready", -"import_check_errors": "", -"data_source": { - "hostname": "aws.rds.something.com", - "port": 25060, - "database": "employees" -} -}` - _, err := w.Write([]byte(out)) - c.Assert(err, qt.IsNil) - })) - - client, err := NewClient(WithBaseURL(ts.URL)) - c.Assert(err, qt.IsNil) - ctx := context.Background() - org := "my-org" - db := "my-db" - - detachReq := &DetachExternalDatabaseRequest{ - Organization: org, - Database: db, - } - di, err := client.DataImports.DetachExternalDatabase(ctx, detachReq) - c.Assert(err, qt.IsNil) - c.Assert(di.ID, qt.Equals, "PUBLIC_ID") - c.Assert(di.Errors, qt.Equals, "") - c.Assert(di.ImportState, qt.Equals, DataImportReady) -} diff --git a/internal/planetscale/ssl_mode.go b/internal/planetscale/ssl_mode.go new file mode 100644 index 00000000..d9a8f934 --- /dev/null +++ b/internal/planetscale/ssl_mode.go @@ -0,0 +1,26 @@ +package planetscale + +type ExternalDataSourceSSLVerificationMode int + +const ( + SSLModeDisabled ExternalDataSourceSSLVerificationMode = iota + SSLModePreferred + SSLModeRequired + SSLModeVerifyCA + SSLModeVerifyIdentity +) + +func (sm ExternalDataSourceSSLVerificationMode) String() string { + switch sm { + case SSLModeDisabled: + return "disabled" + case SSLModePreferred: + return "preferred" + case SSLModeRequired: + return "required" + case SSLModeVerifyCA: + return "verify_ca" + default: + return "verify_identity" + } +}