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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-20260728173342-25f438c2593b
github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260821005945-1f1c05b15138
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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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-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-internal/switchover v0.0.0-20260821005945-1f1c05b15138 h1:diX6mwK1Z71xyrH9sotG6/E/XFswd1QaxfKJxOv4StE=
github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260821005945-1f1c05b15138/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=
Expand Down
18 changes: 12 additions & 6 deletions internal/switchover/endpoint/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ func newEndpointOut(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) *out {
return &out{
Id: endpoint.GetId(),
DisplayName: endpoint.Spec.GetDisplayName(),
SwitchoverPair: endpoint.Spec.GetParentResourceId(),
Environment: endpoint.Spec.GetEnvironment(),
SwitchoverPair: endpoint.Spec.GetParentResourceCrn(),
Environment: endpoint.Spec.GetEnvironmentCrn(),
Target: endpoint.Spec.GetTarget(),
Phase: endpoint.Status.GetPhase(),
Endpoints: formatEndpoints(endpoint.Spec.GetEndpoints()),
Expand All @@ -105,15 +105,21 @@ func formatEndpoints(endpoints []switchoverv1.SwitchoverV1EndpointConfig) string
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 networkCrn := filter.GetNetworkCrn(); networkCrn != "" {
parts = append(parts, "network="+networkCrn)
}
if accessPoint := filter.GetAccessPoint(); accessPoint != "" {
parts = append(parts, "access-point="+accessPoint)
if accessPointCrn := filter.GetAccessPointCrn(); accessPointCrn != "" {
parts = append(parts, "access-point="+accessPointCrn)
}
if hostname := endpoint.GetHostname(); hostname != "" {
parts = append(parts, "hostname="+hostname)
}
if cloud, region := endpoint.GetCloud(), endpoint.GetRegion(); cloud != "" || region != "" {
parts = append(parts, strings.TrimPrefix(cloud+"/"+region, "/"))
}
if connectionType := endpoint.GetConnectionType(); connectionType != "" {
parts = append(parts, connectionType)
}
lines[i] = strings.Join(parts, " ")
}
return strings.Join(lines, "\n")
Expand Down
30 changes: 23 additions & 7 deletions internal/switchover/endpoint/command_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=<name>,type=<private|public>[,network=<network-id>][,access-point=<access-point-id>]". Must be specified exactly twice.`)
cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=<name>,type=<private|public>[,network=<network-id>][,access-point=<access-point-crn>]". A private endpoint sets exactly one of network or access-point. network takes a network ID (the CLI builds its CRN); access-point takes a full CRN, since its canonical form includes a gateway segment. Must be specified exactly twice.`)
pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand)
pcmd.AddContextFlag(cmd, c.CLICommand)
pcmd.AddOutputFlag(cmd)
Expand All @@ -54,9 +54,9 @@ func parseEndpointFlag(raw string) (switchoverv1.SwitchoverV1EndpointConfig, err
case "type":
filter.Type = value
case "network":
filter.NetworkId = switchoverv1.PtrString(value)
filter.NetworkCrn = switchoverv1.PtrString(value)
case "access-point":
filter.AccessPoint = switchoverv1.PtrString(value)
filter.AccessPointCrn = switchoverv1.PtrString(value)
default:
return config, fmt.Errorf(`invalid --endpoint key %q`, key)
}
Expand Down Expand Up @@ -98,12 +98,28 @@ func (c *command) create(cmd *cobra.Command, args []string) error {
return err
}

organizationId := c.Context.GetCurrentOrganization()

// The endpoint's environment travels inside parent_resource_crn (the pair CRN); the create body
// does not take a separate environment_crn. The parent CRN is assembled from the current
// organization, the --environment flag, and the --switchover-pair ID.
parentResourceCrn := fmt.Sprintf("crn://confluent.cloud/organization=%s/environment=%s/switchover-pair=%s", organizationId, environmentId, switchoverPairId)

// network=<id> is given as a plain network ID; assemble its network_crn here (org + the
// endpoint's environment + the id). access_point_crn is passed through as a full CRN because its
// canonical form carries a gateway segment the --endpoint flag does not supply.
for i := range endpoints {
if networkId := endpoints[i].EndpointFilter.GetNetworkCrn(); networkId != "" {
endpoints[i].EndpointFilter.NetworkCrn = switchoverv1.PtrString(
fmt.Sprintf("crn://confluent.cloud/organization=%s/environment=%s/network=%s", organizationId, environmentId, networkId))
}
}

endpoint := switchoverv1.SwitchoverV1SwitchoverEndpoint{
Spec: &switchoverv1.SwitchoverV1SwitchoverEndpointSpec{
DisplayName: switchoverv1.PtrString(displayName),
Endpoints: &endpoints,
Environment: switchoverv1.PtrString(environmentId),
ParentResourceId: switchoverv1.PtrString(switchoverPairId),
DisplayName: switchoverv1.PtrString(displayName),
Endpoints: &endpoints,
ParentResourceCrn: switchoverv1.PtrString(parentResourceCrn),
},
}

Expand Down
4 changes: 2 additions & 2 deletions internal/switchover/endpoint/command_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ func (c *command) list(cmd *cobra.Command, _ []string) error {
list.Add(&listOut{
Id: endpoint.GetId(),
DisplayName: endpoint.Spec.GetDisplayName(),
SwitchoverPair: endpoint.Spec.GetParentResourceId(),
Environment: endpoint.Spec.GetEnvironment(),
SwitchoverPair: endpoint.Spec.GetParentResourceCrn(),
Environment: endpoint.Spec.GetEnvironmentCrn(),
Phase: endpoint.Status.GetPhase(),
})
}
Expand Down
6 changes: 4 additions & 2 deletions internal/switchover/pair/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type out struct {
DisplayName string `human:"Display Name"`
Environment string `human:"Environment"`
ActiveMember string `human:"Active Member"`
FirstActive string `human:"First Active,omitempty"`
FailoverType string `human:"Failover Type,omitempty"`
Phase string `human:"Phase"`
Members string `human:"Members,omitempty"`
Expand Down Expand Up @@ -92,8 +93,9 @@ func newPairOut(pair switchoverv1.SwitchoverV1SwitchoverPair) *out {
return &out{
Id: pair.GetId(),
DisplayName: pair.Spec.GetDisplayName(),
Environment: pair.Spec.GetEnvironment(),
Environment: pair.Spec.GetEnvironmentCrn(),
ActiveMember: pair.Spec.GetActiveMember(),
FirstActive: pair.Spec.GetFirstActive(),
FailoverType: pair.Spec.GetFailoverType(),
Phase: pair.Status.GetPhase(),
Members: formatMembers(pair.Spec.GetMembers()),
Expand All @@ -108,7 +110,7 @@ func formatMembers(members []switchoverv1.SwitchoverV1SwitchoverPairMember) stri
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)
lines[i] = fmt.Sprintf("%s (%s%s)", member.GetName(), member.GetMemberCrn(), location)
}
return strings.Join(lines, "\n")
}
Expand Down
32 changes: 19 additions & 13 deletions internal/switchover/pair/command_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ func (c *command) newCreateCommand() *cobra.Command {
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`,
Text: `Create switchover pair "prod-kafka-dr" between two Kafka clusters (west and east), active on west.`,
Code: `confluent switchover pair create prod-kafka-dr --member name=west,crn=crn://confluent.cloud/organization=abc/environment=env-111111/cloud-cluster=lkc-111111 --member name=east,crn=crn://confluent.cloud/organization=abc/environment=env-222222/cloud-cluster=lkc-222222 --active-member west`,
},
),
}

cmd.Flags().StringArray("member", nil, `A member of the pair, in the form "name=<name>,id=<cluster-id>". Must be specified exactly twice.`)
cmd.Flags().StringArray("member", nil, `A member of the pair, in the form "name=<name>,crn=<member-crn>". The CRN carries the member's own environment, so the two members may live in different environments. 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)
Expand All @@ -45,19 +45,19 @@ func parseMemberFlag(raw string) (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=<name>,id=<cluster-id>"`, raw)
return member, fmt.Errorf(`invalid --member value %q: expected "name=<name>,crn=<member-crn>"`, raw)
}
switch key {
case "name":
member.Name = value
case "id":
member.MemberId = value
case "crn":
member.MemberCrn = value
default:
return member, fmt.Errorf(`invalid --member key %q: expected "name" or "id"`, key)
return member, fmt.Errorf(`invalid --member key %q: expected "name" or "crn"`, key)
}
}
if member.Name == "" || member.MemberId == "" {
return member, fmt.Errorf(`invalid --member value %q: both "name" and "id" are required`, raw)
if member.Name == "" || member.MemberCrn == "" {
return member, fmt.Errorf(`invalid --member value %q: both "name" and "crn" are required`, raw)
}
return member, nil
}
Expand Down Expand Up @@ -92,12 +92,18 @@ func (c *command) create(cmd *cobra.Command, args []string) error {
return err
}

// Each member's CRN is supplied directly (crn=...) and carries its own environment, so members
// may live in different environments than the pair. The pair's own environment_crn is built from
// the current organization plus the --environment flag (which also drives the ?environment=
// query parameter).
environmentCrn := fmt.Sprintf("crn://confluent.cloud/organization=%s/environment=%s", c.Context.GetCurrentOrganization(), environmentId)

pair := switchoverv1.SwitchoverV1SwitchoverPair{
Spec: &switchoverv1.SwitchoverV1SwitchoverPairSpec{
DisplayName: switchoverv1.PtrString(displayName),
Members: &members,
ActiveMember: switchoverv1.PtrString(activeMember),
Environment: switchoverv1.PtrString(environmentId),
DisplayName: switchoverv1.PtrString(displayName),
Members: &members,
ActiveMember: switchoverv1.PtrString(activeMember),
EnvironmentCrn: switchoverv1.PtrString(environmentCrn),
},
}

Expand Down
2 changes: 1 addition & 1 deletion internal/switchover/pair/command_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *command) list(cmd *cobra.Command, _ []string) error {
list.Add(&listOut{
Id: pair.GetId(),
DisplayName: pair.Spec.GetDisplayName(),
Environment: pair.Spec.GetEnvironment(),
Environment: pair.Spec.GetEnvironmentCrn(),
ActiveMember: pair.Spec.GetActiveMember(),
FailoverType: pair.Spec.GetFailoverType(),
Phase: pair.Status.GetPhase(),
Expand Down
10 changes: 7 additions & 3 deletions internal/switchover/pair/command_trigger_switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ 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().String("failover-type", "PLANNED", "The failover semantics to apply: PLANNED, UNPLANNED, or RESTORE.")
cmd.Flags().Bool("force", false, "Skip the confirmation prompt.")
pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand)
pcmd.AddContextFlag(cmd, c.CLICommand)
Expand All @@ -55,15 +55,19 @@ func (c *command) triggerSwitch(cmd *cobra.Command, args []string) error {
return err
}

// The :failover body carries the environment as a CRN (ORC-9794), unlike other operations
// which take it as a query parameter.
environmentCrn := fmt.Sprintf("crn://confluent.cloud/organization=%s/environment=%s", c.Context.GetCurrentOrganization(), environmentId)

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{
Spec: switchoverv1.SwitchoverV1SwitchoverPairFailoverRequestSpec{
Environment: environmentId,
FailoverType: switchoverv1.PtrString(failoverType),
EnvironmentCrn: environmentCrn,
FailoverType: failoverType,
},
}
if activeMember != "" {
Expand Down