From 0249f7cf2c2b403348e98f03a26355aadfbdfdda Mon Sep 17 00:00:00 2001 From: Vlasov Artem Date: Mon, 13 Dec 2021 21:42:44 +0200 Subject: [PATCH] Add customizable dump directory property (#1321) * Add customizable dump directory property Add property for configuration file and arguments * Resolve Comments Co-authored-by: Artem Vlasov --- README.md | 2 + cmd/info.go | 25 ++++++++++- cmd/info_test.go | 47 ++++++++++++++++++++ cmd/root.go | 8 ++++ cmd/testdata/k9s.yml | 32 ++++++++++++++ cmd/testdata/k9s1.yml | 8 ++++ internal/config/config.go | 6 ++- internal/config/config_test.go | 2 + internal/config/flags.go | 2 + internal/config/k9s.go | 73 +++++++++++++++++++++----------- internal/config/k9s_test.go | 36 ++++++++++++++++ internal/config/testdata/k9s.yml | 1 + internal/view/details.go | 2 +- internal/view/live_view.go | 2 +- internal/view/log.go | 6 +-- internal/view/log_test.go | 3 +- internal/view/logger.go | 2 +- internal/view/screen_dump.go | 2 +- internal/view/table.go | 2 +- internal/view/table_helper.go | 9 ++-- internal/view/table_int_test.go | 2 +- internal/view/yaml.go | 4 +- 22 files changed, 231 insertions(+), 45 deletions(-) create mode 100644 cmd/info_test.go create mode 100644 cmd/testdata/k9s.yml create mode 100644 cmd/testdata/k9s1.yml diff --git a/README.md b/README.md index b568683b..de68e541 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,8 @@ K9s uses aliases to navigate most K8s resources. - default view: active: dp + # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) + screenDumpDir: /tmp ``` --- diff --git a/cmd/info.go b/cmd/info.go index e7a7210c..e1f1bbd5 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -6,7 +6,10 @@ import ( "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/ui" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + "os" ) func infoCmd() *cobra.Command { @@ -26,7 +29,7 @@ func printInfo() { printLogo(color.Cyan) printTuple(fmat, "Configuration", config.K9sConfigFile, color.Cyan) printTuple(fmat, "Logs", config.DefaultLogFile, color.Cyan) - printTuple(fmat, "Screen Dumps", config.K9sDumpDir, color.Cyan) + printTuple(fmat, "Screen Dumps", getScreenDumpDirForInfo(), color.Cyan) } func printLogo(c color.Paint) { @@ -35,3 +38,23 @@ func printLogo(c color.Paint) { } fmt.Fprintln(out) } + +// getScreenDumpDirForInfo get default screen dump config dir or from config.K9sConfigFile configuration. +func getScreenDumpDirForInfo() string { + if config.K9sConfigFile == "" { + return config.K9sDefaultScreenDumpDir + } + + f, err := os.ReadFile(config.K9sConfigFile) + if err != nil { + log.Error().Err(err).Msgf("Reads k9s config file %v", err) + return config.K9sDefaultScreenDumpDir + } + + var cfg config.Config + if err := yaml.Unmarshal(f, &cfg); err != nil { + log.Error().Err(err).Msgf("Unmarshal k9s config %v", err) + return config.K9sDefaultScreenDumpDir + } + return cfg.K9s.GetScreenDumpDir() +} diff --git a/cmd/info_test.go b/cmd/info_test.go new file mode 100644 index 00000000..a7f88954 --- /dev/null +++ b/cmd/info_test.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_getScreenDumpDirForInfo(t *testing.T) { + tests := []struct { + name string + k9sConfigFile string + expectedScreenDumpDir string + }{ + { + name: "withK9sConfigFile", + k9sConfigFile: "testdata/k9s.yml", + expectedScreenDumpDir: "/tmp", + }, + { + name: "withEmptyK9sConfigFile", + k9sConfigFile: "", + expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + }, + { + name: "withInvalidK9sConfigFilePath", + k9sConfigFile: "invalid", + expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + }, + { + name: "withScreenDumpDirEmptyInK9sConfigFile", + k9sConfigFile: "testdata/k9s1.yml", + expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + initK9sConfigFile := config.K9sConfigFile + + config.K9sConfigFile = tt.k9sConfigFile + + assert.Equal(t, tt.expectedScreenDumpDir, getScreenDumpDirForInfo()) + + config.K9sConfigFile = initK9sConfigFile + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index 430bdda1..6b53a176 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -108,6 +108,7 @@ func loadConfiguration() *config.Config { k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly) k9sCfg.K9s.OverrideWrite(*k9sFlags.Write) k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) + k9sCfg.K9s.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir) if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil { log.Error().Err(err).Msgf("refine failed") @@ -210,6 +211,13 @@ func initK9sFlags() { false, "Sets write mode by overriding the readOnly configuration setting", ) + rootCmd.Flags().StringVar( + k9sFlags.ScreenDumpDir, + "screen-dump-dir", + "", + "Sets a path to a dir for a screen dumps", + ) + rootCmd.Flags() } func initK8sFlags() { diff --git a/cmd/testdata/k9s.yml b/cmd/testdata/k9s.yml new file mode 100644 index 00000000..0587cd29 --- /dev/null +++ b/cmd/testdata/k9s.yml @@ -0,0 +1,32 @@ +k9s: + refreshRate: 2 + readOnly: false + logger: + tail: 200 + buffer: 2000 + currentContext: minikube + currentCluster: minikube + clusters: + minikube: + namespace: + active: kube-system + favorites: + - default + - kube-public + - istio-system + - all + - kube-system + view: + active: ctx + fred: + namespace: + active: default + favorites: + - default + - kube-public + - istio-system + - all + - kube-system + view: + active: po + screenDumpDir: /tmp diff --git a/cmd/testdata/k9s1.yml b/cmd/testdata/k9s1.yml new file mode 100644 index 00000000..99bb975f --- /dev/null +++ b/cmd/testdata/k9s1.yml @@ -0,0 +1,8 @@ +k9s: + refreshRate: 10 + namespace: + active: fred + favorites: + - blee + - duh + - crap \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 70ced92f..6c8a88a7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,8 +19,8 @@ const K9sConfig = "K9SCONFIG" var ( // K9sConfigFile represents K9s config file location. K9sConfigFile = filepath.Join(K9sHome(), "config.yml") - // K9sDumpDir represents a directory where K9s screen dumps will be persisted. - K9sDumpDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", MustK9sUser())) + // K9sDefaultScreenDumpDir represents a default directory where K9s screen dumps will be persisted. + K9sDefaultScreenDumpDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", MustK9sUser())) ) type ( @@ -116,6 +116,8 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c c.K9s.CurrentCluster = *flags.ClusterName } + EnsurePath(c.K9s.GetScreenDumpDir(), DefaultDirMod) + return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f840f6c0..3c9a1725 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -347,6 +347,7 @@ var expectedConfig = `k9s: memory: critical: 90 warn: 70 + screenDumpDir: /tmp ` var resetConfig = `k9s: @@ -393,4 +394,5 @@ var resetConfig = `k9s: memory: critical: 90 warn: 70 + screenDumpDir: /tmp ` diff --git a/internal/config/flags.go b/internal/config/flags.go index 363752e0..e521ce2c 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -32,6 +32,7 @@ type Flags struct { ReadOnly *bool Write *bool Crumbsless *bool + ScreenDumpDir *string } // NewFlags returns new configuration flags. @@ -47,6 +48,7 @@ func NewFlags() *Flags { ReadOnly: boolPtr(false), Write: boolPtr(false), Crumbsless: boolPtr(false), + ScreenDumpDir: strPtr(K9sDefaultScreenDumpDir), } } diff --git a/internal/config/k9s.go b/internal/config/k9s.go index c84c0ece..53265e71 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -11,35 +11,38 @@ const ( // K9s tracks K9s configuration options. type K9s struct { - RefreshRate int `yaml:"refreshRate"` - MaxConnRetry int `yaml:"maxConnRetry"` - EnableMouse bool `yaml:"enableMouse"` - Headless bool `yaml:"headless"` - Logoless bool `yaml:"logoless"` - Crumbsless bool `yaml:"crumbsless"` - ReadOnly bool `yaml:"readOnly"` - NoIcons bool `yaml:"noIcons"` - Logger *Logger `yaml:"logger"` - CurrentContext string `yaml:"currentContext"` - CurrentCluster string `yaml:"currentCluster"` - Clusters map[string]*Cluster `yaml:"clusters,omitempty"` - Thresholds Threshold `yaml:"thresholds"` - manualRefreshRate int - manualHeadless *bool - manualLogoless *bool - manualCrumbsless *bool - manualReadOnly *bool - manualCommand *string + RefreshRate int `yaml:"refreshRate"` + MaxConnRetry int `yaml:"maxConnRetry"` + EnableMouse bool `yaml:"enableMouse"` + Headless bool `yaml:"headless"` + Logoless bool `yaml:"logoless"` + Crumbsless bool `yaml:"crumbsless"` + ReadOnly bool `yaml:"readOnly"` + NoIcons bool `yaml:"noIcons"` + Logger *Logger `yaml:"logger"` + CurrentContext string `yaml:"currentContext"` + CurrentCluster string `yaml:"currentCluster"` + Clusters map[string]*Cluster `yaml:"clusters,omitempty"` + Thresholds Threshold `yaml:"thresholds"` + ScreenDumpDir string `yaml:"screenDumpDir"` + manualRefreshRate int + manualHeadless *bool + manualLogoless *bool + manualCrumbsless *bool + manualReadOnly *bool + manualCommand *string + manualScreenDumpDir *string } // NewK9s create a new K9s configuration. func NewK9s() *K9s { return &K9s{ - RefreshRate: defaultRefreshRate, - MaxConnRetry: defaultMaxConnRetry, - Logger: NewLogger(), - Clusters: make(map[string]*Cluster), - Thresholds: NewThreshold(), + RefreshRate: defaultRefreshRate, + MaxConnRetry: defaultMaxConnRetry, + Logger: NewLogger(), + Clusters: make(map[string]*Cluster), + Thresholds: NewThreshold(), + ScreenDumpDir: K9sDefaultScreenDumpDir, } } @@ -91,6 +94,11 @@ func (k *K9s) OverrideCommand(cmd string) { k.manualCommand = &cmd } +// OverrideScreenDumpDir set the screen dump dir manually. +func (k *K9s) OverrideScreenDumpDir(dir string) { + k.manualScreenDumpDir = &dir +} + // IsHeadless returns headless setting. func (k *K9s) IsHeadless() bool { h := k.Headless @@ -155,6 +163,20 @@ func (k *K9s) ActiveCluster() *Cluster { return k.Clusters[k.CurrentCluster] } +func (k *K9s) GetScreenDumpDir() string { + screenDumpDir := k.ScreenDumpDir + + if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" { + screenDumpDir = *k.manualScreenDumpDir + } + + if screenDumpDir == "" { + return K9sDefaultScreenDumpDir + } + + return screenDumpDir +} + func (k *K9s) validateDefaults() { if k.RefreshRate <= 0 { k.RefreshRate = defaultRefreshRate @@ -162,6 +184,9 @@ func (k *K9s) validateDefaults() { if k.MaxConnRetry <= 0 { k.MaxConnRetry = defaultMaxConnRetry } + if k.ScreenDumpDir == "" { + k.ScreenDumpDir = K9sDefaultScreenDumpDir + } } func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) { diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index 504a8989..ca1fabc9 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -78,6 +78,7 @@ func TestK9sValidate(t *testing.T) { assert.Equal(t, "ctx1", c.CurrentContext) assert.Equal(t, "c1", c.CurrentCluster) assert.Equal(t, 1, len(c.Clusters)) + assert.Equal(t, config.K9sDefaultScreenDumpDir, c.GetScreenDumpDir()) _, ok := c.Clusters[c.CurrentCluster] assert.True(t, ok) } @@ -130,3 +131,38 @@ func TestK9sActiveCluster(t *testing.T) { assert.Equal(t, "kube-system", cl.Namespace.Active) assert.Equal(t, 5, len(cl.Namespace.Favorites)) } + +func TestGetScreenDumpDir(t *testing.T) { + mk := NewMockKubeSettings() + cfg := config.NewConfig(mk) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) + + assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir()) +} + +func TestGetScreenDumpDirOverride(t *testing.T) { + mk := NewMockKubeSettings() + cfg := config.NewConfig(mk) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg.K9s.OverrideScreenDumpDir("/override") + + assert.Equal(t, "/override", cfg.K9s.GetScreenDumpDir()) +} + +func TestGetScreenDumpDirOverrideEmpty(t *testing.T) { + mk := NewMockKubeSettings() + cfg := config.NewConfig(mk) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg.K9s.OverrideScreenDumpDir("") + + assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir()) +} + +func TestGetScreenDumpDirEmpty(t *testing.T) { + mk := NewMockKubeSettings() + cfg := config.NewConfig(mk) + assert.Nil(t, cfg.Load("testdata/k9s1.yml")) + cfg.K9s.OverrideScreenDumpDir("") + + assert.Equal(t, config.K9sDefaultScreenDumpDir, cfg.K9s.GetScreenDumpDir()) +} diff --git a/internal/config/testdata/k9s.yml b/internal/config/testdata/k9s.yml index 3b6a6ac6..0587cd29 100644 --- a/internal/config/testdata/k9s.yml +++ b/internal/config/testdata/k9s.yml @@ -29,3 +29,4 @@ k9s: - kube-system view: active: po + screenDumpDir: /tmp diff --git a/internal/view/details.go b/internal/view/details.go index 69dd1d85..0d927600 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -278,7 +278,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(d.app.Config.K9s.CurrentCluster, d.title, d.text.GetText(true)); err != nil { + if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.CurrentCluster, d.title, d.text.GetText(true)); err != nil { d.app.Flash().Err(err) } else { d.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/live_view.go b/internal/view/live_view.go index a03e084d..967be431 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -332,7 +332,7 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(v.app.Config.K9s.CurrentCluster, v.title, v.text.GetText(true)); err != nil { + if path, err := saveYAML(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentCluster, v.title, v.text.GetText(true)); err != nil { v.app.Flash().Err(err) } else { v.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/log.go b/internal/view/log.go index a1672e3f..847659d7 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -409,7 +409,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { // SaveCmd dumps the logs to file. func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { - if path, err := saveData(l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)); err != nil { + if path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContext, l.model.GetPath(), l.logs.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) @@ -429,8 +429,8 @@ func ensureDir(dir string) error { return os.MkdirAll(dir, 0744) } -func saveData(cluster, name, data string) (string, error) { - dir := filepath.Join(config.K9sDumpDir, dao.SanitizeFilename(cluster)) +func saveData(screenDumpDir, cluster, name, data string) (string, error) { + dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) if err := ensureDir(dir); err != nil { return "", err } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index 8031bbc4..7e966b80 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -106,8 +106,7 @@ func TestLogViewSave(t *testing.T) { ii.Lines(0, false, ll) v.Flush(ll) - config.K9sDumpDir = "/tmp" - dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) + dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.CurrentCluster) c1, _ := os.ReadDir(dir) v.SaveCmd(nil) c2, _ := os.ReadDir(dir) diff --git a/internal/view/logger.go b/internal/view/logger.go index a0b7ec69..932d13f8 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -152,7 +152,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(l.app.Config.K9s.CurrentCluster, l.title, l.GetText(true)); err != nil { + if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentCluster, l.title, l.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go index 56ff8752..19c4cd98 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -36,7 +36,7 @@ func NewScreenDump(gvr client.GVR) ResourceViewer { } func (s *ScreenDump) dirContext(ctx context.Context) context.Context { - dir := filepath.Join(config.K9sDumpDir, s.App().Config.K9s.CurrentCluster) + dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), s.App().Config.K9s.CurrentCluster) log.Debug().Msgf("SD-DIR %q", dir) config.EnsureFullPath(dir, config.DefaultDirMod) diff --git a/internal/view/table.go b/internal/view/table.go index b3099089..6a415a0b 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -168,7 +168,7 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) { } func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveTable(t.app.Config.K9s.CurrentCluster, t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { + if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.CurrentCluster, t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { t.app.Flash().Err(err) } else { t.app.Flash().Infof("File %s saved successfully!", path) diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index 11dfde30..450be0c7 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -9,17 +9,16 @@ import ( "time" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/rs/zerolog/log" ) -func computeFilename(cluster, ns, title, path string) (string, error) { +func computeFilename(screenDumpDir, cluster, ns, title, path string) (string, error) { now := time.Now().UnixNano() - dir := filepath.Join(config.K9sDumpDir, dao.SanitizeFilename(cluster)) + dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) if err := ensureDir(dir); err != nil { return "", err } @@ -39,13 +38,13 @@ func computeFilename(cluster, ns, title, path string) (string, error) { return strings.ToLower(filepath.Join(dir, fName)), nil } -func saveTable(cluster, title, path string, data render.TableData) (string, error) { +func saveTable(screenDumpDir, cluster, title, path string, data render.TableData) (string, error) { ns := data.Namespace if client.IsClusterWide(ns) { ns = client.NamespaceAll } - fPath, err := computeFilename(cluster, ns, title, path) + fPath, err := computeFilename(screenDumpDir, cluster, ns, title, path) if err != nil { return "", err } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index abcced4a..ea5bd61d 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -24,7 +24,7 @@ func TestTableSave(t *testing.T) { v.Init(makeContext()) v.SetTitle("k9s-test") - dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster) + dir := filepath.Join(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentCluster) c1, _ := os.ReadDir(dir) v.saveCmd(nil) diff --git a/internal/view/yaml.go b/internal/view/yaml.go index 6c5d4d5b..4ed23441 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -61,8 +61,8 @@ func enableRegion(str string) string { return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]") } -func saveYAML(cluster, name, data string) (string, error) { - dir := filepath.Join(config.K9sDumpDir, dao.SanitizeFilename(cluster)) +func saveYAML(screenDumpDir, cluster, name, data string) (string, error) { + dir := filepath.Join(screenDumpDir, dao.SanitizeFilename(cluster)) if err := ensureDir(dir); err != nil { return "", err }