Skip to content

feat(cluster): health/test-connection/utilization commands#83

Merged
omattsson merged 8 commits into
mainfrom
work/65-cluster-health
May 19, 2026
Merged

feat(cluster): health/test-connection/utilization commands#83
omattsson merged 8 commits into
mainfrom
work/65-cluster-health

Conversation

@omattsson
Copy link
Copy Markdown
Owner

@omattsson omattsson commented May 18, 2026

Summary

Adds five cluster subcommands wrapping already-shipped k8s-stack-manager endpoints:

Command Backend
stackctl cluster test-connection <id> POST /api/v1/clusters/:id/test
stackctl cluster health <id> GET /api/v1/clusters/:id/health/summary
stackctl cluster nodes <id> GET /api/v1/clusters/:id/health/nodes
stackctl cluster namespaces <id> GET /api/v1/clusters/:id/namespaces
stackctl cluster utilization <id> GET /api/v1/clusters/:id/utilization

Each command supports the standard --output table|json|yaml plus --quiet. A deriveHealthStatus helper collapses the summary into one label — healthy (all nodes ready), degraded (some not ready), unknown (zero nodes registered).

⚠️ Breaking (Go API)

cli/pkg/types.ClusterHealthSummary is rewritten. The previous fields (Status, CPUUsage, MemUsage, CPUTotal, MemTotal) didn't match the backend's k8s.ClusterSummary JSON shape, so stackctl cluster get was silently rendering zero values for those fields. New shape mirrors the wire format:

type ClusterHealthSummary struct {
    NodeCount         int
    ReadyNodeCount    int
    TotalCPU          string
    TotalMemory       string
    AllocatableCPU    string
    AllocatableMemory string
    NamespaceCount    int
}

Existing CLI users see more health data in cluster get after upgrade, not less. Out-of-tree consumers of cli/pkg/types need to update field references.

Closes #65

Test plan

All three layers green locally; go vet ./... clean.

  • Unit (cmd): every new verb across table/json/yaml/quiet output modes; not-found and unreachable error paths; degraded and unknown health-status branches.
  • Unit (client): URL+verb+JSON round-trip for each new client method; error path returns nil result.
  • Integration: extends cluster_integration_test.go mock to handle the five new routes; TestClusterWorkflow_HealthSurface walks create → test → health → nodes → namespaces → utilization; TestClusterWorkflow_TestConnectionUnreachable covers the 502 path; TestClusterCobra_Health drives Cobra in-process.
  • E2E: TestE2EClusterHealth builds the binary and runs stackctl cluster health 1 in table, json, and quiet modes against the httptest mock.
ok  	github.com/omattsson/stackctl/cli/cmd               11.863s
ok  	github.com/omattsson/stackctl/cli/pkg/client         4.489s
ok  	github.com/omattsson/stackctl/cli/test/e2e           5.272s
ok  	github.com/omattsson/stackctl/cli/test/integration  13.842s

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Cluster health shows unified Health Status, Ready Nodes, namespaces, and CPU/Memory capacity; quiet mode prints derived label (unknown/healthy/degraded). Default test-connection table includes Message and Server Version.
  • Bug Fixes

    • Test-connection unreachable cases now surface clear failure results; quiet mode prints status. Namespace Created timestamps formatted in UTC when present.
  • Documentation

    • Clarified CLI quiet-mode exceptions, stable non-numeric ID outputs, types and testing conventions.
  • Refactor

    • Reordered cluster tables (PODS before CPU/MEMORY) and improved pod-limit display.
  • Tests

    • Expanded unit, integration, and e2e coverage for cluster health, connectivity, nodes, namespaces, and utilization.
  • Chores

    • Added configuration and project metadata (CodeRabbit, mempalace, entities.json).

Review Change Stack

Copilot AI review requested due to automatic review settings May 18, 2026 14:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new stackctl cluster subcommands to surface cluster connectivity and health/utilization endpoints from the backend, and aligns the CLI’s health summary type with the actual API payload to avoid silently rendering zero values.

Changes:

  • Added cluster test-connection, cluster health, cluster nodes, cluster namespaces, and cluster utilization commands with table/json/yaml/quiet output support.
  • Reworked types.ClusterHealthSummary to match the backend JSON shape and updated cluster rendering accordingly (including derived healthy/degraded/unknown).
  • Extended unit, integration, and e2e test coverage for the new client methods, command output modes, and mock server routes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cli/cmd/cluster.go Adds new cluster subcommands and updates cluster health rendering/derivation.
cli/pkg/client/client.go Adds client methods for the new cluster endpoints.
cli/pkg/types/types.go Updates ClusterHealthSummary and adds new API-shaped types for nodes/namespaces/utilization/test.
cli/cmd/cluster_test.go Adds command-level tests for new subcommands across output modes and status derivation branches.
cli/pkg/client/client_test.go Adds unit tests validating method/URL/verb behavior for new client calls.
cli/test/integration/cluster_integration_test.go Extends the integration mock server and adds workflow + cobra health integration tests.
cli/test/e2e/cli_e2e_test.go Extends the e2e mock server routes and adds an e2e cluster health test covering table/json/quiet.

Comment thread cli/cmd/cluster.go Outdated
Comment on lines +395 to +399
// printCluster renders a single cluster in the configured output format.
// deriveHealthStatus collapses the cluster health summary into a single status
// label suitable for the table/quiet output modes. `unknown` covers the case
// where no nodes were reported (registry hasn't seen any, or the cluster is
// brand new) — distinct from `degraded`, which implies known-but-bad.
Comment thread cli/pkg/client/client.go
Comment on lines +1053 to +1061
// TestClusterConnection asks the backend to verify connectivity to the
// cluster's API server. On a reachable cluster the backend responds 200 with
// status=="success"; on an unreachable cluster it returns a non-2xx response
// which surfaces here as a client.APIError.
func (c *Client) TestClusterConnection(id string) (*types.ClusterTestConnectionResult, error) {
var result types.ClusterTestConnectionResult
if err := c.Post(fmt.Sprintf("/api/v1/clusters/%s/test", id), nil, &result); err != nil {
return nil, err
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 530b42cClient.do() now decodes both {error} and {message} body shapes (prefers error, falls back to message), so the backend's "Cluster is unreachable" reaches APIError.Message and surfaces in the user-facing 5xx rendering. TestTestClusterConnection_Unreachable now mocks the real {status,message} shape and asserts both layers.

Olof Mattsson and others added 2 commits May 18, 2026 18:21
Wires five new subcommands around already-shipped backend endpoints so
DevOps users can do connectivity checks and capacity inspection from
the CLI without curl/jq:

  stackctl cluster test-connection <id>   POST   /clusters/:id/test
  stackctl cluster health          <id>   GET    /clusters/:id/health/summary
  stackctl cluster nodes           <id>   GET    /clusters/:id/health/nodes
  stackctl cluster namespaces      <id>   GET    /clusters/:id/namespaces
  stackctl cluster utilization     <id>   GET    /clusters/:id/utilization

The existing `types.ClusterHealthSummary` had stale field names
(`Status`, `CPUUsage`, `MemUsage`, `CPUTotal`, `MemTotal`) that did
not match the backend's `k8s.ClusterSummary` JSON shape, so
`stackctl cluster get` was silently rendering zero values for most
health fields. The type is rewritten to match the wire format
(`node_count`, `ready_node_count`, `total_cpu`, `total_memory`,
`allocatable_cpu`, `allocatable_memory`, `namespace_count`) and the
`cluster get` rendering is updated to use the new fields. Out-of-tree
consumers of `cli/pkg/types` should treat this as a breaking change.

A `deriveHealthStatus` helper collapses the summary into one label —
`healthy` when all nodes ready, `degraded` when some aren't,
`unknown` when the cluster has zero nodes (registry unaware yet).

Tests: unit (cmd + client), integration (Cobra in-process against
httptest mock), and e2e (built binary against httptest mock) all
cover the new verbs.

Closes #65

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…format

- Split the combined comment block so deriveHealthStatus has its own
  docstring above it, and printCluster gets its own leading comment.
  Previously the printCluster comment appeared above deriveHealthStatus,
  leaving printCluster undocumented.

- Change TestTestClusterConnection_Unreachable mock from non-standard
  {status,message} payload to types.ErrorResponse{Error:...}, matching
  the backend's actual gin.H{"error":...} format. Add assertion that the
  error message is surfaced to the caller via APIError.UserFacingError().
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

json.NewEncoder(w).Encode(types.ErrorResponse{Error: "cluster not found"})
return
}
if state.unreachable[id] {
Comment thread cli/pkg/client/client.go
Comment on lines +1053 to +1062
// TestClusterConnection asks the backend to verify connectivity to the
// cluster's API server. On a reachable cluster the backend responds 200 with
// status=="success"; on an unreachable cluster it returns a non-2xx response
// which surfaces here as a client.APIError.
func (c *Client) TestClusterConnection(id string) (*types.ClusterTestConnectionResult, error) {
var result types.ClusterTestConnectionResult
if err := c.Post(fmt.Sprintf("/api/v1/clusters/%s/test", id), nil, &result); err != nil {
return nil, err
}
return &resp, nil
return &result, nil
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 530b42c — decoder now tries error first and falls back to message (with a final fallback to http.StatusText if both are empty), so the backend-provided context surfaces in APIError.Message instead of getting dropped.

Comment on lines +2157 to +2172
func TestTestClusterConnection_Unreachable(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
json.NewEncoder(w).Encode(types.ErrorResponse{Error: "Cluster is unreachable"})
}))
defer server.Close()

c := New(server.URL)
result, err := c.TestClusterConnection("1")
require.Error(t, err)
assert.Nil(t, result)
var apiErr *APIError
require.ErrorAs(t, err, &apiErr)
assert.Contains(t, apiErr.UserFacingError(), "Cluster is unreachable")
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 530b42c — mock now returns the real wire shape ({"status":"error","message":"Cluster is unreachable"}) and the test asserts apiErr.Message == "Cluster is unreachable" plus that UserFacingError() includes both "Server error" and the backend message. Catches regressions in either the decoder or the rendering layer.

Comment thread cli/cmd/cluster.go
Comment on lines 722 to 725
if printer.Quiet {
fmt.Fprintln(printer.Writer, resp.Status)
fmt.Fprintln(printer.Writer, result.Status)
return nil
}
Comment thread cli/cmd/cluster.go
Comment on lines 762 to 765
if printer.Quiet {
fmt.Fprintln(printer.Writer, id)
fmt.Fprintln(printer.Writer, deriveHealthStatus(health))
return nil
}
Comment thread cli/cmd/cluster.go
Comment on lines 806 to 812
if printer.Quiet {
for _, n := range nodes {
fmt.Fprintln(printer.Writer, n.Name)
names := make([]string, len(nodes))
for i, n := range nodes {
names[i] = n.Name
}
printer.PrintIDs(names)
return nil
Comment thread cli/cmd/cluster.go
Comment on lines 857 to 864
if printer.Quiet {
for _, ns := range namespaces {
fmt.Fprintln(printer.Writer, ns.Name)
names := make([]string, len(namespaces))
for i, n := range namespaces {
names[i] = n.Name
}
printer.PrintIDs(names)
return nil
}
Comment thread cli/cmd/cluster.go
Comment on lines 906 to 912
if printer.Quiet {
fmt.Fprintln(printer.Writer, id)
names := make([]string, len(util.Namespaces))
for i, ns := range util.Namespaces {
names[i] = ns.Namespace
}
printer.PrintIDs(names)
return nil
- Remove quiet mode from test-connection, health, nodes, namespaces,
  utilization: none produce numeric IDs per commands.instructions rule
- Update affected tests to assert table output is produced when
  printer.Quiet is set (quiet is silently ignored)
- Fix data race in cluster integration test: read state.unreachable
  under state.mu to avoid concurrent map access
- Standardise 502 mock response to types.ErrorResponse{} in both
  cluster_test.go and cluster_integration_test.go (matches real
  backend gin.H{"error":...} format)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (3)

cli/cmd/cluster.go:815

  • --quiet is currently ignored for cluster nodes. If quiet mode is meant to be supported (per PR description), it needs an explicit if printer.Quiet { ... } branch here (for example, printing node names one per line). Right now the command always prints table/JSON/YAML output.
		nodes, err := c.GetClusterNodes(id)
		if err != nil {
			return err
		}

		switch printer.Format {
		case output.FormatJSON:
			return printer.PrintJSON(nodes)
		case output.FormatYAML:
			return printer.PrintYAML(nodes)
		default:
			headers := []string{"NAME", "STATUS", "PODS", "CPU", "MEMORY"}
			rows := make([][]string, len(nodes))
			for i, n := range nodes {
				rows[i] = []string{
					n.Name,
					printer.StatusColor(n.Status),
					strconv.Itoa(n.PodCount),
					n.Allocatable.CPU,
					n.Allocatable.Memory,
				}
			}
			return printer.PrintTable(headers, rows)
		}
	},

cli/cmd/cluster.go:855

  • --quiet is currently ignored for cluster namespaces. If quiet mode is meant to be supported, add an explicit printer.Quiet branch (e.g., print namespace names one per line). The current implementation always renders table/JSON/YAML output regardless of --quiet.
		namespaces, err := c.GetClusterNamespaces(id)
		if err != nil {
			return err
		}

		switch printer.Format {
		case output.FormatJSON:
			return printer.PrintJSON(namespaces)
		case output.FormatYAML:
			return printer.PrintYAML(namespaces)
		default:
			headers := []string{"NAME", "PHASE", "CREATED"}
			rows := make([][]string, len(namespaces))
			for i, n := range namespaces {
				created := ""
				if !n.CreatedAt.IsZero() {
					created = n.CreatedAt.UTC().Format("2006-01-02 15:04:05")
				}
				rows[i] = []string{n.Name, printer.StatusColor(n.Phase), created}
			}
			return printer.PrintTable(headers, rows)
		}
	},

cli/cmd/cluster.go:902

  • --quiet is currently ignored for cluster utilization. If quiet mode is intended to be supported, add a printer.Quiet branch describing what the quiet output should be (e.g., cluster ID or namespace names). As written, the command always renders table/JSON/YAML output.
		util, err := c.GetClusterUtilization(id)
		if err != nil {
			return err
		}

		switch printer.Format {
		case output.FormatJSON:
			return printer.PrintJSON(util)
		case output.FormatYAML:
			return printer.PrintYAML(util)
		default:
			headers := []string{"NAMESPACE", "CPU USED", "CPU LIMIT", "MEM USED", "MEM LIMIT", "PODS"}
			rows := make([][]string, len(util.Namespaces))
			for i, ns := range util.Namespaces {
				podCol := strconv.Itoa(ns.PodCount)
				if ns.PodLimit > 0 {
					podCol = fmt.Sprintf("%d/%d", ns.PodCount, ns.PodLimit)
				}
				rows[i] = []string{
					ns.Namespace,
					ns.CPUUsed,
					ns.CPULimit,
					ns.MemoryUsed,
					ns.MemoryLimit,
					podCol,
				}
			}
			return printer.PrintTable(headers, rows)
		}
	},

Comment thread cli/cmd/cluster.go
Comment on lines +717 to 733
result, err := c.TestClusterConnection(id)
if err != nil {
return err
}

if printer.Quiet {
fmt.Fprintln(printer.Writer, resp.Status)
return nil
}

switch printer.Format {
case output.FormatJSON:
return printer.PrintJSON(resp)
return printer.PrintJSON(result)
case output.FormatYAML:
return printer.PrintYAML(resp)
return printer.PrintYAML(result)
default:
fields := []output.KeyValue{
{Key: "Status", Value: printer.StatusColor(resp.Status)},
{Key: "Message", Value: resp.Message},
}
if resp.ServerVersion != "" {
fields = append(fields, output.KeyValue{Key: "Server Version", Value: resp.ServerVersion})
}
return printer.PrintSingle(resp, fields)
return printer.PrintSingle(result, []output.KeyValue{
{Key: "Status", Value: printer.StatusColor(result.Status)},
{Key: "Message", Value: result.Message},
{Key: "Server Version", Value: result.ServerVersion},
})
}
Comment thread cli/cmd/cluster.go
Comment on lines 752 to +771
health, err := c.GetClusterHealth(id)
if err != nil {
return err
}

if printer.Quiet {
fmt.Fprintln(printer.Writer, id)
return nil
}

switch printer.Format {
case output.FormatJSON:
return printer.PrintJSON(health)
case output.FormatYAML:
return printer.PrintYAML(health)
default:
readyStatus := "healthy"
if health.ReadyNodeCount < health.NodeCount {
readyStatus = "degraded"
}
fields := []output.KeyValue{
{Key: "Status", Value: printer.StatusColor(readyStatus)},
{Key: "Nodes", Value: strconv.Itoa(health.NodeCount)},
{Key: "Ready Nodes", Value: strconv.Itoa(health.ReadyNodeCount)},
{Key: "Total CPU", Value: health.TotalCPU},
{Key: "Allocatable CPU", Value: health.AllocatableCPU},
{Key: "Total Memory", Value: health.TotalMemory},
{Key: "Allocatable Memory", Value: health.AllocatableMemory},
return printer.PrintSingle(health, []output.KeyValue{
{Key: "Health Status", Value: printer.StatusColor(deriveHealthStatus(health))},
{Key: "Ready Nodes", Value: fmt.Sprintf("%d/%d", health.ReadyNodeCount, health.NodeCount)},
{Key: "CPU Allocatable", Value: health.AllocatableCPU},
{Key: "CPU Total", Value: health.TotalCPU},
{Key: "Memory Allocatable", Value: health.AllocatableMemory},
{Key: "Memory Total", Value: health.TotalMemory},
{Key: "Namespaces", Value: strconv.Itoa(health.NamespaceCount)},
}
return printer.PrintSingle(health, fields)
})
Comment thread cli/pkg/types/types.go Outdated
Comment on lines +301 to +307
// ClusterTestConnectionResult is the response shape of
// POST /api/v1/clusters/:id/test. On success Status == "success"; on a
// reachable-but-erroring cluster the backend returns 502 with Status == "error".
type ClusterTestConnectionResult struct {
Status string `json:"status" yaml:"status"`
Message string `json:"message,omitempty" yaml:"message,omitempty"`
ServerVersion string `json:"server_version,omitempty" yaml:"server_version,omitempty"`
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 530b42c — docstring rewritten. The struct is only populated on HTTP 200; on 502 the client decoder now extracts the backend message field into APIError.Message, which is what the doc now describes.

Comment thread cli/cmd/cluster_test.go Outdated
Comment on lines 1311 to 1327
@@ -1366,47 +1321,34 @@ func TestClusterTestConnectionCmd_QuietOutput(t *testing.T) {

err := clusterTestConnectionCmd.RunE(clusterTestConnectionCmd, []string{"1"})
require.NoError(t, err)
assert.Equal(t, "ok\n", buf.String())
}

func TestClusterTestConnectionCmd_ParseIDError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("should not reach server")
}))
defer server.Close()

_ = setupClusterTestCmd(t, server.URL)

err := clusterTestConnectionCmd.RunE(clusterTestConnectionCmd, []string{" "})
require.Error(t, err)
assert.Contains(t, err.Error(), "must not be empty")
out := buf.String()
assert.Contains(t, out, "Status")
assert.Contains(t, out, "success")
}
Comment thread cli/cmd/cluster_test.go Outdated
Comment on lines 1442 to 1458
func TestClusterHealthCmd_TableOutputIgnoresQuiet(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(types.ErrorResponse{Error: "Internal server error"})
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(sampleClusterHealth())
}))
defer server.Close()

_ = setupClusterTestCmd(t, server.URL)
buf := setupClusterTestCmd(t, server.URL)
printer.Quiet = true

err := clusterHealthCmd.RunE(clusterHealthCmd, []string{"1"})
require.Error(t, err)
assert.Contains(t, err.Error(), "Internal server error")
require.NoError(t, err)
out := buf.String()
assert.Contains(t, out, "Health Status")
assert.Contains(t, out, "healthy")
}
…alth

- Restore quiet branch in test-connection: prints result.Status
- Restore quiet branch in health: prints deriveHealthStatus(health)
- Restore QuietOutput unit tests with exact string assertions
- Restore UnknownWhenZeroNodes quiet assertion (unknown\n)
- Fix ClusterTestConnectionResult docstring: 502 is surfaced as APIError,
  not as a populated struct field (Status == error never reaches caller)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

cli/test/integration/cluster_integration_test.go:103

  • In the mock server, len(parts)==3 leaves action empty unless the path is the special .../health/<sub> case. For any other 3-segment path under /api/v1/clusters/<id>/..., the handler falls through to case action == "" && r.Method == GET and will incorrectly serve the cluster GET response instead of 404. To prevent tests from masking incorrect client URLs, consider explicitly 404'ing non-health len(parts)==3 paths (or fully parsing action/subaction).
			switch len(parts) {
			case 1:
				id = parts[0]
			case 2:
				id = parts[0]
				action = parts[1]
			case 3:
				// /api/v1/clusters/<id>/health/<sub> — handled below as a special case.
				id = parts[0]
			default:
				w.WriteHeader(http.StatusNotFound)
				json.NewEncoder(w).Encode(types.ErrorResponse{Error: "not found"})
				return
			}

Comment thread cli/cmd/cluster.go
Comment on lines 786 to 806
var clusterNodesCmd = &cobra.Command{
Use: "nodes <id>",
Short: "List nodes in a cluster",
Long: `List all nodes in a cluster with status, capacity, and pod count.

Examples:
stackctl cluster nodes 1
stackctl cluster nodes 1 -o json`,
Use: "nodes <id>",
Short: "List nodes in a cluster with their status",
Long: "List per-node health, pod count, and allocatable CPU/memory for the cluster.",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
id, err := parseID(args[0])
if err != nil {
return err
}

c, err := newClient()
if err != nil {
return err
}

nodes, err := c.GetClusterNodes(id)
if err != nil {
return err
}

if printer.Quiet {
for _, n := range nodes {
fmt.Fprintln(printer.Writer, n.Name)
}
return nil
}

switch printer.Format {
Comment thread cli/cmd/cluster.go
Comment on lines 828 to 848
var clusterNamespacesCmd = &cobra.Command{
Use: "namespaces <id>",
Short: "List stack namespaces in a cluster",
Long: `List all stack-* namespaces in a cluster.

Examples:
stackctl cluster namespaces 1
stackctl cluster namespaces 1 -o json`,
Use: "namespaces <id>",
Short: "List stack-* namespaces present in a cluster",
Long: "List namespaces whose names start with stack-, along with their phase and creation time.",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
id, err := parseID(args[0])
if err != nil {
return err
}

c, err := newClient()
if err != nil {
return err
}

namespaces, err := c.GetClusterNamespaces(id)
if err != nil {
return err
}

if printer.Quiet {
for _, ns := range namespaces {
fmt.Fprintln(printer.Writer, ns.Name)
}
return nil
}

switch printer.Format {
Comment thread cli/cmd/cluster.go
Comment on lines 868 to 889
var clusterUtilizationCmd = &cobra.Command{
Use: "utilization <id>",
Short: "Show per-namespace resource utilization for a cluster",
Long: `Show CPU, memory, and pod usage per namespace for a cluster.

Examples:
stackctl cluster utilization 1
stackctl cluster utilization 1 -o json`,
Use: "utilization <id>",
Short: "Show per-namespace resource utilization",
Long: "Show aggregated CPU/memory/pod usage per stack-* namespace in the cluster.",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
id, err := parseID(args[0])
if err != nil {
return err
}

c, err := newClient()
if err != nil {
return err
}

util, err := c.GetClusterUtilization(id)
if err != nil {
return err
}

if printer.Quiet {
fmt.Fprintln(printer.Writer, id)
return nil
}

switch printer.Format {
case output.FormatJSON:
Comment thread cli/cmd/cluster.go

if printer.Quiet {
fmt.Fprintln(printer.Writer, resp.Status)
fmt.Fprintln(printer.Writer, result.Status)
Comment thread cli/cmd/cluster.go

if printer.Quiet {
fmt.Fprintln(printer.Writer, id)
fmt.Fprintln(printer.Writer, deriveHealthStatus(health))
Comment thread cli/cmd/cluster_test.go Outdated
Comment on lines 1500 to 1517
func TestClusterNodesCmd_TableOutputIgnoresQuiet(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode([]types.ClusterNode{node})
json.NewEncoder(w).Encode(sampleClusterNodes())
}))
defer server.Close()

buf := setupClusterTestCmd(t, server.URL)
printer.Format = output.FormatJSON
printer.Quiet = true

err := clusterNodesCmd.RunE(clusterNodesCmd, []string{"1"})
require.NoError(t, err)

var result []types.ClusterNode
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
require.Len(t, result, 1)
assert.Equal(t, "node-1", result[0].Name)
assert.Equal(t, "ready", result[0].Status)
out := buf.String()
assert.Contains(t, out, "node-a")
assert.Contains(t, out, "node-b")
assert.Contains(t, out, "NAME")
}
Comment thread cli/cmd/cluster_test.go Outdated
Comment on lines 1590 to 1607
func TestClusterNamespacesCmd_TableOutputIgnoresQuiet(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode([]types.ClusterNamespace{ns})
json.NewEncoder(w).Encode(sampleClusterNamespaces())
}))
defer server.Close()

buf := setupClusterTestCmd(t, server.URL)
printer.Format = output.FormatJSON
printer.Quiet = true

err := clusterNamespacesCmd.RunE(clusterNamespacesCmd, []string{"1"})
require.NoError(t, err)

var result []types.ClusterNamespace
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
require.Len(t, result, 1)
assert.Equal(t, "stack-dev-alice", result[0].Name)
assert.Equal(t, "Active", result[0].Phase)
out := buf.String()
assert.Contains(t, out, "stack-prod-web")
assert.Contains(t, out, "stack-dev-api")
assert.Contains(t, out, "NAME")
}
Comment thread cli/cmd/cluster_test.go Outdated
Comment on lines 1684 to 1701
func TestClusterUtilizationCmd_TableOutputIgnoresQuiet(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(util)
json.NewEncoder(w).Encode(sampleClusterUtilization())
}))
defer server.Close()

buf := setupClusterTestCmd(t, server.URL)
printer.Format = output.FormatJSON
printer.Quiet = true

err := clusterUtilizationCmd.RunE(clusterUtilizationCmd, []string{"1"})
require.NoError(t, err)

var result types.ClusterUtilization
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
assert.Equal(t, "1", result.ClusterID)
require.Len(t, result.Namespaces, 1)
assert.Equal(t, "stack-dev-alice", result.Namespaces[0].Namespace)
out := buf.String()
assert.Contains(t, out, "stack-prod")
assert.Contains(t, out, "stack-dev")
assert.Contains(t, out, "NAMESPACE")
}
…tilization

- Add quiet block to cluster nodes: prints node names via printer.PrintIDs
- Add quiet block to cluster namespaces: prints namespace names via printer.PrintIDs
- Add quiet block to cluster utilization: prints namespace names via printer.PrintIDs
- Rename *TableOutputIgnoresQuiet tests to *QuietOutput with exact assertions
  (names only, assert.NotContains for table headers)
- Update commands.instructions.md to document all quiet-mode exceptions for
  cluster subcommands so reviewers apply the contract consistently
- Add .coderabbit.yaml to point CodeRabbit at instruction files per path
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

Adds endpoint-specific cluster DTOs and updates client methods; changes cluster CLI outputs and quiet-mode exceptions; extends mock servers and adds unit/integration/E2E tests; updates documentation and adds CodeRabbit and auxiliary YAML/JSON configs.

Changes

Cluster Health and Status CLI Commands

Layer / File(s) Summary
Type/DTO contracts for cluster health
cli/pkg/types/types.go
Introduce endpoint-specific DTOs and update existing type shapes: ClusterHealthSummary (optional resource fields), ClusterTestConnectionResult, ClusterResourceQuantity, ClusterNodeStatus, updated ClusterNamespace, NamespaceResourceUsage, and ClusterUtilization.
Client wrapper methods
cli/pkg/client/client.go
Update client methods and error decoding: TestClusterConnection now returns *types.ClusterTestConnectionResult, GetClusterNodes returns []types.ClusterNodeStatus, and do() decodes both error and message payload shapes.
CLI commands and output behavior
cli/cmd/cluster.go, .github/instructions/commands.instructions.md
Add deriveHealthStatus (unknown/healthy/degraded); change cluster get/health outputs to Health Status, Ready Nodes, allocatable/total CPU and memory, and Namespaces; reorder cluster nodes columns and use allocatable values; cluster namespaces quiet output prints names and formats Created in UTC; cluster utilization conditionally shows podCount/podLimit.
Unit tests for CLI and client
cli/cmd/cluster_test.go, cli/pkg/client/client_test.go
Update and expand tests for new DTOs and CLI/client behavior: table/JSON/YAML/quiet assertions, unreachable connection case, health unknown/degraded cases, node/namespace/utilization expectations, and client request/response validation.
Integration and E2E tests
cli/test/integration/cluster_integration_test.go, cli/test/e2e/cli_e2e_test.go
Extend mock server to serve health/nodes/namespaces/utilization/test endpoints (including unreachable simulation), add workflow and Cobra integration tests, and reuse shared E2E mock server with assertions across output modes.
Docs and project configuration
.coderabbit.yaml, .github/instructions/*.instructions.md, entities.json, mempalace.yaml
Add CodeRabbit configuration and update documentation for CLI command/output/test/type conventions, explicit --quiet exceptions, E2E/Live testing guidance, plus auxiliary entities/mempalace configs.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding five cluster subcommands for health, test-connection, and utilization inspection.
Linked Issues check ✅ Passed All coding requirements from issue #65 are met: five new subcommands implemented, DTOs created, client methods added, and comprehensive tests (unit/integration/e2e) covering all output modes.
Out of Scope Changes check ✅ Passed All changes are within scope: cluster subcommands, related type definitions, documentation/config files, and test updates directly support the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch work/65-cluster-health

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
cli/pkg/client/client.go (1)

1053-1056: ⚡ Quick win

Add @see route annotations to the new public client method comments

These updated public method comments should include explicit @see <HTTP_VERB> <route> entries for consistency with client conventions.

As per coding guidelines: “Every public method has a TSDoc-style Go doc comment with @see HTTP verb + route”.

Also applies to: 1065-1066, 1074-1075, 1083-1084

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/pkg/client/client.go` around lines 1053 - 1056, The public client docs
for TestClusterConnection (and the three adjacent new public methods) are
missing the required TSDoc-style `@see` annotations; update each public method
comment (e.g., the TestClusterConnection comment and the comments immediately at
the next three method declarations) to include an explicit "`@see` <HTTP_VERB>
<route>" line that matches the HTTP endpoint the method calls (use the exact
verb and path used by the client implementation for each method).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.coderabbit.yaml:
- Around line 7-8: The comment for the YAML key request_changes_workflow is
misleading; update the comment to state that this flag controls whether an
auto-approval workflow runs after requested changes or failing checks are
resolved (not review frequency on pushes). Locate the existing comment above
request_changes_workflow and replace it with a concise description like:
"Control auto-approval behavior after requested changes/checks are resolved
(true enables auto-approval workflow)." Keep the YAML key name
request_changes_workflow unchanged.

In @.github/instructions/commands.instructions.md:
- Line 41: Update the docs for the cluster test-connection command to avoid
implying quiet mode will print "error" on API failures; instead state that in
quiet mode the command writes the connection status string (via
fmt.Fprintln(printer.Writer, result.Status)) only for successful (2xx)
responses, while non-2xx responses are returned as command errors (and therefore
will not print "error" to stdout).

In `@cli/pkg/types/types.go`:
- Line 344: ClusterNamespace's CreatedAt and UpdatedAt are non-pointer time.Time
fields with `omitempty`, which doesn't omit zero times; change their types to
pointers (e.g., *time.Time) to match the file's pattern (like
DeletedAt/ExpiresAt) and keep `omitempty` OR, if you prefer Go 1.24+, keep them
as time.Time but change the struct tags to use `omitzero`
(json:"created_at,omitempty,omitzero" / json:"updated_at,omitempty,omitzero") so
zero timestamps are omitted; update both field types/tags for
ClusterNamespace.CreatedAt and ClusterNamespace.UpdatedAt consistently and
adjust any code that constructs or reads these fields to handle pointers if you
choose the pointer approach.

In `@cli/test/integration/cluster_integration_test.go`:
- Around line 96-99: The test HTTP handler currently treats any 3-segment path
as an id lookup (setting id = parts[0]) allowing requests like
/api/v1/clusters/<id>/foo/bar to fall through; update the handler to explicitly
reject 3-segment cluster paths whose second segment is not "health" before the
generic GET-by-id branch: when len(parts)==3 check parts[1] and return a 404 (or
the test-expected error) for non-"health" values, using the existing parts and
id variables and adjusting the same logic locations referenced around the switch
handling (the case handling len(parts)==3 and the generic GET-by-id branch).

---

Nitpick comments:
In `@cli/pkg/client/client.go`:
- Around line 1053-1056: The public client docs for TestClusterConnection (and
the three adjacent new public methods) are missing the required TSDoc-style `@see`
annotations; update each public method comment (e.g., the TestClusterConnection
comment and the comments immediately at the next three method declarations) to
include an explicit "`@see` <HTTP_VERB> <route>" line that matches the HTTP
endpoint the method calls (use the exact verb and path used by the client
implementation for each method).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf3a2ace-8679-4d41-b319-dbf81f865639

📥 Commits

Reviewing files that changed from the base of the PR and between ba9ded4 and 66502f1.

📒 Files selected for processing (9)
  • .coderabbit.yaml
  • .github/instructions/commands.instructions.md
  • cli/cmd/cluster.go
  • cli/cmd/cluster_test.go
  • cli/pkg/client/client.go
  • cli/pkg/client/client_test.go
  • cli/pkg/types/types.go
  • cli/test/e2e/cli_e2e_test.go
  • cli/test/integration/cluster_integration_test.go

Comment thread .coderabbit.yaml Outdated
Comment thread .github/instructions/commands.instructions.md Outdated
Comment thread cli/pkg/types/types.go Outdated
Comment thread cli/test/integration/cluster_integration_test.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/instructions/tests.instructions.md:
- Line 69: Update the live-test command path string in the documentation:
replace the incorrect `./test/live/` with the correct `cli/test/live/` so the
example command reads `go test -tags live ./cli/test/live/ -v`; ensure any other
occurrences in the same section referencing `./test/live/` are updated to
`cli/test/live/` for consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 08488102-a8cc-4986-bc88-957db0cc5af3

📥 Commits

Reviewing files that changed from the base of the PR and between 66502f1 and b456d3c.

📒 Files selected for processing (4)
  • .coderabbit.yaml
  • .github/instructions/output.instructions.md
  • .github/instructions/tests.instructions.md
  • .github/instructions/types.instructions.md
✅ Files skipped from review due to trivial changes (2)
  • .github/instructions/output.instructions.md
  • .github/instructions/types.instructions.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .coderabbit.yaml

Comment thread .github/instructions/tests.instructions.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/pkg/types/types.go`:
- Around line 288-299: Add contract-level godoc for each exported DTO listed
(ClusterHealthSummary, ClusterResourceQuantity, ClusterNodeCondition,
ClusterNodeStatus, ClusterNamespace, NamespaceResourceUsage, ClusterUtilization)
that states the exact API endpoint(s) that produce the type, whether fields are
always present or only populated in non-zero cases (call out each omitempty
field behavior), and note any types only returned for specific HTTP status
codes; update the top-of-type comments in types.go immediately above the
corresponding type declarations (e.g., ClusterHealthSummary,
ClusterResourceQuantity, etc.) to include the endpoint mapping and explicit
population rules so callers know when a field may be zero-valued versus
populated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 767b22c5-d7aa-49e0-9d05-f44164c74f60

📥 Commits

Reviewing files that changed from the base of the PR and between b456d3c and d806e4d.

📒 Files selected for processing (10)
  • .coderabbit.yaml
  • .github/instructions/commands.instructions.md
  • .github/instructions/tests.instructions.md
  • cli/cmd/cluster.go
  • cli/cmd/cluster_test.go
  • cli/pkg/client/client.go
  • cli/pkg/types/types.go
  • cli/test/integration/cluster_integration_test.go
  • entities.json
  • mempalace.yaml
✅ Files skipped from review due to trivial changes (2)
  • entities.json
  • mempalace.yaml
🚧 Files skipped from review as they are similar to previous changes (7)
  • .github/instructions/commands.instructions.md
  • .coderabbit.yaml
  • .github/instructions/tests.instructions.md
  • cli/pkg/client/client.go
  • cli/cmd/cluster.go
  • cli/test/integration/cluster_integration_test.go
  • cli/cmd/cluster_test.go

Comment thread cli/pkg/types/types.go Outdated
Addresses outstanding review threads on PR #83:

- `Client.do()` previously decoded non-2xx bodies as
  `types.ErrorResponse{Error: ...}` only. The
  POST /api/v1/clusters/:id/test endpoint returns
  `{"status":"error","message":"..."}` on a 502 (unreachable cluster),
  so `apiErr.Message` was empty and users saw a generic
  "Server error. Check backend logs." with no context. Decoder now
  tries the `error` field first and falls back to `message`, surfacing
  the backend-provided context in `APIError.Message` and through the
  user-facing 5xx rendering.

- The `ClusterTestConnectionResult` docstring previously claimed the
  502 path returned this struct with `Status: "error"`. That was never
  decoded into the struct — the error path goes through `APIError`.
  Docstring rewritten to describe the actual decoder behavior.

- Added contract-level godoc to every new exported DTO
  (`ClusterHealthSummary`, `ClusterTestConnectionResult`,
  `ClusterResourceQuantity`, `ClusterNodeCondition`,
  `ClusterNodeStatus`, `ClusterNamespace`, `NamespaceResourceUsage`,
  `ClusterUtilization`) per the
  `.github/instructions/types.instructions.md` contract: the
  producing endpoint, when fields are populated vs. zero-valued,
  and any HTTP-status-specific population rules.

- `TestTestClusterConnection_Unreachable` now mocks the real
  `{"status":"error","message":"..."}` wire shape and asserts both
  `apiErr.Message` and the user-facing rendering carry the backend
  message — guarding the regression and validating the new decoder
  fallback path end-to-end through the client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
cli/pkg/client/client_test.go (2)

2100-2119: ⚡ Quick win

Assert the remaining rewritten health fields here.

This contract test still leaves TotalCPU and AllocatableMemory unchecked, so those renamed wire fields can regress back to zero-values without tripping this suite.

Suggested assertions
 	assert.Equal(t, 3, health.NodeCount)
 	assert.Equal(t, 3, health.ReadyNodeCount)
+	assert.Equal(t, "8", health.TotalCPU)
 	assert.Equal(t, "7", health.AllocatableCPU)
 	assert.Equal(t, "16Gi", health.TotalMemory)
+	assert.Equal(t, "14Gi", health.AllocatableMemory)
 	assert.Equal(t, 12, health.NamespaceCount)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/pkg/client/client_test.go` around lines 2100 - 2119, Add assertions in
the test after calling c.GetClusterHealth("1") to verify the renamed/mapped
fields TotalCPU and AllocatableMemory are populated correctly: check that
health.TotalCPU equals "8" and health.AllocatableMemory equals "14Gi" (matching
the types.ClusterHealthSummary values encoded by the test server). Update the
test around New(server.URL) / c.GetClusterHealth("1") to include these two
assertions so regressions of TotalCPU or AllocatableMemory are caught.

2229-2272: ⚡ Quick win

Add matching error-path tests for namespaces and utilization.

This section only exercises the happy path for GetClusterNamespaces and GetClusterUtilization, so a regression in non-2xx propagation for either endpoint would now slip through unnoticed. As per coding guidelines, "Test error cases: API errors (401, 404, 500) and invalid input in unit tests".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/pkg/client/client_test.go` around lines 2229 - 2272, Add tests that
exercise error paths for GetClusterNamespaces and GetClusterUtilization: extend
TestGetClusterNamespaces_Success and TestGetClusterUtilization_Success (or add
new tests) to simulate non-2xx server responses (e.g., 401/404/500) from the
httptest server and assert that c.GetClusterNamespaces and
c.GetClusterUtilization return a non-nil error and do not return successful
data; also add tests for invalid input (e.g., empty cluster ID passed to
New(...).GetClusterNamespaces and GetClusterUtilization) and assert proper error
handling. Use the existing httptest.NewServer handler pattern and the client
constructor New(server.URL) to locate where to inject the failing responses and
checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cli/pkg/client/client_test.go`:
- Around line 2100-2119: Add assertions in the test after calling
c.GetClusterHealth("1") to verify the renamed/mapped fields TotalCPU and
AllocatableMemory are populated correctly: check that health.TotalCPU equals "8"
and health.AllocatableMemory equals "14Gi" (matching the
types.ClusterHealthSummary values encoded by the test server). Update the test
around New(server.URL) / c.GetClusterHealth("1") to include these two assertions
so regressions of TotalCPU or AllocatableMemory are caught.
- Around line 2229-2272: Add tests that exercise error paths for
GetClusterNamespaces and GetClusterUtilization: extend
TestGetClusterNamespaces_Success and TestGetClusterUtilization_Success (or add
new tests) to simulate non-2xx server responses (e.g., 401/404/500) from the
httptest server and assert that c.GetClusterNamespaces and
c.GetClusterUtilization return a non-nil error and do not return successful
data; also add tests for invalid input (e.g., empty cluster ID passed to
New(...).GetClusterNamespaces and GetClusterUtilization) and assert proper error
handling. Use the existing httptest.NewServer handler pattern and the client
constructor New(server.URL) to locate where to inject the failing responses and
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fd82515f-7132-48fa-9f3c-863861916d94

📥 Commits

Reviewing files that changed from the base of the PR and between d806e4d and 530b42c.

📒 Files selected for processing (3)
  • cli/pkg/client/client.go
  • cli/pkg/client/client_test.go
  • cli/pkg/types/types.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/pkg/types/types.go

@omattsson omattsson merged commit 9a09d9f into main May 19, 2026
7 checks passed
@omattsson omattsson deleted the work/65-cluster-health branch May 19, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cluster): health/test-connection/utilization commands

2 participants