From 4b2cf7f7edcb9f780f6bd77b4d064789e74333cf Mon Sep 17 00:00:00 2001 From: Nick Van Wiggeren Date: Thu, 25 Jun 2026 10:35:26 +0000 Subject: [PATCH 1/2] Add pscale branch resize for Postgres branches Adds 'pscale branch resize --cluster-size ', which resizes a Postgres branch's cluster via the new PostgresBranches.Resize client (PATCH .../branches/{branch}/changes). MySQL databases are pointed at 'pscale keyspace resize', since the branch-level cluster change endpoint is Postgres-only. Bumps planetscale-go to v0.171.0. --- go.mod | 2 +- go.sum | 6 +- internal/cmd/branch/branch.go | 1 + internal/cmd/branch/resize.go | 122 ++++++++++++++++++++++++++++++++++ internal/mock/branch.go | 8 +++ 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 internal/cmd/branch/resize.go diff --git a/go.mod b/go.mod index 133d5a6f5..9b6bcbf8a 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/termenv v0.16.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/planetscale/planetscale-go v0.170.0 + github.com/planetscale/planetscale-go v0.171.0 github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 64b1ac034..144c8c65d 100644 --- a/go.sum +++ b/go.sum @@ -176,10 +176,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2vvqGZLvoQfpaGg/j1fNDr4j03s3PRz4rVY= github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q= -github.com/planetscale/planetscale-go v0.168.2-0.20260615174426-c2888d0d78f6 h1:3C+Cq3Y6Lok36kYXSAfgrZgP4wXOkc6uxJSpEviouFQ= -github.com/planetscale/planetscale-go v0.168.2-0.20260615174426-c2888d0d78f6/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0= -github.com/planetscale/planetscale-go v0.170.0 h1:7JoL+ZwdGmsM4+mARQcjcFMCWRjOZnYZzqLo6gRxZlc= -github.com/planetscale/planetscale-go v0.170.0/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0= +github.com/planetscale/planetscale-go v0.171.0 h1:ZBltEV1SANoQDUMc0VVUmGD4urjquZqh/y9XgGxG3Pw= +github.com/planetscale/planetscale-go v0.171.0/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs= github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4/go.mod h1:M52h5IWxAcbdQ1hSZrLAGQC4ZXslxEsK/Wh9nu3wdWs= github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 h1:aRd6vdE1fyuSI4RVj7oCr8lFmgqXvpnPUmN85VbZCp8= diff --git a/internal/cmd/branch/branch.go b/internal/cmd/branch/branch.go index 9eb817589..9743ddfab 100644 --- a/internal/cmd/branch/branch.go +++ b/internal/cmd/branch/branch.go @@ -25,6 +25,7 @@ func BranchCmd(ch *cmdutil.Helper) *cobra.Command { cmd.AddCommand(CreateCmd(ch)) cmd.AddCommand(ListCmd(ch)) cmd.AddCommand(DeleteCmd(ch)) + cmd.AddCommand(ResizeCmd(ch)) cmd.AddCommand(ShowCmd(ch)) cmd.AddCommand(SwitchCmd(ch)) cmd.AddCommand(DiffCmd(ch)) diff --git a/internal/cmd/branch/resize.go b/internal/cmd/branch/resize.go new file mode 100644 index 000000000..528bf20c7 --- /dev/null +++ b/internal/cmd/branch/resize.go @@ -0,0 +1,122 @@ +package branch + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/printer" + ps "github.com/planetscale/planetscale-go/planetscale" + "github.com/spf13/cobra" +) + +// ResizeCmd resizes a Postgres branch's cluster. +func ResizeCmd(ch *cmdutil.Helper) *cobra.Command { + var flags struct { + clusterSize string + } + + cmd := &cobra.Command{ + Use: "resize ", + Short: "Resize a Postgres branch's cluster", + Args: cmdutil.RequiredArgs("database", "branch"), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + database, branch := args[0], args[1] + + if flags.clusterSize == "" { + return errors.New("the --cluster-size flag is required") + } + + client, err := ch.Client() + if err != nil { + return err + } + + db, err := client.Databases.Get(ctx, &ps.GetDatabaseRequest{ + Organization: ch.Config.Organization, + Database: database, + }) + if err != nil { + switch cmdutil.ErrCode(err) { + case ps.ErrNotFound: + return fmt.Errorf("database %s does not exist in organization %s", printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization)) + default: + return cmdutil.HandleError(err) + } + } + + if db.Kind == "mysql" { + return fmt.Errorf("branch resize is only supported for PostgreSQL databases. To resize a MySQL keyspace, use %s", printer.BoldBlue("pscale keyspace resize")) + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Resizing branch %s in %s to %s...", printer.BoldBlue(branch), printer.BoldBlue(database), printer.BoldBlue(flags.clusterSize))) + defer end() + + change, err := client.PostgresBranches.Resize(ctx, &ps.ResizePostgresBranchRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + ClusterSize: flags.clusterSize, + }) + if err != nil { + switch cmdutil.ErrCode(err) { + case ps.ErrNotFound: + return fmt.Errorf("database %s or branch %s does not exist in organization %s", printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) + default: + return cmdutil.HandleError(err) + } + } + end() + + // A nil change request means the branch is already configured with + // the requested cluster size (the API responded 204 No Content). + if change == nil { + if ch.Printer.Format() == printer.Human { + ch.Printer.Printf("Branch %s is already configured with cluster size %s.\n", printer.BoldBlue(branch), printer.BoldBlue(flags.clusterSize)) + } + return nil + } + + if ch.Printer.Format() == printer.Human { + ch.Printer.Printf("Resize of branch %s to %s started (state: %s).\n", printer.BoldBlue(branch), printer.BoldBlue(change.ClusterDisplayName), printer.BoldBlue(change.State)) + return nil + } + + return ch.Printer.PrintResource(toPostgresBranchResize(change)) + }, + } + + cmd.Flags().StringVar(&flags.clusterSize, "cluster-size", "", "New cluster size for the branch. Use 'pscale size cluster list' to see the valid sizes.") + cmd.RegisterFlagCompletionFunc("cluster-size", func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + return cmdutil.ClusterSizesCompletionFunc(ch, cmd, args, toComplete) + }) + + return cmd +} + +type postgresBranchResize struct { + ID string `header:"id" json:"id"` + State string `header:"state" json:"state"` + ClusterSize string `header:"cluster_size" json:"cluster_size"` + PreviousClusterSize string `header:"previous_cluster_size" json:"previous_cluster_size"` + Replicas int `header:"replicas" json:"replicas"` + + orig *ps.PostgresBranchClusterResizeRequest +} + +func toPostgresBranchResize(c *ps.PostgresBranchClusterResizeRequest) *postgresBranchResize { + return &postgresBranchResize{ + ID: c.ID, + State: c.State, + ClusterSize: c.ClusterDisplayName, + PreviousClusterSize: c.PreviousClusterDisplayName, + Replicas: c.Replicas, + orig: c, + } +} + +func (p *postgresBranchResize) MarshalJSON() ([]byte, error) { + return json.MarshalIndent(p.orig, "", " ") +} diff --git a/internal/mock/branch.go b/internal/mock/branch.go index 35f8e4255..4c1adf2da 100644 --- a/internal/mock/branch.go +++ b/internal/mock/branch.go @@ -146,6 +146,9 @@ type PostgresBranchesService struct { ListClusterSKUsFn func(context.Context, *ps.ListBranchClusterSKUsRequest, ...ps.ListOption) ([]*ps.ClusterSKU, error) ListClusterSKUsFnInvoked bool + + ResizeFn func(context.Context, *ps.ResizePostgresBranchRequest) (*ps.PostgresBranchClusterResizeRequest, error) + ResizeFnInvoked bool } func (p *PostgresBranchesService) Create(ctx context.Context, req *ps.CreatePostgresBranchRequest) (*ps.PostgresBranch, error) { @@ -177,3 +180,8 @@ func (p *PostgresBranchesService) ListClusterSKUs(ctx context.Context, req *ps.L p.ListClusterSKUsFnInvoked = true return p.ListClusterSKUsFn(ctx, req, opts...) } + +func (p *PostgresBranchesService) Resize(ctx context.Context, req *ps.ResizePostgresBranchRequest) (*ps.PostgresBranchClusterResizeRequest, error) { + p.ResizeFnInvoked = true + return p.ResizeFn(ctx, req) +} From b571f2a35824202933bd0981393a26feed047e87 Mon Sep 17 00:00:00 2001 From: Nick Van Wiggeren Date: Thu, 25 Jun 2026 10:49:25 +0000 Subject: [PATCH 2/2] branch resize: Postgres-aware size completion + non-silent no-op - Complete --cluster-size from the branch's own Postgres SKUs (PostgresBranches.ListClusterSKUs), returning the fully-qualified SKU names the resize endpoint accepts, instead of org-wide MySQL slugs. - On a 204 no-op, note it on stderr in non-human formats so scripted callers aren't left with completely silent output. Addresses Bugbot review comments on #1276. --- internal/cmd/branch/resize.go | 8 +++-- internal/cmdutil/completions.go | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/internal/cmd/branch/resize.go b/internal/cmd/branch/resize.go index 528bf20c7..74de19292 100644 --- a/internal/cmd/branch/resize.go +++ b/internal/cmd/branch/resize.go @@ -75,6 +75,10 @@ func ResizeCmd(ch *cmdutil.Helper) *cobra.Command { if change == nil { if ch.Printer.Format() == printer.Human { ch.Printer.Printf("Branch %s is already configured with cluster size %s.\n", printer.BoldBlue(branch), printer.BoldBlue(flags.clusterSize)) + } else { + // In non-human formats there is no resource to print, so note + // the no-op on stderr to keep stdout clean for scripts. + fmt.Fprintf(cmd.ErrOrStderr(), "Branch %s is already configured with cluster size %s; no changes applied.\n", branch, flags.clusterSize) } return nil } @@ -88,9 +92,9 @@ func ResizeCmd(ch *cmdutil.Helper) *cobra.Command { }, } - cmd.Flags().StringVar(&flags.clusterSize, "cluster-size", "", "New cluster size for the branch. Use 'pscale size cluster list' to see the valid sizes.") + cmd.Flags().StringVar(&flags.clusterSize, "cluster-size", "", "New cluster size for the branch (a fully-qualified SKU name, e.g. PS_10_GCP_X86). Use 'pscale size cluster list --engine postgresql' to see the valid sizes.") cmd.RegisterFlagCompletionFunc("cluster-size", func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { - return cmdutil.ClusterSizesCompletionFunc(ch, cmd, args, toComplete) + return cmdutil.PostgresBranchClusterSizesCompletionFunc(ch, cmd, args, toComplete) }) return cmd diff --git a/internal/cmdutil/completions.go b/internal/cmdutil/completions.go index f52c8fd02..ea3fb5215 100644 --- a/internal/cmdutil/completions.go +++ b/internal/cmdutil/completions.go @@ -136,6 +136,66 @@ func BranchClusterSizesCompletionFunc(ch *Helper, cmd *cobra.Command, args []str return clusterSizes, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveKeepOrder } +// PostgresBranchClusterSizesCompletionFunc completes cluster sizes for a +// specific Postgres branch. It returns the fully-qualified SKU names (e.g. +// PS_10_GCP_X86) that the branch's resize endpoint accepts, scoped to the +// branch's engine, provider, and architecture. +func PostgresBranchClusterSizesCompletionFunc(ch *Helper, cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + ctx := cmd.Context() + + org := ch.Config.Organization // --org flag + if org == "" { + cfg, err := ch.ConfigFS.DefaultConfig() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + org = cfg.Organization + } + + client, err := ch.Client() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + database, branch := args[0], args[1] + clusterSKUs, err := client.PostgresBranches.ListClusterSKUs(ctx, &ps.ListBranchClusterSKUsRequest{ + Organization: org, + Database: database, + Branch: branch, + }, ps.WithRates()) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + clusterSizes := make([]cobra.Completion, 0) + for _, c := range clusterSKUs { + if c.Enabled && strings.Contains(c.Name, toComplete) && c.Rate != nil && c.Name != "PS_DEV" { + var description strings.Builder + description.WriteString(c.DisplayName) + if *c.Rate > 0 { + fmt.Fprintf(&description, " · $%d/month", *c.Rate) + } + + if c.CPU != "" { + fmt.Fprintf(&description, " · %s vCPUs", c.CPU) + } + + if c.Memory > 0 { + fmt.Fprintf(&description, " · %s memory", FormatParts(c.Memory).IntString()) + } + + if c.Storage != nil && *c.Storage > 0 { + fmt.Fprintf(&description, " · %s storage", FormatPartsGB(*c.Storage).IntString()) + } + + clusterSizes = append(clusterSizes, cobra.CompletionWithDesc(c.Name, description.String())) + } + } + + return clusterSizes, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveKeepOrder +} + func RegionsCompletionFunc(ch *Helper, cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { ctx := cmd.Context()