Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/lint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ var vocabWords = []string{
"whitelist",
"wikipedia",
"workspace",
"switchover",
"yaml",
"yml",
"zstd",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions internal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions internal/switchover/command.go
Original file line number Diff line number Diff line change
@@ -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
}
138 changes: 138 additions & 0 deletions internal/switchover/command_endpoint.go
Original file line number Diff line number Diff line change
@@ -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()
}
40 changes: 40 additions & 0 deletions internal/switchover/command_endpoint_activate.go
Original file line number Diff line number Diff line change
@@ -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 <id>",
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)
}
94 changes: 94 additions & 0 deletions internal/switchover/command_endpoint_create.go
Original file line number Diff line number Diff line change
@@ -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 <name>",
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)
}
Loading