diff --git a/cmd/kubectx/flags_test.go b/cmd/kubectx/flags_test.go index 071e3bf..d7eebdb 100644 --- a/cmd/kubectx/flags_test.go +++ b/cmd/kubectx/flags_test.go @@ -62,10 +62,16 @@ func Test_parseArgs_new(t *testing.T) { want: UnsupportedOp{fmt.Errorf("'-d' needs arguments")}}, {name: "delete - current context", args: []string{"-d", "."}, - want: DeleteOp{[]string{"."}}}, + want: DeleteOp{[]string{"."}, false}}, {name: "delete - multiple contexts", args: []string{"-d", ".", "a", "b"}, - want: DeleteOp{[]string{".", "a", "b"}}}, + want: DeleteOp{[]string{".", "a", "b"}, false}}, + {name: "delete cascading- current context", + args: []string{"-D", "."}, + want: DeleteOp{[]string{"."}, true}}, + {name: "delete cascading - multiple contexts", + args: []string{"-D", ".", "a", "b"}, + want: DeleteOp{[]string{".", "a", "b"}, true}}, {name: "rename context", args: []string{"a=b"}, want: RenameOp{"a", "b"}}, diff --git a/internal/kubeconfig/clusters_test.go b/internal/kubeconfig/clusters_test.go new file mode 100644 index 0000000..4de5281 --- /dev/null +++ b/internal/kubeconfig/clusters_test.go @@ -0,0 +1,124 @@ +package kubeconfig + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ahmetb/kubectx/internal/testutil" +) + +func TestKubeconfig_ClusterOfContext_ctxNotFound(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs(testutil.Ctx("c1")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + _, err := kc.ClusterOfContext("c2") + if err == nil { + t.Fatal("expected err") + } +} + +func TestKubeconfig_ClusterOfContext(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs( + testutil.Ctx("c1").Cluster("c1c1"), + testutil.Ctx("c2").Cluster("c2c2")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + v1, err := kc.ClusterOfContext("c1") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := `c1c1`; v1 != expected { + t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1) + } + + v2, err := kc.ClusterOfContext("c2") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := `c2c2`; v2 != expected { + t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2) + } +} + +func TestKubeconfig_DeleteClusterEntry_errors(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)) + _ = kc.Parse() + err := kc.DeleteClusterEntry("foo") + if err == nil { + t.Fatal("supposed to fail on non-mapping nodes") + } + + kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`)) + _ = kc.Parse() + err = kc.DeleteClusterEntry("foo") + if err == nil { + t.Fatal("supposed to fail if clusters key does not exist") + } + + kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`clusters: "some string"`)) + _ = kc.Parse() + err = kc.DeleteClusterEntry("foo") + if err == nil { + t.Fatal("supposed to fail if clusters key is not an array") + } +} + +func TestKubeconfig_DeleteClusterEntry(t *testing.T) { + test := WithMockKubeconfigLoader( + testutil.KC().WithClusters( + testutil.Cluster("c1"), + testutil.Cluster("c2"), + testutil.Cluster("c3")).ToYAML(t)) + kc := new(Kubeconfig).WithLoader(test) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + if err := kc.DeleteClusterEntry("c1"); err != nil { + t.Fatal(err) + } + if err := kc.Save(); err != nil { + t.Fatal(err) + } + + expected := testutil.KC().WithClusters( + testutil.Cluster("c2"), + testutil.Cluster("c3")).ToYAML(t) + out := test.Output() + if diff := cmp.Diff(expected, out); diff != "" { + t.Fatalf("diff: %s", diff) + } +} + +func TestKubeconfig_CountClusterReferences_errors(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs( + testutil.Ctx("c1").Cluster("c1c1"), + testutil.Ctx("c2").Cluster("c2c2"), + testutil.Ctx("c3").Cluster("c1c1")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + count1, err := kc.CountClusterReferences("c1c1") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := 2; count1 != expected { + t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count1) + } + + count2, err := kc.CountClusterReferences("c2c2") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := 1; count2 != expected { + t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count2) + } +} diff --git a/internal/kubeconfig/users_test.go b/internal/kubeconfig/users_test.go new file mode 100644 index 0000000..f0c9f32 --- /dev/null +++ b/internal/kubeconfig/users_test.go @@ -0,0 +1,124 @@ +package kubeconfig + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/ahmetb/kubectx/internal/testutil" +) + +func TestKubeconfig_UserOfContext_ctxNotFound(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs(testutil.Ctx("c1")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + _, err := kc.UserOfContext("c2") + if err == nil { + t.Fatal("expected err") + } +} + +func TestKubeconfig_UserOfContext(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs( + testutil.Ctx("c1").User("c1u1"), + testutil.Ctx("c2").User("c2u2")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + v1, err := kc.UserOfContext("c1") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := `c1u1`; v1 != expected { + t.Fatalf("c1: expected=\"%s\" got=\"%s\"", expected, v1) + } + + v2, err := kc.UserOfContext("c2") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := `c2u2`; v2 != expected { + t.Fatalf("c2: expected=\"%s\" got=\"%s\"", expected, v2) + } +} + +func TestKubeconfig_DeleteUserEntry_errors(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`[1, 2, 3]`)) + _ = kc.Parse() + err := kc.DeleteUserEntry("foo") + if err == nil { + t.Fatal("supposed to fail on non-mapping nodes") + } + + kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`a: b`)) + _ = kc.Parse() + err = kc.DeleteUserEntry("foo") + if err == nil { + t.Fatal("supposed to fail if users key does not exist") + } + + kc = new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(`users: "some string"`)) + _ = kc.Parse() + err = kc.DeleteUserEntry("foo") + if err == nil { + t.Fatal("supposed to fail if users key is not an array") + } +} + +func TestKubeconfig_DeleteUserEntry(t *testing.T) { + test := WithMockKubeconfigLoader( + testutil.KC().WithUsers( + testutil.User("u1"), + testutil.User("u2"), + testutil.User("u3")).ToYAML(t)) + kc := new(Kubeconfig).WithLoader(test) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + if err := kc.DeleteUserEntry("u1"); err != nil { + t.Fatal(err) + } + if err := kc.Save(); err != nil { + t.Fatal(err) + } + + expected := testutil.KC().WithUsers( + testutil.User("u2"), + testutil.User("u3")).ToYAML(t) + out := test.Output() + if diff := cmp.Diff(expected, out); diff != "" { + t.Fatalf("diff: %s", diff) + } +} + +func TestKubeconfig_CountUserReferences_errors(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs( + testutil.Ctx("c1").User("c1u1"), + testutil.Ctx("c2").User("c2u2"), + testutil.Ctx("c3").User("c1u1")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + count1, err := kc.CountUserReferences("c1u1") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := 2; count1 != expected { + t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count1) + } + + count2, err := kc.CountUserReferences("c2u2") + if err != nil { + t.Fatal("unexpected err", err) + } + if expected := 1; count2 != expected { + t.Fatalf("c1: expected=\"%d\" got=\"%d\"", expected, count2) + } +} diff --git a/internal/testutil/kubeconfigbuilder.go b/internal/testutil/kubeconfigbuilder.go index e7dc674..7fb0e61 100644 --- a/internal/testutil/kubeconfigbuilder.go +++ b/internal/testutil/kubeconfigbuilder.go @@ -21,15 +21,31 @@ import ( "gopkg.in/yaml.v3" ) -type Context struct { +type ContextObj struct { Name string `yaml:"name,omitempty"` Context struct { Namespace string `yaml:"namespace,omitempty"` + User string `yaml:"user,omitempty"` + Cluster string `yaml:"cluster,omitempty"` } `yaml:"context,omitempty"` } -func Ctx(name string) *Context { return &Context{Name: name} } -func (c *Context) Ns(ns string) *Context { c.Context.Namespace = ns; return c } +func Ctx(name string) *ContextObj { return &ContextObj{Name: name} } +func (c *ContextObj) Ns(ns string) *ContextObj { c.Context.Namespace = ns; return c } +func (c *ContextObj) User(user string) *ContextObj { c.Context.User = user; return c } +func (c *ContextObj) Cluster(cluster string) *ContextObj { c.Context.Cluster = cluster; return c } + +type UserObj struct { + Name string `yaml:"name,omitempty"` +} + +func User(name string) *UserObj { return &UserObj{Name: name} } + +type ClusterObj struct { + Name string `yaml:"name,omitempty"` +} + +func Cluster(name string) *ClusterObj { return &ClusterObj{Name: name} } type Kubeconfig map[string]interface{} @@ -41,7 +57,9 @@ func KC() *Kubeconfig { func (k *Kubeconfig) Set(key string, v interface{}) *Kubeconfig { (*k)[key] = v; return k } func (k *Kubeconfig) WithCurrentCtx(s string) *Kubeconfig { (*k)["current-context"] = s; return k } -func (k *Kubeconfig) WithCtxs(c ...*Context) *Kubeconfig { (*k)["contexts"] = c; return k } +func (k *Kubeconfig) WithCtxs(c ...*ContextObj) *Kubeconfig { (*k)["contexts"] = c; return k } +func (k *Kubeconfig) WithUsers(u ...*UserObj) *Kubeconfig { (*k)["users"] = u; return k } +func (k *Kubeconfig) WithClusters(c ...*ClusterObj) *Kubeconfig { (*k)["clusters"] = c; return k } func (k *Kubeconfig) ToYAML(t *testing.T) string { t.Helper()