Repository navigation
k8s: keep credential plugins off the terminal - #41
Merged
Merged
Conversation
A kubeconfig user that is an exec plugin (tsh, aws, kubelogin) could prompt for MFA or a relogin while the TUI owned stdin: keystrokes went to the plugin, its stderr was painted over the frame, and the session looked hung. That is the k9s failure in derailed/k9s#2048. - Mark stdin unavailable on the ExecProvider, as kubectl does when stdin is spoken for. The plugin runs without a terminal and fails fast; one that declares interactiveMode: Always is refused before it runs. - Capture os.Stderr through a pipe before the first client is built, so the plugin's last line is the No cluster reason ("credential plugin tsh: Having problems with relogin, please use 'tsh login…'") instead of "exit code 1". The editor and foreground plugins still get the terminal's stderr. - Retry the version probe once when it timed out with a plugin in play: the plugin's run time is spent inside the request's timeout, which client-go cannot interrupt, so a slow plugin turned into "No cluster: context deadline exceeded" even though it succeeded. - Name the plugin in the connecting toast, wrap the No cluster reason instead of truncating it, and document the behaviour in /setup and docs/cluster-setup.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pierreneter
force-pushed
the
exec-plugin-noninteractive
branch
from
November 12, 2026 00:45
3d9783b to
23c378a
Compare
junior
approved these changes
Nov 12, 2026
Collaborator
|
Looks good, and the model is right: the TUI owns the terminal, Three things, none blocking:
|
junior
added a commit
that referenced
this pull request
Nov 14, 2026
Since #41, os.Stderr is a pipe while the TUI runs, so that a credential plugin cannot write over the screen. When the TUI panics, Bubble Tea prints the panic's value to stdout and then its stack trace with debug.PrintStack, which writes to os.Stderr: the trace went into the pipe, which kept only its last line, so a crash report said what happened but not where. The pipe's reader now also keeps the last 64 KiB it read, as written. When Run returns tea.ErrProgramPanic, main closes the pipe, gives the reader up to a second to finish (a credential plugin that is still running holds the pipe open too), and writes those bytes to the terminal ahead of its own error line. This covers both panics Bubble Tea recovers: one on its event loop, and one in a command's goroutine. Signed-off-by: Adao Oliveira Jr <junior@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A kubeconfig user is often an exec credential plugin —
tsh kube credentials(Teleport),
aws eks get-token,kubelogin,gke-gcloud-auth-plugin. client-goruns it before the first request, and with
interactiveMode: IfAvailable(thedefault, and what
tshwrites) it hands the pluginos.Stdinandos.Stderrwhenever stdin is a TTY.
Under a TUI that is always true, and always wrong:
The session looks hung. This is k9s appears to hang when credentials timeout and need to enter another MFA token derailed/k9s#2048, closed "not planned";
the workaround there is
kill -9.%vand drops its stderr, so theuser sees
exec: executable tsh failed with exit code 1instead ofHaving problems with relogin, please use 'tsh login/tsh kube login' manually.rest.Config.Timeout,which client-go cannot interrupt. A slow plugin (minting a cert, calling STS)
turned into No cluster: context deadline exceeded even though it
succeeded;
rthen worked because the credentials were cached.Connection issues when using k9s with
tsh kube logincompared totsh proxy kubegravitational/teleport#49622 is k9s showing exactly this.Change
One rule: the TUI owns the terminal, the plugin never gets it.
StdinUnavailable = trueonExecProvider— whatkubectl exec -idoeswhen stdin is spoken for.
IfAvailableplugins run without stdin and failfast;
Alwaysplugins are refused before they run, with a k10s reason.k8s.CaptureStderrswapsos.Stderrfor a pipe before the first clientis built (client-go copies
os.Stderrat that point and never looks again).The last line a plugin wrote becomes
k8s.CredentialPluginError, theNo cluster reason, and the chain still unwraps to client-go's error.
$EDITORand foreground plugins get the terminal's real stderr viaStartup.TerminalStderr;k8s.ReleaseStderrrestores it afterRun.are cached by then, so the retry is the network alone. No change without a
plugin (test asserts a single hit).
… connecting · credentials from tsh),the No cluster reason wraps instead of truncating,
/setupanddocs/cluster-setup.mdget a "Credential plugins" section with the Teleportcaveats (
tsh proxy kubefor per-session MFA / L7 LB).Not in this PR
laction that runs the context's exec command interactively throughtea.ExecProcess, then retries.Tests
internal/k8s/credplugin_test.go: fake plugin as a shell script against theexisting TLS test API server — runs without the terminal and its token is
used;
Alwaysis refused and the plugin does not run; the plugin's laststderr line is the error verbatim; a 1s plugin under a 300ms probe succeeds
via the retry (fails with the retry removed); a slow server without a plugin
is still hit once;
CredentialPlugin()naming.internal/ui/credplugin_test.go: toast names the plugin;toTerminalkeeps a caller-set stderr and does nothing without a terminal.
main_test.go:noClusterReasonkeeps the plugin's words intact.just checkandgo test -race ./internal/k8s/green.Unverified
Whether
tsh's WebAuthn tap works with stdin unavailable (touch needs nostdin, but tsh may bail early). Needs a real per-session-MFA cluster. If it
does not, the documented path is
tsh proxy kube <cluster> --exec.