Compare commits

..

3 Commits

Author SHA1 Message Date
Claude
2b0e4de615 kubens: use context to cancel slow-query warning timer after API returns
Replace done channel + defer with context.WithCancel so the warning
goroutine is cancelled immediately when queryNamespaces returns, not
when Run returns (which could be minutes later while user browses fzf).
Also thread the context into queryNamespaces so the k8s List call
respects it.

https://claude.ai/code/session_01XJXHq8WG22iqX8KaDb9RZz
2026-03-09 16:33:05 +00:00
Claude
f1283e7ebc 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
2026-03-09 16:27:40 +00:00
Claude
c57909e31a kubens: show slow-listing warning after 3s during namespace list
When `kubens` is run with no args in non-interactive mode (ListOp),
start a background goroutine that prints a warning to stderr after
3 seconds if the Kubernetes API call is still in progress. The warning
advises users to switch directly with `kubens -f <ns>` to avoid the
slow list call. The goroutine is cancelled immediately if listing
completes before the timeout.

https://claude.ai/code/session_01XJXHq8WG22iqX8KaDb9RZz
2026-03-09 08:15:23 +00:00
4 changed files with 53 additions and 20 deletions

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Comment on PR if author is not ahmetb
if: github.event.pull_request.user.login != 'ahmetb'
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
script: |
const body = [

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

@@ -16,26 +16,23 @@ package main
import (
"bytes"
"context"
"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 +43,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)
}
ctx, cancel := context.WithCancel(context.Background())
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 <-ctx.Done():
}
}()
ns, err := queryNamespaces(ctx, kc)
cancel()
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) {

View File

@@ -21,6 +21,7 @@ import (
"io"
"os"
"slices"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
@@ -52,7 +53,17 @@ func (op ListOp) Run(stdout, stderr io.Writer) error {
return fmt.Errorf("cannot read current namespace: %w", err)
}
ns, err := queryNamespaces(kc)
ctx, cancel := context.WithCancel(context.Background())
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 <-ctx.Done():
}
}()
ns, err := queryNamespaces(ctx, kc)
cancel()
if err != nil {
return fmt.Errorf("could not list namespaces (is the cluster accessible?): %w", err)
}
@@ -67,7 +78,7 @@ func (op ListOp) Run(stdout, stderr io.Writer) error {
return nil
}
func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
func queryNamespaces(ctx context.Context, kc *kubeconfig.Kubeconfig) ([]string, error) {
if os.Getenv("_MOCK_NAMESPACES") != "" {
return []string{"ns1", "ns2"}, nil
}
@@ -81,7 +92,7 @@ func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
var next string
for {
list, err := clientset.CoreV1().Namespaces().List(
context.Background(),
ctx,
metav1.ListOptions{
Limit: 500,
Continue: next,