kubens: pre-fetch namespaces before launching fzf, pipe via stdin

Instead of using FZF_DEFAULT_COMMAND to have fzf spawn a kubens
subprocess, InteractiveSwitchOp now queries namespaces directly and
pipes the result to fzf via an in-memory buffer. This makes the 3-second
slow-cluster warning visible to the user (printed to stderr before fzf's
TUI launches), since fzf swallows stderr from FZF_DEFAULT_COMMAND
subprocesses.

https://claude.ai/code/session_01XJXHq8WG22iqX8KaDb9RZz
This commit is contained in:
Claude
2026-03-09 16:27:40 +00:00
parent c57909e31a
commit f1283e7ebc
2 changed files with 37 additions and 16 deletions

View File

@@ -38,7 +38,7 @@ func parseArgs(argv []string) Op {
if n == 0 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
return InteractiveSwitchOp{}
}
return ListOp{}
}

View File

@@ -19,23 +19,19 @@ import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer"
)
type InteractiveSwitchOp struct {
SelfCmd string
}
type InteractiveSwitchOp struct{}
// TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go.
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
@@ -46,23 +42,48 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
return fmt.Errorf("kubeconfig error: %w", err)
}
ctxNames, err := kc.ContextNames()
ctx, err := kc.GetCurrentContext()
if err != nil {
return fmt.Errorf("failed to get context names: %w", err)
return fmt.Errorf("failed to get current context: %w", err)
}
if len(ctxNames) == 0 {
return errors.New("no contexts found in the kubeconfig file")
if ctx == "" {
return errors.New("current-context is not set")
}
curNs, err := kc.NamespaceOfContext(ctx)
if err != nil {
return fmt.Errorf("cannot read current namespace: %w", err)
}
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-time.After(3 * time.Second):
printer.Warning(stderr, `listing namespaces is taking long, switch to a namespace directly with "kubens -f <ns>"`)
case <-done:
}
}()
ns, err := queryNamespaces(kc)
if err != nil {
return fmt.Errorf("could not list namespaces (is the cluster accessible?): %w", err)
}
var input bytes.Buffer
for _, c := range ns {
s := c
if c == curNs {
s = printer.ActiveItemColor.Sprint(c)
}
fmt.Fprintf(&input, "%s\n", s)
}
cmd := exec.Command("fzf", "--ansi", "--no-preview")
var out bytes.Buffer
cmd.Stdin = os.Stdin
cmd.Stdin = &input
cmd.Stderr = stderr
var out bytes.Buffer
cmd.Stdout = &out
cmd.Env = append(os.Environ(),
fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd),
fmt.Sprintf("%s=1", env.EnvForceColor))
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {