Skip to content
Open
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
195 changes: 195 additions & 0 deletions SWITCHOVER_LOCAL_DEMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Local CLI build with Switchover support

> Paths below assume this repo (and its sibling repos) are cloned under
> `~/sdk_cli_tf_switchover/`, matching the original workspace this branch was
> built in. Adjust paths if you clone elsewhere.

How to reproduce the local `confluent` CLI build in this workspace, which adds
`confluent switchover pair` and `confluent switchover endpoint` commands on
top of the upstream CLI.

## Workspace layout

```
~/sdk_cli_tf_switchover/
├── cli/
├── ccloud-sdk-go-v2-internal/
├── api/
├── cc-switchover/
└── terraform-provider-confluent/
```

- `cli/` — confluentinc/cli, branch `switchover-cli-local-demo`,
[PR #3399](https://github.com/confluentinc/cli/pull/3399)
- `ccloud-sdk-go-v2-internal/` — confluentinc/ccloud-sdk-go-v2-internal,
branch `ORC-10130-sync-switchover-sdk`,
[PR #832](https://github.com/confluentinc/ccloud-sdk-go-v2-internal/pull/832)
- `api/` — confluentinc/api, branch `ORC-10130-switchover-api-spec`,
[PR #2545](https://github.com/confluentinc/api/pull/2545)
- `cc-switchover/` — confluentinc/cc-switchover, branch `master`
(reference/source-of-truth)
- `terraform-provider-confluent/` — confluentinc/terraform-provider-confluent,
branch `master` (not used here)

`cli`'s `go.mod` pins the switchover SDK to
[PR #832](https://github.com/confluentinc/ccloud-sdk-go-v2-internal/pull/832)'s
commit:

```
github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260708221705-f2f8dbb55045
```

To get the `cli` branch locally:

```bash
git clone git@github.com:confluentinc/cli.git
cd cli
git checkout switchover-cli-local-demo
```

`confluentinc/cli` is a public repo with an org ruleset that blocks direct
branch pushes — use `git push-external` (Confluent's Airlock
proprietary-code-scan wrapper), not plain `git push`, to push further changes
to this branch.

## One-time environment setup

1. **Go toolchain** — `cli/.go-version` pins `1.26.4`. If your `goenv` doesn't
have it yet, any Go ≥1.21 binary will auto-fetch it via `GOTOOLCHAIN=auto`:
```bash
export GOTOOLCHAIN=auto
export PATH="$HOME/.goenv/versions/<any-installed-1.2x>/bin:$PATH"
go version # should report go1.26.4 once it downloads
```
2. **goreleaser** — the CLI's `make build` shells out to it:
```bash
brew install goreleaser
```
3. **Docker Desktop**, signed in to an org with access to
`519856050701.dkr.ecr.us-west-2.amazonaws.com` — only needed if you want to
regenerate the switchover SDK from `confluentinc/api`, not for building the
CLI itself.

## Build

```bash
cd ~/sdk_cli_tf_switchover/cli
export GOTOOLCHAIN=auto
export PATH="$HOME/.goenv/versions/<any-installed-1.2x>/bin:$PATH"
make build
```

Binary lands at:

```
~/sdk_cli_tf_switchover/cli/dist/confluent_darwin_arm64_v8.0/confluent
```

(path varies by OS/arch — check `dist/` after the build).

## Alias the binary (recommended)

Prefer a distinctly-named **alias** over adding the `dist/` folder to `PATH`,
so this experimental build doesn't shadow a real installed `confluent` CLI:

```bash
alias confluent-dev="/Users/ewang/sdk_cli_tf_switchover/cli/dist/confluent_darwin_arm64_v8.0/confluent"
```

Already set up in `~/.aliases` (sourced from `~/.zshrc`), alongside the
existing `confluent` alias. New terminals pick it up automatically; in an
already-open terminal run `source ~/.aliases` once. The rest of this README
uses `confluent-dev`.

## Usage

Log in first — the `switchover` command requires cloud login:

```bash
confluent-dev login
```

This hits **production** Confluent Cloud (`https://confluent.cloud`) with
real credentials — it's the CLI's stock login flow, not a mock.

### Logging in against staging (`stag`) instead of production

```bash
confluent-dev login --url https://stag.cpdev.cloud
```

The hostname is `stag.cpdev.cloud` with **no** `confluent.` prefix (unlike
prod's `confluent.cloud`). You'll need a **staging** Confluent Cloud
account — separate from your prod account — and your staging org needs
Switchover Early Access granted before `switchover pair` commands return
anything but an access/authorization error. `devel.cpdev.cloud` works the
same way for the `devel` environment. If you know your staging org ID, add
`--organization <org-id>`.

Everything below (`switchover pair`/`switchover endpoint` commands) works
identically once logged in, regardless of which environment you logged into.

### Bypassing the stag EA gate with a Cloud API key

The Switchover Early Access gate on `stag`/`devel` is only enforced on the
bearer-token (`login`) path — a Cloud API key (Basic auth) sails through it.
If your staging org doesn't have EA granted yet, set both of these and the
`switchover` commands will use Basic auth instead of your login session's
bearer token:

```bash
export CONFLUENT_CLOUD_API_KEY=<key>
export CONFLUENT_CLOUD_API_SECRET=<secret>
```

Create a Cloud API key first (still requires being logged in once, to create
it): `confluent-dev api-key create --resource cloud`. You still need
`confluent-dev login` for non-switchover commands / context; this env var
pair only affects the switchover API calls (see
`pkg/ccloudv2/switchover.go`'s `switchoverApiContext`).

### Switchover pairs

```bash
# Create a pair between two Kafka clusters
confluent-dev switchover pair create prod-kafka-dr \
--member name=west,id=lkc-111111 \
--member name=east,id=lkc-222222 \
--active-member west \
--environment env-abcdef

# Describe it
confluent-dev switchover pair describe sw-123456

# Rename it (the only mutable field)
confluent-dev switchover pair update sw-123456 --display-name "prod-kafka-dr-renamed"

# Trigger a failover (prompts for confirmation — moves live traffic)
confluent-dev switchover pair trigger-switch sw-123456 --active-member east --failover-type CLEAN
```

### Switchover endpoints

```bash
confluent-dev switchover endpoint create prod-kafka-dr-endpoint \
--switchover-pair sw-123456 \
--endpoint name=west-platt,type=private \
--endpoint name=east-platt,type=private

confluent-dev switchover endpoint describe se-123456
confluent-dev switchover endpoint update se-123456 --display-name "renamed-endpoint"
confluent-dev switchover endpoint activate se-123456
```

### Global flags

- `-o json` / `-o yaml` for machine-readable output (default is a human table).
- `--environment env-xxxxx` to override the current context's default environment.
- `--context <name>` to target a specific CLI context/profile.

## Rebuilding after further code changes

Rerun `make build` from `cli/` — it picks up any local edits. Only redo the
SDK pinning step (`go get github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover@<commit>`
in `cli/`) if `ccloud-sdk-go-v2-internal`'s switchover module changes again
upstream.
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-20260728173342-25f438c2593b
github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0
github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0
github.com/confluentinc/ccloud-sdk-go-v2/billing v0.3.0
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-20260728173342-25f438c2593b h1:9KvPeP2mC2MZNprKA1N2ueqCJEWLZAgRmVMIJdDXhAY=
github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover v0.0.0-20260728173342-25f438c2593b/go.mod h1:yv1VtjqyqD7G5gpdrzR2hGyIAfEOuLA3aTQW0TZ6Iek=
github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw=
github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA=
github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw=
Expand Down
2 changes: 2 additions & 0 deletions internal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/confluentinc/cli/v4/internal/secret"
servicequota "github.com/confluentinc/cli/v4/internal/service-quota"
streamshare "github.com/confluentinc/cli/v4/internal/stream-share"
"github.com/confluentinc/cli/v4/internal/switchover"
"github.com/confluentinc/cli/v4/internal/tableflow"
unifiedstreammanager "github.com/confluentinc/cli/v4/internal/unified-stream-manager"
"github.com/confluentinc/cli/v4/internal/update"
Expand Down Expand Up @@ -138,6 +139,7 @@ func NewConfluentCommand(cfg *config.Config) *cobra.Command {
cmd.AddCommand(servicequota.New(prerunner))
cmd.AddCommand(shell.New(cmd, func() *cobra.Command { return NewConfluentCommand(cfg) }))
cmd.AddCommand(streamshare.New(prerunner))
cmd.AddCommand(switchover.New(prerunner))
cmd.AddCommand(tableflow.New(prerunner))
cmd.AddCommand(unifiedstreammanager.New(cfg, prerunner))
cmd.AddCommand(update.New(cfg, prerunner))
Expand Down
22 changes: 22 additions & 0 deletions internal/switchover/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package switchover

import (
"github.com/spf13/cobra"

"github.com/confluentinc/cli/v4/internal/switchover/endpoint"
"github.com/confluentinc/cli/v4/internal/switchover/pair"
pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
)

func New(prerunner pcmd.PreRunner) *cobra.Command {
cmd := &cobra.Command{
Use: "switchover",
Short: "Manage Kafka disaster recovery switchover pairs and endpoints.",
Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireNonAPIKeyCloudLogin},
}
Comment on lines +11 to +16

cmd.AddCommand(pair.New(prerunner))
cmd.AddCommand(endpoint.New(prerunner))

return cmd
}
135 changes: 135 additions & 0 deletions internal/switchover/endpoint/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package endpoint

import (
"encoding/json"
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/tidwall/pretty"
"gopkg.in/yaml.v3"

switchoverv1 "github.com/confluentinc/ccloud-sdk-go-v2-internal/switchover/v1"

pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
"github.com/confluentinc/cli/v4/pkg/output"
)

type command struct {
*pcmd.AuthenticatedCLICommand
}

// out is the detailed human-readable shape used by create/describe/update.
// Machine-readable output (`-o json` / `-o yaml`) is emitted straight from the
// SDK object so it matches the API response verbatim.
type out struct {
Id string `human:"ID"`
DisplayName string `human:"Display Name"`
SwitchoverPair string `human:"Switchover Pair"`
Environment string `human:"Environment"`
Target string `human:"Target,omitempty"`
Phase string `human:"Phase"`
Endpoints string `human:"Endpoints,omitempty"`
Conditions string `human:"Conditions,omitempty"`
}

// listOut is the compact per-row shape for `list` (human output only).
type listOut struct {
Id string `human:"ID"`
DisplayName string `human:"Display Name"`
SwitchoverPair string `human:"Switchover Pair"`
Environment string `human:"Environment"`
Phase string `human:"Phase"`
}

func New(prerunner pcmd.PreRunner) *cobra.Command {
cmd := &cobra.Command{
Use: "endpoint",
Short: "Manage switchover endpoints.",
Long: "Manage switchover endpoints. This API is not yet implemented on the backend; commands will fail against a live Confluent Cloud environment.",
}

c := &command{pcmd.NewAuthenticatedCLICommand(cmd, prerunner)}

cmd.AddCommand(c.newCreateCommand())
cmd.AddCommand(c.newDeleteCommand())
cmd.AddCommand(c.newDescribeCommand())
cmd.AddCommand(c.newListCommand())
cmd.AddCommand(c.newUpdateCommand())

return cmd
}

// printSerialized emits v as JSON or YAML using the SDK object's `json` tags,
// so the output matches the API response verbatim. (output.SerializedOutput's
// YAML path uses yaml.v3, which ignores `json` tags; route YAML through the
// JSON representation to preserve the API field names.)
func printSerialized(cmd *cobra.Command, v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}

if output.GetFormat(cmd) == output.YAML {
var generic any
if err := json.Unmarshal(data, &generic); err != nil {
return err
}
out, err := yaml.Marshal(generic)
if err != nil {
return err
}
output.Print(false, string(out))
return nil
}

output.Print(false, string(pretty.Pretty(data)))
return nil
}

func newEndpointOut(endpoint switchoverv1.SwitchoverV1SwitchoverEndpoint) *out {
return &out{
Id: endpoint.GetId(),
DisplayName: endpoint.Spec.GetDisplayName(),
SwitchoverPair: endpoint.Spec.GetParentResourceId(),
Environment: endpoint.Spec.GetEnvironment(),
Target: endpoint.Spec.GetTarget(),
Phase: endpoint.Status.GetPhase(),
Endpoints: formatEndpoints(endpoint.Spec.GetEndpoints()),
Conditions: formatConditions(endpoint.Status.GetConditions()),
}
}

func formatEndpoints(endpoints []switchoverv1.SwitchoverV1EndpointConfig) string {
lines := make([]string, len(endpoints))
for i, endpoint := range endpoints {
filter := endpoint.EndpointFilter
parts := []string{endpoint.GetName(), filter.GetType()}
if networkId := filter.GetNetworkId(); networkId != "" {
parts = append(parts, "network="+networkId)
}
if accessPoint := filter.GetAccessPoint(); accessPoint != "" {
parts = append(parts, "access-point="+accessPoint)
}
if hostname := endpoint.GetHostname(); hostname != "" {
parts = append(parts, "hostname="+hostname)
}
lines[i] = strings.Join(parts, " ")
}
return strings.Join(lines, "\n")
}

func formatConditions(conditions []switchoverv1.SwitchoverV1Condition) string {
lines := make([]string, len(conditions))
for i, condition := range conditions {
line := fmt.Sprintf("%s=%s", condition.GetType(), condition.GetStatus())
if reason := condition.GetReason(); reason != "" {
line += fmt.Sprintf(" (%s)", reason)
}
if message := condition.GetMessage(); message != "" {
line += ": " + message
}
lines[i] = line
}
return strings.Join(lines, "\n")
}
Loading