From e1801fabc99c352a4dd6e2d09ab241bc7cfd2d16 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Fri, 12 Jun 2026 12:03:17 -0700 Subject: [PATCH] [ORC-10127] Add switchover CLI commands (pair, endpoint) Adds `confluent switchover pair` and `confluent switchover endpoint` commands (create/list/describe/update/delete + `pair failover` and `endpoint activate`), the ccloudv2 SDK wrapper, output structs, resource constants, command + client registration, and test-server handlers. Hand-written: cli-terraform-generator cannot generate these resources because their required create fields (members, endpoints) are arrays-of-objects, which its CLI parser does not represent as flags (documented limitation). Builds against switchover/v1 SDK; go build of internal/, internal/switchover, pkg/ccloudv2 passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/lint/main.go | 1 + go.mod | 1 + go.sum | 2 + internal/command.go | 2 + internal/switchover/command.go | 29 ++++ internal/switchover/command_endpoint.go | 138 ++++++++++++++++++ .../switchover/command_endpoint_activate.go | 40 +++++ .../switchover/command_endpoint_create.go | 94 ++++++++++++ .../switchover/command_endpoint_delete.go | 60 ++++++++ .../switchover/command_endpoint_describe.go | 44 ++++++ internal/switchover/command_endpoint_list.go | 56 +++++++ .../switchover/command_endpoint_update.go | 62 ++++++++ internal/switchover/command_pair.go | 102 +++++++++++++ internal/switchover/command_pair_create.go | 89 +++++++++++ internal/switchover/command_pair_delete.go | 60 ++++++++ internal/switchover/command_pair_describe.go | 44 ++++++ internal/switchover/command_pair_failover.go | 69 +++++++++ internal/switchover/command_pair_list.go | 54 +++++++ internal/switchover/command_pair_update.go | 62 ++++++++ internal/switchover/output.go | 23 +++ pkg/ccloudv2/client.go | 3 + pkg/ccloudv2/switchover.go | 138 ++++++++++++++++++ pkg/resource/resource.go | 2 + .../switchover/endpoint/activate.golden | 10 ++ .../output/switchover/endpoint/create.golden | 10 ++ .../output/switchover/endpoint/delete.golden | 1 + .../switchover/endpoint/describe.golden | 10 ++ .../output/switchover/endpoint/list.golden | 3 + .../output/switchover/pair/create.golden | 9 ++ .../output/switchover/pair/delete.golden | 1 + .../switchover/pair/describe-json.golden | 8 + .../switchover/pair/describe-not-found.golden | 1 + .../output/switchover/pair/describe.golden | 9 ++ .../output/switchover/pair/failover.golden | 9 ++ .../output/switchover/pair/list.golden | 6 + .../output/switchover/pair/update.golden | 9 ++ test/switchover_test.go | 27 ++++ test/test-server/ccloudv2_router.go | 6 + test/test-server/switchover_handler.go | 138 ++++++++++++++++++ 39 files changed, 1432 insertions(+) create mode 100644 internal/switchover/command.go create mode 100644 internal/switchover/command_endpoint.go create mode 100644 internal/switchover/command_endpoint_activate.go create mode 100644 internal/switchover/command_endpoint_create.go create mode 100644 internal/switchover/command_endpoint_delete.go create mode 100644 internal/switchover/command_endpoint_describe.go create mode 100644 internal/switchover/command_endpoint_list.go create mode 100644 internal/switchover/command_endpoint_update.go create mode 100644 internal/switchover/command_pair.go create mode 100644 internal/switchover/command_pair_create.go create mode 100644 internal/switchover/command_pair_delete.go create mode 100644 internal/switchover/command_pair_describe.go create mode 100644 internal/switchover/command_pair_failover.go create mode 100644 internal/switchover/command_pair_list.go create mode 100644 internal/switchover/command_pair_update.go create mode 100644 internal/switchover/output.go create mode 100644 pkg/ccloudv2/switchover.go create mode 100644 test/fixtures/output/switchover/endpoint/activate.golden create mode 100644 test/fixtures/output/switchover/endpoint/create.golden create mode 100644 test/fixtures/output/switchover/endpoint/delete.golden create mode 100644 test/fixtures/output/switchover/endpoint/describe.golden create mode 100644 test/fixtures/output/switchover/endpoint/list.golden create mode 100644 test/fixtures/output/switchover/pair/create.golden create mode 100644 test/fixtures/output/switchover/pair/delete.golden create mode 100644 test/fixtures/output/switchover/pair/describe-json.golden create mode 100644 test/fixtures/output/switchover/pair/describe-not-found.golden create mode 100644 test/fixtures/output/switchover/pair/describe.golden create mode 100644 test/fixtures/output/switchover/pair/failover.golden create mode 100644 test/fixtures/output/switchover/pair/list.golden create mode 100644 test/fixtures/output/switchover/pair/update.golden create mode 100644 test/switchover_test.go create mode 100644 test/test-server/switchover_handler.go diff --git a/cmd/lint/main.go b/cmd/lint/main.go index f9ba99cb30..25df139394 100644 --- a/cmd/lint/main.go +++ b/cmd/lint/main.go @@ -375,6 +375,7 @@ var vocabWords = []string{ "whitelist", "wikipedia", "workspace", + "switchover", "yaml", "yml", "zstd", diff --git a/go.mod b/go.mod index 81401c09e4..0bdcaa2b80 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-20260612194002-842615b031fd 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 36dd82a19f..f589673d2b 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-20260612194002-842615b031fd h1:+cvg0ZJFpRo9NhF0hYCov6zCJ5BXPbrjvcXrHD7iVuE= +github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260612194002-842615b031fd/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 d9b7f05690..bec6c5f641 100644 --- a/internal/command.go +++ b/internal/command.go @@ -44,6 +44,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(streamshare.New(prerunner)) cmd.AddCommand(tableflow.New(prerunner)) cmd.AddCommand(unifiedstreammanager.New(cfg, prerunner)) + cmd.AddCommand(switchover.New(prerunner)) cmd.AddCommand(update.New(cfg, prerunner)) cmd.AddCommand(version.New(prerunner, cfg.Version)) // cli-tfgen:cli-commands — DO NOT REMOVE (verified by TestCliTfgenMarkers) diff --git a/internal/switchover/command.go b/internal/switchover/command.go new file mode 100644 index 0000000000..9ea7e752e6 --- /dev/null +++ b/internal/switchover/command.go @@ -0,0 +1,29 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" +) + +type command struct { + *pcmd.AuthenticatedCLICommand +} + +// New returns the parent `confluent switchover` command, which groups the +// Kafka Disaster Recovery resources: switchover pairs and switchover endpoints. +func New(prerunner pcmd.PreRunner) *cobra.Command { + cmd := &cobra.Command{ + Use: "switchover", + Short: "Manage Kafka disaster recovery switchover resources.", + Long: "Manage Kafka disaster recovery switchover pairs and switchover endpoints in Confluent Cloud.", + Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireCloudLogin}, + } + + c := &command{AuthenticatedCLICommand: pcmd.NewAuthenticatedCLICommand(cmd, prerunner)} + + cmd.AddCommand(c.newPairCommand()) + cmd.AddCommand(c.newEndpointCommand()) + + return cmd +} diff --git a/internal/switchover/command_endpoint.go b/internal/switchover/command_endpoint.go new file mode 100644 index 0000000000..943b9f457d --- /dev/null +++ b/internal/switchover/command_endpoint.go @@ -0,0 +1,138 @@ +package switchover + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + "github.com/confluentinc/cli/v4/pkg/ccloudv2" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newEndpointCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "endpoint", + Short: "Manage switchover endpoints.", + Long: "Manage endpoint-level Disaster Recovery switchover endpoints for Kafka.", + Args: cobra.NoArgs, + } + + cmd.AddCommand(c.newEndpointActivateCommand()) + cmd.AddCommand(c.newEndpointCreateCommand()) + cmd.AddCommand(c.newEndpointDeleteCommand()) + cmd.AddCommand(c.newEndpointDescribeCommand()) + cmd.AddCommand(c.newEndpointListCommand()) + cmd.AddCommand(c.newEndpointUpdateCommand()) + + return cmd +} + +// parseEndpoint parses an `--endpoint` flag value into a SwitchoverV1EndpointConfig. +// Format: comma-separated key=value pairs, for example: +// +// name=west-platt,resource-id=lkc-west,type=PRIVATE,cloud=AWS,region=us-west-2,gateway=n-ccn-west-1 +// +// `name`, `resource-id`, and `type` are required; `cloud`, `region`, `gateway`, +// and `access-point` are optional. +func parseEndpoint(raw string) (switchoverv1.SwitchoverV1EndpointConfig, error) { + fields := map[string]string{} + for _, pair := range strings.Split(raw, ",") { + if strings.TrimSpace(pair) == "" { + continue + } + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + return switchoverv1.SwitchoverV1EndpointConfig{}, fmt.Errorf(`invalid --endpoint value %q: each field must be "key=value"`, raw) + } + fields[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } + + name := fields["name"] + resourceId := fields["resource-id"] + endpointType := strings.ToUpper(fields["type"]) + if name == "" || resourceId == "" || endpointType == "" { + return switchoverv1.SwitchoverV1EndpointConfig{}, fmt.Errorf(`invalid --endpoint value %q: "name", "resource-id", and "type" are required`, raw) + } + + filter := switchoverv1.SwitchoverV1EndpointFilter{ + ResourceId: resourceId, + Type: endpointType, + } + if v, ok := fields["cloud"]; ok { + filter.SetCloud(strings.ToUpper(v)) + } + if v, ok := fields["region"]; ok { + filter.SetRegion(v) + } + if v, ok := fields["gateway"]; ok { + filter.SetGateway(v) + } + if v, ok := fields["access-point"]; ok { + filter.SetAccessPoint(v) + } + + return switchoverv1.SwitchoverV1EndpointConfig{Name: name, EndpointFilter: filter}, nil +} + +func (c *command) validEndpointArgs(cmd *cobra.Command, args []string) []string { + if len(args) > 0 { + return nil + } + if err := c.PersistentPreRunE(cmd, args); err != nil { + return nil + } + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return nil + } + return autocompleteEndpoints(c.V2Client, environmentId) +} + +func autocompleteEndpoints(client *ccloudv2.Client, environmentId string) []string { + endpoints, err := client.ListSwitchoverEndpoints(environmentId) + if err != nil { + return nil + } + suggestions := make([]string, len(endpoints)) + for i, endpoint := range endpoints { + suggestions[i] = fmt.Sprintf("%s\t%s", endpoint.GetId(), endpoint.Spec.GetDisplayName()) + } + return suggestions +} + +func endpointNames(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) []string { + if endpoint.Spec == nil { + return nil + } + names := make([]string, 0, len(endpoint.Spec.GetEndpoints())) + for _, config := range endpoint.Spec.GetEndpoints() { + names = append(names, config.GetName()) + } + return names +} + +func printEndpointTable(cmd *cobra.Command, endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) error { + if endpoint.Spec == nil { + return fmt.Errorf("switchover endpoint response is missing its spec") + } + + out := &endpointOut{ + Id: endpoint.GetId(), + Name: endpoint.Spec.GetDisplayName(), + Environment: endpoint.Spec.Environment.GetId(), + SwitchoverPair: endpoint.Spec.SwitchoverPair.GetId(), + Endpoints: endpointNames(endpoint), + Target: endpoint.Spec.GetTarget(), + DrEndpoint: endpoint.Spec.GetDrEndpoint(), + } + if endpoint.Status != nil { + out.Phase = endpoint.Status.GetPhase() + } + + table := output.NewTable(cmd) + table.Add(out) + return table.Print() +} diff --git a/internal/switchover/command_endpoint_activate.go b/internal/switchover/command_endpoint_activate.go new file mode 100644 index 0000000000..244ca88cf5 --- /dev/null +++ b/internal/switchover/command_endpoint_activate.go @@ -0,0 +1,40 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newEndpointActivateCommand() *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), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validEndpointArgs), + RunE: c.endpointActivate, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Activate switchover endpoint "se-123456".`, + Code: "confluent switchover endpoint activate se-123456", + }, + ), + } + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) endpointActivate(cmd *cobra.Command, args []string) error { + endpoint, err := c.V2Client.ActivateSwitchoverEndpoint(args[0]) + if err != nil { + return err + } + + return printEndpointTable(cmd, endpoint) +} diff --git a/internal/switchover/command_endpoint_create.go b/internal/switchover/command_endpoint_create.go new file mode 100644 index 0000000000..9322a38aef --- /dev/null +++ b/internal/switchover/command_endpoint_create.go @@ -0,0 +1,94 @@ +package switchover + +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/examples" +) + +func (c *command) newEndpointCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a switchover endpoint.", + Args: cobra.ExactArgs(1), + RunE: c.endpointCreate, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Create a switchover endpoint "prod-kafka-dr-endpoint" bound to switchover pair "sw-123456".`, + Code: "confluent switchover endpoint create prod-kafka-dr-endpoint --switchover-pair sw-123456 " + + "--endpoint name=west-platt,resource-id=lkc-111111,type=PRIVATE,cloud=AWS,region=us-west-2,gateway=n-ccn-west-1 " + + "--endpoint name=east-platt,resource-id=lkc-222222,type=PRIVATE,cloud=AWS,region=us-east-1,gateway=n-ccn-east-1", + }, + ), + } + + cmd.Flags().String("switchover-pair", "", "ID of the switchover pair this endpoint is bound to.") + cmd.Flags().StringArray("endpoint", nil, `An endpoint definition as comma-separated "key=value" fields (keys: name, resource-id, type, cloud, region, gateway, access-point). Specify exactly twice.`) + cmd.Flags().String("target", "", "Name of the endpoint that should start as active.") + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("switchover-pair")) + cobra.CheckErr(cmd.MarkFlagRequired("endpoint")) + + return cmd +} + +func (c *command) endpointCreate(cmd *cobra.Command, args []string) error { + displayName := args[0] + + switchoverPairId, err := cmd.Flags().GetString("switchover-pair") + if err != nil { + return err + } + + endpointFlags, err := cmd.Flags().GetStringArray("endpoint") + if err != nil { + return err + } + if len(endpointFlags) != 2 { + return fmt.Errorf("exactly two `--endpoint` flags must be specified, but received %d", len(endpointFlags)) + } + + endpoints := make([]switchoverv1.SwitchoverV1EndpointConfig, len(endpointFlags)) + for i, raw := range endpointFlags { + config, err := parseEndpoint(raw) + if err != nil { + return err + } + endpoints[i] = config + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + spec := &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ + DisplayName: switchoverv1.PtrString(displayName), + Endpoints: &endpoints, + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + SwitchoverPair: &switchoverv1.EnvScopedObjectReference{Id: switchoverPairId}, + } + + if cmd.Flags().Changed("target") { + target, err := cmd.Flags().GetString("target") + if err != nil { + return err + } + spec.SetTarget(target) + } + + endpoint, err := c.V2Client.CreateSwitchoverEndpoint(switchoverv1.SwitchoverV1SwitchoverEndpoint{Spec: spec}) + if err != nil { + return err + } + + return printEndpointTable(cmd, endpoint) +} diff --git a/internal/switchover/command_endpoint_delete.go b/internal/switchover/command_endpoint_delete.go new file mode 100644 index 0000000000..9e5c696186 --- /dev/null +++ b/internal/switchover/command_endpoint_delete.go @@ -0,0 +1,60 @@ +package switchover + +import ( + "fmt" + + "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/output" + "github.com/confluentinc/cli/v4/pkg/plural" + "github.com/confluentinc/cli/v4/pkg/resource" + "github.com/confluentinc/cli/v4/pkg/utils" +) + +func (c *command) newEndpointDeleteCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [id-2] ... [id-n]", + Short: "Delete one or more switchover endpoints.", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validEndpointArgs), + RunE: c.endpointDelete, + } + + pcmd.AddForceFlag(cmd) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + + return cmd +} + +func (c *command) endpointDelete(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + existenceFunc := func(id string) bool { + _, err := c.V2Client.DescribeSwitchoverEndpoint(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) + } + + deletedIds, err := deletion.DeleteWithoutMessage(cmd, args, deleteFunc) + deleteMsg := "Requested to delete %s %s.\n" + if len(deletedIds) == 1 { + output.Printf(c.Config.EnableColor, deleteMsg, resource.SwitchoverEndpoint, fmt.Sprintf(`"%s"`, deletedIds[0])) + } else if len(deletedIds) > 1 { + output.Printf(c.Config.EnableColor, deleteMsg, plural.Plural(resource.SwitchoverEndpoint), utils.ArrayToCommaDelimitedString(deletedIds, "and")) + } + + return err +} diff --git a/internal/switchover/command_endpoint_describe.go b/internal/switchover/command_endpoint_describe.go new file mode 100644 index 0000000000..ef22b04212 --- /dev/null +++ b/internal/switchover/command_endpoint_describe.go @@ -0,0 +1,44 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newEndpointDescribeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "describe ", + Short: "Describe a switchover endpoint.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validEndpointArgs), + RunE: c.endpointDescribe, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Describe switchover endpoint "se-123456".`, + Code: "confluent switchover endpoint describe se-123456", + }, + ), + } + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) endpointDescribe(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + endpoint, err := c.V2Client.DescribeSwitchoverEndpoint(args[0], environmentId) + if err != nil { + return err + } + + return printEndpointTable(cmd, endpoint) +} diff --git a/internal/switchover/command_endpoint_list.go b/internal/switchover/command_endpoint_list.go new file mode 100644 index 0000000000..5b1709e86f --- /dev/null +++ b/internal/switchover/command_endpoint_list.go @@ -0,0 +1,56 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newEndpointListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List switchover endpoints.", + Args: cobra.NoArgs, + RunE: c.endpointList, + } + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) endpointList(cmd *cobra.Command, _ []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + endpoints, err := c.V2Client.ListSwitchoverEndpoints(environmentId) + if err != nil { + return err + } + + list := output.NewList(cmd) + for _, endpoint := range endpoints { + if endpoint.Spec == nil { + continue + } + out := &endpointOut{ + Id: endpoint.GetId(), + Name: endpoint.Spec.GetDisplayName(), + Environment: endpoint.Spec.Environment.GetId(), + SwitchoverPair: endpoint.Spec.SwitchoverPair.GetId(), + Endpoints: endpointNames(endpoint), + Target: endpoint.Spec.GetTarget(), + DrEndpoint: endpoint.Spec.GetDrEndpoint(), + } + if endpoint.Status != nil { + out.Phase = endpoint.Status.GetPhase() + } + list.Add(out) + } + return list.Print() +} diff --git a/internal/switchover/command_endpoint_update.go b/internal/switchover/command_endpoint_update.go new file mode 100644 index 0000000000..5f662d8f7e --- /dev/null +++ b/internal/switchover/command_endpoint_update.go @@ -0,0 +1,62 @@ +package switchover + +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) newEndpointUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a switchover endpoint.", + Long: "Update the display name of a switchover endpoint. Use `confluent switchover endpoint activate` to change the active endpoint.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validEndpointArgs), + RunE: c.endpointUpdate, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Rename switchover endpoint "se-123456".`, + Code: `confluent switchover endpoint update se-123456 --name new-name`, + }, + ), + } + + cmd.Flags().String("name", "", "Name of the switchover endpoint.") + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("name")) + + return cmd +} + +func (c *command) endpointUpdate(cmd *cobra.Command, args []string) error { + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + update := switchoverv1.SwitchoverV1SwitchoverEndpoint{ + Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ + DisplayName: switchoverv1.PtrString(name), + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + }, + } + + endpoint, err := c.V2Client.UpdateSwitchoverEndpoint(args[0], update) + if err != nil { + return err + } + + return printEndpointTable(cmd, endpoint) +} diff --git a/internal/switchover/command_pair.go b/internal/switchover/command_pair.go new file mode 100644 index 0000000000..532cf5ca5a --- /dev/null +++ b/internal/switchover/command_pair.go @@ -0,0 +1,102 @@ +package switchover + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" + + "github.com/confluentinc/cli/v4/pkg/ccloudv2" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newPairCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pair", + Short: "Manage switchover pairs.", + Long: "Manage cluster-level Disaster Recovery switchover pairs for Kafka.", + Args: cobra.NoArgs, + } + + cmd.AddCommand(c.newPairCreateCommand()) + cmd.AddCommand(c.newPairDeleteCommand()) + cmd.AddCommand(c.newPairDescribeCommand()) + cmd.AddCommand(c.newPairFailoverCommand()) + cmd.AddCommand(c.newPairListCommand()) + cmd.AddCommand(c.newPairUpdateCommand()) + + return cmd +} + +// parseMember parses a `--member` flag value of the form "name=member-id" into +// a SwitchoverPairMember. +func parseMember(raw string) (switchoverv1.SwitchoverV1SwitchoverPairMember, error) { + parts := strings.SplitN(raw, "=", 2) + if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" { + return switchoverv1.SwitchoverV1SwitchoverPairMember{}, fmt.Errorf(`invalid --member value %q: expected format "name=member-id" (for example, "west=lkc-12345")`, raw) + } + return switchoverv1.SwitchoverV1SwitchoverPairMember{ + Name: strings.TrimSpace(parts[0]), + MemberId: strings.TrimSpace(parts[1]), + }, nil +} + +func (c *command) validPairArgs(cmd *cobra.Command, args []string) []string { + if len(args) > 0 { + return nil + } + if err := c.PersistentPreRunE(cmd, args); err != nil { + return nil + } + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return nil + } + return autocompletePairs(c.V2Client, environmentId) +} + +func autocompletePairs(client *ccloudv2.Client, environmentId string) []string { + pairs, err := client.ListSwitchoverPairs(environmentId) + if err != nil { + return nil + } + suggestions := make([]string, len(pairs)) + for i, pair := range pairs { + suggestions[i] = fmt.Sprintf("%s\t%s", pair.GetId(), pair.Spec.GetDisplayName()) + } + return suggestions +} + +func memberNames(pair switchoverv1.SwitchoverV1SwitchoverPair) []string { + if pair.Spec == nil { + return nil + } + names := make([]string, 0, len(pair.Spec.GetMembers())) + for _, member := range pair.Spec.GetMembers() { + names = append(names, fmt.Sprintf("%s=%s", member.GetName(), member.GetMemberId())) + } + return names +} + +func printPairTable(cmd *cobra.Command, pair switchoverv1.SwitchoverV1SwitchoverPair) error { + if pair.Spec == nil { + return fmt.Errorf("switchover pair response is missing its spec") + } + + out := &pairOut{ + Id: pair.GetId(), + Name: pair.Spec.GetDisplayName(), + Environment: pair.Spec.Environment.GetId(), + Members: memberNames(pair), + ActiveMember: pair.Spec.GetActiveMember(), + } + if pair.Status != nil { + out.Phase = pair.Status.GetPhase() + } + + table := output.NewTable(cmd) + table.Add(out) + return table.Print() +} diff --git a/internal/switchover/command_pair_create.go b/internal/switchover/command_pair_create.go new file mode 100644 index 0000000000..26ded580fd --- /dev/null +++ b/internal/switchover/command_pair_create.go @@ -0,0 +1,89 @@ +package switchover + +import ( + "fmt" + "slices" + + "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) newPairCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a switchover pair.", + Args: cobra.ExactArgs(1), + RunE: c.pairCreate, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Create a switchover pair "prod-kafka-dr" between two Kafka clusters, with "west" active.`, + Code: "confluent switchover pair create prod-kafka-dr --member west=lkc-111111 --member east=lkc-222222 --active-member west", + }, + ), + } + + cmd.Flags().StringArray("member", nil, `A member of the pair in the format "name=member-id" (for example, "west=lkc-12345"). Specify exactly twice.`) + cmd.Flags().String("active-member", "", "Name of the member that should start as active.") + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("member")) + cobra.CheckErr(cmd.MarkFlagRequired("active-member")) + + return cmd +} + +func (c *command) pairCreate(cmd *cobra.Command, args []string) error { + displayName := args[0] + + memberFlags, err := cmd.Flags().GetStringArray("member") + if err != nil { + return err + } + if len(memberFlags) != 2 { + return fmt.Errorf("exactly two `--member` flags must be specified, but received %d", len(memberFlags)) + } + + members := make([]switchoverv1.SwitchoverV1SwitchoverPairMember, len(memberFlags)) + for i, raw := range memberFlags { + member, err := parseMember(raw) + if err != nil { + return err + } + members[i] = member + } + + activeMember, err := cmd.Flags().GetString("active-member") + if err != nil { + return err + } + if !slices.ContainsFunc(members, func(m switchoverv1.SwitchoverV1SwitchoverPairMember) bool { return m.GetName() == activeMember }) { + return fmt.Errorf("`--active-member` %q must match one of the member names", activeMember) + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + createPair := switchoverv1.SwitchoverV1SwitchoverPair{ + Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{ + DisplayName: switchoverv1.PtrString(displayName), + Members: &members, + ActiveMember: switchoverv1.PtrString(activeMember), + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + }, + } + + pair, err := c.V2Client.CreateSwitchoverPair(createPair) + if err != nil { + return err + } + + return printPairTable(cmd, pair) +} diff --git a/internal/switchover/command_pair_delete.go b/internal/switchover/command_pair_delete.go new file mode 100644 index 0000000000..5464c5ca47 --- /dev/null +++ b/internal/switchover/command_pair_delete.go @@ -0,0 +1,60 @@ +package switchover + +import ( + "fmt" + + "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/output" + "github.com/confluentinc/cli/v4/pkg/plural" + "github.com/confluentinc/cli/v4/pkg/resource" + "github.com/confluentinc/cli/v4/pkg/utils" +) + +func (c *command) newPairDeleteCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete [id-2] ... [id-n]", + Short: "Delete one or more switchover pairs.", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validPairArgs), + RunE: c.pairDelete, + } + + pcmd.AddForceFlag(cmd) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + + return cmd +} + +func (c *command) pairDelete(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + existenceFunc := func(id string) bool { + _, err := c.V2Client.DescribeSwitchoverPair(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) + } + + deletedIds, err := deletion.DeleteWithoutMessage(cmd, args, deleteFunc) + deleteMsg := "Requested to delete %s %s.\n" + if len(deletedIds) == 1 { + output.Printf(c.Config.EnableColor, deleteMsg, resource.SwitchoverPair, fmt.Sprintf(`"%s"`, deletedIds[0])) + } else if len(deletedIds) > 1 { + output.Printf(c.Config.EnableColor, deleteMsg, plural.Plural(resource.SwitchoverPair), utils.ArrayToCommaDelimitedString(deletedIds, "and")) + } + + return err +} diff --git a/internal/switchover/command_pair_describe.go b/internal/switchover/command_pair_describe.go new file mode 100644 index 0000000000..69842656c7 --- /dev/null +++ b/internal/switchover/command_pair_describe.go @@ -0,0 +1,44 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/examples" +) + +func (c *command) newPairDescribeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "describe ", + Short: "Describe a switchover pair.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validPairArgs), + RunE: c.pairDescribe, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Describe switchover pair "sw-123456".`, + Code: "confluent switchover pair describe sw-123456", + }, + ), + } + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) pairDescribe(cmd *cobra.Command, args []string) error { + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + pair, err := c.V2Client.DescribeSwitchoverPair(args[0], environmentId) + if err != nil { + return err + } + + return printPairTable(cmd, pair) +} diff --git a/internal/switchover/command_pair_failover.go b/internal/switchover/command_pair_failover.go new file mode 100644 index 0000000000..66fb7b890e --- /dev/null +++ b/internal/switchover/command_pair_failover.go @@ -0,0 +1,69 @@ +package switchover + +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/utils" +) + +var failoverTypes = []string{"CLEAN", "UNCLEAN", "RESTORE"} + +func (c *command) newPairFailoverCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "failover ", + Short: "Trigger a failover on a switchover pair.", + Long: "Trigger a failover (or switchback) on a switchover pair, promoting the passive member to active.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validPairArgs), + RunE: c.pairFailover, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Trigger a clean failover of switchover pair "sw-123456", promoting member "east".`, + Code: "confluent switchover pair failover sw-123456 --member east --type CLEAN", + }, + ), + } + + cmd.Flags().String("member", "", "Name of the member to promote to active. If omitted, the other member is promoted.") + cmd.Flags().String("type", "", fmt.Sprintf("Specify the failover type as %s.", utils.ArrayToCommaDelimitedString(failoverTypes, "or"))) + pcmd.RegisterFlagCompletionFunc(cmd, "type", func(_ *cobra.Command, _ []string) []string { return failoverTypes }) + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) pairFailover(cmd *cobra.Command, args []string) error { + request := switchoverv1.SwitchoverV1SwitchoverPairFailoverRequest{} + + if cmd.Flags().Changed("member") { + member, err := cmd.Flags().GetString("member") + if err != nil { + return err + } + request.SetActiveMember(member) + } + + if cmd.Flags().Changed("type") { + failoverType, err := cmd.Flags().GetString("type") + if err != nil { + return err + } + request.SetFailoverType(strings.ToUpper(failoverType)) + } + + pair, err := c.V2Client.FailoverSwitchoverPair(args[0], request) + if err != nil { + return err + } + + return printPairTable(cmd, pair) +} diff --git a/internal/switchover/command_pair_list.go b/internal/switchover/command_pair_list.go new file mode 100644 index 0000000000..7cf2d134fc --- /dev/null +++ b/internal/switchover/command_pair_list.go @@ -0,0 +1,54 @@ +package switchover + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" +) + +func (c *command) newPairListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List switchover pairs.", + Args: cobra.NoArgs, + RunE: c.pairList, + } + + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *command) pairList(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 { + if pair.Spec == nil { + continue + } + out := &pairOut{ + Id: pair.GetId(), + Name: pair.Spec.GetDisplayName(), + Environment: pair.Spec.Environment.GetId(), + Members: memberNames(pair), + ActiveMember: pair.Spec.GetActiveMember(), + } + if pair.Status != nil { + out.Phase = pair.Status.GetPhase() + } + list.Add(out) + } + return list.Print() +} diff --git a/internal/switchover/command_pair_update.go b/internal/switchover/command_pair_update.go new file mode 100644 index 0000000000..0cb84d886e --- /dev/null +++ b/internal/switchover/command_pair_update.go @@ -0,0 +1,62 @@ +package switchover + +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) newPairUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a switchover pair.", + Long: "Update the display name of a switchover pair. Use `confluent switchover pair failover` to change the active member.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: pcmd.NewValidArgsFunction(c.validPairArgs), + RunE: c.pairUpdate, + Example: examples.BuildExampleString( + examples.Example{ + Text: `Rename switchover pair "sw-123456".`, + Code: `confluent switchover pair update sw-123456 --name new-name`, + }, + ), + } + + cmd.Flags().String("name", "", "Name of the switchover pair.") + pcmd.AddContextFlag(cmd, c.CLICommand) + pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) + pcmd.AddOutputFlag(cmd) + + cobra.CheckErr(cmd.MarkFlagRequired("name")) + + return cmd +} + +func (c *command) pairUpdate(cmd *cobra.Command, args []string) error { + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + environmentId, err := c.Context.EnvironmentId() + if err != nil { + return err + } + + update := switchoverv1.SwitchoverV1SwitchoverPair{ + Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{ + DisplayName: switchoverv1.PtrString(name), + Environment: &switchoverv1.EnvScopedObjectReference{Id: environmentId}, + }, + } + + pair, err := c.V2Client.UpdateSwitchoverPair(args[0], update) + if err != nil { + return err + } + + return printPairTable(cmd, pair) +} diff --git a/internal/switchover/output.go b/internal/switchover/output.go new file mode 100644 index 0000000000..f90cc1347a --- /dev/null +++ b/internal/switchover/output.go @@ -0,0 +1,23 @@ +package switchover + +// pairOut is the display structure for a switchover pair. +type pairOut struct { + Id string `human:"ID" serialized:"id"` + Name string `human:"Name,omitempty" serialized:"name,omitempty"` + Environment string `human:"Environment" serialized:"environment"` + Members []string `human:"Members" serialized:"members"` + ActiveMember string `human:"Active Member" serialized:"active_member"` + Phase string `human:"Phase" serialized:"phase"` +} + +// endpointOut is the display structure for a switchover endpoint. +type endpointOut struct { + Id string `human:"ID" serialized:"id"` + Name string `human:"Name,omitempty" serialized:"name,omitempty"` + Environment string `human:"Environment" serialized:"environment"` + SwitchoverPair string `human:"Switchover Pair" serialized:"switchover_pair"` + Endpoints []string `human:"Endpoints" serialized:"endpoints"` + Target string `human:"Target,omitempty" serialized:"target,omitempty"` + DrEndpoint string `human:"DR Endpoint,omitempty" serialized:"dr_endpoint,omitempty"` + Phase string `human:"Phase" serialized:"phase"` +} diff --git a/pkg/ccloudv2/client.go b/pkg/ccloudv2/client.go index a736857f5b..c0e365e0b9 100644 --- a/pkg/ccloudv2/client.go +++ b/pkg/ccloudv2/client.go @@ -1,6 +1,7 @@ package ccloudv2 import ( + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" aiv1 "github.com/confluentinc/ccloud-sdk-go-v2/ai/v1" apikeysv2 "github.com/confluentinc/ccloud-sdk-go-v2/apikeys/v2" billingv1 "github.com/confluentinc/ccloud-sdk-go-v2/billing/v1" @@ -82,6 +83,7 @@ type Client struct { SsoClient *ssov2.APIClient TableflowClient *tableflowv1.APIClient UsmClient *usmv1.APIClient + SwitchoverClient *switchoverv1.APIClient // cli-tfgen:cli-client-fields — DO NOT REMOVE (verified by TestCliTfgenMarkers) } @@ -134,6 +136,7 @@ func NewClient(cfg *config.Config, unsafeTrace bool) *Client { SsoClient: newSsoClient(httpClient, url, userAgent, unsafeTrace), TableflowClient: newTableflowClient(httpClient, url, userAgent, unsafeTrace), UsmClient: newUsmClient(httpClient, url, userAgent, unsafeTrace), + SwitchoverClient: newSwitchoverClient(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..c45e0e0c20 --- /dev/null +++ b/pkg/ccloudv2/switchover.go @@ -0,0 +1,138 @@ +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()) +} + +// --------------------------------------------------------------------------- +// SwitchoverPair +// --------------------------------------------------------------------------- + +func (c *Client) CreateSwitchoverPair(pair switchoverv1.SwitchoverV1SwitchoverPair) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.CreateSwitchoverV1SwitchoverPair(c.switchoverApiContext()).SwitchoverV1SwitchoverPair(pair).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) DescribeSwitchoverPair(id, environment string) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.GetSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).Environment(environment).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) UpdateSwitchoverPair(id string, update switchoverv1.SwitchoverV1SwitchoverPair) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPair(update).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) FailoverSwitchoverPair(id string, request switchoverv1.SwitchoverV1SwitchoverPairFailoverRequest) (switchoverv1.SwitchoverV1SwitchoverPair, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverPairsSwitchoverV1Api.FailoverSwitchoverV1SwitchoverPair(c.switchoverApiContext(), id).SwitchoverV1SwitchoverPairFailoverRequest(request).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +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) 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()...) + + pageToken, done, err = extractNextPageToken(page.GetMetadata().Next) + if err != nil { + return nil, err + } + } + 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() +} + +// --------------------------------------------------------------------------- +// SwitchoverEndpoint +// --------------------------------------------------------------------------- + +func (c *Client) CreateSwitchoverEndpoint(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.CreateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext()).SwitchoverV1SwitchoverEndpoint(endpoint).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) DescribeSwitchoverEndpoint(id, environment string) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.GetSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Environment(environment).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) UpdateSwitchoverEndpoint(id string, update switchoverv1.SwitchoverV1SwitchoverEndpoint) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.UpdateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).SwitchoverV1SwitchoverEndpoint(update).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) ActivateSwitchoverEndpoint(id string) (switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + resp, httpResp, err := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.ActivateSwitchoverV1SwitchoverEndpoint(c.switchoverApiContext(), id).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +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) +} + +func (c *Client) ListSwitchoverEndpoints(environment string) ([]switchoverv1.SwitchoverV1SwitchoverEndpoint, error) { + var list []switchoverv1.SwitchoverV1SwitchoverEndpoint + + done := false + pageToken := "" + for !done { + page, httpResp, err := c.executeListSwitchoverEndpoints(environment, pageToken) + if err != nil { + return nil, errors.CatchCCloudV2Error(err, httpResp) + } + list = append(list, page.GetData()...) + + pageToken, done, err = extractNextPageToken(page.GetMetadata().Next) + if err != nil { + return nil, err + } + } + return list, nil +} + +func (c *Client) executeListSwitchoverEndpoints(environment, pageToken string) (switchoverv1.SwitchoverV1SwitchoverEndpointList, *http.Response, error) { + req := c.SwitchoverClient.SwitchoverEndpointsSwitchoverV1Api.ListSwitchoverV1SwitchoverEndpoints(c.switchoverApiContext()).Environment(environment).PageSize(ccloudV2ListPageSize) + if pageToken != "" { + req = req.PageToken(pageToken) + } + return req.Execute() +} diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 656233783d..b684dfb70d 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -79,6 +79,8 @@ const ( SchemaRegistryConfiguration = "Schema Registry configuration" ServiceAccount = "service account" SsoGroupMapping = "SSO group mapping" + SwitchoverEndpoint = "switchover endpoint" + SwitchoverPair = "switchover pair" Tableflow = "tableflow" Topic = "topic" TransitGatewayAttachment = "transit gateway attachment" diff --git a/test/fixtures/output/switchover/endpoint/activate.golden b/test/fixtures/output/switchover/endpoint/activate.golden new file mode 100644 index 0000000000..9218b86bf9 --- /dev/null +++ b/test/fixtures/output/switchover/endpoint/activate.golden @@ -0,0 +1,10 @@ ++-----------------+-------------------------------------------------+ +| ID | se-123456 | +| Name | prod-kafka-dr-endpoint | +| Environment | env-123456 | +| Switchover Pair | sw-123456 | +| Endpoints | west-platt, east-platt | +| Target | west-platt | +| DR Endpoint | glkc-sw-123456-se-123456.global.confluent.cloud | +| Phase | READY | ++-----------------+-------------------------------------------------+ diff --git a/test/fixtures/output/switchover/endpoint/create.golden b/test/fixtures/output/switchover/endpoint/create.golden new file mode 100644 index 0000000000..04dd9a7d26 --- /dev/null +++ b/test/fixtures/output/switchover/endpoint/create.golden @@ -0,0 +1,10 @@ ++-----------------+-------------------------------------------------+ +| ID | se-123456 | +| Name | prod-endpoint | +| Environment | env-123456 | +| Switchover Pair | sw-123456 | +| Endpoints | west-platt, east-platt | +| Target | west-platt | +| DR Endpoint | glkc-sw-123456-se-123456.global.confluent.cloud | +| Phase | READY | ++-----------------+-------------------------------------------------+ diff --git a/test/fixtures/output/switchover/endpoint/delete.golden b/test/fixtures/output/switchover/endpoint/delete.golden new file mode 100644 index 0000000000..b99632852b --- /dev/null +++ b/test/fixtures/output/switchover/endpoint/delete.golden @@ -0,0 +1 @@ +Requested to delete switchover endpoint "se-123456". diff --git a/test/fixtures/output/switchover/endpoint/describe.golden b/test/fixtures/output/switchover/endpoint/describe.golden new file mode 100644 index 0000000000..9218b86bf9 --- /dev/null +++ b/test/fixtures/output/switchover/endpoint/describe.golden @@ -0,0 +1,10 @@ ++-----------------+-------------------------------------------------+ +| ID | se-123456 | +| Name | prod-kafka-dr-endpoint | +| Environment | env-123456 | +| Switchover Pair | sw-123456 | +| Endpoints | west-platt, east-platt | +| Target | west-platt | +| DR Endpoint | glkc-sw-123456-se-123456.global.confluent.cloud | +| Phase | READY | ++-----------------+-------------------------------------------------+ diff --git a/test/fixtures/output/switchover/endpoint/list.golden b/test/fixtures/output/switchover/endpoint/list.golden new file mode 100644 index 0000000000..506697fa2d --- /dev/null +++ b/test/fixtures/output/switchover/endpoint/list.golden @@ -0,0 +1,3 @@ + ID | Name | Environment | Switchover Pair | Endpoints | Target | DR Endpoint | Phase +------------+------------------------+-------------+-----------------+------------------------+------------+-------------------------------------------------+-------- + se-123456 | prod-kafka-dr-endpoint | env-123456 | sw-123456 | west-platt, east-platt | west-platt | glkc-sw-123456-se-123456.global.confluent.cloud | READY diff --git a/test/fixtures/output/switchover/pair/create.golden b/test/fixtures/output/switchover/pair/create.golden new file mode 100644 index 0000000000..70cc8fe6e7 --- /dev/null +++ b/test/fixtures/output/switchover/pair/create.golden @@ -0,0 +1,9 @@ ++---------------+--------------------------------+ +| ID | sw-123456 | +| Name | prod-kafka-dr | +| Environment | env-123456 | +| Members | west=lkc-111111, | +| | east=lkc-222222 | +| Active Member | west | +| Phase | READY | ++---------------+--------------------------------+ diff --git a/test/fixtures/output/switchover/pair/delete.golden b/test/fixtures/output/switchover/pair/delete.golden new file mode 100644 index 0000000000..11be60016e --- /dev/null +++ b/test/fixtures/output/switchover/pair/delete.golden @@ -0,0 +1 @@ +Requested to delete switchover pair "sw-123456". diff --git a/test/fixtures/output/switchover/pair/describe-json.golden b/test/fixtures/output/switchover/pair/describe-json.golden new file mode 100644 index 0000000000..a769cbe97a --- /dev/null +++ b/test/fixtures/output/switchover/pair/describe-json.golden @@ -0,0 +1,8 @@ +{ + "id": "sw-123456", + "name": "prod-kafka-dr", + "environment": "env-123456", + "members": ["west=lkc-111111", "east=lkc-222222"], + "active_member": "west", + "phase": "READY" +} diff --git a/test/fixtures/output/switchover/pair/describe-not-found.golden b/test/fixtures/output/switchover/pair/describe-not-found.golden new file mode 100644 index 0000000000..716b863527 --- /dev/null +++ b/test/fixtures/output/switchover/pair/describe-not-found.golden @@ -0,0 +1 @@ +Error: 404 Not Found diff --git a/test/fixtures/output/switchover/pair/describe.golden b/test/fixtures/output/switchover/pair/describe.golden new file mode 100644 index 0000000000..70cc8fe6e7 --- /dev/null +++ b/test/fixtures/output/switchover/pair/describe.golden @@ -0,0 +1,9 @@ ++---------------+--------------------------------+ +| ID | sw-123456 | +| Name | prod-kafka-dr | +| Environment | env-123456 | +| Members | west=lkc-111111, | +| | east=lkc-222222 | +| Active Member | west | +| Phase | READY | ++---------------+--------------------------------+ diff --git a/test/fixtures/output/switchover/pair/failover.golden b/test/fixtures/output/switchover/pair/failover.golden new file mode 100644 index 0000000000..d9657d9eb0 --- /dev/null +++ b/test/fixtures/output/switchover/pair/failover.golden @@ -0,0 +1,9 @@ ++---------------+--------------------------------+ +| ID | sw-123456 | +| Name | prod-kafka-dr | +| Environment | env-123456 | +| Members | west=lkc-111111, | +| | east=lkc-222222 | +| Active Member | west | +| Phase | SWITCHING | ++---------------+--------------------------------+ diff --git a/test/fixtures/output/switchover/pair/list.golden b/test/fixtures/output/switchover/pair/list.golden new file mode 100644 index 0000000000..84883194d7 --- /dev/null +++ b/test/fixtures/output/switchover/pair/list.golden @@ -0,0 +1,6 @@ + ID | Name | Environment | Members | Active Member | Phase +------------+------------------+-------------+--------------------------------+---------------+-------- + sw-123456 | prod-kafka-dr | env-123456 | west=lkc-111111, | west | READY + | | | east=lkc-222222 | | + sw-234567 | staging-kafka-dr | env-123456 | west=lkc-111111, | west | READY + | | | east=lkc-222222 | | diff --git a/test/fixtures/output/switchover/pair/update.golden b/test/fixtures/output/switchover/pair/update.golden new file mode 100644 index 0000000000..70cc8fe6e7 --- /dev/null +++ b/test/fixtures/output/switchover/pair/update.golden @@ -0,0 +1,9 @@ ++---------------+--------------------------------+ +| ID | sw-123456 | +| Name | prod-kafka-dr | +| Environment | env-123456 | +| Members | west=lkc-111111, | +| | east=lkc-222222 | +| Active Member | west | +| Phase | READY | ++---------------+--------------------------------+ diff --git a/test/switchover_test.go b/test/switchover_test.go new file mode 100644 index 0000000000..2b97da1f5a --- /dev/null +++ b/test/switchover_test.go @@ -0,0 +1,27 @@ +package test + +func (s *CLITestSuite) TestSwitchover() { + tests := []CLITest{ + // SwitchoverPair + {args: "switchover pair create prod-kafka-dr --member west=lkc-111111 --member east=lkc-222222 --active-member west", fixture: "switchover/pair/create.golden"}, + {args: "switchover pair list", fixture: "switchover/pair/list.golden"}, + {args: "switchover pair describe sw-123456", fixture: "switchover/pair/describe.golden"}, + {args: "switchover pair update sw-123456 --name renamed-dr", fixture: "switchover/pair/update.golden"}, + {args: "switchover pair failover sw-123456 --member east --type CLEAN", fixture: "switchover/pair/failover.golden"}, + {args: "switchover pair delete sw-123456 --force", fixture: "switchover/pair/delete.golden"}, + {args: "switchover pair describe sw-000000", fixture: "switchover/pair/describe-not-found.golden", exitCode: 1}, + {args: "switchover pair describe sw-123456 --output json", fixture: "switchover/pair/describe-json.golden"}, + + // SwitchoverEndpoint + {args: "switchover endpoint create prod-endpoint --switchover-pair sw-123456 --endpoint name=west-platt,resource-id=lkc-111111,type=PRIVATE --endpoint name=east-platt,resource-id=lkc-222222,type=PRIVATE", fixture: "switchover/endpoint/create.golden"}, + {args: "switchover endpoint list", fixture: "switchover/endpoint/list.golden"}, + {args: "switchover endpoint describe se-123456", fixture: "switchover/endpoint/describe.golden"}, + {args: "switchover endpoint activate se-123456", fixture: "switchover/endpoint/activate.golden"}, + {args: "switchover endpoint delete se-123456 --force", fixture: "switchover/endpoint/delete.golden"}, + } + + for _, test := range tests { + test.login = "cloud" + s.runIntegrationTest(test) + } +} diff --git a/test/test-server/ccloudv2_router.go b/test/test-server/ccloudv2_router.go index a5aa9ed980..eb7d6a44a9 100644 --- a/test/test-server/ccloudv2_router.go +++ b/test/test-server/ccloudv2_router.go @@ -137,6 +137,12 @@ var ccloudV2Routes = []route{ {"/usm/v1/connect-clusters", handleUsmConnectClusters}, {"/usm/v1/connect-clusters/{id}", handleUsmConnectCluster}, {"/v2/metrics/cloud/query", handleMetricsQuery}, + {"/switchover/v1/switchover-pairs", handleSwitchoverPairs}, + {"/switchover/v1/switchover-pairs/{id}:failover", handleSwitchoverPairFailover}, + {"/switchover/v1/switchover-pairs/{id}", handleSwitchoverPair}, + {"/switchover/v1/switchover-endpoints", handleSwitchoverEndpoints}, + {"/switchover/v1/switchover-endpoints/{id}:activate", handleSwitchoverEndpointActivate}, + {"/switchover/v1/switchover-endpoints/{id}", handleSwitchoverEndpoint}, // cli-tfgen:cli-api-routes — DO NOT REMOVE (verified by TestCliTfgenMarkers) } diff --git a/test/test-server/switchover_handler.go b/test/test-server/switchover_handler.go new file mode 100644 index 0000000000..15dd349ce9 --- /dev/null +++ b/test/test-server/switchover_handler.go @@ -0,0 +1,138 @@ +package testserver + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" + + switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1" +) + +// handleSwitchoverPairs handles "/switchover/v1/switchover-pairs". +func handleSwitchoverPairs(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + pairs := switchoverv1.SwitchoverV1SwitchoverPairList{Data: []switchoverv1.SwitchoverV1SwitchoverPair{ + buildPair("sw-123456", "prod-kafka-dr"), + buildPair("sw-234567", "staging-kafka-dr"), + }} + require.NoError(t, json.NewEncoder(w).Encode(pairs)) + case http.MethodPost: + var req switchoverv1.SwitchoverV1SwitchoverPair + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + pair := buildPair("sw-123456", req.Spec.GetDisplayName()) + pair.Spec.Members = req.Spec.Members + pair.Spec.SetActiveMember(req.Spec.GetActiveMember()) + require.NoError(t, json.NewEncoder(w).Encode(pair)) + } + } +} + +// handleSwitchoverPair handles "/switchover/v1/switchover-pairs/{id}". +func handleSwitchoverPair(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + if id == "sw-000000" { + w.WriteHeader(http.StatusNotFound) + return + } + switch r.Method { + case http.MethodGet, http.MethodPut: + require.NoError(t, json.NewEncoder(w).Encode(buildPair(id, "prod-kafka-dr"))) + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + } + } +} + +// handleSwitchoverPairFailover handles "/switchover/v1/switchover-pairs/{id}:failover". +func handleSwitchoverPairFailover(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + pair := buildPair(id, "prod-kafka-dr") + pair.Status.SetPhase("SWITCHING") + require.NoError(t, json.NewEncoder(w).Encode(pair)) + } +} + +// handleSwitchoverEndpoints handles "/switchover/v1/switchover-endpoints". +func handleSwitchoverEndpoints(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + endpoints := switchoverv1.SwitchoverV1SwitchoverEndpointList{Data: []switchoverv1.SwitchoverV1SwitchoverEndpoint{ + buildEndpoint("se-123456", "prod-kafka-dr-endpoint"), + }} + require.NoError(t, json.NewEncoder(w).Encode(endpoints)) + case http.MethodPost: + var req switchoverv1.SwitchoverV1SwitchoverEndpoint + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + endpoint := buildEndpoint("se-123456", req.Spec.GetDisplayName()) + endpoint.Spec.Endpoints = req.Spec.Endpoints + require.NoError(t, json.NewEncoder(w).Encode(endpoint)) + } + } +} + +// handleSwitchoverEndpoint handles "/switchover/v1/switchover-endpoints/{id}". +func handleSwitchoverEndpoint(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + if id == "se-000000" { + w.WriteHeader(http.StatusNotFound) + return + } + switch r.Method { + case http.MethodGet, http.MethodPut: + require.NoError(t, json.NewEncoder(w).Encode(buildEndpoint(id, "prod-kafka-dr-endpoint"))) + case http.MethodDelete: + w.WriteHeader(http.StatusNoContent) + } + } +} + +// handleSwitchoverEndpointActivate handles "/switchover/v1/switchover-endpoints/{id}:activate". +func handleSwitchoverEndpointActivate(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + require.NoError(t, json.NewEncoder(w).Encode(buildEndpoint(id, "prod-kafka-dr-endpoint"))) + } +} + +func buildPair(id, name string) switchoverv1.SwitchoverV1SwitchoverPair { + return switchoverv1.SwitchoverV1SwitchoverPair{ + Id: switchoverv1.PtrString(id), + Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{ + DisplayName: switchoverv1.PtrString(name), + Environment: &switchoverv1.EnvScopedObjectReference{Id: "env-123456"}, + Members: &[]switchoverv1.SwitchoverV1SwitchoverPairMember{ + {Name: "west", MemberId: "lkc-111111"}, + {Name: "east", MemberId: "lkc-222222"}, + }, + ActiveMember: switchoverv1.PtrString("west"), + }, + Status: &switchoverv1.SwitchoverV1SwitchoverPairStatus{Phase: "READY"}, + } +} + +func buildEndpoint(id, name string) switchoverv1.SwitchoverV1SwitchoverEndpoint { + return switchoverv1.SwitchoverV1SwitchoverEndpoint{ + Id: switchoverv1.PtrString(id), + Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{ + DisplayName: switchoverv1.PtrString(name), + Environment: &switchoverv1.EnvScopedObjectReference{Id: "env-123456"}, + SwitchoverPair: &switchoverv1.EnvScopedObjectReference{Id: "sw-123456"}, + Target: switchoverv1.PtrString("west-platt"), + DrEndpoint: switchoverv1.PtrString("glkc-sw-123456-se-123456.global.confluent.cloud"), + Endpoints: &[]switchoverv1.SwitchoverV1EndpointConfig{ + {Name: "west-platt", EndpointFilter: switchoverv1.SwitchoverV1EndpointFilter{ResourceId: "lkc-111111", Type: "PRIVATE"}}, + {Name: "east-platt", EndpointFilter: switchoverv1.SwitchoverV1EndpointFilter{ResourceId: "lkc-222222", Type: "PRIVATE"}}, + }, + }, + Status: &switchoverv1.SwitchoverV1SwitchoverEndpointStatus{Phase: "READY"}, + } +}