From 1f1f7964ac5d5bc5fc8cfac71ae68ab513d75398 Mon Sep 17 00:00:00 2001 From: Elom Gomez Date: Fri, 2 Oct 2026 16:21:26 -0500 Subject: [PATCH 1/2] Add keyspace parameters commands Add `pscale keyspace parameters` to list a keyspace's VTTablet and MySQL parameters, change or reset them with `parameters set`, and follow or cancel the resulting changes with `parameters changes`. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 15 ++ internal/cmd/keyspace/keyspace.go | 1 + internal/cmd/keyspace/parameters.go | 144 ++++++++++++++ internal/cmd/keyspace/parameters_changes.go | 210 ++++++++++++++++++++ internal/cmd/keyspace/parameters_set.go | 176 ++++++++++++++++ internal/mock/keyspace.go | 43 ++++ internal/planetscale/keyspace_parameters.go | 188 ++++++++++++++++++ internal/planetscale/keyspaces.go | 6 + 8 files changed, 783 insertions(+) create mode 100644 internal/cmd/keyspace/parameters.go create mode 100644 internal/cmd/keyspace/parameters_changes.go create mode 100644 internal/cmd/keyspace/parameters_set.go create mode 100644 internal/planetscale/keyspace_parameters.go diff --git a/AGENTS.md b/AGENTS.md index aeb6b307..7b3cab45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -477,6 +477,21 @@ pscale keyspace resize status --org --forma External create required flags: `--host`, `--source-database`, `--username`, `--password`, `--ssl-mode` (`disabled`, `preferred`, `required`, `verify_ca`, `verify_identity`). Default `--port` is `3306`. +Keyspace VTTablet and MySQL parameters: list them with current and default values, then change them with `parameters set`. Prefix each parameter with its component (`vttablet.` or `mysqld.`); `--reset` sets a parameter back to its default. Changes for both components are submitted together and roll out to the keyspace; follow them with `parameters changes list`. Only one unfinished change per component can exist on a keyspace at a time. + +```bash +pscale keyspace parameters list --org --format json +pscale keyspace parameters list --org --format json --component vttablet +pscale keyspace parameters set --org --format json \ + --parameters vttablet.vreplication-parallel-insert-workers=4 \ + --parameters vttablet.vreplication_max_time_to_retry_on_error=720h +pscale keyspace parameters set --org --format json \ + --reset vttablet.vreplication-parallel-insert-workers +pscale keyspace parameters changes list --org --format json +pscale keyspace parameters changes show --org --format json +pscale keyspace parameters changes cancel --org --format json +``` + ## Vitess MoveTables Copy tables between keyspaces with `pscale branch vtctld move-tables`. `pscale workflow` is deprecated; use `move-tables` instead. JSON output includes `next_steps` — follow those commands. Typical order: create the target keyspace (`keyspace create` or `keyspace create-external`), create the workflow, poll `status`, switch replica traffic, then primary traffic (ask the user first), then `complete --dry-run` and `complete` after approval. Create with `--auto-start=false` to leave streams stopped, then `start` when you are ready. diff --git a/internal/cmd/keyspace/keyspace.go b/internal/cmd/keyspace/keyspace.go index 13085e91..7ca665af 100644 --- a/internal/cmd/keyspace/keyspace.go +++ b/internal/cmd/keyspace/keyspace.go @@ -32,6 +32,7 @@ func KeyspaceCmd(ch *cmdutil.Helper) *cobra.Command { cmd.AddCommand(ReadOnlyRegionsCmd(ch)) cmd.AddCommand(UpdateSettingsCmd(ch)) cmd.AddCommand(SettingsCmd(ch)) + cmd.AddCommand(ParametersCmd(ch)) return cmd } diff --git a/internal/cmd/keyspace/parameters.go b/internal/cmd/keyspace/parameters.go new file mode 100644 index 00000000..0cb3cec6 --- /dev/null +++ b/internal/cmd/keyspace/parameters.go @@ -0,0 +1,144 @@ +package keyspace + +import ( + "encoding/json" + "fmt" + "slices" + "strings" + + "github.com/planetscale/cli/internal/cmdutil" + ps "github.com/planetscale/cli/internal/planetscale" + "github.com/planetscale/cli/internal/printer" + "github.com/spf13/cobra" +) + +var parameterComponents = []string{"vttablet", "mysqld"} + +// ParametersCmd lists and changes the VTTablet and MySQL parameters of a keyspace. +func ParametersCmd(ch *cmdutil.Helper) *cobra.Command { + var flags struct { + component string + } + + long := `List the VTTablet and MySQL parameters of a keyspace, including their current and default values. + +To change parameters, use 'pscale keyspace parameters set --parameters component.name=value'.` + + run := func(cmd *cobra.Command, args []string) error { + database, branch, keyspace := args[0], args[1], args[2] + + if flags.component != "" && !slices.Contains(parameterComponents, flags.component) { + return fmt.Errorf("invalid --component %q, must be one of: %s", flags.component, strings.Join(parameterComponents, ", ")) + } + + client, err := ch.Client() + if err != nil { + return err + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching parameters for keyspace %s in %s/%s...", printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch))) + defer end() + + parameters, err := client.Keyspaces.ListParameters(cmd.Context(), &ps.ListKeyspaceParametersRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + }) + 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() + + if parameters == nil { + return keyspaceNotFoundError(ch, database, branch, keyspace) + } + + var selected []*ps.VitessParameter + if flags.component == "" || flags.component == "vttablet" { + selected = append(selected, parameters.VTTablet...) + } + if flags.component == "" || flags.component == "mysqld" { + selected = append(selected, parameters.MySQL...) + } + + return ch.Printer.PrintResource(toKeyspaceParameters(selected)) + } + + registerFlags := func(cmd *cobra.Command) { + cmd.Flags().StringVar(&flags.component, "component", "", "Only show parameters for this component: vttablet or mysqld.") + } + + cmd := &cobra.Command{ + Use: "parameters ", + Aliases: []string{"params"}, + Short: "List and change the VTTablet and MySQL parameters of a keyspace", + Long: long, + Args: cmdutil.RequiredArgs("database", "branch", "keyspace"), + RunE: run, + } + registerFlags(cmd) + + listCmd := &cobra.Command{ + Use: "list ", + Short: "List the VTTablet and MySQL parameters of a keyspace", + Long: long, + Args: cmdutil.RequiredArgs("database", "branch", "keyspace"), + RunE: run, + } + registerFlags(listCmd) + + cmd.AddCommand(listCmd, parametersSetCmd(ch), parametersChangesCmd(ch)) + return cmd +} + +func keyspaceNotFoundError(ch *cmdutil.Helper, database, branch, keyspace string) error { + return fmt.Errorf("keyspace %s does not exist in branch %s (database: %s, organization: %s)", printer.BoldBlue(keyspace), printer.BoldBlue(branch), printer.BoldBlue(database), printer.BoldBlue(ch.Config.Organization)) +} + +type keyspaceParameter struct { + Component string `header:"component" json:"component"` + Name string `header:"name" json:"name"` + Value string `header:"value" json:"value"` + Default string `header:"default" json:"default_value"` + Type string `header:"type" json:"parameter_type"` + Override bool `header:"override" json:"override"` + + orig *ps.VitessParameter +} + +func toKeyspaceParameters(parameters []*ps.VitessParameter) []*keyspaceParameter { + out := make([]*keyspaceParameter, 0, len(parameters)) + for _, param := range parameters { + out = append(out, &keyspaceParameter{ + Component: param.Component, + Name: param.Name, + Value: stringValue(param.Value), + Default: stringValue(param.DefaultValue), + Type: param.ParameterType, + Override: param.Override, + orig: param, + }) + } + return out +} + +func (p *keyspaceParameter) MarshalJSON() ([]byte, error) { + return json.MarshalIndent(p.orig, "", " ") +} + +func (p *keyspaceParameter) MarshalCSVValue() interface{} { + return []*keyspaceParameter{p} +} + +func stringValue(value *string) string { + if value == nil { + return "" + } + return *value +} diff --git a/internal/cmd/keyspace/parameters_changes.go b/internal/cmd/keyspace/parameters_changes.go new file mode 100644 index 00000000..f226e524 --- /dev/null +++ b/internal/cmd/keyspace/parameters_changes.go @@ -0,0 +1,210 @@ +package keyspace + +import ( + "encoding/json" + "fmt" + "sort" + "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 parametersChangesCmd(ch *cmdutil.Helper) *cobra.Command { + cmd := &cobra.Command{ + Use: "changes ", + Short: "Manage parameter changes to a keyspace", + } + cmd.AddCommand(parametersChangesListCmd(ch), parametersChangesShowCmd(ch), parametersChangesCancelCmd(ch)) + return cmd +} + +func parametersChangesListCmd(ch *cmdutil.Helper) *cobra.Command { + var flags struct { + page int + perPage int + } + + cmd := &cobra.Command{ + Use: "list ", + Short: "List parameter changes to a keyspace", + Args: cmdutil.RequiredArgs("database", "branch", "keyspace"), + Aliases: []string{"ls"}, + RunE: func(cmd *cobra.Command, args []string) error { + database, branch, keyspace := args[0], args[1], args[2] + + client, err := ch.Client() + if err != nil { + return err + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching parameter changes for keyspace %s in %s/%s...", printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch))) + defer end() + + changes, err := client.Keyspaces.ListConfigChanges(cmd.Context(), &ps.ListKeyspaceConfigChangesRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + Page: flags.page, + PerPage: flags.perPage, + }) + if err != nil { + return parameterChangeError(ch, err, database, branch, keyspace) + } + end() + + if len(changes) == 0 && ch.Printer.Format() == printer.Human { + ch.Printer.Printf("No parameter changes found for keyspace %s in %s/%s.\n", printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch)) + return nil + } + + return ch.Printer.PrintResource(toKeyspaceConfigChanges(changes)) + }, + } + + cmd.Flags().IntVar(&flags.page, "page", 0, "Page number to fetch") + cmd.Flags().IntVar(&flags.perPage, "per-page", 25, "Number of results per page") + return cmd +} + +func parametersChangesShowCmd(ch *cmdutil.Helper) *cobra.Command { + return &cobra.Command{ + Use: "show ", + Short: "Show a parameter change to a keyspace", + Args: cmdutil.RequiredArgs("database", "branch", "keyspace", "change-id"), + Aliases: []string{"get"}, + RunE: func(cmd *cobra.Command, args []string) error { + database, branch, keyspace, changeID := args[0], args[1], args[2], args[3] + + client, err := ch.Client() + if err != nil { + return err + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching parameter change %s for keyspace %s...", printer.BoldBlue(changeID), printer.BoldBlue(keyspace))) + defer end() + + change, err := client.Keyspaces.GetConfigChange(cmd.Context(), &ps.GetKeyspaceConfigChangeRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + ID: changeID, + }) + if err != nil { + if cmdutil.ErrCode(err) == ps.ErrNotFound { + return fmt.Errorf("parameter change %s does not exist for keyspace %s in %s/%s", printer.BoldBlue(changeID), printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch)) + } + return cmdutil.HandleError(err) + } + end() + + return ch.Printer.PrintResource(toKeyspaceConfigChange(change)) + }, + } +} + +func parametersChangesCancelCmd(ch *cmdutil.Helper) *cobra.Command { + return &cobra.Command{ + Use: "cancel ", + Short: "Cancel a pending parameter change to a keyspace", + Args: cmdutil.RequiredArgs("database", "branch", "keyspace", "change-id"), + RunE: func(cmd *cobra.Command, args []string) error { + database, branch, keyspace, changeID := args[0], args[1], args[2], args[3] + + client, err := ch.Client() + if err != nil { + return err + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Canceling parameter change %s for keyspace %s...", printer.BoldBlue(changeID), printer.BoldBlue(keyspace))) + defer end() + + if err := client.Keyspaces.CancelConfigChange(cmd.Context(), &ps.CancelKeyspaceConfigChangeRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + ID: changeID, + }); err != nil { + if cmdutil.ErrCode(err) == ps.ErrNotFound { + return fmt.Errorf("parameter change %s does not exist for keyspace %s in %s/%s", printer.BoldBlue(changeID), printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch)) + } + return cmdutil.HandleError(err) + } + end() + + if ch.Printer.Format() == printer.Human { + ch.Printer.Printf("Canceled parameter change %s for keyspace %s in %s/%s.\n", printer.BoldBlue(changeID), printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch)) + return nil + } + + return ch.Printer.PrintResource(map[string]string{ + "result": "change canceled", + "change_id": changeID, + }) + }, + } +} + +type keyspaceConfigChange struct { + ID string `header:"id" json:"id"` + Component string `header:"component" json:"change_type"` + State string `header:"state" json:"state"` + Changes string `header:"changes" json:"changes"` + CreatedAt string `header:"created at" json:"created_at"` + + orig *ps.KeyspaceConfigChange +} + +func toKeyspaceConfigChange(change *ps.KeyspaceConfigChange) *keyspaceConfigChange { + return &keyspaceConfigChange{ + ID: change.ID, + Component: change.ChangeType, + State: change.State, + Changes: formatParameterChanges(change), + CreatedAt: change.CreatedAt.UTC().Format("2006-01-02 15:04:05"), + orig: change, + } +} + +func toKeyspaceConfigChanges(changes []*ps.KeyspaceConfigChange) []*keyspaceConfigChange { + out := make([]*keyspaceConfigChange, 0, len(changes)) + for _, change := range changes { + out = append(out, toKeyspaceConfigChange(change)) + } + return out +} + +func (c *keyspaceConfigChange) MarshalJSON() ([]byte, error) { + return json.MarshalIndent(c.orig, "", " ") +} + +func (c *keyspaceConfigChange) MarshalCSVValue() interface{} { + return []*keyspaceConfigChange{c} +} + +func formatParameterChanges(change *ps.KeyspaceConfigChange) string { + names := make([]string, 0, len(change.NewOptions)) + for name := range change.NewOptions { + names = append(names, name) + } + sort.Strings(names) + + parts := make([]string, 0, len(names)) + for _, name := range names { + before := "(default)" + if previous, ok := change.PreviousOptions[name]; ok && previous != nil { + before = *previous + } + after := "(default)" + if value := change.NewOptions[name]; value != nil { + after = *value + } + parts = append(parts, fmt.Sprintf("%s: %s → %s", name, before, after)) + } + return strings.Join(parts, ", ") +} diff --git a/internal/cmd/keyspace/parameters_set.go b/internal/cmd/keyspace/parameters_set.go new file mode 100644 index 00000000..6b26af10 --- /dev/null +++ b/internal/cmd/keyspace/parameters_set.go @@ -0,0 +1,176 @@ +package keyspace + +import ( + "context" + "fmt" + "slices" + "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 parametersSetCmd(ch *cmdutil.Helper) *cobra.Command { + var flags struct { + parameters []string + resets []string + } + + cmd := &cobra.Command{ + Use: "set ", + Short: "Change the VTTablet and MySQL parameters of a keyspace", + Long: `Change VTTablet or MySQL parameters on a keyspace. Pass each parameter as component.name=value, where component is vttablet or mysqld, and pass --reset component.name to set a parameter back to its default. + +All changes are submitted together and rolled out to the keyspace. Use 'pscale keyspace parameters changes list' to follow the rollout.`, + Example: ` pscale keyspace parameters set \ + --parameters vttablet.vreplication-parallel-insert-workers=4 \ + --parameters vttablet.vreplication_max_time_to_retry_on_error=720h + + pscale keyspace parameters set \ + --reset vttablet.vreplication-parallel-insert-workers`, + Args: cmdutil.RequiredArgs("database", "branch", "keyspace"), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + database, branch, keyspace := args[0], args[1], args[2] + + changes, err := parseParameterChanges(flags.parameters, flags.resets) + if err != nil { + return err + } + + client, err := ch.Client() + if err != nil { + return err + } + + end := ch.Printer.PrintProgress(fmt.Sprintf("Submitting parameter changes for keyspace %s in %s/%s...", printer.BoldBlue(keyspace), printer.BoldBlue(database), printer.BoldBlue(branch))) + defer end() + + drafts := make([]*ps.KeyspaceConfigChange, 0, len(changes)) + for _, component := range parameterComponents { + options, ok := changes[component] + if !ok { + continue + } + + draft, err := client.Keyspaces.CreateConfigChange(ctx, &ps.CreateKeyspaceConfigChangeRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + ChangeType: component, + Options: options, + }) + if err != nil { + cancelDrafts(ctx, client, ch.Config.Organization, database, branch, keyspace, drafts) + return parameterChangeError(ch, err, database, branch, keyspace) + } + drafts = append(drafts, draft) + } + + ids := make([]string, 0, len(drafts)) + for _, draft := range drafts { + ids = append(ids, draft.ID) + } + + if err := client.Keyspaces.SubmitConfigChanges(ctx, &ps.SubmitConfigChangesRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + IDs: ids, + }); err != nil { + cancelDrafts(ctx, client, ch.Config.Organization, database, branch, keyspace, drafts) + return parameterChangeError(ch, err, database, branch, keyspace) + } + + submitted := make([]*ps.KeyspaceConfigChange, 0, len(drafts)) + for _, draft := range drafts { + change, err := client.Keyspaces.GetConfigChange(ctx, &ps.GetKeyspaceConfigChangeRequest{ + Organization: ch.Config.Organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + ID: draft.ID, + }) + if err != nil { + change = draft + } + submitted = append(submitted, change) + } + end() + + return ch.Printer.PrintResource(toKeyspaceConfigChanges(submitted)) + }, + } + + cmd.Flags().StringArrayVar(&flags.parameters, "parameters", nil, "Set a parameter as component.name=value, where component is vttablet or mysqld (e.g. vttablet.vreplication-parallel-insert-workers=4). Repeatable. Use 'pscale keyspace parameters list' to see available parameters.") + cmd.Flags().StringArrayVar(&flags.resets, "reset", nil, "Set a parameter back to its default, as component.name (e.g. vttablet.vreplication-parallel-insert-workers). Repeatable.") + + return cmd +} + +// parseParameterChanges groups --parameters and --reset values by component. +// A nil value resets the parameter to its default. +func parseParameterChanges(sets, resets []string) (map[string]map[string]*string, error) { + if len(sets) == 0 && len(resets) == 0 { + return nil, fmt.Errorf("pass at least one --parameters component.name=value or --reset component.name") + } + + changes := make(map[string]map[string]*string) + add := func(flag, raw, key string, value *string) error { + component, name, found := strings.Cut(key, ".") + if !found || component == "" || name == "" { + return fmt.Errorf("invalid %s %q: parameter must be prefixed with its component, e.g. vttablet.%s", flag, raw, key) + } + if !slices.Contains(parameterComponents, component) { + return fmt.Errorf("invalid %s %q: component must be one of: %s", flag, raw, strings.Join(parameterComponents, ", ")) + } + if _, exists := changes[component][name]; exists { + return fmt.Errorf("parameter %s.%s is passed more than once", component, name) + } + if changes[component] == nil { + changes[component] = make(map[string]*string) + } + changes[component][name] = value + return nil + } + + for _, set := range sets { + key, value, found := strings.Cut(set, "=") + if !found { + return nil, fmt.Errorf("invalid --parameters %q: expected component.name=value (e.g. vttablet.vreplication-parallel-insert-workers=4)", set) + } + if err := add("--parameters", set, key, &value); err != nil { + return nil, err + } + } + + for _, reset := range resets { + if err := add("--reset", reset, reset, nil); err != nil { + return nil, err + } + } + + return changes, nil +} + +func cancelDrafts(ctx context.Context, client *ps.Client, organization, database, branch, keyspace string, drafts []*ps.KeyspaceConfigChange) { + for _, draft := range drafts { + _ = client.Keyspaces.CancelConfigChange(ctx, &ps.CancelKeyspaceConfigChangeRequest{ + Organization: organization, + Database: database, + Branch: branch, + Keyspace: keyspace, + ID: draft.ID, + }) + } +} + +func parameterChangeError(ch *cmdutil.Helper, err error, database, branch, keyspace string) error { + if cmdutil.ErrCode(err) == ps.ErrNotFound { + return keyspaceNotFoundError(ch, database, branch, keyspace) + } + return cmdutil.HandleError(err) +} diff --git a/internal/mock/keyspace.go b/internal/mock/keyspace.go index a9b7f60d..cfb14d4f 100644 --- a/internal/mock/keyspace.go +++ b/internal/mock/keyspace.go @@ -48,6 +48,19 @@ type KeyspacesService struct { UpdateSettingsFn func(context.Context, *ps.UpdateKeyspaceSettingsRequest) (*ps.Keyspace, error) UpdateSettingsFnInvoked bool + + ListParametersFn func(context.Context, *ps.ListKeyspaceParametersRequest) (*ps.KeyspaceParameters, error) + ListParametersFnInvoked bool + CreateConfigChangeFn func(context.Context, *ps.CreateKeyspaceConfigChangeRequest) (*ps.KeyspaceConfigChange, error) + CreateConfigChangeFnInvoked bool + SubmitConfigChangesFn func(context.Context, *ps.SubmitConfigChangesRequest) error + SubmitConfigChangesFnInvoked bool + ListConfigChangesFn func(context.Context, *ps.ListKeyspaceConfigChangesRequest) ([]*ps.KeyspaceConfigChange, error) + ListConfigChangesFnInvoked bool + GetConfigChangeFn func(context.Context, *ps.GetKeyspaceConfigChangeRequest) (*ps.KeyspaceConfigChange, error) + GetConfigChangeFnInvoked bool + CancelConfigChangeFn func(context.Context, *ps.CancelKeyspaceConfigChangeRequest) error + CancelConfigChangeFnInvoked bool } func (s *KeyspacesService) List(ctx context.Context, req *ps.ListKeyspacesRequest) ([]*ps.Keyspace, error) { @@ -119,3 +132,33 @@ func (s *KeyspacesService) UpdateSettings(ctx context.Context, req *ps.UpdateKey s.UpdateSettingsFnInvoked = true return s.UpdateSettingsFn(ctx, req) } + +func (s *KeyspacesService) ListParameters(ctx context.Context, req *ps.ListKeyspaceParametersRequest) (*ps.KeyspaceParameters, error) { + s.ListParametersFnInvoked = true + return s.ListParametersFn(ctx, req) +} + +func (s *KeyspacesService) CreateConfigChange(ctx context.Context, req *ps.CreateKeyspaceConfigChangeRequest) (*ps.KeyspaceConfigChange, error) { + s.CreateConfigChangeFnInvoked = true + return s.CreateConfigChangeFn(ctx, req) +} + +func (s *KeyspacesService) SubmitConfigChanges(ctx context.Context, req *ps.SubmitConfigChangesRequest) error { + s.SubmitConfigChangesFnInvoked = true + return s.SubmitConfigChangesFn(ctx, req) +} + +func (s *KeyspacesService) ListConfigChanges(ctx context.Context, req *ps.ListKeyspaceConfigChangesRequest) ([]*ps.KeyspaceConfigChange, error) { + s.ListConfigChangesFnInvoked = true + return s.ListConfigChangesFn(ctx, req) +} + +func (s *KeyspacesService) GetConfigChange(ctx context.Context, req *ps.GetKeyspaceConfigChangeRequest) (*ps.KeyspaceConfigChange, error) { + s.GetConfigChangeFnInvoked = true + return s.GetConfigChangeFn(ctx, req) +} + +func (s *KeyspacesService) CancelConfigChange(ctx context.Context, req *ps.CancelKeyspaceConfigChangeRequest) error { + s.CancelConfigChangeFnInvoked = true + return s.CancelConfigChangeFn(ctx, req) +} diff --git a/internal/planetscale/keyspace_parameters.go b/internal/planetscale/keyspace_parameters.go new file mode 100644 index 00000000..8c5c89dc --- /dev/null +++ b/internal/planetscale/keyspace_parameters.go @@ -0,0 +1,188 @@ +package planetscale + +import ( + "context" + "fmt" + "net/http" + "path" + "time" +) + +// VitessParameter is a VTTablet or MySQL parameter that can be set on a keyspace. +type VitessParameter struct { + Name string `json:"name"` + Title string `json:"title,omitempty"` + Component string `json:"component"` + Description string `json:"description"` + Category string `json:"category"` + ParameterType string `json:"parameter_type"` + DefaultValue *string `json:"default_value"` + Value *string `json:"value"` + Override bool `json:"override"` + Min any `json:"min,omitempty"` + Max any `json:"max,omitempty"` + Options []string `json:"options,omitempty"` +} + +// KeyspaceParameters are the parameters of a keyspace, grouped by component. +type KeyspaceParameters struct { + VTTablet []*VitessParameter `json:"vttablet"` + MySQL []*VitessParameter `json:"mysqld"` +} + +// KeyspaceConfigChange is a request to change a keyspace's parameters. +type KeyspaceConfigChange struct { + ID string `json:"id"` + State string `json:"state"` + ChangeType string `json:"change_type"` + KeyspaceName string `json:"keyspace_name"` + PreviousOptions map[string]*string `json:"previous_options"` + NewOptions map[string]*string `json:"new_options"` + ErrorMessage *string `json:"error_message"` + QueuedUntil *time.Time `json:"queued_until"` + StartedAt *time.Time `json:"started_at"` + CompletedAt *time.Time `json:"completed_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Actor *Actor `json:"actor"` +} + +type ListKeyspaceParametersRequest struct { + Organization string + Database string + Branch string + Keyspace string +} + +// CreateKeyspaceConfigChangeRequest creates a draft change for one component. +// A nil option value resets that parameter to its default. +type CreateKeyspaceConfigChangeRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + Keyspace string `json:"-"` + ChangeType string `json:"change_type"` + Options map[string]*string `json:"options"` +} + +// SubmitConfigChangesRequest applies draft config changes on a branch. +type SubmitConfigChangesRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"-"` + IDs []string `json:"ids"` +} + +type ListKeyspaceConfigChangesRequest struct { + Organization string + Database string + Branch string + Keyspace string + Page int + PerPage int +} + +type GetKeyspaceConfigChangeRequest struct { + Organization string + Database string + Branch string + Keyspace string + ID string +} + +type CancelKeyspaceConfigChangeRequest struct { + Organization string + Database string + Branch string + Keyspace string + ID string +} + +type branchVitessParametersResponse struct { + Keyspaces map[string]*KeyspaceParameters `json:"keyspaces"` +} + +type keyspaceConfigChangesResponse struct { + ConfigChanges []*KeyspaceConfigChange `json:"data"` +} + +// ListParameters returns the VTTablet and MySQL parameters of a keyspace, or +// nil when the branch has no keyspace with that name. +func (s *keyspacesService) ListParameters(ctx context.Context, listReq *ListKeyspaceParametersRequest) (*KeyspaceParameters, error) { + req, err := s.client.newRequest(http.MethodGet, path.Join(databaseBranchAPIPath(listReq.Organization, listReq.Database, listReq.Branch), "vitess-parameters"), nil) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + + resp := &branchVitessParametersResponse{} + if err := s.client.do(ctx, req, resp); err != nil { + return nil, err + } + + return resp.Keyspaces[listReq.Keyspace], nil +} + +func (s *keyspacesService) CreateConfigChange(ctx context.Context, createReq *CreateKeyspaceConfigChangeRequest) (*KeyspaceConfigChange, error) { + req, err := s.client.newRequest(http.MethodPost, keyspaceConfigChangesAPIPath(createReq.Organization, createReq.Database, createReq.Branch, createReq.Keyspace), createReq) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + + change := &KeyspaceConfigChange{} + if err := s.client.do(ctx, req, change); err != nil { + return nil, err + } + + return change, nil +} + +func (s *keyspacesService) SubmitConfigChanges(ctx context.Context, submitReq *SubmitConfigChangesRequest) error { + req, err := s.client.newRequest(http.MethodPost, path.Join(databaseBranchAPIPath(submitReq.Organization, submitReq.Database, submitReq.Branch), "config-changes", "submit"), submitReq) + if err != nil { + return fmt.Errorf("error creating http request: %w", err) + } + + return s.client.do(ctx, req, nil) +} + +func (s *keyspacesService) ListConfigChanges(ctx context.Context, listReq *ListKeyspaceConfigChangesRequest) ([]*KeyspaceConfigChange, error) { + values := defaultListOptions(WithPage(listReq.Page), WithPerPage(listReq.PerPage)) + req, err := s.client.newRequest(http.MethodGet, keyspaceConfigChangesAPIPath(listReq.Organization, listReq.Database, listReq.Branch, listReq.Keyspace), nil, WithQueryParams(*values.URLValues)) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + + resp := &keyspaceConfigChangesResponse{} + if err := s.client.do(ctx, req, resp); err != nil { + return nil, err + } + + return resp.ConfigChanges, nil +} + +func (s *keyspacesService) GetConfigChange(ctx context.Context, getReq *GetKeyspaceConfigChangeRequest) (*KeyspaceConfigChange, error) { + req, err := s.client.newRequest(http.MethodGet, path.Join(keyspaceConfigChangesAPIPath(getReq.Organization, getReq.Database, getReq.Branch, getReq.Keyspace), getReq.ID), nil) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + + change := &KeyspaceConfigChange{} + if err := s.client.do(ctx, req, change); err != nil { + return nil, err + } + + return change, nil +} + +func (s *keyspacesService) CancelConfigChange(ctx context.Context, cancelReq *CancelKeyspaceConfigChangeRequest) error { + req, err := s.client.newRequest(http.MethodDelete, path.Join(keyspaceConfigChangesAPIPath(cancelReq.Organization, cancelReq.Database, cancelReq.Branch, cancelReq.Keyspace), cancelReq.ID), nil) + if err != nil { + return fmt.Errorf("error creating http request: %w", err) + } + + return s.client.do(ctx, req, nil) +} + +func keyspaceConfigChangesAPIPath(org, db, branch, keyspace string) string { + return path.Join(keyspaceAPIPath(org, db, branch, keyspace), "config-changes") +} diff --git a/internal/planetscale/keyspaces.go b/internal/planetscale/keyspaces.go index 8792c8c6..08a4c6a2 100644 --- a/internal/planetscale/keyspaces.go +++ b/internal/planetscale/keyspaces.go @@ -285,6 +285,12 @@ type KeyspacesService interface { ResizeStatus(context.Context, *KeyspaceResizeStatusRequest) (*KeyspaceResizeRequest, error) RolloutStatus(context.Context, *KeyspaceRolloutStatusRequest) (*KeyspaceRollout, error) UpdateSettings(context.Context, *UpdateKeyspaceSettingsRequest) (*Keyspace, error) + ListParameters(context.Context, *ListKeyspaceParametersRequest) (*KeyspaceParameters, error) + CreateConfigChange(context.Context, *CreateKeyspaceConfigChangeRequest) (*KeyspaceConfigChange, error) + SubmitConfigChanges(context.Context, *SubmitConfigChangesRequest) error + ListConfigChanges(context.Context, *ListKeyspaceConfigChangesRequest) ([]*KeyspaceConfigChange, error) + GetConfigChange(context.Context, *GetKeyspaceConfigChangeRequest) (*KeyspaceConfigChange, error) + CancelConfigChange(context.Context, *CancelKeyspaceConfigChangeRequest) error } type keyspacesService struct { From 3b34d70622cb4e7e831345b5751ecd6d178afbe4 Mon Sep 17 00:00:00 2001 From: Elom Gomez Date: Fri, 2 Oct 2026 16:29:49 -0500 Subject: [PATCH 2/2] Use namespace for keyspace parameters and skip unchanged values Match `pscale branch parameters` by calling vttablet and mysqld namespaces, both for the `--namespace` filter and the `--parameters namespace.name=value` flag. Change summaries now list only the parameters whose values change. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 4 +-- internal/cmd/keyspace/parameters.go | 20 +++++------ internal/cmd/keyspace/parameters_changes.go | 32 +++++++++++------ internal/cmd/keyspace/parameters_set.go | 38 ++++++++++----------- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7b3cab45..be5797c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -477,11 +477,11 @@ pscale keyspace resize status --org --forma External create required flags: `--host`, `--source-database`, `--username`, `--password`, `--ssl-mode` (`disabled`, `preferred`, `required`, `verify_ca`, `verify_identity`). Default `--port` is `3306`. -Keyspace VTTablet and MySQL parameters: list them with current and default values, then change them with `parameters set`. Prefix each parameter with its component (`vttablet.` or `mysqld.`); `--reset` sets a parameter back to its default. Changes for both components are submitted together and roll out to the keyspace; follow them with `parameters changes list`. Only one unfinished change per component can exist on a keyspace at a time. +Keyspace VTTablet and MySQL parameters: list them with current and default values, then change them with `parameters set`. Prefix each parameter with its namespace (`vttablet.` or `mysqld.`); `--reset` sets a parameter back to its default. Changes for both namespaces are submitted together and roll out to the keyspace; follow them with `parameters changes list`. Only one unfinished change per namespace can exist on a keyspace at a time. ```bash pscale keyspace parameters list --org --format json -pscale keyspace parameters list --org --format json --component vttablet +pscale keyspace parameters list --org --format json --namespace vttablet pscale keyspace parameters set --org --format json \ --parameters vttablet.vreplication-parallel-insert-workers=4 \ --parameters vttablet.vreplication_max_time_to_retry_on_error=720h diff --git a/internal/cmd/keyspace/parameters.go b/internal/cmd/keyspace/parameters.go index 0cb3cec6..ede12d9c 100644 --- a/internal/cmd/keyspace/parameters.go +++ b/internal/cmd/keyspace/parameters.go @@ -12,23 +12,23 @@ import ( "github.com/spf13/cobra" ) -var parameterComponents = []string{"vttablet", "mysqld"} +var parameterNamespaces = []string{"vttablet", "mysqld"} // ParametersCmd lists and changes the VTTablet and MySQL parameters of a keyspace. func ParametersCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { - component string + namespace string } long := `List the VTTablet and MySQL parameters of a keyspace, including their current and default values. -To change parameters, use 'pscale keyspace parameters set --parameters component.name=value'.` +To change parameters, use 'pscale keyspace parameters set --parameters namespace.name=value'.` run := func(cmd *cobra.Command, args []string) error { database, branch, keyspace := args[0], args[1], args[2] - if flags.component != "" && !slices.Contains(parameterComponents, flags.component) { - return fmt.Errorf("invalid --component %q, must be one of: %s", flags.component, strings.Join(parameterComponents, ", ")) + if flags.namespace != "" && !slices.Contains(parameterNamespaces, flags.namespace) { + return fmt.Errorf("invalid --namespace %q, must be one of: %s", flags.namespace, strings.Join(parameterNamespaces, ", ")) } client, err := ch.Client() @@ -60,10 +60,10 @@ To change parameters, use 'pscale keyspace parameters set ", Short: "Change the VTTablet and MySQL parameters of a keyspace", - Long: `Change VTTablet or MySQL parameters on a keyspace. Pass each parameter as component.name=value, where component is vttablet or mysqld, and pass --reset component.name to set a parameter back to its default. + Long: `Change VTTablet or MySQL parameters on a keyspace. Pass each parameter as namespace.name=value, where namespace is vttablet or mysqld, and pass --reset namespace.name to set a parameter back to its default. All changes are submitted together and rolled out to the keyspace. Use 'pscale keyspace parameters changes list' to follow the rollout.`, Example: ` pscale keyspace parameters set \ @@ -49,8 +49,8 @@ All changes are submitted together and rolled out to the keyspace. Use 'pscale k defer end() drafts := make([]*ps.KeyspaceConfigChange, 0, len(changes)) - for _, component := range parameterComponents { - options, ok := changes[component] + for _, namespace := range parameterNamespaces { + options, ok := changes[namespace] if !ok { continue } @@ -60,7 +60,7 @@ All changes are submitted together and rolled out to the keyspace. Use 'pscale k Database: database, Branch: branch, Keyspace: keyspace, - ChangeType: component, + ChangeType: namespace, Options: options, }) if err != nil { @@ -105,42 +105,42 @@ All changes are submitted together and rolled out to the keyspace. Use 'pscale k }, } - cmd.Flags().StringArrayVar(&flags.parameters, "parameters", nil, "Set a parameter as component.name=value, where component is vttablet or mysqld (e.g. vttablet.vreplication-parallel-insert-workers=4). Repeatable. Use 'pscale keyspace parameters list' to see available parameters.") - cmd.Flags().StringArrayVar(&flags.resets, "reset", nil, "Set a parameter back to its default, as component.name (e.g. vttablet.vreplication-parallel-insert-workers). Repeatable.") + cmd.Flags().StringArrayVar(&flags.parameters, "parameters", nil, "Set a parameter as namespace.name=value, where namespace is vttablet or mysqld (e.g. vttablet.vreplication-parallel-insert-workers=4). Repeatable. Use 'pscale keyspace parameters list' to see available parameters.") + cmd.Flags().StringArrayVar(&flags.resets, "reset", nil, "Set a parameter back to its default, as namespace.name (e.g. vttablet.vreplication-parallel-insert-workers). Repeatable.") return cmd } -// parseParameterChanges groups --parameters and --reset values by component. +// parseParameterChanges groups --parameters and --reset values by namespace. // A nil value resets the parameter to its default. func parseParameterChanges(sets, resets []string) (map[string]map[string]*string, error) { if len(sets) == 0 && len(resets) == 0 { - return nil, fmt.Errorf("pass at least one --parameters component.name=value or --reset component.name") + return nil, fmt.Errorf("pass at least one --parameters namespace.name=value or --reset namespace.name") } changes := make(map[string]map[string]*string) add := func(flag, raw, key string, value *string) error { - component, name, found := strings.Cut(key, ".") - if !found || component == "" || name == "" { - return fmt.Errorf("invalid %s %q: parameter must be prefixed with its component, e.g. vttablet.%s", flag, raw, key) + namespace, name, found := strings.Cut(key, ".") + if !found || namespace == "" || name == "" { + return fmt.Errorf("invalid %s %q: parameter must be prefixed with its namespace, e.g. vttablet.%s", flag, raw, key) } - if !slices.Contains(parameterComponents, component) { - return fmt.Errorf("invalid %s %q: component must be one of: %s", flag, raw, strings.Join(parameterComponents, ", ")) + if !slices.Contains(parameterNamespaces, namespace) { + return fmt.Errorf("invalid %s %q: namespace must be one of: %s", flag, raw, strings.Join(parameterNamespaces, ", ")) } - if _, exists := changes[component][name]; exists { - return fmt.Errorf("parameter %s.%s is passed more than once", component, name) + if _, exists := changes[namespace][name]; exists { + return fmt.Errorf("parameter %s.%s is passed more than once", namespace, name) } - if changes[component] == nil { - changes[component] = make(map[string]*string) + if changes[namespace] == nil { + changes[namespace] = make(map[string]*string) } - changes[component][name] = value + changes[namespace][name] = value return nil } for _, set := range sets { key, value, found := strings.Cut(set, "=") if !found { - return nil, fmt.Errorf("invalid --parameters %q: expected component.name=value (e.g. vttablet.vreplication-parallel-insert-workers=4)", set) + return nil, fmt.Errorf("invalid --parameters %q: expected namespace.name=value (e.g. vttablet.vreplication-parallel-insert-workers=4)", set) } if err := add("--parameters", set, key, &value); err != nil { return nil, err