Skip to content

Fix zsh shell function & cluster-name collision across different stores - #236

Open
ZeikoFr wants to merge 4 commits into
MichaelSp:mainfrom
ZeikoFr:main
Open

Fix zsh shell function & cluster-name collision across different stores#236
ZeikoFr wants to merge 4 commits into
MichaelSp:mainfrom
ZeikoFr:main

Conversation

@ZeikoFr

@ZeikoFr ZeikoFr commented Aug 6, 2026

Copy link
Copy Markdown

Two bugs in kswitch's interactive switcher, found while running it day-to-day against a mix of OVH, Scaleway, GKE, EKS, and Gardener stores:

  1. zsh incompatibility in the generated shell function — the KUBECONFIG-merging code in kswitch init zsh/kswitch init bash (and the Homebrew companion hack/switch/switch.sh) used read -ra ... <<<, which is bash-only. Under zsh this prints bad option: -a on every switch and silently drops any other paths already present in $KUBECONFIG during the merge.
  2. Cross-store cluster-name collision when resolving the selected item pkg.Switcher() resolved the interactively-selected item's backing store and tags via global maps keyed only by the bare cluster path (e.g. "prod"). Path is only unique within a single store. When two different stores each have a cluster with the same path (e.g. an OVH store and a Scaleway store both have a cluster named prod), whichever store was indexed last silently won the map entry so selecting one store's context could fetch and write out a different store's kubeconfig, then fail with context was not found for specified context because the two providers' context-naming conventions differ.

Changes

  • cmd/kswitch/init.go, hack/switch/switch.sh: replace the bash-only read -ra ... <<< loop with a portable while/case/parameter-expansion loop that behaves identically under bash and zsh.
  • pkg/tui/run.go: Run() now also returns the selected item's own storeID and tags (already carried on the TUI's internal item struct), instead of only the path and context name.
  • pkg/main.go: Switcher() uses the storeID/tags returned by the TUI directly to resolve the backing store and fetch the kubeconfig, removing the path-keyed pathToStoreID/pathToTagsMapping global maps and their helpers entirely. pkg/subcommands/set-context/set_context.go (the non-interactive set-context command) already resolved the store per-item correctly and needed no change.

Testing

  • go test ./... passes.
  • Reproduced bug rename: replace all kubeswitch references with kswitch #2 against real infrastructure: two stores (OVH kwarto, Scaleway scw-loginline) each with a cluster named prod; before the fix, selecting the OVH entry fetched the Scaleway cluster's kubeconfig (confirmed via the fetched cluster's ID/server not matching the selected store), and current-context was left un-stripped with no matching context entry.
  • Rebuilt and re-tested against the same infrastructure after the fix: selecting kwarto/kubernetes-admin@prod now correctly fetches the OVH cluster's kubeconfig, with current-context matching its own context entry, and kubectl resolves cluster/context/server correctly against the result.
  • Reproduced and re-verified fix feat: maintain gardenctl-v2 config alongside gardenlogin config #1 under both bash and zsh: no more bad option: -a, and extra $KUBECONFIG entries are preserved through the merge.

@MichaelSp

Copy link
Copy Markdown
Owner

hey @ZeikoFr thanks for catching these glitches.
Would you please add tests so future me is not breaking that again 😻

@ZeikoFr

ZeikoFr commented Aug 24, 2026

Copy link
Copy Markdown
Author

Hi @MichaelSp It should be good now, I've generated the test using cc-golang-skill, I've also add an handling of timeout for GKE, I had an organisation that have hundreds of projects and only 1 or 2 GKE cluster in one of them and the timeout was global, causing some issues

Comment thread pkg/store/kubeconfig_store_gke.go Outdated
channel <- storetypes.SearchResult{
Error: fmt.Errorf("failed to list GKE clusters for project with ID %q: %w", projectId, err),
wg.Add(1)
go func(projectName, projectId string) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think this is a non-trivial decision. What you do here is an unbound number of requests with following side-effects

  • Google API 429 / quota failures
    • valid clusters disappearing from results and thus non-deterministic behavior based on account size
      • that would be a deal-breaker because I can already see issues coming my way 🤣
  • connection and goroutine spikes - I could live with just that ✅

What do you think?

@ZeikoFr ZeikoFr Sep 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've removed this from the PR, it should not have landed here my bad for opening a PR on my main branch, this change is superseeded in #296 with defaultMaxConcurrentProjectRequests

Sources the wrapper function in a real shell and asserts the resulting
KUBECONFIG, for both copies of the script (the `kswitch init bash|zsh`
template and the homebrew `hack/switch/switch.sh`) in both bash and zsh:
entries are kept in order, stale switch temp kubeconfigs and empty
segments are dropped and entries containing spaces survive.

Reverting the portable merge loop fails the zsh half of the matrix,
because zsh has no `read -a` and silently dropped every pre-existing
KUBECONFIG entry. Two static checks come along: neither copy may
reintroduce `read -a`, and their merge blocks have to stay in sync.
Extracts the selection plumbing into helpers that can be tested without a
TTY: tui.itemFor builds a list item from a discovered context, and
tui.selectionFor returns what Run returns for a finished model. The store
lookup in Switcher moves into pkg.resolveStore, which now reports an
unknown storeID instead of dereferencing a nil store.

The tests pin down that the storeID and tags travel with the selected
item: two stores exposing a cluster under the same path ("prod") each
resolve to their own store and tags, whichever one the cursor sits on.
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.

2 participants