Skip to content

feat(stack): add stack exec (#179) - #181

Merged
omattsson merged 2 commits into
mainfrom
feat/179-stack-exec
Oct 30, 2026
Merged

omattsson merged 2 commits into
mainfrom
feat/179-stack-exec

Conversation

@omattsson

@omattsson omattsson commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #179.

Adds stackctl stack exec <name|id> <chart> [-c CONTAINER] -- COMMAND [ARGS...]. It runs one command in a container of the stack through the server and streams the output. The route is GET /api/v1/stack-instances/:id/exec (omattsson/k8s-stack-manager#723, #725; first release v0.15.0).

Behaviour

  • -- is required. The words after it are the command, sent as a list in the first WebSocket message, never in the URL. There is no shell: use -- sh -c '...'.
  • stdout and stderr frames go unchanged to stdout and stderr. There is no stdin and no TTY. -o and -q are ignored.
  • Exit codes:
    • the remote command's exit code passes through
    • 124: the server time limit (close 4001)
    • 130: Ctrl-C or SIGTERM (a second Ctrl-C exits at once)
    • 255: an error of stackctl or the server, with a message on stderr
    • 1: invalid flags or arguments
  • Errors map to clear messages: 400, 403 (container, cluster, service-account token, API key), 404 (chart, container, feature_disabled, plain 404 → upgrade hint), 429, 503, and the closes 1008, 1009, 1011, 4400, 4408 and 4502. stackctl never connects again.
  • cluster create / cluster update get --allow-exec and --allow-port-forward (the per-cluster switches; without them an admin had to use the API). cluster get shows both switches.
  • cluster create / cluster update also get --api-server-url and --use-in-cluster. The server needs one of them, but before only --from-file could send them. create refuses a kubeconfig flag without --api-server-url, and --use-in-cluster together with a kubeconfig flag, before any request.

Security

  • Auth by header only; the stream dial never falls back to ?token=.
  • --insecure, and an http:// API URL to a host other than localhost, are refused before any request unless --allow-insecure-stream is set (shared with port-forward).
  • All server text is sanitized: error bodies, close reasons, error frames, and the pod and container names.
  • --debug logs only the started frame, never output or arguments.
  • A frame may be at most 1 MiB, and output is not buffered.
  • An exit code outside 0–255 gives 255.
  • The help text warns that the output can show secrets and contain terminal escape sequences.
  • govulncheck reports nothing.
  • Fixed a deadlock where a second Ctrl-C hung while stderr was blocked; a regression test covers it.

Other changes

  • closeOnCancel: a Ctrl-C during the WebSocket handshake now ends the dial at once. Before, it could wait up to 45 s; this also affected stack port-forward.
  • ExitError (cmd/exitcode.go), handled in main.go.
  • swagger.json refreshed (exec route, exec_enabled, cluster allow_exec / allow_port_forward).
  • README, CLAUDE.md and the agent guide are updated.

Testing

  • go build, make lint (vet and staticcheck), go test ./..., go test -race -run Exec ./pkg/client ./cmd, contract tests and govulncheck all pass.
  • Unit tests use a fake WebSocket server and goleak. They cover streaming before the end, binary data, every close code and handshake error, a cancelled context, and unknown frames.
  • An e2e test runs the real binary: exit 7 passes through, and a 403 gives 255.
  • Manual test against k8s-stack-manager origin/main (de64681) installed with its Helm chart in Docker Desktop Kubernetes. Exec, port-forward and the cluster switches were all on. These cases worked:
    • output: stdout and stderr kept apart, output streaming before the command ended, 8 MB of random bytes with the same MD5, NUL bytes and invalid UTF-8
    • exit codes: the remote codes 0, 3, 124, 137 and 255 passed through; the server time limit gave 124; Ctrl-C gave 130 at once, also with stderr blocked
    • errors: unknown chart, stack or container; an invalid container name; another user's stack; an API key; no ready pod; the cluster switch off; exec off on the server (feature_disabled); a command over 4096 bytes; a session revoked mid-command
    • --insecure refused without --allow-insecure-stream; --debug output; audit entries
    • port-forward still works

Open

  • Maintainers: please confirm the exit codes 124, 130 and 255. A remote command can return the same codes, and stderr shows which it is.
  • Check ExecServerVersion and the upgrade hints against the final v0.15.0 release.

🤖 Generated with Claude Code

stackctl stack exec <name|id> <chart> [-c CONTAINER] -- COMMAND [ARGS...]
runs one command in a container of the stack through the server
(GET /api/v1/stack-instances/:id/exec, k8s-stack-manager v0.15.0 or later)
and streams its stdout and stderr unchanged. No stdin, no TTY.

- The command goes as an argument list in the first message, never in the
  URL. Header auth only; the stream dial never falls back to ?token=.
- Exit codes: the remote exit code; 124 for the server time limit (4001);
  130 for Ctrl-C or SIGTERM (a second Ctrl-C exits at once); 255 for an
  error of stackctl or the server; 1 for invalid flags or arguments.
  ExitError in cmd/exitcode.go, main.go exits with its code.
- --insecure and an http:// URL to a non-loopback host are refused unless
  --allow-insecure-stream is set (shared with port-forward).
- Server text is sanitized; --debug shows the started frame, never output.
- A Ctrl-C during the WebSocket handshake now ends the dial at once
  (closeOnCancel), also for port-forward.
- cluster create/update --allow-exec and --allow-port-forward set the
  per-cluster switches; cluster get shows them.
- swagger.json refreshed with the exec route and the cluster switches.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…reate/update

The server needs api_server_url for a cluster with a kubeconfig, and
use_in_cluster for the cluster it runs in, but the flags of cluster create
could send neither: only --from-file could register a cluster.

- create and update: --api-server-url and --use-in-cluster.
- create refuses a kubeconfig flag without --api-server-url, and
  --use-in-cluster with a kubeconfig flag, before any request.
- update sends --use-in-cluster=false (the server removes the stored
  kubeconfig on true).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@omattsson
omattsson merged commit a588c68 into main Oct 30, 2026
8 checks passed
@omattsson
omattsson deleted the feat/179-stack-exec branch October 30, 2026 21:02
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: stackctl stack exec

1 participant