Skip to content
Draft
2 changes: 1 addition & 1 deletion 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/ksql v0.5.0
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 All @@ -36,7 +37,6 @@ require (
github.com/confluentinc/ccloud-sdk-go-v2/identity-provider v0.3.0
github.com/confluentinc/ccloud-sdk-go-v2/kafka-quotas v0.4.0
github.com/confluentinc/ccloud-sdk-go-v2/kafkarest v0.0.0-20250909043602-f80bee0eb280
github.com/confluentinc/ccloud-sdk-go-v2/ksql v0.2.0
github.com/confluentinc/ccloud-sdk-go-v2/mds v0.4.0
github.com/confluentinc/ccloud-sdk-go-v2/metrics v0.2.0
github.com/confluentinc/ccloud-sdk-go-v2/networking v0.14.0
Expand Down
4 changes: 2 additions & 2 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/ksql v0.5.0 h1:/t7vM8UkEGJWfdDxsRkDCaRhDoDhAupphRl2QZ039zI=
github.com/confluentinc/ccloud-sdk-go-v2-internal/ksql v0.5.0/go.mod h1:NHFvzwBb2RJmy7RVvq9Z3gregvm08HgquCUtsjTj1Ko=
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 Expand Up @@ -214,8 +216,6 @@ github.com/confluentinc/ccloud-sdk-go-v2/kafka-quotas v0.4.0 h1:T9e7lNj/VjxE89+t
github.com/confluentinc/ccloud-sdk-go-v2/kafka-quotas v0.4.0/go.mod h1:7gqwWFIyj2MAGpL/kf6SGXm/pi2Z6qpMJIjKlgEEhhg=
github.com/confluentinc/ccloud-sdk-go-v2/kafkarest v0.0.0-20250909043602-f80bee0eb280 h1:GFVI3pGckhpP66Xb05usB8txzubnnoigZHp292ax5Rg=
github.com/confluentinc/ccloud-sdk-go-v2/kafkarest v0.0.0-20250909043602-f80bee0eb280/go.mod h1:b8v8EIBtpQDx0zAxCpGxhuSWBRAwh/+PRFNtaBR5P7c=
github.com/confluentinc/ccloud-sdk-go-v2/ksql v0.2.0 h1:g6OHa1iW3HO3N/YiTAL9Q6Y7rdjMBAjOPYK37akTt0M=
github.com/confluentinc/ccloud-sdk-go-v2/ksql v0.2.0/go.mod h1:0LAvd4VqlaRwKU4yvDEkVCtV43yNezt56+hBe9Lmg7Q=
github.com/confluentinc/ccloud-sdk-go-v2/mds v0.4.0 h1:jIXXhGi+Xn+XYFCErnMvd035QijbYXla1Bo8W7V7lFM=
github.com/confluentinc/ccloud-sdk-go-v2/mds v0.4.0/go.mod h1:ufn9In8kDsyJ7Nru2ygpAaWdGw7DSDTOTtDhQVSmZjs=
github.com/confluentinc/ccloud-sdk-go-v2/metrics v0.2.0 h1:TWwZHdfo2XNKrnGOuxXx4LF8WgahqqDC47Ap51L4thM=
Expand Down
2 changes: 1 addition & 1 deletion internal/ksql/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/oauth2"

ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2/ksql/v2"
ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2-internal/ksql/v2"

pauth "github.com/confluentinc/cli/v4/pkg/auth"
"github.com/confluentinc/cli/v4/pkg/ccloudv2"
Expand Down
1 change: 1 addition & 0 deletions internal/ksql/command_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func newClusterCommand(cfg *config.Config, prerunner pcmd.PreRunner) *cobra.Comm
cmd.AddCommand(c.newDeleteCommand())
cmd.AddCommand(c.newDescribeCommand())
cmd.AddCommand(c.newListCommand())
cmd.AddCommand(c.newUpdateCommand())

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9eee25: added Hidden: true on the cobra command. The subcommand is still reachable for testing and review, but won't appear in ksql cluster --help or in customer-facing discovery. Drop Hidden when the SDK is regenerated and the shim is replaced with the real PATCH call.

} else {
c := &ksqlCommand{pcmd.NewAuthenticatedWithMDSCLICommand(cmd, prerunner)}
cmd.AddCommand(c.newListCommandOnPrem())
Expand Down
2 changes: 1 addition & 1 deletion internal/ksql/command_cluster_configureacls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (

"github.com/spf13/cobra"

ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2-internal/ksql/v2"
kafkarestv3 "github.com/confluentinc/ccloud-sdk-go-v2/kafkarest/v3"
ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2/ksql/v2"

"github.com/confluentinc/cli/v4/pkg/acl"
"github.com/confluentinc/cli/v4/pkg/ccstructs"
Expand Down
2 changes: 1 addition & 1 deletion internal/ksql/command_cluster_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/oauth2"

ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2/ksql/v2"
ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2-internal/ksql/v2"

pauth "github.com/confluentinc/cli/v4/pkg/auth"
pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
Expand Down
163 changes: 163 additions & 0 deletions internal/ksql/command_cluster_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package ksql

import (
"fmt"
"sort"

"github.com/spf13/cobra"

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

// validCsuSizes mirrors cc-control-plane-ksql's authoritative list.
//
//nolint:gochecknoglobals
var validCsuSizes = []int32{4, 8, 12, 16, 20, 24, 28}

//nolint:gochecknoglobals
var maxSelfServeCSU = func() int32 {
maxCsu := int32(0)
for _, v := range validCsuSizes {
if v > maxCsu {
maxCsu = v
}
}
return maxCsu
}()

func csuSupportTicketMessage() string {
return fmt.Sprintf(
"CSU values above %d require a support ticket. "+
"Please contact Confluent Support to request a larger cluster size.",
maxSelfServeCSU)
}

// buildUpdateExamples returns the customer-facing help examples for the
// update command — extracted so it's directly unit-testable.
func buildUpdateExamples() string {
return examples.BuildExampleString(
examples.Example{
Text: `Expand ksqlDB cluster "lksqlc-12345" to 8 CSUs.`,
Code: "confluent ksql cluster update lksqlc-12345 --csu 8",
},
examples.Example{
Text: `Shrink ksqlDB cluster "lksqlc-12345" to 4 CSUs.`,
Code: "confluent ksql cluster update lksqlc-12345 --csu 4",
},
)
}

// buildCsuFlagUsage returns the help text for the --csu flag — extracted
// so it's directly unit-testable.
func buildCsuFlagUsage() string {
return fmt.Sprintf("Target number of CSUs for the cluster. Valid values: %s.",
formatCsuList(validCsuSizes))
}

func (c *ksqlCommand) newUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <id>",
Short: "Update a ksqlDB cluster.",
Long: buildUpdateLongDescription(),
Args: cobra.ExactArgs(1),
ValidArgsFunction: pcmd.NewValidArgsFunction(c.validArgs),
RunE: c.update,
Hidden: true, // until cc-api #2507 merges + public SDK regenerates
Example: buildUpdateExamples(),
}

cmd.Flags().Int32("csu", 0, buildCsuFlagUsage())
pcmd.AddContextFlag(cmd, c.CLICommand)
pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand)
pcmd.AddOutputFlag(cmd)

cobra.CheckErr(cmd.MarkFlagRequired("csu"))

return cmd
}

func buildUpdateLongDescription() string {
return fmt.Sprintf(
"Update an existing ksqlDB cluster. Currently only the CSU count may be modified. "+
"Both expansion (increase) and shrink (decrease) are supported.\n\n"+
"Valid CSU values are %s. Larger sizes require a support ticket. "+
"The cluster will undergo a rolling restart to apply the new size; "+
"the command returns once the resize has been accepted by the control plane. "+
"Shrink requests are precondition-checked against the cluster's running "+
"persistent-query count and refused if the new size cannot host them; "+
"drop excess queries with `TERMINATE <query_id>` and retry.",
formatCsuList(validCsuSizes))
Comment on lines +82 to +92

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9eee25: replaced the raw multi-line string with concatenated string literals and an explicit \n\n paragraph break. No more leading whitespace on continuation lines.

}

func (c *ksqlCommand) update(cmd *cobra.Command, args []string) error {
csu, err := cmd.Flags().GetInt32("csu")
if err != nil {
return err
}
if err := validateCsuForUpdate(csu); err != nil {
return err
}

environmentId, err := c.Context.EnvironmentId()
if err != nil {
return err
}

clusterId := args[0]

// Client-side no-op short-circuit; direction is server-arbitrated.
current, err := c.V2Client.DescribeKsqlCluster(clusterId, environmentId)
if err != nil {
return errors.CatchKSQLNotFoundError(err, clusterId)
}
currentCsu := current.Spec.GetCsu()
if currentCsu == csu {
return fmt.Errorf("ksqlDB cluster %q is already at %d CSUs; no change requested",
clusterId, csu)
}

cluster, err := c.V2Client.UpdateKsqlCluster(clusterId, environmentId, csu)
if err != nil {
return err
}

// Rolling-restart notice prints only AFTER the PATCH was accepted.
output.ErrPrintf(c.Config.EnableColor,
"Resizing ksqlDB cluster %q from %d to %d CSUs. A rolling restart will be "+
"performed asynchronously; the cluster will continue serving queries during the resize.\n",
clusterId, currentCsu, csu)

table := output.NewTable(cmd)
table.Add(c.formatClusterForDisplayAndList(&cluster))
return table.Print()
}

// validateCsuForUpdate fail-fast checks before issuing the API call.
func validateCsuForUpdate(csu int32) error {
if csu > maxSelfServeCSU {
return fmt.Errorf("%d CSUs: %s", csu, csuSupportTicketMessage())
}
Comment on lines +139 to +142

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9b9eee25: added maxSelfServeCSU derived once from validCsuSizes (max over the slice). Both validateCsuForUpdate and csuSupportTicketMessage reference it, so extending validCsuSizes later moves the threshold automatically.

for _, valid := range validCsuSizes {
if csu == valid {
return nil
}
}
return fmt.Errorf("%d is not a valid CSU size for cluster update. Valid sizes are %s",
csu, formatCsuList(validCsuSizes))
}

func formatCsuList(sizes []int32) string {
sorted := append([]int32(nil), sizes...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
out := ""
for i, s := range sorted {
if i > 0 {
out += ", "
}
out += fmt.Sprintf("%d", s)
}
return out
}
127 changes: 127 additions & 0 deletions internal/ksql/command_cluster_update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package ksql

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateCsuForUpdate(t *testing.T) {
tests := []struct {
name string
csu int32
expectErr bool
errContains string
}{
{name: "valid 4", csu: 4},
{name: "valid 8", csu: 8},
{name: "valid 12", csu: 12},
{name: "valid 16", csu: 16},
{name: "valid 20", csu: 20},
{name: "valid 24", csu: 24},
{name: "valid 28", csu: 28},
{
name: "legacy size 1 rejected",
csu: 1,
expectErr: true,
errContains: "not a valid CSU size",
},
{
name: "legacy size 2 rejected",
csu: 2,
expectErr: true,
errContains: "not a valid CSU size",
},
{
name: "in-range but non-canonical (5) rejected",
csu: 5,
expectErr: true,
errContains: "not a valid CSU size",
},
{
name: "in-range but non-canonical (10) rejected",
csu: 10,
expectErr: true,
errContains: "not a valid CSU size",
},
{
name: "above 28 routes to support-ticket message",
csu: 32,
expectErr: true,
errContains: "support ticket",
},
{
name: "well above ceiling routes to support-ticket message",
csu: 128,
expectErr: true,
errContains: "support ticket",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateCsuForUpdate(tc.csu)
if tc.expectErr {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errContains)
} else {
require.NoError(t, err)
}
})
}
}

func TestFormatCsuList(t *testing.T) {
require.Equal(t, "4, 8, 12, 16, 20, 24, 28", formatCsuList(validCsuSizes))
// Input order should not matter; output is sorted ascending.
require.Equal(t, "4, 8, 16", formatCsuList([]int32{16, 4, 8}))
}

// TestBuildUpdateLongDescription pins the customer-facing help text. KSQL-15168
// rewrote this to advertise both expansion and shrink (and the TERMINATE
// remediation when the server refuses a shrink). A future change that
// reverts to expand-only wording, drops the TERMINATE guidance, or drops
// the valid CSU listing would break this test.
func TestBuildUpdateLongDescription(t *testing.T) {
long := buildUpdateLongDescription()

require.Contains(t, long, "Both expansion (increase) and shrink (decrease) are supported",
"long description must advertise both directions")
require.Contains(t, long, "TERMINATE <query_id>",
"long description must surface the customer-side remediation for a refused shrink")
require.Contains(t, long, "4, 8, 12, 16, 20, 24, 28",
"long description must enumerate valid CSU sizes (kept in sync with validCsuSizes)")
require.Contains(t, long, "rolling restart",
"long description must call out the rolling-restart behavior")
}

// TestCsuSupportTicketMessage pins the support-ticket fallback message.
func TestCsuSupportTicketMessage(t *testing.T) {
msg := csuSupportTicketMessage()
require.Contains(t, msg, "support ticket")
require.Contains(t, msg, "28", "message must name the self-serve ceiling")
}

// TestBuildUpdateExamples pins the customer-facing help examples — covers
// both expand and shrink lines.
func TestBuildUpdateExamples(t *testing.T) {
out := buildUpdateExamples()
require.Contains(t, out, "Expand ksqlDB cluster")
require.Contains(t, out, "Shrink ksqlDB cluster")
require.Contains(t, out, "--csu 8")
require.Contains(t, out, "--csu 4")
require.Contains(t, out, "lksqlc-12345")
}

// TestBuildCsuFlagUsage pins the --csu flag's help text.
func TestBuildCsuFlagUsage(t *testing.T) {
out := buildCsuFlagUsage()
require.Contains(t, out, "Target number of CSUs")
require.Contains(t, out, "4, 8, 12, 16, 20, 24, 28",
"flag usage must list all valid CSU sizes")
}

// TestMaxSelfServeCSU verifies the cap is derived from validCsuSizes.
func TestMaxSelfServeCSU(t *testing.T) {
require.Equal(t, int32(28), maxSelfServeCSU)
}
2 changes: 1 addition & 1 deletion pkg/ccloudv2/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ccloudv2

import (
ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2-internal/ksql/v2"
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"
Expand All @@ -20,7 +21,6 @@ import (
iamv2 "github.com/confluentinc/ccloud-sdk-go-v2/iam/v2"
identityproviderv2 "github.com/confluentinc/ccloud-sdk-go-v2/identity-provider/v2"
kafkaquotasv1 "github.com/confluentinc/ccloud-sdk-go-v2/kafka-quotas/v1"
ksqlv2 "github.com/confluentinc/ccloud-sdk-go-v2/ksql/v2"
mdsv2 "github.com/confluentinc/ccloud-sdk-go-v2/mds/v2"
networkingaccesspointv1 "github.com/confluentinc/ccloud-sdk-go-v2/networking-access-point/v1"
networkingdnsforwarderv1 "github.com/confluentinc/ccloud-sdk-go-v2/networking-dnsforwarder/v1"
Expand Down
Loading