diff --git a/go.mod b/go.mod index 30f43c701e..5631adcbd8 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/charmbracelet/lipgloss v0.11.0 github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 - github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-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 diff --git a/go.sum b/go.sum index 13c899cce4..5bf9cb5085 100644 --- a/go.sum +++ b/go.sum @@ -172,8 +172,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 h1:19qEGhkbZa5fopKCe0VPIV+Sasby4Pv10z9ZaktwWso= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52/go.mod h1:62EMf+5uFEt1BJ2q8WMrUoI9VUSxAbDnmZCGRt/MbA0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-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= diff --git a/internal/switchover/endpoint/command.go b/internal/switchover/endpoint/command.go index b39c6b738e..511047dacf 100644 --- a/internal/switchover/endpoint/command.go +++ b/internal/switchover/endpoint/command.go @@ -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()), @@ -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") diff --git a/internal/switchover/endpoint/command_create.go b/internal/switchover/endpoint/command_create.go index fcedfdc540..9b10c3ccf3 100644 --- a/internal/switchover/endpoint/command_create.go +++ b/internal/switchover/endpoint/command_create.go @@ -29,7 +29,7 @@ func (c *command) newCreateCommand() *cobra.Command { } cmd.Flags().String("switchover-pair", "", "The ID of the switchover pair this endpoint is bound to.") - cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,type=[,network=][,access-point=]". Must be specified exactly twice.`) + cmd.Flags().StringArray("endpoint", nil, `An endpoint side, in the form "name=,type=[,network=][,access-point=]". 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) @@ -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) } @@ -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= 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), }, } diff --git a/internal/switchover/endpoint/command_list.go b/internal/switchover/endpoint/command_list.go index ceabfb76b1..a6624fa700 100644 --- a/internal/switchover/endpoint/command_list.go +++ b/internal/switchover/endpoint/command_list.go @@ -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(), }) } diff --git a/internal/switchover/pair/command.go b/internal/switchover/pair/command.go index 14300e7a49..7b111c3fb3 100644 --- a/internal/switchover/pair/command.go +++ b/internal/switchover/pair/command.go @@ -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"` @@ -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()), @@ -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") } diff --git a/internal/switchover/pair/command_create.go b/internal/switchover/pair/command_create.go index 7af14e2a62..140bb9a2a5 100644 --- a/internal/switchover/pair/command_create.go +++ b/internal/switchover/pair/command_create.go @@ -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=,id=". Must be specified exactly twice.`) + cmd.Flags().StringArray("member", nil, `A member of the pair, in the form "name=,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) @@ -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=,id="`, raw) + return member, fmt.Errorf(`invalid --member value %q: expected "name=,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 } @@ -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), }, } diff --git a/internal/switchover/pair/command_list.go b/internal/switchover/pair/command_list.go index fbbbae80d9..716110538e 100644 --- a/internal/switchover/pair/command_list.go +++ b/internal/switchover/pair/command_list.go @@ -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(), diff --git a/internal/switchover/pair/command_trigger_switch.go b/internal/switchover/pair/command_trigger_switch.go index 34171e1e15..8cec4576a8 100644 --- a/internal/switchover/pair/command_trigger_switch.go +++ b/internal/switchover/pair/command_trigger_switch.go @@ -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) @@ -55,6 +55,10 @@ 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 @@ -62,8 +66,8 @@ func (c *command) triggerSwitch(cmd *cobra.Command, args []string) error { req := switchoverv1.SwitchoverV1SwitchoverPairFailoverRequest{ Spec: switchoverv1.SwitchoverV1SwitchoverPairFailoverRequestSpec{ - Environment: environmentId, - FailoverType: switchoverv1.PtrString(failoverType), + EnvironmentCrn: environmentCrn, + FailoverType: failoverType, }, } if activeMember != "" {