mirror of
https://github.com/ahmetb/kubectx.git
synced 2026-07-17 01:52:32 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
120
internal/kubeconfig/multifile_scenario_test.go
Normal file
120
internal/kubeconfig/multifile_scenario_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user