fix: use 2-space YAML indent in Save() to match kubectl's kubeconfig format

enc.SetIndent(0) in go.yaml.in/yaml/v3 does not produce zero indentation;
instead, it falls back to the library default of 4 spaces. This caused
kubectx/kubens to rewrite the entire kubeconfig with 4-space indentation on
every save, drastically changing the file format from the standard 2-space
format that kubectl produces.

The unexpected whole-file reformatting triggered kubeconfig management tools
(Docker Desktop, Rancher Desktop, corporate k8s auth helpers, etc.) that watch
~/.kube/config via FSEvents and regenerate it from their source-of-truth when
they detect format changes -- discarding the namespace or context change that
was just written.

Changing to enc.SetIndent(2) matches kubectl's standard output format and
minimises the diff on each save to only the field(s) actually changed.

Fixes #499.

https://claude.ai/code/session_01DfqecMkM2DGSMQU3ji1hJL
This commit is contained in:
Claude
2026-05-15 15:42:54 +00:00
parent c8393ea883
commit ff863f4f1c
3 changed files with 122 additions and 2 deletions

View File

@@ -206,7 +206,7 @@ func (k *Kubeconfig) Save() error {
return fmt.Errorf("failed to reset file %d: %w", i, err)
}
enc := yaml.NewEncoder(k.files[i].f)
enc.SetIndent(0)
enc.SetIndent(2)
if err := enc.Encode(k.files[i].config.YNode()); err != nil {
return fmt.Errorf("failed to encode file %d: %w", i, err)
}

View File

@@ -0,0 +1,120 @@
package kubeconfig
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestSetNamespace_MultiFileScenario tests the scenario where current-context is in
// file1 but the actual context definition is in file2.
func TestSetNamespace_MultiFileScenario(t *testing.T) {
dir := t.TempDir()
f1 := filepath.Join(dir, "config1")
f2 := filepath.Join(dir, "config2")
// File 1: has current-context but the context definition is in file 2
os.WriteFile(f1, []byte(`apiVersion: v1
kind: Config
current-context: ctx-in-file2
contexts: []
clusters: []
users: []
`), 0644)
// File 2: has the actual context definition
os.WriteFile(f2, []byte(`apiVersion: v1
kind: Config
contexts:
- context:
cluster: my-cluster
user: my-user
name: ctx-in-file2
clusters:
- cluster:
server: https://test
name: my-cluster
users:
- name: my-user
user: {}
`), 0644)
t.Setenv("KUBECONFIG", f1+":"+f2)
kc := new(Kubeconfig).WithLoader(DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
cur, err := kc.GetCurrentContext()
if err != nil {
t.Fatal(err)
}
t.Logf("Current context: %s", cur)
if err := kc.SetNamespace("ctx-in-file2", "my-namespace"); err != nil {
t.Fatalf("SetNamespace failed: %v", err)
}
if err := kc.Save(); err != nil {
t.Fatalf("Save failed: %v", err)
}
c1, _ := os.ReadFile(f1)
c2, _ := os.ReadFile(f2)
t.Logf("File1 after save:\n%s", c1)
t.Logf("File2 after save:\n%s", c2)
if !strings.Contains(string(c2), "namespace: my-namespace") {
t.Errorf("FAIL: namespace NOT saved to file2. File2:\n%s", c2)
}
}
// TestSetNamespace_CurrentContextInFile2 tests when the current-context is defined
// in file2 (where kubectl logs "Config loaded from file: /path/to/config" for multiple files,
// it shows the first file, potentially misleading).
func TestSetNamespace_AllInSameFile(t *testing.T) {
dir := t.TempDir()
f1 := filepath.Join(dir, "config1")
os.WriteFile(f1, []byte(`apiVersion: v1
kind: Config
current-context: my-ctx
contexts:
- context:
cluster: my-cluster
user: my-user
name: my-ctx
clusters:
- cluster:
server: https://test
name: my-cluster
users:
- name: my-user
user: {}
`), 0644)
t.Setenv("KUBECONFIG", f1)
kc := new(Kubeconfig).WithLoader(DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
t.Fatal(err)
}
if err := kc.SetNamespace("my-ctx", "my-namespace"); err != nil {
t.Fatalf("SetNamespace failed: %v", err)
}
if err := kc.Save(); err != nil {
t.Fatalf("Save failed: %v", err)
}
content, _ := os.ReadFile(f1)
t.Logf("File after save:\n%s", content)
if !strings.Contains(string(content), "namespace: my-namespace") {
t.Errorf("FAIL: namespace NOT in file. Content:\n%s", content)
}
}

View File

@@ -47,7 +47,7 @@ func (k *Kubeconfig) ToYAML(t *testing.T) string {
t.Helper()
var v strings.Builder
enc := yaml.NewEncoder(&v)
enc.SetIndent(0)
enc.SetIndent(2)
if err := enc.Encode(*k); err != nil {
t.Fatalf("failed to encode mock kubeconfig: %v", err)
}