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 <artemv@tradeix.com>
mine
Vlasov Artem 2021-12-13 21:42:44 +02:00 committed by GitHub
parent 08ee61a298
commit 0249f7cf2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 231 additions and 45 deletions

View File

@ -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
```
---

View File

@ -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()
}

47
cmd/info_test.go Normal file
View File

@ -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
})
}
}

View File

@ -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() {

32
cmd/testdata/k9s.yml vendored Normal file
View File

@ -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

8
cmd/testdata/k9s1.yml vendored Normal file
View File

@ -0,0 +1,8 @@
k9s:
refreshRate: 10
namespace:
active: fred
favorites:
- blee
- duh
- crap

View File

@ -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
}

View File

@ -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
`

View File

@ -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),
}
}

View File

@ -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) {

View File

@ -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())
}

View File

@ -29,3 +29,4 @@ k9s:
- kube-system
view:
active: po
screenDumpDir: /tmp

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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
}