fix(runner): support chaining kubectl commands with && || ; in K8s CLI action - #506
Conversation
…I action The K8s CLI action (kubectl_command_executor) tokenized the command with shlex.Split and exec'd a single kubectl with no shell, so `kubectl get ... && kubectl get ...` collapsed into one invocation — the second command's tokens became positional NAME args of the first, producing `error: name cannot be provided when a selector is specified` (issue #33447). Split the command into segments on the sequential operators && || ; and run each as its own kubectl process with shell short-circuit semantics (&& on success, || on failure, ; always), aggregating stdout/stderr and returning the last-executed exit code. Every segment's verb is validated against the read-only allowlist BEFORE any segment runs, so a chain like `get && delete` is rejected atomically rather than executing the read half first — the per-command allowlist guarantee is preserved for each command, not just the first. Pipes and redirects are rejected with a clear error (no shell; piping to a non-kubectl binary would escape the verb allowlist). Tests cover the exact reported command, per-segment allowlist enforcement, all five short-circuit cases, AllowWrite chained mutations, pipe/redirect rejection, and empty-segment rejection. Fixes #33447 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for sequentially chaining multiple kubectl commands using &&, ||, and ; operators, while explicitly rejecting unsupported shell operators like pipes and redirects. It also ensures that all command segments are validated against the read-only allowlist before any execution begins. The review feedback suggests improving context cancellation handling by checking ctx.Err() at the start of the execution loop and immediately after running a command segment. Additionally, it is recommended to gracefully handle trailing semicolons, which are currently incorrectly rejected as empty segments.
… ';'
- Check ctx.Err() at the top of the segment loop (don't start another kubectl
after cancel/deadline) and again right after cmd.Run() (a context-killed
process returns an ExitError that must not be read as a normal non-zero exit,
which would otherwise look like success on the last segment). Propagate the
context error and stop the chain.
- Accept a trailing ';' ("kubectl get pods ;") as the valid shell no-op it is,
and restore the clearer "empty command after stripping prefix" message for a
bare/empty command. Leading/trailing/doubled &&/|| stay rejected.
- Extract execResult() to avoid repeating the result map literal.
Adds tests for a cancelled context (no segment runs, error propagates) and a
trailing semicolon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
📦 Image Tags Updated |
|
Addressed all three @gemini-code-assist comments in
Added tests:
(Note: the branch also picked up an automated |
|
Thanks for the detailed update, @RamanKharchee. The implementation of segment-based execution, context-aware cancellation, and the robust test suite for short-circuiting and validation logic looks excellent. The approach to handle |
Description
The K8s CLI action (
kubectl_command_executor) fails when two valid kubectl commands are joined with&&. Reported in nudgebee/nudgebee-enterprise#33447 (runbook template "Clean Up Evicted and Failed Pods"):Root cause:
KubectlExecutor.Runtokenized the command withshlex.Splitand exec'd a single kubectl with no shell (exec.CommandContext(ctx, "kubectl", args...)).shlextreats&&as an ordinary word, so every token from&&onward (&&,kubectl,get,pods, …) became positional NAME arguments of the firstkubectl get pods. With--field-selectoralso set, kubectl emits exactlyname cannot be provided when a selector is specified. Only the first verb was ever allowlist-checked.What changed
runner/pkg/kube/exec.go:&&,||,;and run each as its own kubectl process, with shell short-circuit semantics (&&runs on success,||on failure,;always).&&/||are equal-precedence, left-associative, so evaluating left-to-right against the carried exit code reproduces bash for mixed chains. stdout/stderr are aggregated;exit_codeis the last executed command's status.get pods && delete pod foois rejected atomically, not after executing the read half. The per-command allowlist guarantee is preserved for each command, not just the first.grep,awk) would escape the verb allowlist — out of scope by design.runner/pkg/kube/exec_test.go: the exact reported command, per-segment allowlist enforcement (0 executions on rejection), all five short-circuit cases,AllowWritechained mutations, pipe/redirect rejection, empty-segment rejection.Type of change
How Has This Been Tested?
go test ./pkg/kube/— all pass (existing + new)go vet ./pkg/kube/andgofmt— cleanReview Notes → Risks & Counterarguments
validateSegmentbeing called for all segments beforerunSegments, andTestKubectl_Chained_ValidatesEverySegment.| grep, that's a separate, larger decision (would need a shell + full-string verb parsing).a&&bwith no surrounding spaces) are not split —shlexyields them as one token. Standard usage has spaces around operators (as in the report). Noted as a known minor limitation rather than adding a raw-string pre-parser that would fight shlex quoting.k8s-agent) but the issue is onnudgebee-enterprise, so GitHub won't auto-close it — please close nudgebee/nudgebee-enterprise#33447 manually on merge.🤖 Generated with Claude Code