diff --git a/internal/cmd/group_aws_migrate.go b/internal/cmd/group_aws_migrate.go new file mode 100644 index 00000000..b4f4c7e0 --- /dev/null +++ b/internal/cmd/group_aws_migrate.go @@ -0,0 +1,206 @@ +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + "github.com/tursodatabase/turso-cli/internal" + "github.com/tursodatabase/turso-cli/internal/prompt" +) + +var groupAwsMigrationCmd = &cobra.Command{ + Use: "aws-migration", + Short: "Manage AWS migration of the group", +} + +func init() { + groupCmd.AddCommand(groupAwsMigrationCmd) + groupAwsMigrationCmd.AddCommand(groupAwsMigrationInfoCmd) + groupAwsMigrationCmd.AddCommand(groupAwsMigrationStartCmd) + groupAwsMigrationCmd.AddCommand(groupAwsMigrationAbortCmd) +} + +var groupAwsMigrationInfoCmd = &cobra.Command{ + Use: "info ", + Short: "Migration status for the group", + Args: cobra.ExactArgs(1), + ValidArgsFunction: noFilesArg, + RunE: func(cmd *cobra.Command, args []string) error { + group := args[0] + if group == "" { + return fmt.Errorf("the first argument must contain a group name") + } + + cmd.SilenceUsage = true + client, err := authedTursoClient() + if err != nil { + return err + } + + _, err = client.Groups.Get(group) + if err != nil { + return err + } + + info, err := client.Groups.GetAwsMigrationInfo(group) + if err != nil { + return err + } + + if info.Status == "pending" { + fmt.Printf("AWS migration is %v\n%v\n", internal.Emph("in progress"), info.Comment) + } else if info.Status == "finished" { + fmt.Printf("AWS migration is %v\n", internal.Emph("finished")) + } else if info.Status == "aborted" { + fmt.Printf("AWS migration was %v\n", internal.Emph("aborted")) + } else if info.Status == "none" { + fmt.Printf("AWS migration is %v\n", internal.Emph("not started")) + } + + return nil + }, +} + +var groupAwsMigrationStartCmd = &cobra.Command{ + Use: "start ", + Short: "Start AWS migration process of the group", + Args: cobra.ExactArgs(1), + ValidArgsFunction: noFilesArg, + RunE: func(cmd *cobra.Command, args []string) error { + group := args[0] + if group == "" { + return fmt.Errorf("the first argument must contain a group name") + } + + cmd.SilenceUsage = true + client, err := authedTursoClient() + if err != nil { + return err + } + + _, err = client.Groups.Get(group) + if err != nil { + return err + } + + info, err := client.Groups.GetAwsMigrationInfo(group) + if err != nil { + return err + } + + if info.Status == "pending" { + fmt.Printf("AWS migration is %v\n%v\n", internal.Emph("in progress"), info.Comment) + return nil + } else if info.Status == "finished" { + fmt.Printf("AWS migration is %v\n", internal.Emph("finished")) + return nil + } else if info.Status == "aborted" { + fmt.Printf("AWS migration was %v\nPlease, contact with support@turso.tech for further assistance with group migration\n", internal.Emph("aborted")) + return nil + } + + fmt.Printf("%v\n\n", info.Comment) + + ok, err := promptConfirmation(fmt.Sprintf("Are you sure you want to migrate group %s from Fly to AWS?", internal.Emph(group))) + if err != nil { + return fmt.Errorf("could not get prompt confirmed by user: %w", err) + } + + if !ok { + fmt.Println("Group migration cancelled by the user.") + return nil + } + + spinner := prompt.Spinner(fmt.Sprintf("AWS migration of group %v is in progress", group)) + defer spinner.Stop() + + err = client.Groups.StartAwsMigration(group) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Minute) + defer cancel() + + for { + select { + case <-ctx.Done(): + spinner.Stop() + fmt.Printf("AWS migration for group %v still on-going.\n\n"+ + "You can check status of the migration with: `turso group migration info `.\n"+ + "If migrations for certain databases haven't started, you can abort the group migration with: `turso group migration abort `.\n\n"+ + "Contact support@turso.tech in case of any issues\n", internal.Emph(group)) + return nil + case <-time.NewTimer(5 * time.Second).C: + info, err := client.Groups.GetAwsMigrationInfo(group) + if err != nil { + return err + } + if info.Status == "finished" { + spinner.Stop() + fmt.Printf("Group %v was successfully migrated from Fly to AWS", internal.Emph(group)) + return nil + } else { + spinner.Text(info.Comment) + } + } + } + }, +} + +var groupAwsMigrationAbortCmd = &cobra.Command{ + Use: "abort ", + Short: "Abort AWS migration process of the group", + Args: cobra.ExactArgs(1), + ValidArgsFunction: noFilesArg, + RunE: func(cmd *cobra.Command, args []string) error { + group := args[0] + if group == "" { + return fmt.Errorf("the first argument must contain a group name") + } + + cmd.SilenceUsage = true + client, err := authedTursoClient() + if err != nil { + return err + } + + _, err = client.Groups.Get(group) + if err != nil { + return err + } + + info, err := client.Groups.GetAwsMigrationInfo(group) + if err != nil { + return err + } + + if info.Status == "none" { + fmt.Printf("AWS migration is %v", internal.Emph("not started")) + return nil + } else if info.Status == "aborted" { + fmt.Printf("AWS migration was already %v", internal.Emph("aborted")) + return nil + } else if info.Status == "finished" { + fmt.Printf("AWS migration was already %v", internal.Emph("finished")) + return nil + } + + ok, err := promptConfirmation(fmt.Sprintf("Are you sure you want to abort group migration %s from Fly to AWS?", internal.Emph(group))) + if err != nil { + return fmt.Errorf("could not get prompt confirmed by user: %w", err) + } + + if !ok { + fmt.Println("Group migration abort cancelled by the user.") + return nil + } + + spinner := prompt.Spinner(fmt.Sprintf("AWS migration of group %v aborted", group)) + defer spinner.Stop() + + return client.Groups.AbortAwsMigration(group) + }, +} diff --git a/internal/turso/groups.go b/internal/turso/groups.go index d282fd61..2ab8f3b3 100644 --- a/internal/turso/groups.go +++ b/internal/turso/groups.go @@ -381,6 +381,60 @@ func (d *GroupsClient) Transfer(group string, to string) error { return nil } +type AwsMigrationInfo struct { + Status string `json:"status"` + Comment string `json:"comment"` +} + +func (d *GroupsClient) GetAwsMigrationInfo(group string) (AwsMigrationInfo, error) { + url := d.URL(fmt.Sprintf("/%s/aws/migration/info", group)) + r, err := d.client.Get(url, nil) + if err != nil { + return AwsMigrationInfo{}, fmt.Errorf("failed to get group migration info: %w", err) + } + defer r.Body.Close() + + if r.StatusCode != http.StatusOK { + err := parseResponseError(r) + return AwsMigrationInfo{}, fmt.Errorf("failed to get group migration info: %w", err) + } + result, err := unmarshal[AwsMigrationInfo](r) + if err != nil { + return AwsMigrationInfo{}, fmt.Errorf("failed to parse group migration info: %w", err) + } + return result, nil +} + +func (d *GroupsClient) StartAwsMigration(group string) error { + url := d.URL(fmt.Sprintf("/%s/aws/migration/start", group)) + r, err := d.client.Post(url, nil) + if err != nil { + return fmt.Errorf("failed to start group migration: %w", err) + } + defer r.Body.Close() + + if r.StatusCode != http.StatusOK { + err := parseResponseError(r) + return fmt.Errorf("failed to start group migration: %w", err) + } + return nil +} + +func (d *GroupsClient) AbortAwsMigration(group string) error { + url := d.URL(fmt.Sprintf("/%s/aws/migration/abort", group)) + r, err := d.client.Post(url, nil) + if err != nil { + return fmt.Errorf("failed to abort group migration: %w", err) + } + defer r.Body.Close() + + if r.StatusCode != http.StatusOK { + err := parseResponseError(r) + return fmt.Errorf("failed to abort group migration: %w", err) + } + return nil +} + func (d *GroupsClient) URL(suffix string) string { prefix := "/v1" if d.client.Org != "" {