From 4d851634a54556c3f5f4c31bd8d543fd310b9c47 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Wed, 8 Jul 2026 13:46:42 -0700 Subject: [PATCH 01/10] [local-test-only] Add switchover pair/endpoint commands Wires the ccloud-sdk-go-v2-internal/switchover SDK into a new confluent switchover pair/endpoint command group, for local demo purposes only. Co-Authored-By: Claude Sonnet 5 --- go.mod | 1 + go.sum | 2 + internal/command.go | 2 + internal/switchover/command.go | 22 +++ internal/switchover/endpoint/command.go | 37 +++++ .../switchover/endpoint/command_activate.go | 37 +++++ .../switchover/endpoint/command_create.go | 135 ++++++++++++++++++ .../switchover/endpoint/command_describe.go | 43 ++++++ .../switchover/endpoint/command_update.go | 53 +++++++ internal/switchover/pair/command.go | 35 +++++ internal/switchover/pair/command_create.go | 122 ++++++++++++++++ internal/switchover/pair/command_describe.go | 43 ++++++ .../switchover/pair/command_trigger_switch.go | 69 +++++++++ internal/switchover/pair/command_update.go | 53 +++++++ pkg/ccloudv2/client.go | 4 + pkg/ccloudv2/switchover.go | 64 +++++++++ 16 files changed, 722 insertions(+) create mode 100644 internal/switchover/command.go create mode 100644 internal/switchover/endpoint/command.go create mode 100644 internal/switchover/endpoint/command_activate.go create mode 100644 internal/switchover/endpoint/command_create.go create mode 100644 internal/switchover/endpoint/command_describe.go create mode 100644 internal/switchover/endpoint/command_update.go create mode 100644 internal/switchover/pair/command.go create mode 100644 internal/switchover/pair/command_create.go create mode 100644 internal/switchover/pair/command_describe.go create mode 100644 internal/switchover/pair/command_trigger_switch.go create mode 100644 internal/switchover/pair/command_update.go create mode 100644 pkg/ccloudv2/switchover.go diff --git a/go.mod b/go.mod index f0d4267617..1e8a96d3e1 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/charmbracelet/lipgloss v0.11.0 github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 + github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10 github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 github.com/confluentinc/ccloud-sdk-go-v2/billing v0.3.0 diff --git a/go.sum b/go.sum index a78db71d5a..ae09be3379 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 h1:19qEGhkbZa5fopKCe0VPIV+Sasby4Pv10z9ZaktwWso= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52/go.mod h1:62EMf+5uFEt1BJ2q8WMrUoI9VUSxAbDnmZCGRt/MbA0= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10 h1:7pfKk5+4RkphBMn8TFwEPwbnSKwUjx1VORwJcX0Q8Q0= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10/go.mod h1:75XYxEMix/kXHlKza4pP9P2in9w1tB3vM2TY3G4mHl4= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA= github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw= diff --git a/internal/command.go b/internal/command.go index 4b83164ff9..731207ac06 100644 --- a/internal/command.go +++ b/internal/command.go @@ -45,6 +45,7 @@ import ( "github.com/confluentinc/cli/v4/internal/secret" servicequota "github.com/confluentinc/cli/v4/internal/service-quota" streamshare "github.com/confluentinc/cli/v4/internal/stream-share" + "github.com/confluentinc/cli/v4/internal/switchover" "github.com/confluentinc/cli/v4/internal/tableflow" unifiedstreammanager "github.com/confluentinc/cli/v4/internal/unified-stream-manager" "github.com/confluentinc/cli/v4/internal/update" @@ -138,6 +139,7 @@ func NewConfluentCommand(cfg *config.Config) *cobra.Command { cmd.AddCommand(servicequota.New(prerunner)) cmd.AddCommand(shell.New(cmd, func() *cobra.Command { return NewConfluentCommand(cfg) })) cmd.AddCommand(streamshare.New(prerunner)) + cmd.AddCommand(switchover.New(prerunner)) cmd.AddCommand(tableflow.New(prerunner)) cmd.AddCommand(unifiedstreammanager.New(cfg, prerunner)) cmd.AddCommand(update.New(cfg, prerunner)) diff --git a/internal/switchover/command.go b/internal/switchover/command.go new file mode 100644 index 0000000000..d2a9f1515b --- /dev/null +++ b/internal/switchover/command.go @@ -0,0 +1,22 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + "github.com/confluentinc/cli/v4/internal/switchover/endpoint" + "github.com/confluentinc/cli/v4/internal/switchover/pair" + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" +) + +func New(prerunner pcmd.PreRunner) *cobra.Command { + cmd := &cobra.Command{ + Use: "switchover", + Short: "Manage Kafka disaster recovery switchover pairs and endpoints.", + Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireNonAPIKeyCloudLogin}, + } + + cmd.AddCommand(pair.New(prerunner)) + cmd.AddCommand(endpoint.New(prerunner)) + + return cmd +} diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go new file mode 100644 index 0000000000..001bcca4a3 --- /dev/null +++ b/internal/switchover/endpoint/command.go @@ -0,0 +1,37 @@ +package endpoint + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" +) + +type command struct { + *pcmd.AuthenticatedCLICommand +} + +type out struct { + Id string `human:"ID" serialized:"id"` + DisplayName string `human:"Display Name" serialized:"display_name"` + SwitchoverPair string `human:"Switchover Pair" serialized:"switchover_pair"` + Environment string `human:"Environment" serialized:"environment"` + DrEndpoint string `human:"DR Endpoint" serialized:"dr_endpoint"` + Phase string `human:"Phase" serialized:"phase"` +} + +func New(prerunner pcmd.PreRunner) *cobra.Command { + cmd := &cobra.Command{ + Use: "endpoint", + Short: "Manage switchover endpoints.", + Long: "Manage switchover endpoints. This API is not yet implemented on the backend; commands will fail against a live Confluent Cloud environment.", + } + + c := &command{pcmd.NewAuthenticatedCLICommand(cmd, prerunner)} + + cmd.AddCommand(c.newCreateCommand()) + cmd.AddCommand(c.newDescribeCommand()) + cmd.AddCommand(c.newUpdateCommand()) + cmd.AddCommand(c.newActivateCommand()) + + return cmd +} diff --git a/internal/switchover/endpoint/command_activate.go b/internal/switchover/endpoint/command_activate.go new file mode 100644 index 0000000000..7fc1b83b7c --- /dev/null +++ b/internal/switchover/endpoint/command_activate.go @@ -0,0 +1,37 @@ +package endpoint + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newActivateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "activate ", + Short: "Activate a switchover endpoint.", + Long: "Activate a switchover endpoint, applying its desired routing target.", + Args: cobra.ExactArgs(1), + RunE: c.activate, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Activate switchover endpoint "se-123456".`, + Code: `confluent switchover endpoint activate se-123456`, + }, + ), + } + + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) activate(cmd *cobra.Command, args []string) error { + result, err := c.V2Client.ActivateSwitchoverEndpoint(args[0]) + if err != nil { + return err + } + + return printSwitchoverEndpoint(cmd, result) +} diff --git a/internal/switchover/endpoint/command_create.go b/internal/switchover/endpoint/command_create.go new file mode 100644 index 0000000000..9c9c18f1fb --- /dev/null +++ b/internal/switchover/endpoint/command_create.go @@ -0,0 +1,135 @@ +package endpoint + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a switchover endpoint.", + Long: "Create a switchover endpoint bound to a switchover pair.", + Args: cobra.ExactArgs(1), + RunE: c.create, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Create switchover endpoint "prod-kafka-dr-endpoint" for switchover pair "sw-123456".`, + Code: `confluent switchover endpoint create prod-kafka-dr-endpoint --switchover-pair sw-123456 --endpoint name=west-platt,resource-id=lkc-west,type=PRIVATE --endpoint name=east-platt,resource-id=lkc-east,type=PRIVATE`, + }, + ), + } + + cmd.Flags().String("switchover-pair", "", "The ID of the switchover pair this endpoint is bound to.") + cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,resource-id=,type=[,cloud=][,region=][,gateway=][,access-point=]". Must be specified exactly twice.`) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("switchover-pair")) + cobra.CheckErr(cmd.MarkFlagRequired("endpoint")) + + return cmd +} + +func parseEndpointFlag(raw string) (switchoverv1.SwitchoverV1EndpointConfig, error) { + config := switchoverv1.SwitchoverV1EndpointConfig{} + filter := switchoverv1.SwitchoverV1EndpointFilter{} + for _, part := range strings.Split(raw, ",") { + key, value, ok := strings.Cut(part, "=") + if !ok { + return config, fmt.Errorf(`invalid --endpoint value %q: expected "key=value" pairs`, raw) + } + switch key { + case "name": + config.Name = value + case "resource-id": + filter.ResourceId = value + case "type": + filter.Type = value + case "cloud": + filter.Cloud = switchoverv1.PtrString(value) + case "region": + filter.Region = switchoverv1.PtrString(value) + case "gateway": + filter.Gateway = switchoverv1.PtrString(value) + case "access-point": + filter.AccessPoint = switchoverv1.PtrString(value) + default: + return config, fmt.Errorf(`invalid --endpoint key %q`, key) + } + } + if config.Name == "" || filter.ResourceId == "" || filter.Type == "" { + return config, fmt.Errorf(`invalid --endpoint value %q: "name", "resource-id", and "type" are required`, raw) + } + config.EndpointFilter = filter + return config, nil +} + +func (c *command) create(cmd *cobra.Command, args []string) error { + displayName := args[0] + + switchoverPairId, err := cmd.Flags().GetString("switchover-pair") + if err != nil { + return err + } + + rawEndpoints, err := cmd.Flags().GetStringArray("endpoint") + if err != nil { + return err + } + if len(rawEndpoints) != 2 { + return fmt.Errorf(`exactly two --endpoint flags are required, got %d`, len(rawEndpoints)) + } + + endpoints := make([]switchoverv1.SwitchoverV1EndpointConfig, len(rawEndpoints)) + for i, raw := range rawEndpoints { + endpointConfig, err := parseEndpointFlag(raw) + if err != nil { + return err + } + endpoints[i] = endpointConfig + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + endpoint := switchoverv1.SwitchoverV1SwitchoverEndpoint{ + Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ + DisplayName: switchoverv1.PtrString(displayName), + Endpoints: &endpoints, + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + SwitchoverPair: &switchoverv1.EnvScopedObjectReference{Id: switchoverPairId}, + }, + } + + result, err := c.V2Client.CreateSwitchoverEndpoint(endpoint) + if err != nil { + return err + } + + return printSwitchoverEndpoint(cmd, result) +} + +func printSwitchoverEndpoint(cmd *cobra.Command, endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) error { + table := output.NewTable(cmd) + table.Add(&out{ + Id: endpoint.GetId(), + DisplayName: endpoint.Spec.GetDisplayName(), + SwitchoverPair: endpoint.Spec.SwitchoverPair.GetId(), + Environment: endpoint.Spec.Environment.GetId(), + DrEndpoint: endpoint.Spec.GetDrEndpoint(), + Phase: endpoint.Status.GetPhase(), + }) + return table.Print() +} diff --git a/internal/switchover/endpoint/command_describe.go b/internal/switchover/endpoint/command_describe.go new file mode 100644 index 0000000000..ea0386cf24 --- /dev/null +++ b/internal/switchover/endpoint/command_describe.go @@ -0,0 +1,43 @@ +package endpoint + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newDescribeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "describe ", + Short: "Describe a switchover endpoint.", + Args: cobra.ExactArgs(1), + RunE: c.describe, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Describe switchover endpoint "se-123456".`, + Code: `confluent switchover endpoint describe se-123456`, + }, + ), + } + + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) describe(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + endpoint, err := c.V2Client.GetSwitchoverEndpoint(args[0], environmentId) + if err != nil { + return err + } + + return printSwitchoverEndpoint(cmd, endpoint) +} diff --git a/internal/switchover/endpoint/command_update.go b/internal/switchover/endpoint/command_update.go new file mode 100644 index 0000000000..a4213c1488 --- /dev/null +++ b/internal/switchover/endpoint/command_update.go @@ -0,0 +1,53 @@ +package endpoint + +import ( + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a switchover endpoint.", + Long: "Update a switchover endpoint's display name. This is the only mutable field.", + Args: cobra.ExactArgs(1), + RunE: c.update, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Rename switchover endpoint "se-123456".`, + Code: `confluent switchover endpoint update se-123456 --display-name "prod-kafka-dr-endpoint-renamed"`, + }, + ), + } + + cmd.Flags().String("display-name", "", "A human-readable name for the switchover endpoint.") + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("display-name")) + + return cmd +} + +func (c *command) update(cmd *cobra.Command, args []string) error { + displayName, err := cmd.Flags().GetString("display-name") + if err != nil { + return err + } + + endpoint := switchoverv1.SwitchoverV1SwitchoverEndpoint{ + Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ + DisplayName: switchoverv1.PtrString(displayName), + }, + } + + result, err := c.V2Client.UpdateSwitchoverEndpoint(args[0], endpoint) + if err != nil { + return err + } + + return printSwitchoverEndpoint(cmd, result) +} diff --git a/internal/switchover/pair/command.go b/internal/switchover/pair/command.go new file mode 100644 index 0000000000..f15d5eacf5 --- /dev/null +++ b/internal/switchover/pair/command.go @@ -0,0 +1,35 @@ +package pair + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" +) + +type command struct { + *pcmd.AuthenticatedCLICommand +} + +type out struct { + Id string `human:"ID" serialized:"id"` + DisplayName string `human:"Display Name" serialized:"display_name"` + ActiveMember string `human:"Active Member" serialized:"active_member"` + Environment string `human:"Environment" serialized:"environment"` + Phase string `human:"Phase" serialized:"phase"` +} + +func New(prerunner pcmd.PreRunner) *cobra.Command { + cmd := &cobra.Command{ + Use: "pair", + Short: "Manage switchover pairs.", + } + + c := &command{pcmd.NewAuthenticatedCLICommand(cmd, prerunner)} + + cmd.AddCommand(c.newCreateCommand()) + cmd.AddCommand(c.newDescribeCommand()) + cmd.AddCommand(c.newUpdateCommand()) + cmd.AddCommand(c.newTriggerSwitchCommand()) + + return cmd +} diff --git a/internal/switchover/pair/command_create.go b/internal/switchover/pair/command_create.go new file mode 100644 index 0000000000..a4335401ea --- /dev/null +++ b/internal/switchover/pair/command_create.go @@ -0,0 +1,122 @@ +package pair + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a switchover pair.", + Long: "Create a switchover pair between two Kafka clusters for disaster recovery.", + Args: cobra.ExactArgs(1), + RunE: c.create, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Create switchover pair "prod-kafka-dr" between clusters "lkc-111111" (west) and "lkc-222222" (east), active on west.`, + Code: `confluent switchover pair create prod-kafka-dr --member name=west,id=lkc-111111 --member name=east,id=lkc-222222 --active-member west`, + }, + ), + } + + cmd.Flags().StringArray("member", nil, `A member of the pair, in the form "name=,id=". Must be specified exactly twice.`) + cmd.Flags().String("active-member", "", "The name of the member that starts as active; must match one of the --member names.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("member")) + cobra.CheckErr(cmd.MarkFlagRequired("active-member")) + + return cmd +} + +func parseMemberFlag(raw string) (switchoverv1.SwitchoverV1SwitchoverPairMember, error) { + member := switchoverv1.SwitchoverV1SwitchoverPairMember{} + for _, part := range strings.Split(raw, ",") { + key, value, ok := strings.Cut(part, "=") + if !ok { + return member, fmt.Errorf(`invalid --member value %q: expected "name=,id="`, raw) + } + switch key { + case "name": + member.Name = value + case "id": + member.MemberId = value + default: + return member, fmt.Errorf(`invalid --member key %q: expected "name" or "id"`, key) + } + } + if member.Name == "" || member.MemberId == "" { + return member, fmt.Errorf(`invalid --member value %q: both "name" and "id" are required`, raw) + } + return member, nil +} + +func (c *command) create(cmd *cobra.Command, args []string) error { + displayName := args[0] + + rawMembers, err := cmd.Flags().GetStringArray("member") + if err != nil { + return err + } + if len(rawMembers) != 2 { + return fmt.Errorf(`exactly two --member flags are required, got %d`, len(rawMembers)) + } + + members := make([]switchoverv1.SwitchoverV1SwitchoverPairMember, len(rawMembers)) + for i, raw := range rawMembers { + member, err := parseMemberFlag(raw) + if err != nil { + return err + } + members[i] = member + } + + activeMember, err := cmd.Flags().GetString("active-member") + if err != nil { + return err + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + pair := switchoverv1.SwitchoverV1SwitchoverPair{ + Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{ + DisplayName: switchoverv1.PtrString(displayName), + Members: &members, + ActiveMember: switchoverv1.PtrString(activeMember), + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + }, + } + + result, err := c.V2Client.CreateSwitchoverPair(pair) + if err != nil { + return err + } + + return printSwitchoverPair(cmd, result) +} + +func printSwitchoverPair(cmd *cobra.Command, pair switchoverv1.SwitchoverV1SwitchoverPair) error { + table := output.NewTable(cmd) + table.Add(&out{ + Id: pair.GetId(), + DisplayName: pair.Spec.GetDisplayName(), + ActiveMember: pair.Spec.GetActiveMember(), + Environment: pair.Spec.Environment.GetId(), + Phase: pair.Status.GetPhase(), + }) + return table.Print() +} diff --git a/internal/switchover/pair/command_describe.go b/internal/switchover/pair/command_describe.go new file mode 100644 index 0000000000..058c2a8bef --- /dev/null +++ b/internal/switchover/pair/command_describe.go @@ -0,0 +1,43 @@ +package pair + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newDescribeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "describe ", + Short: "Describe a switchover pair.", + Args: cobra.ExactArgs(1), + RunE: c.describe, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Describe switchover pair "sw-123456".`, + Code: `confluent switchover pair describe sw-123456`, + }, + ), + } + + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) describe(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + pair, err := c.V2Client.GetSwitchoverPair(args[0], environmentId) + if err != nil { + return err + } + + return printSwitchoverPair(cmd, pair) +} diff --git a/internal/switchover/pair/command_trigger_switch.go b/internal/switchover/pair/command_trigger_switch.go new file mode 100644 index 0000000000..c83f5fee1c --- /dev/null +++ b/internal/switchover/pair/command_trigger_switch.go @@ -0,0 +1,69 @@ +package pair + +import ( + "fmt" + + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/deletion" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newTriggerSwitchCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "trigger-switch ", + Short: "Trigger a failover or switchback on a switchover pair.", + Long: "Trigger a failover (or switchback) on a switchover pair. This redirects live traffic between the pair's members.", + Args: cobra.ExactArgs(1), + RunE: c.triggerSwitch, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Fail switchover pair "sw-123456" over to its "east" member.`, + Code: `confluent switchover pair trigger-switch sw-123456 --active-member east`, + }, + ), + } + + cmd.Flags().String("active-member", "", "The name of the member to promote to active. If omitted, the other member is promoted.") + cmd.Flags().String("failover-type", "CLEAN", "The failover semantics to apply: CLEAN, UNCLEAN, or RESTORE.") + cmd.Flags().Bool("force", false, "Skip the confirmation prompt.") + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) triggerSwitch(cmd *cobra.Command, args []string) error { + id := args[0] + + activeMember, err := cmd.Flags().GetString("active-member") + if err != nil { + return err + } + + failoverType, err := cmd.Flags().GetString("failover-type") + if err != nil { + return err + } + + promptMsg := fmt.Sprintf(`This triggers a %s failover on switchover pair "%s", redirecting live traffic between regions. Do you want to proceed?`, failoverType, id) + if err := deletion.ConfirmPrompt(cmd, promptMsg); err != nil { + return err + } + + req := switchoverv1.SwitchoverV1SwitchoverPairFailoverRequest{ + FailoverType: switchoverv1.PtrString(failoverType), + } + if activeMember != "" { + req.ActiveMember = switchoverv1.PtrString(activeMember) + } + + result, err := c.V2Client.TriggerSwitchoverPairFailover(id, req) + if err != nil { + return err + } + + return printSwitchoverPair(cmd, result) +} diff --git a/internal/switchover/pair/command_update.go b/internal/switchover/pair/command_update.go new file mode 100644 index 0000000000..7b201f23c7 --- /dev/null +++ b/internal/switchover/pair/command_update.go @@ -0,0 +1,53 @@ +package pair + +import ( + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a switchover pair.", + Long: "Update a switchover pair's display name. This is the only mutable field.", + Args: cobra.ExactArgs(1), + RunE: c.update, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Rename switchover pair "sw-123456".`, + Code: `confluent switchover pair update sw-123456 --display-name "prod-kafka-dr-renamed"`, + }, + ), + } + + cmd.Flags().String("display-name", "", "A human-readable name for the switchover pair.") + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("display-name")) + + return cmd +} + +func (c *command) update(cmd *cobra.Command, args []string) error { + displayName, err := cmd.Flags().GetString("display-name") + if err != nil { + return err + } + + pair := switchoverv1.SwitchoverV1SwitchoverPair{ + Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{ + DisplayName: switchoverv1.PtrString(displayName), + }, + } + + result, err := c.V2Client.UpdateSwitchoverPair(args[0], pair) + if err != nil { + return err + } + + return printSwitchoverPair(cmd, result) +} diff --git a/pkg/ccloudv2/client.go b/pkg/ccloudv2/client.go index a736857f5b..26e2f70803 100644 --- a/pkg/ccloudv2/client.go +++ b/pkg/ccloudv2/client.go @@ -38,6 +38,8 @@ import ( tableflowv1 "github.com/confluentinc/ccloud-sdk-go-v2/tableflow/v1" usmv1 "github.com/confluentinc/ccloud-sdk-go-v2/usm/v1" + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + "github.com/confluentinc/cli/v4/pkg/config" testserver "github.com/confluentinc/cli/v4/test/test-server" ) @@ -80,6 +82,7 @@ type Client struct { ServiceQuotaClient *servicequotav1.APIClient SrcmClient *srcmv3.APIClient SsoClient *ssov2.APIClient + SwitchoverClient *switchoverv1.APIClient TableflowClient *tableflowv1.APIClient UsmClient *usmv1.APIClient // cli-tfgen:cli-client-fields — DO NOT REMOVE (verified by TestCliTfgenMarkers) @@ -132,6 +135,7 @@ func NewClient(cfg *config.Config, unsafeTrace bool) *Client { ServiceQuotaClient: newServiceQuotaClient(httpClient, url, userAgent, unsafeTrace), SrcmClient: newSrcmClient(httpClient, url, userAgent, unsafeTrace), SsoClient: newSsoClient(httpClient, url, userAgent, unsafeTrace), + SwitchoverClient: newSwitchoverClient(httpClient, url, userAgent, unsafeTrace), TableflowClient: newTableflowClient(httpClient, url, userAgent, unsafeTrace), UsmClient: newUsmClient(httpClient, url, userAgent, unsafeTrace), // cli-tfgen:cli-client-init — DO NOT REMOVE (verified by TestCliTfgenMarkers) diff --git a/pkg/ccloudv2/switchover.go b/pkg/ccloudv2/switchover.go new file mode 100644 index 0000000000..c1664094de --- /dev/null +++ b/pkg/ccloudv2/switchover.go @@ -0,0 +1,64 @@ +package ccloudv2 + +import ( + "context" + "net/http" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + "github.com/confluentinc/cli/v4/pkg/errors" +) + +func newSwitchoverClient(httpClient *http.Client, url, userAgent string, unsafeTrace bool) *switchoverv1.APIClient { + cfg := switchoverv1.NewConfiguration() + cfg.Debug = unsafeTrace + cfg.HTTPClient = httpClient + cfg.Servers = switchoverv1.ServerConfigurations{{URL: url}} + cfg.UserAgent = userAgent + + return switchoverv1.NewAPIClient(cfg) +} + +func (c *Client) switchoverApiContext() context.Context { + return context.WithValue(context.Background(), switchoverv1.ContextAccessToken, c.cfg.Context().GetAuthToken()) +} + +func (c *Client) CreateSwitchoverPair(pair switchoverv1.SwitchoverV1SwitchoverPair) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.CreateSwitchoverV1SwitchoverPair(c.switchoverApiContext()).SwitchoverV1SwitchoverPair(pair).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) GetSwitchoverPair(id, environment string) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.GetSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).Environment(environment).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) UpdateSwitchoverPair(id string, pair switchoverv1.SwitchoverV1SwitchoverPair) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPair(pair).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) TriggerSwitchoverPairFailover(id string, req switchoverv1.SwitchoverV1SwitchoverPairFailoverRequest) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.FailoverSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPairFailoverRequest(req).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) CreateSwitchoverEndpoint(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.CreateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext()).SwitchoverV1SwitchoverEndpoint(endpoint).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) GetSwitchoverEndpoint(id, environment string) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.GetSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Environment(environment).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) UpdateSwitchoverEndpoint(id string, endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).SwitchoverV1SwitchoverEndpoint(endpoint).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) ActivateSwitchoverEndpoint(id string) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.ActivateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Execute() + return res, errors.CatchCCloudV2Error(err, httpResp) +} From c9376261a9a515e007f26879b9743594b5cbc4e0 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Wed, 8 Jul 2026 14:28:58 -0700 Subject: [PATCH 02/10] [local-test-only] Add local build/usage README Documents how to build and use the switchover CLI commands from this branch, for local demo/sharing purposes only. Co-Authored-By: Claude Sonnet 5 --- SWITCHOVER_LOCAL_DEMO.md | 209 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 SWITCHOVER_LOCAL_DEMO.md diff --git a/SWITCHOVER_LOCAL_DEMO.md b/SWITCHOVER_LOCAL_DEMO.md new file mode 100644 index 0000000000..78c1ad55d4 --- /dev/null +++ b/SWITCHOVER_LOCAL_DEMO.md @@ -0,0 +1,209 @@ +# Local CLI build with Switchover support + +> Paths below assume this repo (and its sibling repos) are cloned under +> `~/sdk_cli_tf_switchover/`, matching the original workspace this branch was +> built in. Adjust paths if you clone elsewhere. + +How to reproduce the local `confluent` CLI build in this workspace, which adds +`confluent switchover pair` and `confluent switchover endpoint` commands on +top of the upstream CLI. + +## Workspace layout + +``` +~/sdk_cli_tf_switchover/ +├── cli/ confluentinc/cli — branch `switchover-cli-local-demo` (PR #3399) +├── ccloud-sdk-go-v2-internal/ confluentinc/ccloud-sdk-go-v2-internal — branch `master`, unmodified +├── cc-switchover/ confluentinc/cc-switchover — branch `master`, unmodified (reference only) +└── terraform-provider-confluent/ confluentinc/terraform-provider-confluent — branch `master`, unmodified (not used here) +``` + +The switchover Go SDK (`ccloud-sdk-go-v2-internal/switchover/v1`) is already +merged to that repo's `master` (PR #818) and was verified against the current +API spec (`confluentinc/api` PR #2545, `switchover/openapi.yaml`) — a full +regen produced a byte-for-byte identical tree, so `cli` pins directly to +`ccloud-sdk-go-v2-internal` master's current commit rather than a new branch: + +``` +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10 +``` + +(that pseudo-version encodes commit `4e3e503e8b10c538379dedd05fa055b41c14298b` +on `ccloud-sdk-go-v2-internal`'s `master`). + +**Branch:** the changes below are on `cli` branch +[`switchover-cli-local-demo`](https://github.com/confluentinc/cli/tree/switchover-cli-local-demo), +pushed as PR **[confluentinc/cli#3399 — "\[local-test-only\] Add switchover +pair/endpoint commands"](https://github.com/confluentinc/cli/pull/3399)**. +This PR is for local-test-only purposes and is not intended to be merged — +`cc-switchover`'s backend doesn't fully implement the API yet, and a fuller, +tested implementation already exists in +[confluentinc/cli#3382](https://github.com/confluentinc/cli/pull/3382) +(branch `ORC-10127-add-switchover-commands`, which also covers `list`/`delete` +and has integration test coverage). To get this branch locally: + +```bash +git clone git@github.com:confluentinc/cli.git +cd cli +git checkout switchover-cli-local-demo +``` + +`confluentinc/cli` is a public repo with an org ruleset that blocks direct +branch pushes — new branches must go through `git push-external` (Confluent's +Airlock proprietary-code-scan wrapper), not plain `git push`, if you need to +push further changes to this branch yourself. + +## One-time environment setup + +1. **Go toolchain** — `cli/.go-version` pins `1.26.4`. If your `goenv` doesn't + have it yet, any Go ≥1.21 binary will auto-fetch it via `GOTOOLCHAIN=auto`: + ```bash + export GOTOOLCHAIN=auto + export PATH="$HOME/.goenv/versions//bin:$PATH" + go version # should report go1.26.4 once it downloads + ``` +2. **goreleaser** — the CLI's `make build` shells out to it: + ```bash + brew install goreleaser + ``` +3. **Docker Desktop**, signed in to an org with access to + `519856050701.dkr.ecr.us-west-2.amazonaws.com` — only needed if you want to + regenerate the switchover SDK from `confluentinc/api`, not for building the + CLI itself. + +## Build + +```bash +cd ~/sdk_cli_tf_switchover/cli +export GOTOOLCHAIN=auto +export PATH="$HOME/.goenv/versions//bin:$PATH" +make build +``` + +Binary lands at: + +``` +~/sdk_cli_tf_switchover/cli/dist/confluent_darwin_arm64_v8.0/confluent +``` + +(path varies by OS/arch — check `dist/` after the build). + +## Alias the binary (recommended) + +This binary isn't installed anywhere on `PATH`, so you either type the full +path every time or set up a shortcut. Prefer a distinctly-named **alias** +over adding the `dist/` folder to `PATH`: putting `dist/` on `PATH` would +shadow any real `confluent` CLI you have installed, so every plain +`confluent` invocation in that shell — including unrelated, real work — +would silently run this experimental build instead. + +```bash +alias confluent-dev="~/sdk_cli_tf_switchover/cli/dist/confluent_darwin_arm64_v8.0/confluent" +``` + +Add that line to your shell rc file (e.g. `~/.zshrc` or `~/.aliases` if +sourced from it) to make it permanent, or just run it once per terminal +session. Your regular `confluent` command (if installed) is unaffected +either way. The rest of this README uses `confluent-dev`. + +## Usage + +Log in first — the `switchover` command requires cloud login +(`RequireNonAPIKeyCloudLogin`): + +```bash +confluent-dev login +``` + +This hits **production** Confluent Cloud (`https://confluent.cloud`) with +real credentials — it's the CLI's stock login flow, not a mock. + +### Logging in against staging (`stag`) instead of production + +```bash +confluent-dev login --url https://stag.cpdev.cloud +``` + +Note the hostname is `stag.cpdev.cloud` with **no** `confluent.` prefix +(unlike prod's `confluent.cloud`) — `confluent.stag.cpdev.cloud` doesn't +resolve (`NXDOMAIN`); `stag.cpdev.cloud` does, and `api.stag.cpdev.cloud` is +just a CNAME alias to it. Verify with `host stag.cpdev.cloud` if in doubt. + +`--url`'s help text says "for on-premises deployments," but the code path is +generic: `pkg/ccloudv2/utils.go`'s `IsCCloudURL` matches any URL containing +`confluent.cloud`, `cpdev.cloud`, or the gov domains and routes it through +the normal Cloud login (not the on-prem/MDS path), so a staging URL works +here. You'll need: + +- A **staging** Confluent Cloud account — separate from your prod account, + same email won't necessarily carry over. A generic + "incorrect email, password, or organization ID" error after entering a + password usually means this account doesn't exist on staging. +- There's no `--sso` flag — SSO is automatic. After you enter your email, + the CLI asks the backend whether that account is SSO-enabled + (`pkg/auth/login_credentials_manager.go`'s `isSSOUser`) and transparently + switches to a browser-based flow instead of prompting for a password if + so. `--no-browser` exists to do that browser flow headlessly, but it + doesn't force SSO for an account the backend doesn't already recognize as one. +- If you know your staging org ID, try `--organization `. +- Your staging org still needs Switchover Early Access granted before + `switchover pair` commands return anything but an access/authorization + error — Early Access isn't bypassed just by using staging. +- `devel.cpdev.cloud` works the same way for the `devel` environment, if needed. + +Everything below (`switchover pair`/`switchover endpoint` commands) works +identically once logged in, regardless of which environment you logged into +— the CLI just talks to whichever environment's API you authenticated +against. + +### Switchover pairs (backend implemented: create/get/update/trigger-switch) + +```bash +# Create a pair between two Kafka clusters +confluent-dev switchover pair create prod-kafka-dr \ + --member name=west,id=lkc-111111 \ + --member name=east,id=lkc-222222 \ + --active-member west \ + --environment env-abcdef + +# Describe it +confluent-dev switchover pair describe sw-123456 + +# Rename it (the only mutable field) +confluent-dev switchover pair update sw-123456 --display-name "prod-kafka-dr-renamed" + +# Trigger a failover (prompts for confirmation — moves live traffic) +confluent-dev switchover pair trigger-switch sw-123456 --active-member east --failover-type CLEAN +``` + +`list` and `delete` are intentionally not implemented in the CLI — +`cc-switchover`'s backend doesn't implement those two RPCs yet (they return +errors regardless of client). + +### Switchover endpoints (backend not implemented yet — commands will fail against a live environment) + +```bash +confluent-dev switchover endpoint create prod-kafka-dr-endpoint \ + --switchover-pair sw-123456 \ + --endpoint name=west-platt,resource-id=lkc-west,type=PRIVATE \ + --endpoint name=east-platt,resource-id=lkc-east,type=PRIVATE + +confluent-dev switchover endpoint describe se-123456 +confluent-dev switchover endpoint update se-123456 --display-name "renamed-endpoint" +confluent-dev switchover endpoint activate se-123456 +``` + +These exist to demo the command surface ahead of the backend +(`SwitchoverEndpointService` in `cc-switchover` is still template/stub code). + +### Global flags + +- `-o json` / `-o yaml` for machine-readable output (default is a human table). +- `--environment env-xxxxx` to override the current context's default environment. +- `--context ` to target a specific CLI context/profile. + +## Rebuilding after further code changes + +Just rerun `make build` from `cli/` — it picks up any local edits. No need to +redo the SDK pinning steps unless `ccloud-sdk-go-v2-internal`'s switchover +module actually changes upstream. From da3e97d42a3655b8de7b32fc72ff196236e1b789 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Wed, 8 Jul 2026 15:44:45 -0700 Subject: [PATCH 03/10] [local-test-only] Bump switchover SDK to PR #832, fix endpoint create Pins ccloud-sdk-go-v2-internal/switchover to PR #832's commit (spec sync fixes: phase enum ERROR->FAILED, dr_endpoint removed, switchover_pair flattened to switchover_pair_id, EndpointFilter resource_id/cloud/region removed). Updates internal/switchover/endpoint/command_create.go and command.go to match: drop resource-id/cloud/region from the --endpoint flag, use the flat SwitchoverPairId field, remove the DrEndpoint output column. Co-Authored-By: Claude Sonnet 5 --- SWITCHOVER_LOCAL_DEMO.md | 115 ++++++------------ go.mod | 2 +- go.sum | 4 +- internal/switchover/endpoint/command.go | 1 - .../switchover/endpoint/command_create.go | 25 ++-- 5 files changed, 46 insertions(+), 101 deletions(-) diff --git a/SWITCHOVER_LOCAL_DEMO.md b/SWITCHOVER_LOCAL_DEMO.md index 78c1ad55d4..89a2777c1d 100644 --- a/SWITCHOVER_LOCAL_DEMO.md +++ b/SWITCHOVER_LOCAL_DEMO.md @@ -13,34 +13,19 @@ top of the upstream CLI. ``` ~/sdk_cli_tf_switchover/ ├── cli/ confluentinc/cli — branch `switchover-cli-local-demo` (PR #3399) -├── ccloud-sdk-go-v2-internal/ confluentinc/ccloud-sdk-go-v2-internal — branch `master`, unmodified -├── cc-switchover/ confluentinc/cc-switchover — branch `master`, unmodified (reference only) -└── terraform-provider-confluent/ confluentinc/terraform-provider-confluent — branch `master`, unmodified (not used here) +├── ccloud-sdk-go-v2-internal/ confluentinc/ccloud-sdk-go-v2-internal — branch `ORC-10130-sync-switchover-sdk` (PR #832) +├── api/ confluentinc/api — branch `ORC-10130-switchover-api-spec` (PR #2545) +├── cc-switchover/ confluentinc/cc-switchover — branch `master` (reference/source-of-truth) +└── terraform-provider-confluent/ confluentinc/terraform-provider-confluent — branch `master` (not used here) ``` -The switchover Go SDK (`ccloud-sdk-go-v2-internal/switchover/v1`) is already -merged to that repo's `master` (PR #818) and was verified against the current -API spec (`confluentinc/api` PR #2545, `switchover/openapi.yaml`) — a full -regen produced a byte-for-byte identical tree, so `cli` pins directly to -`ccloud-sdk-go-v2-internal` master's current commit rather than a new branch: +`cli`'s `go.mod` pins the switchover SDK to PR #832's commit: ``` -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10 +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045 ``` -(that pseudo-version encodes commit `4e3e503e8b10c538379dedd05fa055b41c14298b` -on `ccloud-sdk-go-v2-internal`'s `master`). - -**Branch:** the changes below are on `cli` branch -[`switchover-cli-local-demo`](https://github.com/confluentinc/cli/tree/switchover-cli-local-demo), -pushed as PR **[confluentinc/cli#3399 — "\[local-test-only\] Add switchover -pair/endpoint commands"](https://github.com/confluentinc/cli/pull/3399)**. -This PR is for local-test-only purposes and is not intended to be merged — -`cc-switchover`'s backend doesn't fully implement the API yet, and a fuller, -tested implementation already exists in -[confluentinc/cli#3382](https://github.com/confluentinc/cli/pull/3382) -(branch `ORC-10127-add-switchover-commands`, which also covers `list`/`delete` -and has integration test coverage). To get this branch locally: +To get the `cli` branch locally: ```bash git clone git@github.com:confluentinc/cli.git @@ -49,9 +34,9 @@ git checkout switchover-cli-local-demo ``` `confluentinc/cli` is a public repo with an org ruleset that blocks direct -branch pushes — new branches must go through `git push-external` (Confluent's -Airlock proprietary-code-scan wrapper), not plain `git push`, if you need to -push further changes to this branch yourself. +branch pushes — use `git push-external` (Confluent's Airlock +proprietary-code-scan wrapper), not plain `git push`, to push further changes +to this branch. ## One-time environment setup @@ -90,26 +75,21 @@ Binary lands at: ## Alias the binary (recommended) -This binary isn't installed anywhere on `PATH`, so you either type the full -path every time or set up a shortcut. Prefer a distinctly-named **alias** -over adding the `dist/` folder to `PATH`: putting `dist/` on `PATH` would -shadow any real `confluent` CLI you have installed, so every plain -`confluent` invocation in that shell — including unrelated, real work — -would silently run this experimental build instead. +Prefer a distinctly-named **alias** over adding the `dist/` folder to `PATH`, +so this experimental build doesn't shadow a real installed `confluent` CLI: ```bash -alias confluent-dev="~/sdk_cli_tf_switchover/cli/dist/confluent_darwin_arm64_v8.0/confluent" +alias confluent-dev="/Users/ewang/sdk_cli_tf_switchover/cli/dist/confluent_darwin_arm64_v8.0/confluent" ``` -Add that line to your shell rc file (e.g. `~/.zshrc` or `~/.aliases` if -sourced from it) to make it permanent, or just run it once per terminal -session. Your regular `confluent` command (if installed) is unaffected -either way. The rest of this README uses `confluent-dev`. +Already set up in `~/.aliases` (sourced from `~/.zshrc`), alongside the +existing `confluent` alias. New terminals pick it up automatically; in an +already-open terminal run `source ~/.aliases` once. The rest of this README +uses `confluent-dev`. ## Usage -Log in first — the `switchover` command requires cloud login -(`RequireNonAPIKeyCloudLogin`): +Log in first — the `switchover` command requires cloud login: ```bash confluent-dev login @@ -124,39 +104,18 @@ real credentials — it's the CLI's stock login flow, not a mock. confluent-dev login --url https://stag.cpdev.cloud ``` -Note the hostname is `stag.cpdev.cloud` with **no** `confluent.` prefix -(unlike prod's `confluent.cloud`) — `confluent.stag.cpdev.cloud` doesn't -resolve (`NXDOMAIN`); `stag.cpdev.cloud` does, and `api.stag.cpdev.cloud` is -just a CNAME alias to it. Verify with `host stag.cpdev.cloud` if in doubt. - -`--url`'s help text says "for on-premises deployments," but the code path is -generic: `pkg/ccloudv2/utils.go`'s `IsCCloudURL` matches any URL containing -`confluent.cloud`, `cpdev.cloud`, or the gov domains and routes it through -the normal Cloud login (not the on-prem/MDS path), so a staging URL works -here. You'll need: - -- A **staging** Confluent Cloud account — separate from your prod account, - same email won't necessarily carry over. A generic - "incorrect email, password, or organization ID" error after entering a - password usually means this account doesn't exist on staging. -- There's no `--sso` flag — SSO is automatic. After you enter your email, - the CLI asks the backend whether that account is SSO-enabled - (`pkg/auth/login_credentials_manager.go`'s `isSSOUser`) and transparently - switches to a browser-based flow instead of prompting for a password if - so. `--no-browser` exists to do that browser flow headlessly, but it - doesn't force SSO for an account the backend doesn't already recognize as one. -- If you know your staging org ID, try `--organization `. -- Your staging org still needs Switchover Early Access granted before - `switchover pair` commands return anything but an access/authorization - error — Early Access isn't bypassed just by using staging. -- `devel.cpdev.cloud` works the same way for the `devel` environment, if needed. +The hostname is `stag.cpdev.cloud` with **no** `confluent.` prefix (unlike +prod's `confluent.cloud`). You'll need a **staging** Confluent Cloud +account — separate from your prod account — and your staging org needs +Switchover Early Access granted before `switchover pair` commands return +anything but an access/authorization error. `devel.cpdev.cloud` works the +same way for the `devel` environment. If you know your staging org ID, add +`--organization `. Everything below (`switchover pair`/`switchover endpoint` commands) works -identically once logged in, regardless of which environment you logged into -— the CLI just talks to whichever environment's API you authenticated -against. +identically once logged in, regardless of which environment you logged into. -### Switchover pairs (backend implemented: create/get/update/trigger-switch) +### Switchover pairs ```bash # Create a pair between two Kafka clusters @@ -176,26 +135,19 @@ confluent-dev switchover pair update sw-123456 --display-name "prod-kafka-dr-ren confluent-dev switchover pair trigger-switch sw-123456 --active-member east --failover-type CLEAN ``` -`list` and `delete` are intentionally not implemented in the CLI — -`cc-switchover`'s backend doesn't implement those two RPCs yet (they return -errors regardless of client). - -### Switchover endpoints (backend not implemented yet — commands will fail against a live environment) +### Switchover endpoints ```bash confluent-dev switchover endpoint create prod-kafka-dr-endpoint \ --switchover-pair sw-123456 \ - --endpoint name=west-platt,resource-id=lkc-west,type=PRIVATE \ - --endpoint name=east-platt,resource-id=lkc-east,type=PRIVATE + --endpoint name=west-platt,type=private \ + --endpoint name=east-platt,type=private confluent-dev switchover endpoint describe se-123456 confluent-dev switchover endpoint update se-123456 --display-name "renamed-endpoint" confluent-dev switchover endpoint activate se-123456 ``` -These exist to demo the command surface ahead of the backend -(`SwitchoverEndpointService` in `cc-switchover` is still template/stub code). - ### Global flags - `-o json` / `-o yaml` for machine-readable output (default is a human table). @@ -204,6 +156,7 @@ These exist to demo the command surface ahead of the backend ## Rebuilding after further code changes -Just rerun `make build` from `cli/` — it picks up any local edits. No need to -redo the SDK pinning steps unless `ccloud-sdk-go-v2-internal`'s switchover -module actually changes upstream. +Rerun `make build` from `cli/` — it picks up any local edits. Only redo the +SDK pinning step (`go get github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover@` +in `cli/`) if `ccloud-sdk-go-v2-internal`'s switchover module changes again +upstream. diff --git a/go.mod b/go.mod index 1e8a96d3e1..2e2cd82009 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/charmbracelet/lipgloss v0.11.0 github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 - github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10 + github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045 github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 github.com/confluentinc/ccloud-sdk-go-v2/billing v0.3.0 diff --git a/go.sum b/go.sum index ae09be3379..575ac0c292 100644 --- a/go.sum +++ b/go.sum @@ -172,8 +172,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 h1:19qEGhkbZa5fopKCe0VPIV+Sasby4Pv10z9ZaktwWso= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52/go.mod h1:62EMf+5uFEt1BJ2q8WMrUoI9VUSxAbDnmZCGRt/MbA0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10 h1:7pfKk5+4RkphBMn8TFwEPwbnSKwUjx1VORwJcX0Q8Q0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260707163957-4e3e503e8b10/go.mod h1:75XYxEMix/kXHlKza4pP9P2in9w1tB3vM2TY3G4mHl4= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045 h1:GEU1ljOJlCPnBCx1TqMc/YU99abXeSTDQelERw3BPAo= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045/go.mod h1:75XYxEMix/kXHlKza4pP9P2in9w1tB3vM2TY3G4mHl4= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA= github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw= diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go index 001bcca4a3..b5b40a171a 100644 --- a/internal/switchover/endpoint/command.go +++ b/internal/switchover/endpoint/command.go @@ -15,7 +15,6 @@ type out struct { DisplayName string `human:"Display Name" serialized:"display_name"` SwitchoverPair string `human:"Switchover Pair" serialized:"switchover_pair"` Environment string `human:"Environment" serialized:"environment"` - DrEndpoint string `human:"DR Endpoint" serialized:"dr_endpoint"` Phase string `human:"Phase" serialized:"phase"` } diff --git a/internal/switchover/endpoint/command_create.go b/internal/switchover/endpoint/command_create.go index 9c9c18f1fb..ae874f801d 100644 --- a/internal/switchover/endpoint/command_create.go +++ b/internal/switchover/endpoint/command_create.go @@ -23,13 +23,13 @@ func (c *command) newCreateCommand() *cobra.Command { Example: examples.BuildExampleString( examples.Example{ Text: `Create switchover endpoint "prod-kafka-dr-endpoint" for switchover pair "sw-123456".`, - Code: `confluent switchover endpoint create prod-kafka-dr-endpoint --switchover-pair sw-123456 --endpoint name=west-platt,resource-id=lkc-west,type=PRIVATE --endpoint name=east-platt,resource-id=lkc-east,type=PRIVATE`, + Code: `confluent switchover endpoint create prod-kafka-dr-endpoint --switchover-pair sw-123456 --endpoint name=west-platt,type=private --endpoint name=east-platt,type=private`, }, ), } cmd.Flags().String("switchover-pair", "", "The ID of the switchover pair this endpoint is bound to.") - cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,resource-id=,type=[,cloud=][,region=][,gateway=][,access-point=]". Must be specified exactly twice.`) + cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,type=[,gateway=][,access-point=]". Must be specified exactly twice.`) pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) pcmd.AddContextFlag(cmd, c.CLICommand) pcmd.AddOutputFlag(cmd) @@ -51,14 +51,8 @@ func parseEndpointFlag(raw string) (switchoverv1.SwitchoverV1EndpointConfig, err switch key { case "name": config.Name = value - case "resource-id": - filter.ResourceId = value case "type": filter.Type = value - case "cloud": - filter.Cloud = switchoverv1.PtrString(value) - case "region": - filter.Region = switchoverv1.PtrString(value) case "gateway": filter.Gateway = switchoverv1.PtrString(value) case "access-point": @@ -67,8 +61,8 @@ func parseEndpointFlag(raw string) (switchoverv1.SwitchoverV1EndpointConfig, err return config, fmt.Errorf(`invalid --endpoint key %q`, key) } } - if config.Name == "" || filter.ResourceId == "" || filter.Type == "" { - return config, fmt.Errorf(`invalid --endpoint value %q: "name", "resource-id", and "type" are required`, raw) + if config.Name == "" || filter.Type == "" { + return config, fmt.Errorf(`invalid --endpoint value %q: "name" and "type" are required`, raw) } config.EndpointFilter = filter return config, nil @@ -106,10 +100,10 @@ func (c *command) create(cmd *cobra.Command, args []string) error { endpoint := switchoverv1.SwitchoverV1SwitchoverEndpoint{ Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ - DisplayName: switchoverv1.PtrString(displayName), - Endpoints: &endpoints, - Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, - SwitchoverPair: &switchoverv1.EnvScopedObjectReference{Id: switchoverPairId}, + DisplayName: switchoverv1.PtrString(displayName), + Endpoints: &endpoints, + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + SwitchoverPairId: switchoverv1.PtrString(switchoverPairId), }, } @@ -126,9 +120,8 @@ func printSwitchoverEndpoint(cmd *cobra.Command, endpoint switchoverv1.Switchove table.Add(&out{ Id: endpoint.GetId(), DisplayName: endpoint.Spec.GetDisplayName(), - SwitchoverPair: endpoint.Spec.SwitchoverPair.GetId(), + SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), Environment: endpoint.Spec.Environment.GetId(), - DrEndpoint: endpoint.Spec.GetDrEndpoint(), Phase: endpoint.Status.GetPhase(), }) return table.Print() From 6572f57d6a85b9bf4652df84f6114b928c72225e Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Wed, 8 Jul 2026 16:01:28 -0700 Subject: [PATCH 04/10] [local-test-only] Hyperlink PR references in README Co-Authored-By: Claude Sonnet 5 --- SWITCHOVER_LOCAL_DEMO.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/SWITCHOVER_LOCAL_DEMO.md b/SWITCHOVER_LOCAL_DEMO.md index 89a2777c1d..2dd0eb9df9 100644 --- a/SWITCHOVER_LOCAL_DEMO.md +++ b/SWITCHOVER_LOCAL_DEMO.md @@ -12,14 +12,28 @@ top of the upstream CLI. ``` ~/sdk_cli_tf_switchover/ -├── cli/ confluentinc/cli — branch `switchover-cli-local-demo` (PR #3399) -├── ccloud-sdk-go-v2-internal/ confluentinc/ccloud-sdk-go-v2-internal — branch `ORC-10130-sync-switchover-sdk` (PR #832) -├── api/ confluentinc/api — branch `ORC-10130-switchover-api-spec` (PR #2545) -├── cc-switchover/ confluentinc/cc-switchover — branch `master` (reference/source-of-truth) -└── terraform-provider-confluent/ confluentinc/terraform-provider-confluent — branch `master` (not used here) +├── cli/ +├── ccloud-sdk-go-v2-internal/ +├── api/ +├── cc-switchover/ +└── terraform-provider-confluent/ ``` -`cli`'s `go.mod` pins the switchover SDK to PR #832's commit: +- `cli/` — confluentinc/cli, branch `switchover-cli-local-demo`, + [PR #3399](https://github.com/confluentinc/cli/pull/3399) +- `ccloud-sdk-go-v2-internal/` — confluentinc/ccloud-sdk-go-v2-internal, + branch `ORC-10130-sync-switchover-sdk`, + [PR #832](https://github.com/confluentinc/ccloud-sdk-go-v2-internal/pull/832) +- `api/` — confluentinc/api, branch `ORC-10130-switchover-api-spec`, + [PR #2545](https://github.com/confluentinc/api/pull/2545) +- `cc-switchover/` — confluentinc/cc-switchover, branch `master` + (reference/source-of-truth) +- `terraform-provider-confluent/` — confluentinc/terraform-provider-confluent, + branch `master` (not used here) + +`cli`'s `go.mod` pins the switchover SDK to +[PR #832](https://github.com/confluentinc/ccloud-sdk-go-v2-internal/pull/832)'s +commit: ``` github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045 From 6b5021bf3897e4cd1dc084e6910592be60850c08 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Thu, 9 Jul 2026 12:33:09 -0700 Subject: [PATCH 05/10] [local-test-only] Support Cloud API key auth for switchover calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stag Switchover Early Access gate only applies to the bearer/login path, not Cloud API keys — add a CONFLUENT_CLOUD_API_KEY / CONFLUENT_CLOUD_API_SECRET escape hatch to switchoverApiContext for local demo/testing against staging orgs without EA granted yet, and document it in the README. Co-Authored-By: Claude Sonnet 5 --- SWITCHOVER_LOCAL_DEMO.md | 19 +++++++++++++++++++ pkg/ccloudv2/switchover.go | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/SWITCHOVER_LOCAL_DEMO.md b/SWITCHOVER_LOCAL_DEMO.md index 2dd0eb9df9..100c881d74 100644 --- a/SWITCHOVER_LOCAL_DEMO.md +++ b/SWITCHOVER_LOCAL_DEMO.md @@ -129,6 +129,25 @@ same way for the `devel` environment. If you know your staging org ID, add Everything below (`switchover pair`/`switchover endpoint` commands) works identically once logged in, regardless of which environment you logged into. +### Bypassing the stag EA gate with a Cloud API key + +The Switchover Early Access gate on `stag`/`devel` is only enforced on the +bearer-token (`login`) path — a Cloud API key (Basic auth) sails through it. +If your staging org doesn't have EA granted yet, set both of these and the +`switchover` commands will use Basic auth instead of your login session's +bearer token: + +```bash +export CONFLUENT_CLOUD_API_KEY= +export CONFLUENT_CLOUD_API_SECRET= +``` + +Create a Cloud API key first (still requires being logged in once, to create +it): `confluent-dev api-key create --resource cloud`. You still need +`confluent-dev login` for non-switchover commands / context; this env var +pair only affects the switchover API calls (see +`pkg/ccloudv2/switchover.go`'s `switchoverApiContext`). + ### Switchover pairs ```bash diff --git a/pkg/ccloudv2/switchover.go b/pkg/ccloudv2/switchover.go index c1664094de..73bbe1ddbb 100644 --- a/pkg/ccloudv2/switchover.go +++ b/pkg/ccloudv2/switchover.go @@ -3,6 +3,7 @@ package ccloudv2 import ( "context" "net/http" + "os" switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" @@ -19,7 +20,15 @@ func newSwitchoverClient(httpClient *http.Client, url, userAgent string, unsafeT return switchoverv1.NewAPIClient(cfg) } +// switchoverApiContext normally authenticates with the logged-in session's +// bearer token. Local-test-only escape hatch: if CONFLUENT_CLOUD_API_KEY and +// CONFLUENT_CLOUD_API_SECRET (a Cloud API key, `api-key create --resource +// cloud`) are set, use Basic auth instead — the stag Switchover Early Access +// gate only applies to the bearer/login path, not Cloud API keys. func (c *Client) switchoverApiContext() context.Context { + if key, secret := os.Getenv("CONFLUENT_CLOUD_API_KEY"), os.Getenv("CONFLUENT_CLOUD_API_SECRET"); key != "" && secret != "" { + return context.WithValue(context.Background(), switchoverv1.ContextBasicAuth, switchoverv1.BasicAuth{UserName: key, Password: secret}) + } return context.WithValue(context.Background(), switchoverv1.ContextAccessToken, c.cfg.Context().GetAuthToken()) } From cb834a5555f75bb80f28e5aa352285a314fd3004 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Thu, 16 Jul 2026 17:17:17 -0700 Subject: [PATCH 06/10] Sync switchover SDK bump and migrate commands to regenerated API Bumps ccloud-sdk-go-v2-internal/switchover to v0.0.0-20260716234419-9fed8981fa8c (regenerated from confluentinc/api#2545) and migrates the switchover commands to the updated API surface: - Pair/Endpoint update now send the dedicated SwitchoverV1Switchover{Pair, Endpoint}UpdateRequest bodies. - Spec.Environment changed from EnvScopedObjectReference to a flat env-id string across create/print paths. - Pair failover request is now nested under Spec and carries the required environment in the body; trigger-switch gained an --environment flag. - EndpointFilter.Gateway was renamed to NetworkId; the --endpoint "gateway=" key is now "network=". - Removed `endpoint activate`: the Activate operation was dropped from the API. Also merges origin/main to keep the branch in sync. Validation: - go build ./... clean - make build (goreleaser) produces a working binary; `switchover` command tree verified (activate gone, trigger-switch has --environment). Co-Authored-By: Claude Opus 4.8 (1M context) --- go.mod | 2 +- go.sum | 4 +- internal/switchover/endpoint/command.go | 1 - .../switchover/endpoint/command_activate.go | 37 ------------------- .../switchover/endpoint/command_create.go | 10 ++--- .../switchover/endpoint/command_update.go | 4 +- internal/switchover/pair/command_create.go | 4 +- .../switchover/pair/command_trigger_switch.go | 14 ++++++- internal/switchover/pair/command_update.go | 4 +- pkg/ccloudv2/switchover.go | 13 ++----- 10 files changed, 30 insertions(+), 63 deletions(-) delete mode 100644 internal/switchover/endpoint/command_activate.go diff --git a/go.mod b/go.mod index d734ef2c79..dade060f4d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/charmbracelet/lipgloss v0.11.0 github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 - github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045 + github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260716234419-9fed8981fa8c github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 github.com/confluentinc/ccloud-sdk-go-v2/billing v0.3.0 diff --git a/go.sum b/go.sum index 1d9b3bc6e6..4b228a677c 100644 --- a/go.sum +++ b/go.sum @@ -172,8 +172,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 h1:19qEGhkbZa5fopKCe0VPIV+Sasby4Pv10z9ZaktwWso= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52/go.mod h1:62EMf+5uFEt1BJ2q8WMrUoI9VUSxAbDnmZCGRt/MbA0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045 h1:GEU1ljOJlCPnBCx1TqMc/YU99abXeSTDQelERw3BPAo= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045/go.mod h1:75XYxEMix/kXHlKza4pP9P2in9w1tB3vM2TY3G4mHl4= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260716234419-9fed8981fa8c h1:3MD0b07bfAl0UTgspFYC7R3LkVMWvapm1BcZ5LNxSA4= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260716234419-9fed8981fa8c/go.mod h1:yv1VtjqyqD7G5gpdrzR2hGyIAfEOuLA3aTQW0TZ6Iek= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA= github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw= diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go index b5b40a171a..7ac709a32e 100644 --- a/internal/switchover/endpoint/command.go +++ b/internal/switchover/endpoint/command.go @@ -30,7 +30,6 @@ func New(prerunner pcmd.PreRunner) *cobra.Command { cmd.AddCommand(c.newCreateCommand()) cmd.AddCommand(c.newDescribeCommand()) cmd.AddCommand(c.newUpdateCommand()) - cmd.AddCommand(c.newActivateCommand()) return cmd } diff --git a/internal/switchover/endpoint/command_activate.go b/internal/switchover/endpoint/command_activate.go deleted file mode 100644 index 7fc1b83b7c..0000000000 --- a/internal/switchover/endpoint/command_activate.go +++ /dev/null @@ -1,37 +0,0 @@ -package endpoint - -import ( - "github.com/spf13/cobra" - - pcmd "github.com/confluentinc/cli/v4/pkg/cmd" - "github.com/confluentinc/cli/v4/pkg/examples" -) - -func (c *command) newActivateCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "activate ", - Short: "Activate a switchover endpoint.", - Long: "Activate a switchover endpoint, applying its desired routing target.", - Args: cobra.ExactArgs(1), - RunE: c.activate, - Example: examples.BuildExampleString( - examples.Example{ - Text: `Activate switchover endpoint "se-123456".`, - Code: `confluent switchover endpoint activate se-123456`, - }, - ), - } - - pcmd.AddOutputFlag(cmd) - - return cmd -} - -func (c *command) activate(cmd *cobra.Command, args []string) error { - result, err := c.V2Client.ActivateSwitchoverEndpoint(args[0]) - if err != nil { - return err - } - - return printSwitchoverEndpoint(cmd, result) -} diff --git a/internal/switchover/endpoint/command_create.go b/internal/switchover/endpoint/command_create.go index ae874f801d..0e3f752ef0 100644 --- a/internal/switchover/endpoint/command_create.go +++ b/internal/switchover/endpoint/command_create.go @@ -29,7 +29,7 @@ func (c *command) newCreateCommand() *cobra.Command { } cmd.Flags().String("switchover-pair", "", "The ID of the switchover pair this endpoint is bound to.") - cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,type=[,gateway=][,access-point=]". Must be specified exactly twice.`) + cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,type=[,network=][,access-point=]". Must be specified exactly twice.`) pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) pcmd.AddContextFlag(cmd, c.CLICommand) pcmd.AddOutputFlag(cmd) @@ -53,8 +53,8 @@ func parseEndpointFlag(raw string) (switchoverv1.SwitchoverV1EndpointConfig, err config.Name = value case "type": filter.Type = value - case "gateway": - filter.Gateway = switchoverv1.PtrString(value) + case "network": + filter.NetworkId = switchoverv1.PtrString(value) case "access-point": filter.AccessPoint = switchoverv1.PtrString(value) default: @@ -102,7 +102,7 @@ func (c *command) create(cmd *cobra.Command, args []string) error { Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ DisplayName: switchoverv1.PtrString(displayName), Endpoints: &endpoints, - Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + Environment: switchoverv1.PtrString(environmentId), SwitchoverPairId: switchoverv1.PtrString(switchoverPairId), }, } @@ -121,7 +121,7 @@ func printSwitchoverEndpoint(cmd *cobra.Command, endpoint switchoverv1.Switchove Id: endpoint.GetId(), DisplayName: endpoint.Spec.GetDisplayName(), SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), - Environment: endpoint.Spec.Environment.GetId(), + Environment: endpoint.Spec.GetEnvironment(), Phase: endpoint.Status.GetPhase(), }) return table.Print() diff --git a/internal/switchover/endpoint/command_update.go b/internal/switchover/endpoint/command_update.go index a4213c1488..e33ca03270 100644 --- a/internal/switchover/endpoint/command_update.go +++ b/internal/switchover/endpoint/command_update.go @@ -38,8 +38,8 @@ func (c *command) update(cmd *cobra.Command, args []string) error { return err } - endpoint := switchoverv1.SwitchoverV1SwitchoverEndpoint{ - Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ + endpoint := switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequest{ + Spec: switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequestSpec{ DisplayName: switchoverv1.PtrString(displayName), }, } diff --git a/internal/switchover/pair/command_create.go b/internal/switchover/pair/command_create.go index a4335401ea..e1bc2c074d 100644 --- a/internal/switchover/pair/command_create.go +++ b/internal/switchover/pair/command_create.go @@ -97,7 +97,7 @@ func (c *command) create(cmd *cobra.Command, args []string) error { DisplayName: switchoverv1.PtrString(displayName), Members: &members, ActiveMember: switchoverv1.PtrString(activeMember), - Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + Environment: switchoverv1.PtrString(environmentId), }, } @@ -115,7 +115,7 @@ func printSwitchoverPair(cmd *cobra.Command, pair switchoverv1.SwitchoverV1Switc Id: pair.GetId(), DisplayName: pair.Spec.GetDisplayName(), ActiveMember: pair.Spec.GetActiveMember(), - Environment: pair.Spec.Environment.GetId(), + Environment: pair.Spec.GetEnvironment(), Phase: pair.Status.GetPhase(), }) return table.Print() diff --git a/internal/switchover/pair/command_trigger_switch.go b/internal/switchover/pair/command_trigger_switch.go index c83f5fee1c..34171e1e15 100644 --- a/internal/switchover/pair/command_trigger_switch.go +++ b/internal/switchover/pair/command_trigger_switch.go @@ -30,6 +30,8 @@ func (c *command) newTriggerSwitchCommand() *cobra.Command { cmd.Flags().String("active-member", "", "The name of the member to promote to active. If omitted, the other member is promoted.") cmd.Flags().String("failover-type", "CLEAN", "The failover semantics to apply: CLEAN, UNCLEAN, or RESTORE.") cmd.Flags().Bool("force", false, "Skip the confirmation prompt.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) pcmd.AddOutputFlag(cmd) return cmd @@ -48,16 +50,24 @@ func (c *command) triggerSwitch(cmd *cobra.Command, args []string) error { return err } + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + promptMsg := fmt.Sprintf(`This triggers a %s failover on switchover pair "%s", redirecting live traffic between regions. Do you want to proceed?`, failoverType, id) if err := deletion.ConfirmPrompt(cmd, promptMsg); err != nil { return err } req := switchoverv1.SwitchoverV1SwitchoverPairFailoverRequest{ - FailoverType: switchoverv1.PtrString(failoverType), + Spec: switchoverv1.SwitchoverV1SwitchoverPairFailoverRequestSpec{ + Environment: environmentId, + FailoverType: switchoverv1.PtrString(failoverType), + }, } if activeMember != "" { - req.ActiveMember = switchoverv1.PtrString(activeMember) + req.Spec.ActiveMember = switchoverv1.PtrString(activeMember) } result, err := c.V2Client.TriggerSwitchoverPairFailover(id, req) diff --git a/internal/switchover/pair/command_update.go b/internal/switchover/pair/command_update.go index 7b201f23c7..562673401d 100644 --- a/internal/switchover/pair/command_update.go +++ b/internal/switchover/pair/command_update.go @@ -38,8 +38,8 @@ func (c *command) update(cmd *cobra.Command, args []string) error { return err } - pair := switchoverv1.SwitchoverV1SwitchoverPair{ - Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{ + pair := switchoverv1.SwitchoverV1SwitchoverPairUpdateRequest{ + Spec: switchoverv1.SwitchoverV1SwitchoverPairUpdateRequestSpec{ DisplayName: switchoverv1.PtrString(displayName), }, } diff --git a/pkg/ccloudv2/switchover.go b/pkg/ccloudv2/switchover.go index 73bbe1ddbb..6f6e5e96cb 100644 --- a/pkg/ccloudv2/switchover.go +++ b/pkg/ccloudv2/switchover.go @@ -42,8 +42,8 @@ func (c *Client) GetSwitchoverPair(id, environment string) (switchoverv1.Switcho return res, errors.CatchCCloudV2Error(err, httpResp) } -func (c *Client) UpdateSwitchoverPair(id string, pair switchoverv1.SwitchoverV1SwitchoverPair) (switchoverv1.SwitchoverV1SwitchoverPair, error) { - res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPair(pair).Execute() +func (c *Client) UpdateSwitchoverPair(id string, update switchoverv1.SwitchoverV1SwitchoverPairUpdateRequest) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPairUpdateRequest(update).Execute() return res, errors.CatchCCloudV2Error(err, httpResp) } @@ -62,12 +62,7 @@ func (c *Client) GetSwitchoverEndpoint(id, environment string) (switchoverv1.Swi return res, errors.CatchCCloudV2Error(err, httpResp) } -func (c *Client) UpdateSwitchoverEndpoint(id string, endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { - res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).SwitchoverV1SwitchoverEndpoint(endpoint).Execute() - return res, errors.CatchCCloudV2Error(err, httpResp) -} - -func (c *Client) ActivateSwitchoverEndpoint(id string) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { - res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.ActivateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Execute() +func (c *Client) UpdateSwitchoverEndpoint(id string, update switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequest) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).SwitchoverV1SwitchoverEndpointUpdateRequest(update).Execute() return res, errors.CatchCCloudV2Error(err, httpResp) } From 4ac8be50237d3b160491dbb141ac1704b46faf2b Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Thu, 16 Jul 2026 19:06:05 -0700 Subject: [PATCH 07/10] Pass required environment on switchover pair/endpoint update The switchover update operations require the `environment` query parameter (required: true in the OpenAPI spec), but the pair/endpoint update commands neither exposed `--environment` nor forwarded it, so `confluent switchover pair update` / `endpoint update` would fail against a live backend. Add the shared `--environment` flag to both update commands and thread the resolved environment id through the UpdateSwitchoverPair/UpdateSwitchoverEndpoint client wrappers via `.Environment(...)`, matching the get/failover paths and the auth/context design (env is explicit; org implicit from the credential). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/switchover/endpoint/command_update.go | 9 ++++++++- internal/switchover/pair/command_update.go | 9 ++++++++- pkg/ccloudv2/switchover.go | 8 ++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/internal/switchover/endpoint/command_update.go b/internal/switchover/endpoint/command_update.go index e33ca03270..9cb0bff91b 100644 --- a/internal/switchover/endpoint/command_update.go +++ b/internal/switchover/endpoint/command_update.go @@ -25,6 +25,8 @@ func (c *command) newUpdateCommand() *cobra.Command { } cmd.Flags().String("display-name", "", "A human-readable name for the switchover endpoint.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) pcmd.AddOutputFlag(cmd) cobra.CheckErr(cmd.MarkFlagRequired("display-name")) @@ -38,13 +40,18 @@ func (c *command) update(cmd *cobra.Command, args []string) error { return err } + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + endpoint := switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequest{ Spec: switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequestSpec{ DisplayName: switchoverv1.PtrString(displayName), }, } - result, err := c.V2Client.UpdateSwitchoverEndpoint(args[0], endpoint) + result, err := c.V2Client.UpdateSwitchoverEndpoint(args[0], environmentId, endpoint) if err != nil { return err } diff --git a/internal/switchover/pair/command_update.go b/internal/switchover/pair/command_update.go index 562673401d..83e4817c72 100644 --- a/internal/switchover/pair/command_update.go +++ b/internal/switchover/pair/command_update.go @@ -25,6 +25,8 @@ func (c *command) newUpdateCommand() *cobra.Command { } cmd.Flags().String("display-name", "", "A human-readable name for the switchover pair.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) pcmd.AddOutputFlag(cmd) cobra.CheckErr(cmd.MarkFlagRequired("display-name")) @@ -38,13 +40,18 @@ func (c *command) update(cmd *cobra.Command, args []string) error { return err } + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + pair := switchoverv1.SwitchoverV1SwitchoverPairUpdateRequest{ Spec: switchoverv1.SwitchoverV1SwitchoverPairUpdateRequestSpec{ DisplayName: switchoverv1.PtrString(displayName), }, } - result, err := c.V2Client.UpdateSwitchoverPair(args[0], pair) + result, err := c.V2Client.UpdateSwitchoverPair(args[0], environmentId, pair) if err != nil { return err } diff --git a/pkg/ccloudv2/switchover.go b/pkg/ccloudv2/switchover.go index 6f6e5e96cb..d307d7b163 100644 --- a/pkg/ccloudv2/switchover.go +++ b/pkg/ccloudv2/switchover.go @@ -42,8 +42,8 @@ func (c *Client) GetSwitchoverPair(id, environment string) (switchoverv1.Switcho return res, errors.CatchCCloudV2Error(err, httpResp) } -func (c *Client) UpdateSwitchoverPair(id string, update switchoverv1.SwitchoverV1SwitchoverPairUpdateRequest) (switchoverv1.SwitchoverV1SwitchoverPair, error) { - res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPairUpdateRequest(update).Execute() +func (c *Client) UpdateSwitchoverPair(id, environment string, update switchoverv1.SwitchoverV1SwitchoverPairUpdateRequest) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).Environment(environment).SwitchoverV1SwitchoverPairUpdateRequest(update).Execute() return res, errors.CatchCCloudV2Error(err, httpResp) } @@ -62,7 +62,7 @@ func (c *Client) GetSwitchoverEndpoint(id, environment string) (switchoverv1.Swi return res, errors.CatchCCloudV2Error(err, httpResp) } -func (c *Client) UpdateSwitchoverEndpoint(id string, update switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequest) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { - res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).SwitchoverV1SwitchoverEndpointUpdateRequest(update).Execute() +func (c *Client) UpdateSwitchoverEndpoint(id, environment string, update switchoverv1.SwitchoverV1SwitchoverEndpointUpdateRequest) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Environment(environment).SwitchoverV1SwitchoverEndpointUpdateRequest(update).Execute() return res, errors.CatchCCloudV2Error(err, httpResp) } From b0793a916d0c94ab09014c64b88503ad590968bb Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Thu, 16 Jul 2026 19:52:26 -0700 Subject: [PATCH 08/10] Add list and delete commands for switchover pairs and endpoints Wire `switchover pair list`/`pair delete` and `switchover endpoint list`/`endpoint delete`, matching the switchover API surface. Adds List*/Delete* wrappers in pkg/ccloudv2/switchover.go (token-based pagination) and SwitchoverPair/SwitchoverEndpoint resource constants. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/switchover/endpoint/command.go | 2 + .../switchover/endpoint/command_delete.go | 55 ++++++++++++++ internal/switchover/endpoint/command_list.go | 64 +++++++++++++++++ internal/switchover/pair/command.go | 2 + internal/switchover/pair/command_delete.go | 55 ++++++++++++++ internal/switchover/pair/command_list.go | 58 +++++++++++++++ pkg/ccloudv2/switchover.go | 71 +++++++++++++++++++ pkg/resource/resource.go | 2 + 8 files changed, 309 insertions(+) create mode 100644 internal/switchover/endpoint/command_delete.go create mode 100644 internal/switchover/endpoint/command_list.go create mode 100644 internal/switchover/pair/command_delete.go create mode 100644 internal/switchover/pair/command_list.go diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go index 7ac709a32e..c73c7cbbdc 100644 --- a/internal/switchover/endpoint/command.go +++ b/internal/switchover/endpoint/command.go @@ -28,7 +28,9 @@ func New(prerunner pcmd.PreRunner) *cobra.Command { c := &command{pcmd.NewAuthenticatedCLICommand(cmd, prerunner)} cmd.AddCommand(c.newCreateCommand()) + cmd.AddCommand(c.newDeleteCommand()) cmd.AddCommand(c.newDescribeCommand()) + cmd.AddCommand(c.newListCommand()) cmd.AddCommand(c.newUpdateCommand()) return cmd diff --git a/internal/switchover/endpoint/command_delete.go b/internal/switchover/endpoint/command_delete.go new file mode 100644 index 0000000000..497cf1ddaf --- /dev/null +++ b/internal/switchover/endpoint/command_delete.go @@ -0,0 +1,55 @@ +package endpoint + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/deletion" + "github.com/confluentinc/cli/v4/pkg/examples" + "github.com/confluentinc/cli/v4/pkg/resource" +) + +func (c *command) newDeleteCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [id-2] ... [id-n]", + Short: "Delete one or more switchover endpoints.", + Args: cobra.MinimumNArgs(1), + RunE: c.delete, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Delete switchover endpoint "se-123456" in the current environment.`, + Code: "confluent switchover endpoint delete se-123456", + }, + ), + } + + pcmd.AddForceFlag(cmd) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) delete(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + existenceFunc := func(id string) bool { + _, err := c.V2Client.GetSwitchoverEndpoint(id, environmentId) + return err == nil + } + + if err := deletion.ValidateAndConfirm(cmd, args, existenceFunc, resource.SwitchoverEndpoint); err != nil { + return err + } + + deleteFunc := func(id string) error { + return c.V2Client.DeleteSwitchoverEndpoint(id, environmentId) + } + + _, err = deletion.Delete(cmd, args, deleteFunc, resource.SwitchoverEndpoint) + return err +} diff --git a/internal/switchover/endpoint/command_list.go b/internal/switchover/endpoint/command_list.go new file mode 100644 index 0000000000..74a9ace45c --- /dev/null +++ b/internal/switchover/endpoint/command_list.go @@ -0,0 +1,64 @@ +package endpoint + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List switchover endpoints.", + Args: cobra.NoArgs, + RunE: c.list, + Example: examples.BuildExampleString( + examples.Example{ + Text: "List switchover endpoints in the current environment.", + Code: "confluent switchover endpoint list", + }, + examples.Example{ + Text: `List switchover endpoints for switchover pair "sw-123456".`, + Code: "confluent switchover endpoint list --switchover-pair sw-123456", + }, + ), + } + + cmd.Flags().String("switchover-pair", "", "Filter the results by the associated switchover pair ID.") + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) list(cmd *cobra.Command, _ []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + switchoverPair, err := cmd.Flags().GetString("switchover-pair") + if err != nil { + return err + } + + endpoints, err := c.V2Client.ListSwitchoverEndpoints(environmentId, switchoverPair) + if err != nil { + return err + } + + list := output.NewList(cmd) + for _, endpoint := range endpoints { + list.Add(&out{ + Id: endpoint.GetId(), + DisplayName: endpoint.Spec.GetDisplayName(), + SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), + Environment: endpoint.Spec.GetEnvironment(), + Phase: endpoint.Status.GetPhase(), + }) + } + return list.Print() +} diff --git a/internal/switchover/pair/command.go b/internal/switchover/pair/command.go index f15d5eacf5..7ba6709de9 100644 --- a/internal/switchover/pair/command.go +++ b/internal/switchover/pair/command.go @@ -27,7 +27,9 @@ func New(prerunner pcmd.PreRunner) *cobra.Command { c := &command{pcmd.NewAuthenticatedCLICommand(cmd, prerunner)} cmd.AddCommand(c.newCreateCommand()) + cmd.AddCommand(c.newDeleteCommand()) cmd.AddCommand(c.newDescribeCommand()) + cmd.AddCommand(c.newListCommand()) cmd.AddCommand(c.newUpdateCommand()) cmd.AddCommand(c.newTriggerSwitchCommand()) diff --git a/internal/switchover/pair/command_delete.go b/internal/switchover/pair/command_delete.go new file mode 100644 index 0000000000..e3d1e0bef4 --- /dev/null +++ b/internal/switchover/pair/command_delete.go @@ -0,0 +1,55 @@ +package pair + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/deletion" + "github.com/confluentinc/cli/v4/pkg/examples" + "github.com/confluentinc/cli/v4/pkg/resource" +) + +func (c *command) newDeleteCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [id-2] ... [id-n]", + Short: "Delete one or more switchover pairs.", + Args: cobra.MinimumNArgs(1), + RunE: c.delete, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Delete switchover pair "sw-123456" in the current environment.`, + Code: "confluent switchover pair delete sw-123456", + }, + ), + } + + pcmd.AddForceFlag(cmd) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) delete(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + existenceFunc := func(id string) bool { + _, err := c.V2Client.GetSwitchoverPair(id, environmentId) + return err == nil + } + + if err := deletion.ValidateAndConfirm(cmd, args, existenceFunc, resource.SwitchoverPair); err != nil { + return err + } + + deleteFunc := func(id string) error { + return c.V2Client.DeleteSwitchoverPair(id, environmentId) + } + + _, err = deletion.Delete(cmd, args, deleteFunc, resource.SwitchoverPair) + return err +} diff --git a/internal/switchover/pair/command_list.go b/internal/switchover/pair/command_list.go new file mode 100644 index 0000000000..9e6ea0c7f9 --- /dev/null +++ b/internal/switchover/pair/command_list.go @@ -0,0 +1,58 @@ +package pair + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List switchover pairs.", + Args: cobra.NoArgs, + RunE: c.list, + Example: examples.BuildExampleString( + examples.Example{ + Text: "List switchover pairs in the current environment.", + Code: "confluent switchover pair list", + }, + examples.Example{ + Text: `List switchover pairs in environment "env-123456".`, + Code: "confluent switchover pair list --environment env-123456", + }, + ), + } + + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) list(cmd *cobra.Command, _ []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + pairs, err := c.V2Client.ListSwitchoverPairs(environmentId) + if err != nil { + return err + } + + list := output.NewList(cmd) + for _, pair := range pairs { + list.Add(&out{ + Id: pair.GetId(), + DisplayName: pair.Spec.GetDisplayName(), + ActiveMember: pair.Spec.GetActiveMember(), + Environment: pair.Spec.GetEnvironment(), + Phase: pair.Status.GetPhase(), + }) + } + return list.Print() +} diff --git a/pkg/ccloudv2/switchover.go b/pkg/ccloudv2/switchover.go index d307d7b163..6a067be74f 100644 --- a/pkg/ccloudv2/switchover.go +++ b/pkg/ccloudv2/switchover.go @@ -52,6 +52,40 @@ func (c *Client) TriggerSwitchoverPairFailover(id string, req switchoverv1.Switc return res, errors.CatchCCloudV2Error(err, httpResp) } +func (c *Client) ListSwitchoverPairs(environment string) ([]switchoverv1.SwitchoverV1SwitchoverPair, error) { + var list []switchoverv1.SwitchoverV1SwitchoverPair + + done := false + pageToken := "" + for !done { + page, httpResp, err := c.executeListSwitchoverPairs(environment, pageToken) + if err != nil { + return nil, errors.CatchCCloudV2Error(err, httpResp) + } + list = append(list, page.GetData()...) + + metadata := page.GetMetadata() + pagination := metadata.GetPagination() + pageToken = pagination.GetNextPageToken() + done = pageToken == "" + } + + return list, nil +} + +func (c *Client) executeListSwitchoverPairs(environment, pageToken string) (switchoverv1.SwitchoverV1SwitchoverPairList, *http.Response, error) { + req := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.ListSwitchoverV1SwitchoverPairs(c.switchoverApiContext()).Environment(environment).PageSize(ccloudV2ListPageSize) + if pageToken != "" { + req = req.PageToken(pageToken) + } + return req.Execute() +} + +func (c *Client) DeleteSwitchoverPair(id, environment string) error { + httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.DeleteSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).Environment(environment).Execute() + return errors.CatchCCloudV2Error(err, httpResp) +} + func (c *Client) CreateSwitchoverEndpoint(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.CreateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext()).SwitchoverV1SwitchoverEndpoint(endpoint).Execute() return res, errors.CatchCCloudV2Error(err, httpResp) @@ -66,3 +100,40 @@ func (c *Client) UpdateSwitchoverEndpoint(id, environment string, update switcho res, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Environment(environment).SwitchoverV1SwitchoverEndpointUpdateRequest(update).Execute() return res, errors.CatchCCloudV2Error(err, httpResp) } + +func (c *Client) ListSwitchoverEndpoints(environment, switchoverPair string) ([]switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + var list []switchoverv1.SwitchoverV1SwitchoverEndpoint + + done := false + pageToken := "" + for !done { + page, httpResp, err := c.executeListSwitchoverEndpoints(environment, switchoverPair, pageToken) + if err != nil { + return nil, errors.CatchCCloudV2Error(err, httpResp) + } + list = append(list, page.GetData()...) + + metadata := page.GetMetadata() + pagination := metadata.GetPagination() + pageToken = pagination.GetNextPageToken() + done = pageToken == "" + } + + return list, nil +} + +func (c *Client) executeListSwitchoverEndpoints(environment, switchoverPair, pageToken string) (switchoverv1.SwitchoverV1SwitchoverEndpointList, *http.Response, error) { + req := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.ListSwitchoverV1SwitchoverEndpoints(c.switchoverApiContext()).Environment(environment).PageSize(ccloudV2ListPageSize) + if switchoverPair != "" { + req = req.SwitchoverPair(switchoverPair) + } + if pageToken != "" { + req = req.PageToken(pageToken) + } + return req.Execute() +} + +func (c *Client) DeleteSwitchoverEndpoint(id, environment string) error { + httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.DeleteSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Environment(environment).Execute() + return errors.CatchCCloudV2Error(err, httpResp) +} diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 8cd05b3df7..353bc88b4c 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -81,6 +81,8 @@ const ( SchemaRegistryConfiguration = "Schema Registry configuration" ServiceAccount = "service account" SsoGroupMapping = "SSO group mapping" + SwitchoverPair = "switchover pair" + SwitchoverEndpoint = "switchover endpoint" Tableflow = "tableflow" Topic = "topic" TransitGatewayAttachment = "transit gateway attachment" From 218f02bb466a5325d7e061e19e3ec1c43509ab66 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Tue, 28 Jul 2026 10:21:23 -0700 Subject: [PATCH 09/10] Enrich switchover pair/endpoint describe & list output describe/list previously emitted only 5 flat fields in every format, so `-o json`/`-o yaml` dropped members, failover_type, endpoint config, conditions, and metadata. - Serialized output (`-o json`/`-o yaml`) now mirrors the API response verbatim (full spec + status), routed through the SDK object's JSON tags (fixes YAML previously emitting Go field names like `apiversion`). - Human `describe` now shows Members, Failover Type, and Conditions (pair) and Endpoints, Target, Conditions (endpoint). - Human `list` gains a Failover Type column (pair) and stays compact. Co-Authored-By: Claude Opus 4.8 --- internal/switchover/endpoint/command.go | 108 +++++++++++++++++- .../switchover/endpoint/command_create.go | 13 +-- internal/switchover/endpoint/command_list.go | 12 +- internal/switchover/pair/command.go | 102 ++++++++++++++++- internal/switchover/pair/command_create.go | 13 +-- internal/switchover/pair/command_list.go | 15 ++- 6 files changed, 236 insertions(+), 27 deletions(-) diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go index c73c7cbbdc..c0a3ecd281 100644 --- a/internal/switchover/endpoint/command.go +++ b/internal/switchover/endpoint/command.go @@ -1,21 +1,45 @@ package endpoint import ( + "encoding/json" + "fmt" + "strings" + "github.com/spf13/cobra" + "github.com/tidwall/pretty" + "gopkg.in/yaml.v3" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" ) type command struct { *pcmd.AuthenticatedCLICommand } +// out is the detailed human-readable shape used by create/describe/update. +// Machine-readable output (`-o json` / `-o yaml`) is emitted straight from the +// SDK object so it matches the API response verbatim. type out struct { - Id string `human:"ID" serialized:"id"` - DisplayName string `human:"Display Name" serialized:"display_name"` - SwitchoverPair string `human:"Switchover Pair" serialized:"switchover_pair"` - Environment string `human:"Environment" serialized:"environment"` - Phase string `human:"Phase" serialized:"phase"` + Id string `human:"ID"` + DisplayName string `human:"Display Name"` + SwitchoverPair string `human:"Switchover Pair"` + Environment string `human:"Environment"` + Target string `human:"Target,omitempty"` + Phase string `human:"Phase"` + Endpoints string `human:"Endpoints,omitempty"` + Conditions string `human:"Conditions,omitempty"` +} + +// listOut is the compact per-row shape for `list` (human output only). +type listOut struct { + Id string `human:"ID"` + DisplayName string `human:"Display Name"` + SwitchoverPair string `human:"Switchover Pair"` + Environment string `human:"Environment"` + Phase string `human:"Phase"` } func New(prerunner pcmd.PreRunner) *cobra.Command { @@ -35,3 +59,77 @@ func New(prerunner pcmd.PreRunner) *cobra.Command { return cmd } + +// printSerialized emits v as JSON or YAML using the SDK object's `json` tags, +// so the output matches the API response verbatim. (output.SerializedOutput's +// YAML path uses yaml.v3, which ignores `json` tags; route YAML through the +// JSON representation to preserve the API field names.) +func printSerialized(cmd *cobra.Command, v any) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + + if output.GetFormat(cmd) == output.YAML { + var generic any + if err := json.Unmarshal(data, &generic); err != nil { + return err + } + out, err := yaml.Marshal(generic) + if err != nil { + return err + } + output.Print(false, string(out)) + return nil + } + + output.Print(false, string(pretty.Pretty(data))) + return nil +} + +func newEndpointOut(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) *out { + return &out{ + Id: endpoint.GetId(), + DisplayName: endpoint.Spec.GetDisplayName(), + SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), + Environment: endpoint.Spec.GetEnvironment(), + Target: endpoint.Spec.GetTarget(), + Phase: endpoint.Status.GetPhase(), + Endpoints: formatEndpoints(endpoint.Spec.GetEndpoints()), + Conditions: formatConditions(endpoint.Status.GetConditions()), + } +} + +func formatEndpoints(endpoints []switchoverv1.SwitchoverV1EndpointConfig) string { + lines := make([]string, len(endpoints)) + for i, endpoint := range endpoints { + filter := endpoint.EndpointFilter + parts := []string{endpoint.GetName(), filter.GetType()} + if networkId := filter.GetNetworkId(); networkId != "" { + parts = append(parts, "network="+networkId) + } + if accessPoint := filter.GetAccessPoint(); accessPoint != "" { + parts = append(parts, "access-point="+accessPoint) + } + if hostname := endpoint.GetHostname(); hostname != "" { + parts = append(parts, "hostname="+hostname) + } + lines[i] = strings.Join(parts, " ") + } + return strings.Join(lines, "\n") +} + +func formatConditions(conditions []switchoverv1.SwitchoverV1Condition) string { + lines := make([]string, len(conditions)) + for i, condition := range conditions { + line := fmt.Sprintf("%s=%s", condition.GetType(), condition.GetStatus()) + if reason := condition.GetReason(); reason != "" { + line += fmt.Sprintf(" (%s)", reason) + } + if message := condition.GetMessage(); message != "" { + line += ": " + message + } + lines[i] = line + } + return strings.Join(lines, "\n") +} diff --git a/internal/switchover/endpoint/command_create.go b/internal/switchover/endpoint/command_create.go index 0e3f752ef0..79ad79152f 100644 --- a/internal/switchover/endpoint/command_create.go +++ b/internal/switchover/endpoint/command_create.go @@ -116,13 +116,12 @@ func (c *command) create(cmd *cobra.Command, args []string) error { } func printSwitchoverEndpoint(cmd *cobra.Command, endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) error { + // Serialized output mirrors the API response verbatim (full spec/status). + if output.GetFormat(cmd).IsSerialized() { + return printSerialized(cmd, endpoint) + } + table := output.NewTable(cmd) - table.Add(&out{ - Id: endpoint.GetId(), - DisplayName: endpoint.Spec.GetDisplayName(), - SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), - Environment: endpoint.Spec.GetEnvironment(), - Phase: endpoint.Status.GetPhase(), - }) + table.Add(newEndpointOut(endpoint)) return table.Print() } diff --git a/internal/switchover/endpoint/command_list.go b/internal/switchover/endpoint/command_list.go index 74a9ace45c..323841e53c 100644 --- a/internal/switchover/endpoint/command_list.go +++ b/internal/switchover/endpoint/command_list.go @@ -3,6 +3,8 @@ package endpoint import ( "github.com/spf13/cobra" + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" "github.com/confluentinc/cli/v4/pkg/examples" "github.com/confluentinc/cli/v4/pkg/output" @@ -50,9 +52,17 @@ func (c *command) list(cmd *cobra.Command, _ []string) error { return err } + // Serialized output mirrors the API response verbatim (full spec/status). + if output.GetFormat(cmd).IsSerialized() { + if endpoints == nil { + endpoints = []switchoverv1.SwitchoverV1SwitchoverEndpoint{} + } + return printSerialized(cmd, endpoints) + } + list := output.NewList(cmd) for _, endpoint := range endpoints { - list.Add(&out{ + list.Add(&listOut{ Id: endpoint.GetId(), DisplayName: endpoint.Spec.GetDisplayName(), SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), diff --git a/internal/switchover/pair/command.go b/internal/switchover/pair/command.go index 7ba6709de9..14300e7a49 100644 --- a/internal/switchover/pair/command.go +++ b/internal/switchover/pair/command.go @@ -1,21 +1,46 @@ package pair import ( + "encoding/json" + "fmt" + "strings" + "github.com/spf13/cobra" + "github.com/tidwall/pretty" + "gopkg.in/yaml.v3" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" ) type command struct { *pcmd.AuthenticatedCLICommand } +// out is the detailed human-readable shape used by create/describe/update/ +// trigger-switch. Machine-readable output (`-o json` / `-o yaml`) is emitted +// straight from the SDK object so it matches the API response verbatim. type out struct { - Id string `human:"ID" serialized:"id"` - DisplayName string `human:"Display Name" serialized:"display_name"` - ActiveMember string `human:"Active Member" serialized:"active_member"` - Environment string `human:"Environment" serialized:"environment"` - Phase string `human:"Phase" serialized:"phase"` + Id string `human:"ID"` + DisplayName string `human:"Display Name"` + Environment string `human:"Environment"` + ActiveMember string `human:"Active Member"` + FailoverType string `human:"Failover Type,omitempty"` + Phase string `human:"Phase"` + Members string `human:"Members,omitempty"` + Conditions string `human:"Conditions,omitempty"` +} + +// listOut is the compact per-row shape for `list` (human output only). +type listOut struct { + Id string `human:"ID"` + DisplayName string `human:"Display Name"` + Environment string `human:"Environment"` + ActiveMember string `human:"Active Member"` + FailoverType string `human:"Failover Type"` + Phase string `human:"Phase"` } func New(prerunner pcmd.PreRunner) *cobra.Command { @@ -35,3 +60,70 @@ func New(prerunner pcmd.PreRunner) *cobra.Command { return cmd } + +// printSerialized emits v as JSON or YAML using the SDK object's `json` tags, +// so the output matches the API response verbatim. (output.SerializedOutput's +// YAML path uses yaml.v3, which ignores `json` tags; route YAML through the +// JSON representation to preserve the API field names.) +func printSerialized(cmd *cobra.Command, v any) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + + if output.GetFormat(cmd) == output.YAML { + var generic any + if err := json.Unmarshal(data, &generic); err != nil { + return err + } + out, err := yaml.Marshal(generic) + if err != nil { + return err + } + output.Print(false, string(out)) + return nil + } + + output.Print(false, string(pretty.Pretty(data))) + return nil +} + +func newPairOut(pair switchoverv1.SwitchoverV1SwitchoverPair) *out { + return &out{ + Id: pair.GetId(), + DisplayName: pair.Spec.GetDisplayName(), + Environment: pair.Spec.GetEnvironment(), + ActiveMember: pair.Spec.GetActiveMember(), + FailoverType: pair.Spec.GetFailoverType(), + Phase: pair.Status.GetPhase(), + Members: formatMembers(pair.Spec.GetMembers()), + Conditions: formatConditions(pair.Status.GetConditions()), + } +} + +func formatMembers(members []switchoverv1.SwitchoverV1SwitchoverPairMember) string { + lines := make([]string, len(members)) + for i, member := range members { + location := "" + if member.Location != nil { + location = fmt.Sprintf(", %s/%s", member.Location.GetCloud(), member.Location.GetRegion()) + } + lines[i] = fmt.Sprintf("%s (%s%s)", member.GetName(), member.GetMemberId(), location) + } + return strings.Join(lines, "\n") +} + +func formatConditions(conditions []switchoverv1.SwitchoverV1Condition) string { + lines := make([]string, len(conditions)) + for i, condition := range conditions { + line := fmt.Sprintf("%s=%s", condition.GetType(), condition.GetStatus()) + if reason := condition.GetReason(); reason != "" { + line += fmt.Sprintf(" (%s)", reason) + } + if message := condition.GetMessage(); message != "" { + line += ": " + message + } + lines[i] = line + } + return strings.Join(lines, "\n") +} diff --git a/internal/switchover/pair/command_create.go b/internal/switchover/pair/command_create.go index e1bc2c074d..7af14e2a62 100644 --- a/internal/switchover/pair/command_create.go +++ b/internal/switchover/pair/command_create.go @@ -110,13 +110,12 @@ func (c *command) create(cmd *cobra.Command, args []string) error { } func printSwitchoverPair(cmd *cobra.Command, pair switchoverv1.SwitchoverV1SwitchoverPair) error { + // Serialized output mirrors the API response verbatim (full spec/status). + if output.GetFormat(cmd).IsSerialized() { + return printSerialized(cmd, pair) + } + table := output.NewTable(cmd) - table.Add(&out{ - Id: pair.GetId(), - DisplayName: pair.Spec.GetDisplayName(), - ActiveMember: pair.Spec.GetActiveMember(), - Environment: pair.Spec.GetEnvironment(), - Phase: pair.Status.GetPhase(), - }) + table.Add(newPairOut(pair)) return table.Print() } diff --git a/internal/switchover/pair/command_list.go b/internal/switchover/pair/command_list.go index 9e6ea0c7f9..fbbbae80d9 100644 --- a/internal/switchover/pair/command_list.go +++ b/internal/switchover/pair/command_list.go @@ -3,6 +3,8 @@ package pair import ( "github.com/spf13/cobra" + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" "github.com/confluentinc/cli/v4/pkg/examples" "github.com/confluentinc/cli/v4/pkg/output" @@ -44,13 +46,22 @@ func (c *command) list(cmd *cobra.Command, _ []string) error { return err } + // Serialized output mirrors the API response verbatim (full spec/status). + if output.GetFormat(cmd).IsSerialized() { + if pairs == nil { + pairs = []switchoverv1.SwitchoverV1SwitchoverPair{} + } + return printSerialized(cmd, pairs) + } + list := output.NewList(cmd) for _, pair := range pairs { - list.Add(&out{ + list.Add(&listOut{ Id: pair.GetId(), DisplayName: pair.Spec.GetDisplayName(), - ActiveMember: pair.Spec.GetActiveMember(), Environment: pair.Spec.GetEnvironment(), + ActiveMember: pair.Spec.GetActiveMember(), + FailoverType: pair.Spec.GetFailoverType(), Phase: pair.Status.GetPhase(), }) } From 6a89677cdc425e883e5811eed4ed5ffb7ea4e7ab Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Tue, 28 Jul 2026 10:41:49 -0700 Subject: [PATCH 10/10] Bump switchover SDK to parent_resource_id; wire endpoint create Update ccloud-sdk-go-v2-internal/switchover to the latest PR #832 commit (25f438c2), which renames the endpoint spec field switchover_pair_id -> parent_resource_id to match the deployed backend contract. Wire endpoint create/describe/list to the renamed field. Verified against stag: the request body now carries `parent_resource_id` and clears backend field validation (previously failed with 400 "spec.parent_resource_id is required"). Co-Authored-By: Claude Opus 4.8 --- go.mod | 2 +- go.sum | 4 ++-- internal/switchover/endpoint/command.go | 2 +- internal/switchover/endpoint/command_create.go | 2 +- internal/switchover/endpoint/command_list.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index dade060f4d..30f43c701e 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/charmbracelet/lipgloss v0.11.0 github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 - github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260716234419-9fed8981fa8c + github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260728173342-25f438c2593b github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 github.com/confluentinc/ccloud-sdk-go-v2/billing v0.3.0 diff --git a/go.sum b/go.sum index 4b228a677c..13c899cce4 100644 --- a/go.sum +++ b/go.sum @@ -172,8 +172,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 h1:19qEGhkbZa5fopKCe0VPIV+Sasby4Pv10z9ZaktwWso= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52/go.mod h1:62EMf+5uFEt1BJ2q8WMrUoI9VUSxAbDnmZCGRt/MbA0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260716234419-9fed8981fa8c h1:3MD0b07bfAl0UTgspFYC7R3LkVMWvapm1BcZ5LNxSA4= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260716234419-9fed8981fa8c/go.mod h1:yv1VtjqyqD7G5gpdrzR2hGyIAfEOuLA3aTQW0TZ6Iek= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260728173342-25f438c2593b h1:9KvPeP2mC2MZNprKA1N2ueqCJEWLZAgRmVMIJdDXhAY= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260728173342-25f438c2593b/go.mod h1:yv1VtjqyqD7G5gpdrzR2hGyIAfEOuLA3aTQW0TZ6Iek= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA= github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw= diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go index c0a3ecd281..b39c6b738e 100644 --- a/internal/switchover/endpoint/command.go +++ b/internal/switchover/endpoint/command.go @@ -91,7 +91,7 @@ func newEndpointOut(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) *out { return &out{ Id: endpoint.GetId(), DisplayName: endpoint.Spec.GetDisplayName(), - SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), + SwitchoverPair: endpoint.Spec.GetParentResourceId(), Environment: endpoint.Spec.GetEnvironment(), Target: endpoint.Spec.GetTarget(), Phase: endpoint.Status.GetPhase(), diff --git a/internal/switchover/endpoint/command_create.go b/internal/switchover/endpoint/command_create.go index 79ad79152f..fcedfdc540 100644 --- a/internal/switchover/endpoint/command_create.go +++ b/internal/switchover/endpoint/command_create.go @@ -103,7 +103,7 @@ func (c *command) create(cmd *cobra.Command, args []string) error { DisplayName: switchoverv1.PtrString(displayName), Endpoints: &endpoints, Environment: switchoverv1.PtrString(environmentId), - SwitchoverPairId: switchoverv1.PtrString(switchoverPairId), + ParentResourceId: switchoverv1.PtrString(switchoverPairId), }, } diff --git a/internal/switchover/endpoint/command_list.go b/internal/switchover/endpoint/command_list.go index 323841e53c..ceabfb76b1 100644 --- a/internal/switchover/endpoint/command_list.go +++ b/internal/switchover/endpoint/command_list.go @@ -65,7 +65,7 @@ func (c *command) list(cmd *cobra.Command, _ []string) error { list.Add(&listOut{ Id: endpoint.GetId(), DisplayName: endpoint.Spec.GetDisplayName(), - SwitchoverPair: endpoint.Spec.GetSwitchoverPairId(), + SwitchoverPair: endpoint.Spec.GetParentResourceId(), Environment: endpoint.Spec.GetEnvironment(), Phase: endpoint.Status.GetPhase(), })