Skip to content

fix(podshell): select a container so multi-container pods can open a shell - #581

Merged
blue4209211 merged 4 commits into
mainfrom
fix/pod-shell-container-selection
Aug 20, 2026
Merged

fix(podshell): select a container so multi-container pods can open a shell#581
blue4209211 merged 4 commits into
mainfrom
fix/pod-shell-container-selection

Conversation

@RamanKharchee

Copy link
Copy Markdown
Contributor

Summary

Opening the in-app shell on a pod with more than one container fails immediately:

[stream closed: unable to upgrade connection: a container name must be specified
 for pod app-dev-748d4bdf5b-ppszg, choose one of: [nextjs-static-init app nginx]]

openSession built PodExecOptions with no Container, so the API server had nothing to attach to and refused the SPDY upgrade. Single-container pods worked only because the API server can infer the target in that case — which is why this went unnoticed.

Worth noting the sibling exec call sites, pkg/podexec/executor.go:87 and pkg/podexec/profiler.go:606, both already pass Container. podshell was the one outlier, not a pattern the codebase gets wrong.

What changed

resolveContainer picks the target the way kubectl does:

  1. an explicitly requested container, validated against the pod first — an unknown name otherwise fails deep inside the stream with a message the user can't act on, so it is rejected up front with the available names;
  2. the kubectl.kubernetes.io/default-container annotation, so the in-app shell lands where kubectl exec on the same pod would;
  3. otherwise the first container in spec order.

A stale annotation naming a removed container falls back rather than breaking the shell.

Init containers are deliberately excluded. They have already terminated by the time a pod is Running, so offering them would only produce a confusing failure — relevant here, since the reported pod's nextjs-static-init would otherwise be a candidate.

waitRunning now returns the pod it already fetched, so resolution costs no extra API round trip.

Wire changes (both backward compatible)

  • Request gains optional container. Omitting it keeps today's behaviour, so existing clients are unaffected — this is also the compatibility floor: the deployed UI sends no container, so a fix that required one would break every current client.
  • start responses now carry container (what it attached to) and containers (what else it could have). This is deliberately slightly ahead of the UI: it lets a container switcher ship later with no second agent rollout, which is the slow part for on-prem installs. Both fields are omitempty and ignored by current clients. Happy to drop them if you'd rather keep the surface minimal.

Type of change

  • Bug fix (non-breaking)

Chart version

Test plan

  • go test ./pkg/podshell/... — passes, including 7 new table cases covering single-container, multi-container spec order, annotation precedence, stale annotation fallback, explicit request, unknown request (asserts the error lists the valid names), and the empty-container guard.
  • make fmt clean, go vet ./pkg/podshell/... clean, full make test green across all packages.
  • make lint could not be run locally — the installed golangci-lint 2.8.0 is built with go1.25.5 while the module targets go1.26.3, so it refuses with can't load config. This is a stale local toolchain, not a finding; CI runs the pinned linter.
  • Not exercised against a live multi-container pod — no cluster access from where this was written. The failure is entirely in the exec request construction and the resolution logic is unit-tested, but the end-to-end path deserves one manual check: open a shell on a multi-container pod and confirm it attaches, then confirm a pod carrying kubectl.kubernetes.io/default-container lands in the annotated container.

Related issues

Reported against the in-app pod terminal, alongside nudgebee-enterprise#36589 (terminal failing to open) and nudgebee-enterprise#36642 (typing latency).

🤖 Generated with Claude Code

@RamanKharchee
RamanKharchee requested a review from a team as a code owner August 20, 2026 09:39

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the pod shell manager to support selecting a specific container to exec into, mirroring kubectl's container resolution behavior (explicit choice, default-container annotation, or first container in spec order). It also returns the list of available containers in the response to enable container switching without additional API calls. The review feedback suggests optimizing the waitRunning function by performing an initial check before allocating a ticker, and adding a test case to explicitly verify that init containers are ignored during container resolution.

Comment thread runner/pkg/podshell/session.go
Comment thread runner/pkg/podshell/session_test.go
…shell

Opening a shell on a pod with more than one container failed immediately
with:

    [stream closed: unable to upgrade connection: a container name must be
    specified for pod app-dev-..., choose one of: [nextjs-static-init app nginx]]

`openSession` built PodExecOptions without a Container, so the API server
had nothing to attach to. Single-container pods worked only because the
API server can infer the target there. The sibling exec call sites in
pkg/podexec (executor.go, profiler.go) both already pass Container —
podshell was the outlier.

Resolve one the way kubectl does: an explicitly requested container wins,
then the kubectl.kubernetes.io/default-container annotation, then the first
container in spec order. A requested name is validated against the pod
first, since an unknown container otherwise fails deep inside the stream
with a message the user cannot act on. A stale annotation pointing at a
removed container falls back rather than breaking the shell.

Init containers are excluded — they have already terminated by the time the
pod is Running, so offering them only produces a confusing failure.

Request gains an optional `container` field, and start now reports both the
container it attached to and the full list. That lets the UI offer a
container switcher without another API call, and without a second agent
rollout — which is the slow part for on-prem installs. Omitting the field
keeps the existing behaviour, so older clients are unaffected.

waitRunning now returns the pod it already fetched, so resolution costs no
extra API round trip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@RamanKharchee
RamanKharchee force-pushed the fix/pod-shell-container-selection branch from 749680e to 4750c7e Compare August 20, 2026 09:43
@RamanKharchee

Copy link
Copy Markdown
Contributor Author

Thanks — one taken, one declined.

Init-container test — added. Fair point: I asserted the exclusion in a comment and in the PR description but never locked it down. TestResolveContainerIgnoresInitContainers now covers all three angles, using the reporter's actual pod shape (nextjs-static-init as the init container, app + nginx as the real ones): the default resolves to app rather than the init container, naming the init container explicitly is rejected rather than silently accepted, and containerNames advertises only the two exec-able containers to the client. That last one matters — if init containers leaked into the containers list, a future picker would offer choices that cannot work.

waitRunning early Get — declining. The loop already issues the Get at the top of each iteration, before it ever touches the ticker:

for {
    pod, err := m.cs.CoreV1().Pods(ns).Get(cctx, name, metav1.GetOptions{})
    if err == nil && pod.Status.Phase == corev1.PodRunning {
        return pod, nil
    }
    select { case <-cctx.Done(): ...; case <-t.C: }
}

So an already-Running pod returns after exactly one API call with no scheduling delay — the happy path is not paying a tick. The only saving would be one time.NewTicker allocation, against a call that has just made a network round trip to the API server, and it would mean restructuring pre-existing code this PR only touched to change a return type. Not worth the churn for an unmeasurable gain.

go test ./runner/... and go vet ./... clean. Amended into the single commit.

mayankpande88
mayankpande88 previously approved these changes Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 Image Tags Updated

I've automatically updated the image tags in `charts/nudgebee-agent/values.yaml` to the latest versions from GHCR for the `main` branch.

The image tags are now synchronized with the latest builds and ready for release.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Image Tags Updated

I've automatically updated the image tags in `charts/nudgebee-agent/values.yaml` to the latest versions from GHCR for the `main` branch.

The image tags are now synchronized with the latest builds and ready for release.

@blue4209211
blue4209211 merged commit 7a2f3c9 into main Aug 20, 2026
7 checks passed
@blue4209211
blue4209211 deleted the fix/pod-shell-container-selection branch August 20, 2026 12:42
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.

3 participants