// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package config_test import ( "errors" "fmt" "log/slog" "os" "path/filepath" "testing" "github.com/adrg/xdg" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/mock" m "github.com/petergtz/pegomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/cli-runtime/pkg/genericclioptions" ) func init() { slog.SetDefault(slog.New(slog.DiscardHandler)) } func TestConfigSave(t *testing.T) { config.AppConfigFile = "/tmp/k9s-test/k9s.yaml" sd := "/tmp/k9s-test/screen-dumps" cl, ct := "cl-1", "ct-1-1" _ = os.RemoveAll(("/tmp/k9s-test")) uu := map[string]struct { ct string flags *genericclioptions.ConfigFlags k9sFlags *config.Flags }{ "happy": { ct: "ct-1-1", flags: &genericclioptions.ConfigFlags{ ClusterName: &cl, Context: &ct, }, k9sFlags: &config.Flags{ ScreenDumpDir: &sd, }, }, } for k, u := range uu { xdg.Reload() t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, err := c.K9s.ActivateContext(u.ct) require.NoError(t, err) if u.flags != nil { c.K9s.Override(u.k9sFlags) require.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))) } require.NoError(t, c.Save(true)) bb, err := os.ReadFile(config.AppConfigFile) require.NoError(t, err) ee, err := os.ReadFile("testdata/configs/default.yaml") require.NoError(t, err) assert.Equal(t, string(ee), string(bb)) }) } } func TestSetActiveView(t *testing.T) { var ( cfgFile = "testdata/kubes/test.yaml" view = "dp" ) uu := map[string]struct { ct string flags *genericclioptions.ConfigFlags k9sFlags *config.Flags view string e string }{ "empty": { view: data.DefaultView, e: data.DefaultView, }, "not-exists": { ct: "fred", view: data.DefaultView, e: data.DefaultView, }, "happy": { ct: "ct-1-1", view: "xray", e: "xray", }, "cli-override": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, }, k9sFlags: &config.Flags{ Command: &view, }, ct: "ct-1-1", view: "xray", e: "dp", }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, _ = c.K9s.ActivateContext(u.ct) if u.flags != nil { require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) c.K9s.Override(u.k9sFlags) } c.SetActiveView(u.view) assert.Equal(t, u.e, c.ActiveView()) }) } } func TestActiveContextName(t *testing.T) { var ( cfgFile = "testdata/kubes/test.yaml" ct2 = "ct-1-2" ) uu := map[string]struct { flags *genericclioptions.ConfigFlags k9sFlags *config.Flags ct string e string }{ "empty": {}, "happy": { ct: "ct-1-1", e: "ct-1-1", }, "cli-override": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Context: &ct2, }, k9sFlags: &config.Flags{}, ct: "ct-1-1", e: "ct-1-2", }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, _ = c.K9s.ActivateContext(u.ct) if u.flags != nil { require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) c.K9s.Override(u.k9sFlags) } assert.Equal(t, u.e, c.ActiveContextName()) }) } } func TestActiveView(t *testing.T) { var ( cfgFile = "testdata/kubes/test.yaml" view = "dp" ) uu := map[string]struct { ct string flags *genericclioptions.ConfigFlags k9sFlags *config.Flags e string }{ "empty": { e: data.DefaultView, }, "not-exists": { ct: "fred", e: data.DefaultView, }, "happy": { ct: "ct-1-1", e: data.DefaultView, }, "happy1": { ct: "ct-1-2", e: data.DefaultView, }, "cli-override": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, }, k9sFlags: &config.Flags{ Command: &view, }, e: "dp", }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, _ = c.K9s.ActivateContext(u.ct) if u.flags != nil { require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) c.K9s.Override(u.k9sFlags) } assert.Equal(t, u.e, c.ActiveView()) }) } } func TestFavNamespaces(t *testing.T) { uu := map[string]struct { ct string e []string }{ "empty": {}, "not-exists": { ct: "fred", }, "happy": { ct: "ct-1-1", e: []string{client.DefaultNamespace}, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, _ = c.K9s.ActivateContext(u.ct) assert.Equal(t, u.e, c.FavNamespaces()) }) } } func TestContextAliasesPath(t *testing.T) { uu := map[string]struct { ct string e string }{ "empty": {}, "not-exists": { ct: "fred", }, "happy": { ct: "ct-1-1", e: "/tmp/test/cl-1/ct-1-1/aliases.yaml", }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, _ = c.K9s.ActivateContext(u.ct) assert.Equal(t, u.e, c.ContextAliasesPath()) }) } } func TestContextPluginsPath(t *testing.T) { uu := map[string]struct { ct, e string err error }{ "empty": { err: errors.New(`no context found for: ""`), }, "happy": { ct: "ct-1-1", e: "/tmp/test/cl-1/ct-1-1/plugins.yaml", }, "not-exists": { ct: "fred", err: errors.New(`no context found for: "fred"`), }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := mock.NewMockConfig(t) _, _ = c.K9s.ActivateContext(u.ct) s, err := c.ContextPluginsPath() if err != nil { assert.Equal(t, u.err, err) } assert.Equal(t, u.e, s) }) } } func TestConfigLoader(t *testing.T) { uu := map[string]struct { f string err string }{ "happy": { f: "testdata/configs/k9s.yaml", }, "toast": { f: "testdata/configs/k9s_toast.yaml", err: `k9s config file "testdata/configs/k9s_toast.yaml" load failed: Additional property disablePodCounts is not allowed Additional property shellPods is not allowed Invalid type. Expected: boolean, given: string`, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { cfg := config.NewConfig(nil) if err := cfg.Load(u.f, true); err != nil { assert.Equal(t, u.err, err.Error()) } }) } } func TestConfigActivateContext(t *testing.T) { uu := map[string]struct { cl, ct string err string }{ "happy": { ct: "ct-1-2", cl: "cl-1", }, "toast": { ct: "fred", cl: "cl-1", err: `set current context failed. no context found for: "fred"`, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { cfg := mock.NewMockConfig(t) ct, err := cfg.ActivateContext(u.ct) if err != nil { assert.Equal(t, u.err, err.Error()) return } require.NoError(t, err) assert.Equal(t, u.cl, ct.ClusterName) }) } } func TestConfigCurrentContext(t *testing.T) { var ( cfgFile = "testdata/kubes/test.yaml" ct2 = "ct-1-2" ) uu := map[string]struct { flags *genericclioptions.ConfigFlags err error context string cluster string namespace string }{ "override-context": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Context: &ct2, }, cluster: "cl-1", context: "ct-1-2", namespace: "ns-2", }, "use-current-context": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, }, cluster: "cl-1", context: "ct-1-1", namespace: client.DefaultNamespace, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { cfg := mock.NewMockConfig(t) err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags)) require.NoError(t, err) ct, err := cfg.CurrentContext() require.NoError(t, err) assert.Equal(t, u.cluster, ct.ClusterName) assert.Equal(t, u.namespace, ct.Namespace.Active) }) } } func TestConfigRefine(t *testing.T) { var ( cfgFile = "testdata/kubes/test.yaml" cl1 = "cl-1" ct2 = "ct-1-2" ns1, ns2, nsx = "ns-1", "ns-2", "ns-x" trueVal = true ) uu := map[string]struct { flags *genericclioptions.ConfigFlags k9sFlags *config.Flags err string context string cluster string namespace string }{ "no-override": { namespace: "default", }, "override-cluster": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, ClusterName: &cl1, }, cluster: "cl-1", context: "ct-1-1", namespace: client.DefaultNamespace, }, "override-cluster-context": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, ClusterName: &cl1, Context: &ct2, }, cluster: "cl-1", context: "ct-1-2", namespace: "ns-2", }, "override-bad-cluster": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, ClusterName: &ns1, }, cluster: "cl-1", context: "ct-1-1", namespace: client.DefaultNamespace, }, "override-ns": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Namespace: &ns2, }, cluster: "cl-1", context: "ct-1-1", namespace: "ns-2", }, "all-ns": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Namespace: &ns2, }, k9sFlags: &config.Flags{ AllNamespaces: &trueVal, }, cluster: "cl-1", context: "ct-1-1", namespace: client.NamespaceAll, }, "override-bad-ns": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Namespace: &nsx, }, cluster: "cl-1", context: "ct-1-1", namespace: "ns-x", }, "override-context": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Context: &ct2, }, cluster: "cl-1", context: "ct-1-2", namespace: "ns-2", }, "override-bad-context": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, Context: &ns1, }, err: `k8sflags. unable to activate context "ns-1": no context found for: "ns-1"`, }, "use-current-context": { flags: &genericclioptions.ConfigFlags{ KubeConfig: &cfgFile, }, cluster: "cl-1", context: "ct-1-1", namespace: client.DefaultNamespace, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { cfg := mock.NewMockConfig(t) err := cfg.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags)) if err != nil { assert.Equal(t, u.err, err.Error()) } else { require.NoError(t, err) assert.Equal(t, u.context, cfg.K9s.ActiveContextName()) assert.Equal(t, u.namespace, cfg.ActiveNamespace()) } }) } } func TestConfigValidate(t *testing.T) { cfg := mock.NewMockConfig(t) cfg.SetConnection(mock.NewMockConnection()) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.Validate("ct-1-1", "cl-1") } func TestConfigLoad(t *testing.T) { cfg := mock.NewMockConfig(t) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) assert.Equal(t, 2, cfg.K9s.RefreshRate) assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) } func TestConfigLoadCrap(t *testing.T) { cfg := mock.NewMockConfig(t) assert.Error(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true)) } func TestConfigSaveFile(t *testing.T) { cfg := mock.NewMockConfig(t) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.K9s.RefreshRate = 100 cfg.K9s.APIServerTimeout = "30s" cfg.K9s.ReadOnly = true cfg.K9s.Logger.TailCount = 500 cfg.K9s.Logger.BufferSize = 800 cfg.K9s.UI.UseFullGVRTitle = true cfg.Validate("ct-1-1", "cl-1") path := filepath.Join(os.TempDir(), "k9s.yaml") require.NoError(t, cfg.SaveFile(path)) raw, err := os.ReadFile(path) require.NoError(t, err) ee, err := os.ReadFile("testdata/configs/expected.yaml") require.NoError(t, err) assert.Equal(t, string(ee), string(raw)) } func TestConfigReset(t *testing.T) { cfg := mock.NewMockConfig(t) require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.Reset() cfg.Validate("ct-1-1", "cl-1") path := filepath.Join(os.TempDir(), "k9s.yaml") require.NoError(t, cfg.SaveFile(path)) bb, err := os.ReadFile(path) require.NoError(t, err) ee, err := os.ReadFile("testdata/configs/k9s.yaml") require.NoError(t, err) assert.Equal(t, string(ee), string(bb)) } // Helpers... func TestSetup(t *testing.T) { m.RegisterMockTestingT(t) m.RegisterMockFailHandler(func(m string, i ...int) { fmt.Println("Boom!", m, i) }) }