diff --git a/AGENTS.md b/AGENTS.md index aeb6b307..a544e4fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -545,9 +545,9 @@ pscale role create --org --format json --inheri pscale role create --org --format json --inherited-roles neki_viewer,pg_read_all_data pscale role create --org --format json --inherited-roles neki_operator,postgres -# Role connection details for a branch replica, read-only replica, or PgBouncer +# Role connection details for a branch replica, dedicated read replica, or PgBouncer pscale role get --org --format json --replica -pscale role get --org --format json --read-only-replica +pscale role get --org --format json --dedicated-read-replica pscale role get --org --format json --bouncer # Neki: router is a username suffix; replica and shard are libpq options on the URL @@ -573,34 +573,35 @@ pscale branch resize cancel --org --format json - At least one of `--cluster-size`, `--replicas`, or `--parameters` is required. - Neki `--inherited-roles` may include `neki_viewer` (must also include `pg_read_all_data`) and `neki_operator` (must also include `postgres`). The API rejects those pairings if the required role is missing. -- `--replica`, `--read-only-replica`, and `--bouncer` are mutually exclusive. `--read-only-replica` and `--bouncer` are Postgres-only and cannot combine with `--router` or `--shard`. On Neki, `--replica`, `--shard`, and `--router` can be combined. `--router` rewrites `username` to `user|`. `--replica` and `--shard` set `options` (`-c __neki.target=REPLICA`, `-c __neki.shard=…`) and add them to `database_url`. List names with `pscale branch router list` and `pscale branch shard list` (use the shard name, not the API id). +- `--replica`, `--dedicated-read-replica`, and `--bouncer` are mutually exclusive. `--dedicated-read-replica` and `--bouncer` are Postgres-only and cannot combine with `--router` or `--shard`. The deprecated `--read-only-replica` flag is a compatibility alias for `--dedicated-read-replica`. On Neki, `--replica`, `--shard`, and `--router` can be combined. `--router` rewrites `username` to `user|`. `--replica` and `--shard` set `options` (`-c __neki.target=REPLICA`, `-c __neki.shard=…`) and add them to `database_url`. List names with `pscale branch router list` and `pscale branch shard list` (use the shard name, not the API id). - `--parameters` values are validated against the catalog before submission; unknown or immutable parameters fail fast. Parameters with `"restart": true` in the catalog restart the database when applied — surface this to the user before changing them. - Change request `state` is one of `queued`, `pending`, `resizing`, `completed`, `canceled`. Only `completed` and `canceled` are terminal. Without `--wait`, poll `resize status` instead of assuming completion. - A no-op (branch already matches the requested configuration) prints `{"result": "no_change", "branch": ""}` in JSON mode instead of a change request. - `resize cancel` prints `{"result": "canceled", "branch": ""}` in JSON mode. - MySQL databases are rejected: use `pscale keyspace resize` for Vitess keyspaces. -## Postgres read-only replicas +## Postgres dedicated read replicas -Read-only replicas provide dedicated regional capacity for Postgres queries -that can tolerate replication lag. They are separate from the replicas in the -primary branch cluster and from Vitess read-only regions. +Dedicated read replicas provide dedicated regional capacity for Postgres +queries that can tolerate replication lag. They are separate from the replicas +in the primary branch cluster and from Vitess read-only regions. ```bash -pscale read-only-replica list --org --format json -pscale read-only-replica show --org --format json -pscale read-only-replica create --region --org --format json -pscale read-only-replica create --region --replicas 2 --cluster-size PS_10_GCP_X86 --org --format json -pscale read-only-replica update --replicas 3 --org --format json -pscale read-only-replica update --cluster-size PS_20_GCP_X86 --parameters pgconf.max_connections=300 --org --format json -pscale read-only-replica delete --org --format json --force +pscale dedicated-read-replica list --org --format json +pscale dedicated-read-replica show --org --format json +pscale dedicated-read-replica create --region --org --format json +pscale dedicated-read-replica create --region --replicas 2 --cluster-size PS_10_GCP_X86 --org --format json +pscale dedicated-read-replica update --replicas 3 --org --format json +pscale dedicated-read-replica update --cluster-size PS_20_GCP_X86 --parameters pgconf.max_connections=300 --org --format json +pscale dedicated-read-replica delete --org --format json --force ``` - `create` requires a name and `--region`. The API defaults to one instance and the primary cluster size when `--replicas` and `--cluster-size` are omitted. -- `show`, `update`, and `delete` identify the read-only replica by name. +- `show`, `update`, and `delete` identify the dedicated read replica by name. - `update` requires at least one of `--replicas`, `--cluster-size`, or repeatable `--parameters namespace.name=value`. Parameter values must be greater than or equal to the primary's corresponding values. -- Creating and updating replicas is asynchronous; inspect `state` and `ready` in the returned object or with `list`. +- Creating and updating dedicated read replicas is asynchronous; inspect `state` and `ready` in the returned object or with `list`. - `delete` requires explicit approval before using `--force`. +- `read-only-replica` remains available as a backwards-compatible command alias. It prints a deprecation warning on stderr; stdout is unchanged. - PostgreSQL only. For Vitess/MySQL, use `pscale keyspace read-only-regions`. ## Postgres switchovers diff --git a/internal/cmd/readonlyreplica/create.go b/internal/cmd/dedicatedreadreplica/create.go similarity index 82% rename from internal/cmd/readonlyreplica/create.go rename to internal/cmd/dedicatedreadreplica/create.go index 945d01fa..d27e9552 100644 --- a/internal/cmd/readonlyreplica/create.go +++ b/internal/cmd/dedicatedreadreplica/create.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -// CreateCmd creates a read-only replica. +// CreateCmd creates a dedicated read replica. func CreateCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { region string @@ -19,8 +19,8 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "create ", - Short: "Create a read-only replica", - Long: `Create a read-only replica for a PostgreSQL database branch. + Short: "Create a dedicated read replica", + Long: `Create a dedicated read replica for a PostgreSQL database branch. Region is required. The replica count defaults to 1 and the cluster size defaults to the primary cluster size when those flags are omitted.`, @@ -33,7 +33,7 @@ defaults to the primary cluster size when those flags are omitted.`, if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } @@ -49,7 +49,7 @@ defaults to the primary cluster size when those flags are omitted.`, req.Replicas = &flags.replicas } - end := ch.Printer.PrintProgress(fmt.Sprintf("Creating read-only replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Creating dedicated read replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replica, err := client.PostgresReadOnlyReplicas.Create(ctx, req) @@ -65,7 +65,7 @@ defaults to the primary cluster size when those flags are omitted.`, end() if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Read-only replica %s is being created for %s/%s (state: %s).\n", + ch.Printer.Printf("Dedicated read replica %s is being created for %s/%s (state: %s).\n", printer.BoldBlue(replica.Name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(replica.State)) return nil } @@ -73,7 +73,7 @@ defaults to the primary cluster size when those flags are omitted.`, }, } - cmd.Flags().StringVar(&flags.region, "region", "", "Region slug for the read-only replica") + cmd.Flags().StringVar(&flags.region, "region", "", "Region slug for the dedicated read replica") cmd.Flags().IntVar(&flags.replicas, "replicas", 1, "Number of instances serving reads") cmd.Flags().StringVar(&flags.clusterSize, "cluster-size", "", "Cluster size SKU; defaults to the primary cluster size") cmd.MarkFlagRequired("region") // nolint:errcheck diff --git a/internal/cmd/readonlyreplica/readonlyreplica.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go similarity index 65% rename from internal/cmd/readonlyreplica/readonlyreplica.go rename to internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go index 9bd290b0..015316fe 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "encoding/json" @@ -9,15 +9,35 @@ import ( "github.com/spf13/cobra" ) -// Cmd manages read-only replicas for Postgres branches. +// Cmd manages dedicated read replicas for Postgres branches. func Cmd(ch *cmdutil.Helper) *cobra.Command { + return cmd(ch) +} + +const deprecatedName = "read-only-replica" + +const deprecationMessage = "use dedicated-read-replica instead" + +// DeprecatedCmd preserves the previous command name without advertising it. +// Cobra only prints Deprecated for the command that runs, so subcommands warn +// through PersistentPreRunE the same way the deprecated workflow command does. +func DeprecatedCmd(ch *cmdutil.Helper) *cobra.Command { + cmd := cmd(ch) + cmd.Use = deprecatedName + " " + cmd.Hidden = true + cmd.Deprecated = deprecationMessage + cmd.PersistentPreRunE = cmdutil.WarnDeprecated(deprecatedName, deprecationMessage, cmdutil.CheckAuthentication(ch.Config)) + return cmd +} + +func cmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ - Use: "read-only-replica ", - Short: "Manage read-only replicas for a Postgres branch", - Long: `Manage read-only replicas for a PostgreSQL database branch. + Use: "dedicated-read-replica ", + Short: "Manage dedicated read replicas for a Postgres branch", + Long: `Manage dedicated read replicas for a PostgreSQL database branch. -Read-only replicas provide dedicated capacity for queries that can tolerate -replication lag. They accept read traffic only. +Dedicated read replicas provide dedicated capacity for queries that can +tolerate replication lag. They accept read traffic only. This command is only available for PostgreSQL databases.`, PersistentPreRunE: cmdutil.CheckAuthentication(ch.Config), @@ -35,7 +55,7 @@ This command is only available for PostgreSQL databases.`, return cmd } -// ReadOnlyReplica is the human/JSON/CSV view of a Postgres read-only replica. +// ReadOnlyReplica is the human/JSON/CSV view of a Postgres dedicated read replica. type ReadOnlyReplica struct { ID string `header:"id" json:"id"` Name string `header:"name" json:"name"` diff --git a/internal/cmd/readonlyreplica/readonlyreplica_test.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go similarity index 84% rename from internal/cmd/readonlyreplica/readonlyreplica_test.go rename to internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go index 636fbf1f..08534f44 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica_test.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go @@ -1,8 +1,9 @@ -package readonlyreplica +package dedicatedreadreplica import ( "bytes" "context" + "encoding/json" "testing" "time" @@ -62,6 +63,51 @@ func databaseService(c *qt.C, org, database string) *mock.DatabaseService { } } +func TestCmdUsesDedicatedReadReplicaName(t *testing.T) { + c := qt.New(t) + + cmd := Cmd(&cmdutil.Helper{Config: &config.Config{}}) + + c.Assert(cmd.Use, qt.Equals, "dedicated-read-replica ") + c.Assert(cmd.Aliases, qt.HasLen, 0) +} + +func TestDeprecatedCmdIsHidden(t *testing.T) { + c := qt.New(t) + + cmd := DeprecatedCmd(&cmdutil.Helper{Config: &config.Config{}}) + + c.Assert(cmd.Use, qt.Equals, "read-only-replica ") + c.Assert(cmd.Hidden, qt.IsTrue) + c.Assert(cmd.Deprecated, qt.Equals, "use dedicated-read-replica instead") +} + +func TestDeprecatedCmdWarnsOnSubcommand(t *testing.T) { + c := qt.New(t) + var stdout, stderr bytes.Buffer + org, database, branch := "planetscale", "mydb", "main" + svc := &mock.PostgresReadOnlyReplicasService{ + ListFn: func(ctx context.Context, req *ps.ListPostgresReadOnlyReplicasRequest) ([]*ps.PostgresReadOnlyReplica, error) { + return []*ps.PostgresReadOnlyReplica{testReplica()}, nil + }, + } + + ch := testHelper(org, databaseService(c, org, database), svc, printer.JSON, &stdout) + ch.Config.AccessToken = "token" + + cmd := DeprecatedCmd(ch) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"list", database, branch, "--org", org}) + c.Assert(cmd.Execute(), qt.IsNil) + c.Assert(svc.ListFnInvoked, qt.IsTrue) + c.Assert(stderr.String(), qt.Equals, "Command \"read-only-replica\" is deprecated, use dedicated-read-replica instead\n") + + var got []map[string]any + c.Assert(json.Unmarshal(stdout.Bytes(), &got), qt.IsNil) + c.Assert(got, qt.HasLen, 1) + c.Assert(got[0]["name"], qt.Equals, "analytics") +} + func TestListCmd(t *testing.T) { c := qt.New(t) var buf bytes.Buffer @@ -192,7 +238,7 @@ func TestDeleteCmd(t *testing.T) { c.Assert(cmd.Execute(), qt.IsNil) c.Assert(svc.DeleteFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.JSONEquals, map[string]string{ - "result": "read-only replica deleted", + "result": "dedicated read replica deleted", "name": "analytics", "database": database, "branch": branch, diff --git a/internal/cmd/readonlyreplica/delete.go b/internal/cmd/dedicatedreadreplica/delete.go similarity index 68% rename from internal/cmd/readonlyreplica/delete.go rename to internal/cmd/dedicatedreadreplica/delete.go index bd5a06f7..6857bae2 100644 --- a/internal/cmd/readonlyreplica/delete.go +++ b/internal/cmd/dedicatedreadreplica/delete.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" @@ -9,13 +9,13 @@ import ( "github.com/spf13/cobra" ) -// DeleteCmd deletes a read-only replica by name. +// DeleteCmd deletes a dedicated read replica by name. func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { var force bool cmd := &cobra.Command{ Use: "delete ", - Short: "Delete a read-only replica", + Short: "Delete a dedicated read replica", Args: cmdutil.RequiredArgs("database", "branch", "name"), Aliases: []string{"rm"}, RunE: func(cmd *cobra.Command, args []string) error { @@ -26,18 +26,18 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } if !force { confirmationName := fmt.Sprintf("%s/%s/%s", database, branch, name) - if err := ch.Printer.ConfirmCommand(confirmationName, "delete read-only replica", "deletion of read-only replica"); err != nil { + if err := ch.Printer.ConfirmCommand(confirmationName, "delete dedicated read replica", "deletion of dedicated read replica"); err != nil { return err } } - end := ch.Printer.PrintProgress(fmt.Sprintf("Deleting read-only replica %s from %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Deleting dedicated read replica %s from %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() err = client.PostgresReadOnlyReplicas.Delete(ctx, &ps.DeletePostgresReadOnlyReplicaRequest{ @@ -49,7 +49,7 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - return fmt.Errorf("read-only replica %s does not exist on %s/%s (organization: %s)", + return fmt.Errorf("dedicated read replica %s does not exist on %s/%s (organization: %s)", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) default: return cmdutil.HandleError(err) @@ -58,13 +58,13 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { end() if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Read-only replica %s was successfully deleted from %s/%s.\n", + ch.Printer.Printf("Dedicated read replica %s was successfully deleted from %s/%s.\n", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch)) return nil } return ch.Printer.PrintResource(map[string]string{ - "result": "read-only replica deleted", + "result": "dedicated read replica deleted", "name": name, "database": database, "branch": branch, @@ -72,6 +72,6 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { }, } - cmd.Flags().BoolVar(&force, "force", false, "Delete a read-only replica without confirmation") + cmd.Flags().BoolVar(&force, "force", false, "Delete a dedicated read replica without confirmation") return cmd } diff --git a/internal/cmd/readonlyreplica/list.go b/internal/cmd/dedicatedreadreplica/list.go similarity index 73% rename from internal/cmd/readonlyreplica/list.go rename to internal/cmd/dedicatedreadreplica/list.go index 0aa62ea5..7c431559 100644 --- a/internal/cmd/readonlyreplica/list.go +++ b/internal/cmd/dedicatedreadreplica/list.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" @@ -9,11 +9,11 @@ import ( "github.com/spf13/cobra" ) -// ListCmd lists read-only replicas for a Postgres branch. +// ListCmd lists dedicated read replicas for a Postgres branch. func ListCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "list ", - Short: "List read-only replicas for a Postgres branch", + Short: "List dedicated read replicas for a Postgres branch", Args: cmdutil.RequiredArgs("database", "branch"), Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { @@ -24,11 +24,11 @@ func ListCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } - end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching read-only replicas for %s/%s", printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching dedicated read replicas for %s/%s", printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replicas, err := client.PostgresReadOnlyReplicas.List(ctx, &ps.ListPostgresReadOnlyReplicasRequest{ @@ -48,7 +48,7 @@ func ListCmd(ch *cmdutil.Helper) *cobra.Command { end() if len(replicas) == 0 && ch.Printer.Format() == printer.Human { - ch.Printer.Printf("No read-only replicas exist for %s/%s.\n", printer.BoldBlue(database), printer.BoldBlue(branch)) + ch.Printer.Printf("No dedicated read replicas exist for %s/%s.\n", printer.BoldBlue(database), printer.BoldBlue(branch)) return nil } diff --git a/internal/cmd/readonlyreplica/show.go b/internal/cmd/dedicatedreadreplica/show.go similarity index 73% rename from internal/cmd/readonlyreplica/show.go rename to internal/cmd/dedicatedreadreplica/show.go index 72072334..adf86073 100644 --- a/internal/cmd/readonlyreplica/show.go +++ b/internal/cmd/dedicatedreadreplica/show.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" @@ -9,11 +9,11 @@ import ( "github.com/spf13/cobra" ) -// ShowCmd shows a read-only replica by name. +// ShowCmd shows a dedicated read replica by name. func ShowCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "show ", - Short: "Show a read-only replica", + Short: "Show a dedicated read replica", Args: cmdutil.RequiredArgs("database", "branch", "name"), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -23,11 +23,11 @@ func ShowCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } - end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching read-only replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching dedicated read replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replica, err := client.PostgresReadOnlyReplicas.Get(ctx, &ps.GetPostgresReadOnlyReplicaRequest{ @@ -39,7 +39,7 @@ func ShowCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - return fmt.Errorf("read-only replica %s does not exist on %s/%s (organization: %s)", + return fmt.Errorf("dedicated read replica %s does not exist on %s/%s (organization: %s)", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) default: return cmdutil.HandleError(err) diff --git a/internal/cmd/readonlyreplica/update.go b/internal/cmd/dedicatedreadreplica/update.go similarity index 79% rename from internal/cmd/readonlyreplica/update.go rename to internal/cmd/dedicatedreadreplica/update.go index 77d88d92..a5a85495 100644 --- a/internal/cmd/readonlyreplica/update.go +++ b/internal/cmd/dedicatedreadreplica/update.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "errors" @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -// UpdateCmd updates a read-only replica. +// UpdateCmd updates a dedicated read replica. func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { replicas int @@ -21,13 +21,13 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "update ", - Short: "Update a read-only replica", - Long: `Update a read-only replica's cluster size, instance count, and/or + Short: "Update a dedicated read replica", + Long: `Update a dedicated read replica's cluster size, instance count, and/or PostgreSQL configuration parameters. Parameter values must be greater than or equal to the primary branch's corresponding values.`, - Example: ` pscale read-only-replica update mydb main analytics --replicas 2 - pscale read-only-replica update mydb main analytics --cluster-size PS_20_GCP_X86 - pscale read-only-replica update mydb main analytics --parameters pgconf.max_connections=300`, + Example: ` pscale dedicated-read-replica update mydb main analytics --replicas 2 + pscale dedicated-read-replica update mydb main analytics --cluster-size PS_20_GCP_X86 + pscale dedicated-read-replica update mydb main analytics --parameters pgconf.max_connections=300`, Args: cmdutil.RequiredArgs("database", "branch", "name"), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -46,7 +46,7 @@ equal to the primary branch's corresponding values.`, if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } @@ -62,14 +62,14 @@ equal to the primary branch's corresponding values.`, req.Replicas = &flags.replicas } - end := ch.Printer.PrintProgress(fmt.Sprintf("Updating read-only replica %s on %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Updating dedicated read replica %s on %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replica, err := client.PostgresReadOnlyReplicas.Update(ctx, req) if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - return fmt.Errorf("read-only replica %s does not exist on %s/%s (organization: %s)", + return fmt.Errorf("dedicated read replica %s does not exist on %s/%s (organization: %s)", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) default: return cmdutil.HandleError(err) @@ -78,7 +78,7 @@ equal to the primary branch's corresponding values.`, end() if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Update requested for read-only replica %s on %s/%s (state: %s).\n", + ch.Printer.Printf("Update requested for dedicated read replica %s on %s/%s (state: %s).\n", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(replica.State)) return nil } diff --git a/internal/cmd/role/get.go b/internal/cmd/role/get.go index 763eaa1c..6b8ade36 100644 --- a/internal/cmd/role/get.go +++ b/internal/cmd/role/get.go @@ -13,11 +13,11 @@ import ( func GetCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { - replica bool - readOnlyReplica string - bouncer string - router string - shard string + replica bool + dedicatedReadReplica string + bouncer string + router string + shard string } cmd := &cobra.Command{ @@ -44,7 +44,7 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { Branch: branch, RoleId: roleID, Replica: flags.replica, - ReadOnlyReplica: flags.readOnlyReplica, + ReadOnlyReplica: flags.dedicatedReadReplica, Bouncer: flags.bouncer, Router: flags.router, Shard: flags.shard, @@ -52,7 +52,7 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - notFoundFormat, notFoundArgs := roleGetNotFound(roleID, branch, database, ch.Config.Organization, flags.readOnlyReplica, flags.bouncer, flags.router, flags.shard) + notFoundFormat, notFoundArgs := roleGetNotFound(roleID, branch, database, ch.Config.Organization, flags.dedicatedReadReplica, flags.bouncer, flags.router, flags.shard) return cmdutil.HandleNotFoundWithServiceTokenCheck( ctx, cmd, ch.Config, ch.Client, err, @@ -82,11 +82,15 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { } cmd.Flags().BoolVar(&flags.replica, "replica", false, "Return connection details for a branch replica. On Neki this sets libpq options, not a username suffix.") - cmd.Flags().StringVar(&flags.readOnlyReplica, "read-only-replica", "", "Return connection details for a read-only replica (name). Postgres only.") + cmd.Flags().StringVar(&flags.dedicatedReadReplica, "dedicated-read-replica", "", "Return connection details for a dedicated read replica (name). Postgres only.") + cmd.Flags().StringVar(&flags.dedicatedReadReplica, "read-only-replica", "", "Deprecated alias for --dedicated-read-replica.") + cmd.Flags().MarkDeprecated("read-only-replica", "use --dedicated-read-replica instead") // nolint:errcheck cmd.Flags().StringVar(&flags.bouncer, "bouncer", "", "Return connection details for a PgBouncer (name). Postgres only.") cmd.Flags().StringVar(&flags.router, "router", "", "Return connection details for a Neki router group (name). List routers with: pscale branch router list .") cmd.Flags().StringVar(&flags.shard, "shard", "", "Return connection details that pin a Neki shard (name from pscale branch shard list).") - cmd.MarkFlagsMutuallyExclusive("replica", "read-only-replica", "bouncer") + cmd.MarkFlagsMutuallyExclusive("replica", "dedicated-read-replica", "read-only-replica", "bouncer") + cmd.MarkFlagsMutuallyExclusive("dedicated-read-replica", "router") + cmd.MarkFlagsMutuallyExclusive("dedicated-read-replica", "shard") cmd.MarkFlagsMutuallyExclusive("read-only-replica", "router") cmd.MarkFlagsMutuallyExclusive("read-only-replica", "shard") cmd.MarkFlagsMutuallyExclusive("bouncer", "router") @@ -95,15 +99,15 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { return cmd } -func roleGetNotFound(roleID, branch, database, org, readOnlyReplica, bouncer, router, shard string) (string, []any) { +func roleGetNotFound(roleID, branch, database, org, dedicatedReadReplica, bouncer, router, shard string) (string, []any) { type extra struct { label string value string } extras := make([]extra, 0, 4) - if readOnlyReplica != "" { - extras = append(extras, extra{"read-only replica", readOnlyReplica}) + if dedicatedReadReplica != "" { + extras = append(extras, extra{"dedicated read replica", dedicatedReadReplica}) } if bouncer != "" { extras = append(extras, extra{"PgBouncer", bouncer}) diff --git a/internal/cmd/role/get_test.go b/internal/cmd/role/get_test.go index 03a096f6..78166ba2 100644 --- a/internal/cmd/role/get_test.go +++ b/internal/cmd/role/get_test.go @@ -87,7 +87,17 @@ func TestRole_GetCmdConnectionTargets(t *testing.T) { databaseURL: "postgresql://app.branch%7Creplica:@primary.pg.psdb.cloud:5432/postgres?sslmode=verify-full", }, { - name: "read-only replica", + name: "dedicated read replica", + args: []string{"mydb", "main", "role-id", "--dedicated-read-replica", "analytics"}, + request: ps.GetPostgresRoleRequest{ + ReadOnlyReplica: "analytics", + }, + username: "app.read-only|replica", + accessHostURL: "analytics.pg.psdb.cloud", + databaseURL: "postgresql://app.read-only%7Creplica:@analytics.pg.psdb.cloud:5432/postgres?sslmode=verify-full", + }, + { + name: "deprecated read-only replica alias", args: []string{"mydb", "main", "role-id", "--read-only-replica", "analytics"}, request: ps.GetPostgresRoleRequest{ ReadOnlyReplica: "analytics", @@ -161,6 +171,15 @@ func TestRole_GetCmdConnectionTargets(t *testing.T) { } } +func TestRole_GetCmdDedicatedReadReplicaFlags(t *testing.T) { + c := qt.New(t) + + cmd := GetCmd(&cmdutil.Helper{}) + + c.Assert(cmd.Flags().Lookup("dedicated-read-replica").Deprecated, qt.Equals, "") + c.Assert(cmd.Flags().Lookup("read-only-replica").Deprecated, qt.Equals, "use --dedicated-read-replica instead") +} + func TestRole_GetCmdRejectsMultipleConnectionTargets(t *testing.T) { c := qt.New(t) @@ -172,16 +191,23 @@ func TestRole_GetCmdRejectsMultipleConnectionTargets(t *testing.T) { }, } - cmd := GetCmd(ch) - cmd.SetArgs([]string{"mydb", "main", "role-id", "--replica", "--bouncer", "pool"}) + tests := [][]string{ + {"mydb", "main", "role-id", "--replica", "--bouncer", "pool"}, + {"mydb", "main", "role-id", "--bouncer", "pool", "--router", "default"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--read-only-replica", "analytics"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--bouncer", "pool"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--router", "default"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--shard", "shzabc"}, + {"mydb", "main", "role-id", "--read-only-replica", "analytics", "--router", "default"}, + } - c.Assert(cmd.Execute(), qt.IsNotNil) - c.Assert(svc.GetFnInvoked, qt.IsFalse) + for _, args := range tests { + cmd := GetCmd(ch) + cmd.SetArgs(args) - cmd = GetCmd(ch) - cmd.SetArgs([]string{"mydb", "main", "role-id", "--bouncer", "pool", "--router", "default"}) - c.Assert(cmd.Execute(), qt.IsNotNil) - c.Assert(svc.GetFnInvoked, qt.IsFalse) + c.Assert(cmd.Execute(), qt.IsNotNil) + c.Assert(svc.GetFnInvoked, qt.IsFalse) + } } func TestRole_GetCmdNekiConnectionTargets(t *testing.T) { @@ -305,9 +331,9 @@ func TestRole_GetCmdConnectionTargetNotFound(t *testing.T) { target string }{ { - name: "read-only replica", - args: []string{"mydb", "main", "role-id", "--read-only-replica", "missing-replica"}, - targetType: "read-only replica", + name: "dedicated read replica", + args: []string{"mydb", "main", "role-id", "--dedicated-read-replica", "missing-replica"}, + targetType: "dedicated read replica", target: "missing-replica", }, { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b230e1b3..bdac8678 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -43,6 +43,7 @@ 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/dedicatedreadreplica" "github.com/planetscale/cli/internal/cmd/deployrequest" "github.com/planetscale/cli/internal/cmd/importcmd" "github.com/planetscale/cli/internal/cmd/insights" @@ -55,7 +56,6 @@ import ( "github.com/planetscale/cli/internal/cmd/password" "github.com/planetscale/cli/internal/cmd/pgbouncer" "github.com/planetscale/cli/internal/cmd/ping" - "github.com/planetscale/cli/internal/cmd/readonlyreplica" "github.com/planetscale/cli/internal/cmd/region" "github.com/planetscale/cli/internal/cmd/shell" "github.com/planetscale/cli/internal/cmd/signup" @@ -406,9 +406,10 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer. pgbouncerCmd.GroupID = "postgres" rootCmd.AddCommand(pgbouncerCmd) - readOnlyReplicaCmd := readonlyreplica.Cmd(ch) - readOnlyReplicaCmd.GroupID = "postgres" - rootCmd.AddCommand(readOnlyReplicaCmd) + dedicatedReadReplicaCmd := dedicatedreadreplica.Cmd(ch) + dedicatedReadReplicaCmd.GroupID = "postgres" + rootCmd.AddCommand(dedicatedReadReplicaCmd) + rootCmd.AddCommand(dedicatedreadreplica.DeprecatedCmd(ch)) trafficCmd := trafficcontrol.TrafficCmd(ch) trafficCmd.GroupID = "postgres" diff --git a/internal/planetscale/postgres_read_only_replicas.go b/internal/planetscale/postgres_read_only_replicas.go index c0714adc..24ee8726 100644 --- a/internal/planetscale/postgres_read_only_replicas.go +++ b/internal/planetscale/postgres_read_only_replicas.go @@ -8,7 +8,7 @@ import ( "time" ) -// PostgresReadOnlyReplica represents a read-only replica for a Postgres branch. +// PostgresReadOnlyReplica represents a dedicated read replica for a Postgres branch. type PostgresReadOnlyReplica struct { ID string `json:"id"` Name string `json:"name"` @@ -28,14 +28,14 @@ type PostgresReadOnlyReplica struct { Parameters []*PostgresParameter `json:"parameters"` } -// ListPostgresReadOnlyReplicasRequest encapsulates listing read-only replicas. +// ListPostgresReadOnlyReplicasRequest encapsulates listing dedicated read replicas. type ListPostgresReadOnlyReplicasRequest struct { Organization string Database string Branch string } -// GetPostgresReadOnlyReplicaRequest encapsulates getting a read-only replica by name. +// GetPostgresReadOnlyReplicaRequest encapsulates getting a dedicated read replica by name. type GetPostgresReadOnlyReplicaRequest struct { Organization string Database string @@ -43,7 +43,7 @@ type GetPostgresReadOnlyReplicaRequest struct { Replica string } -// CreatePostgresReadOnlyReplicaRequest encapsulates creating a read-only replica. +// CreatePostgresReadOnlyReplicaRequest encapsulates creating a dedicated read replica. type CreatePostgresReadOnlyReplicaRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -54,7 +54,7 @@ type CreatePostgresReadOnlyReplicaRequest struct { ClusterSize string `json:"cluster_size,omitempty"` } -// UpdatePostgresReadOnlyReplicaRequest encapsulates updating a read-only replica. +// UpdatePostgresReadOnlyReplicaRequest encapsulates updating a dedicated read replica. type UpdatePostgresReadOnlyReplicaRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -65,7 +65,7 @@ type UpdatePostgresReadOnlyReplicaRequest struct { Parameters map[string]map[string]string `json:"parameters,omitempty"` } -// DeletePostgresReadOnlyReplicaRequest encapsulates deleting a read-only replica. +// DeletePostgresReadOnlyReplicaRequest encapsulates deleting a dedicated read replica. type DeletePostgresReadOnlyReplicaRequest struct { Organization string Database string @@ -73,8 +73,8 @@ type DeletePostgresReadOnlyReplicaRequest struct { Replica string } -// PostgresReadOnlyReplicasService is an interface for the Postgres read-only -// replicas API. +// PostgresReadOnlyReplicasService is an interface for the Postgres dedicated +// read replicas API. type PostgresReadOnlyReplicasService interface { List(context.Context, *ListPostgresReadOnlyReplicasRequest) ([]*PostgresReadOnlyReplica, error) Get(context.Context, *GetPostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) @@ -92,7 +92,7 @@ var _ PostgresReadOnlyReplicasService = &postgresReadOnlyReplicasService{} func (s *postgresReadOnlyReplicasService) List(ctx context.Context, listReq *ListPostgresReadOnlyReplicasRequest) ([]*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodGet, postgresReadOnlyReplicasAPIPath(listReq.Organization, listReq.Database, listReq.Branch), nil) if err != nil { - return nil, fmt.Errorf("error creating request for list postgres read-only replicas: %w", err) + return nil, fmt.Errorf("error creating request for list postgres dedicated read replicas: %w", err) } replicas := []*PostgresReadOnlyReplica{} @@ -105,7 +105,7 @@ func (s *postgresReadOnlyReplicasService) List(ctx context.Context, listReq *Lis func (s *postgresReadOnlyReplicasService) Get(ctx context.Context, getReq *GetPostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodGet, postgresReadOnlyReplicaAPIPath(getReq.Organization, getReq.Database, getReq.Branch, getReq.Replica), nil) if err != nil { - return nil, fmt.Errorf("error creating request for get postgres read-only replica: %w", err) + return nil, fmt.Errorf("error creating request for get postgres dedicated read replica: %w", err) } replica := &PostgresReadOnlyReplica{} @@ -118,7 +118,7 @@ func (s *postgresReadOnlyReplicasService) Get(ctx context.Context, getReq *GetPo func (s *postgresReadOnlyReplicasService) Create(ctx context.Context, createReq *CreatePostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodPost, postgresReadOnlyReplicasAPIPath(createReq.Organization, createReq.Database, createReq.Branch), createReq) if err != nil { - return nil, fmt.Errorf("error creating request for create postgres read-only replica: %w", err) + return nil, fmt.Errorf("error creating request for create postgres dedicated read replica: %w", err) } replica := &PostgresReadOnlyReplica{} @@ -131,7 +131,7 @@ func (s *postgresReadOnlyReplicasService) Create(ctx context.Context, createReq func (s *postgresReadOnlyReplicasService) Update(ctx context.Context, updateReq *UpdatePostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodPatch, postgresReadOnlyReplicaAPIPath(updateReq.Organization, updateReq.Database, updateReq.Branch, updateReq.Replica), updateReq) if err != nil { - return nil, fmt.Errorf("error creating request for update postgres read-only replica: %w", err) + return nil, fmt.Errorf("error creating request for update postgres dedicated read replica: %w", err) } replica := &PostgresReadOnlyReplica{} @@ -144,7 +144,7 @@ func (s *postgresReadOnlyReplicasService) Update(ctx context.Context, updateReq func (s *postgresReadOnlyReplicasService) Delete(ctx context.Context, deleteReq *DeletePostgresReadOnlyReplicaRequest) error { req, err := s.client.newRequest(http.MethodDelete, postgresReadOnlyReplicaAPIPath(deleteReq.Organization, deleteReq.Database, deleteReq.Branch, deleteReq.Replica), nil) if err != nil { - return fmt.Errorf("error creating request for delete postgres read-only replica: %w", err) + return fmt.Errorf("error creating request for delete postgres dedicated read replica: %w", err) } return s.client.do(ctx, req, nil) } diff --git a/internal/planetscale/postgres_roles_test.go b/internal/planetscale/postgres_roles_test.go index b246a226..c905fb67 100644 --- a/internal/planetscale/postgres_roles_test.go +++ b/internal/planetscale/postgres_roles_test.go @@ -398,7 +398,7 @@ func TestPostgresRoles_GetConnectionTargets(t *testing.T) { accessHost: "primary.planetscale.com", }, { - name: "read-only replica", + name: "dedicated read replica", request: GetPostgresRoleRequest{ReadOnlyReplica: "analytics"}, query: url.Values{"read_only_replica": []string{"analytics"}}, username: "test-user.replica-id|replica",