diff --git a/internal/view/container.go b/internal/view/container.go index 2c9493ec..2e73c1ac 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -29,6 +29,7 @@ func NewContainer(gvr client.GVR) ResourceViewer { c.SetEnvFn(c.k9sEnv) c.GetTable().SetEnterFn(c.viewLogs) c.GetTable().SetColorerFn(render.Container{}.ColorerFunc()) + c.GetTable().SetDecorateFn(c.decorateRows) c.SetBindKeysFn(c.bindKeys) c.GetTable().SetDecorateFn(c.portForwardIndicator) @@ -48,6 +49,10 @@ func (c *Container) portForwardIndicator(data render.TableData) render.TableData return data } +func (c *Container) decorateRows(data render.TableData) render.TableData { + return decorateCpuMemHeaderRows(c.App(), data) +} + // Name returns the component name. func (c *Container) Name() string { return containerTitle } diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 2ec5223c..e3d68206 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "github.com/derailed/k9s/internal" @@ -177,3 +178,40 @@ func fqn(ns, n string) string { } return ns + "/" + n } + +func decorateCpuMemHeaderRows(app *App, data render.TableData) render.TableData { + for colIndex, header := range data.Header { + check := "" + if header.Name == "%CPU/L" { + check = "cpu" + } + if header.Name == "%MEM/L" { + check = "memory" + } + if len(check) == 0 { + continue + } + for _, re := range data.RowEvents { + if re.Row.Fields[colIndex] == render.NAValue { + continue + } + n, err := strconv.Atoi(re.Row.Fields[colIndex]) + if err != nil { + continue + } + if n > 100 { + n = 100 + } + severity := app.Config.K9s.Thresholds.LevelFor(check, n) + if severity == config.SeverityLow { + continue + } + color := app.Config.K9s.Thresholds.SeverityColor(check, n) + if len(color) > 0 { + re.Row.Fields[colIndex] = "[" + color + "::b]" + re.Row.Fields[colIndex] + } + } + } + + return data +} diff --git a/internal/view/log.go b/internal/view/log.go index ac5107fe..9218a383 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "fmt" + "github.com/atotto/clipboard" "io" "os" "path/filepath" + "regexp" "strings" "time" @@ -29,6 +31,9 @@ const ( flushTimeout = 50 * time.Millisecond ) +// InvalidCharsRX contains invalid filename characters. +var invalidPathCharsRX = regexp.MustCompile(`[:/\\]+`) + // Log represents a generic log viewer. type Log struct { *tview.Flex @@ -180,13 +185,14 @@ func (l *Log) bindKeys() { ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true), ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), - ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), + tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true), ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true), ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), + ui.KeyC: ui.NewKeyAction("Copy", l.cpCmd, true), }) } @@ -284,18 +290,32 @@ func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func (l *Log) cpCmd(evt *tcell.EventKey) *tcell.EventKey { + l.app.Flash().Info("Content copied to clipboard...") + if err := clipboard.WriteAll(l.logs.GetText(true)); err != nil { + l.app.Flash().Err(err) + } + return nil +} + +func sanitizeFilename(name string) string { + processedString := invalidPathCharsRX.ReplaceAllString(name, "-") + + return processedString +} + func ensureDir(dir string) error { return os.MkdirAll(dir, 0744) } func saveData(cluster, name, data string) (string, error) { - dir := filepath.Join(config.K9sDumpDir, cluster) + dir := filepath.Join(config.K9sDumpDir, sanitizeFilename(cluster)) if err := ensureDir(dir); err != nil { return "", err } now := time.Now().UnixNano() - fName := fmt.Sprintf("%s-%d.log", strings.Replace(name, "/", "-", -1), now) + fName := fmt.Sprintf("%s-%d.log", sanitizeFilename(name), now) path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index 80cef2f6..1c89ce69 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -16,7 +16,7 @@ func TestLogAutoScroll(t *testing.T) { v.GetModel().Set(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")}) v.GetModel().Notify() - assert.Equal(t, 14, len(v.Hints())) + assert.Equal(t, 15, len(v.Hints())) v.toggleAutoScrollCmd(nil) assert.Equal(t, "Autoscroll: Off FullScreen: Off Timestamps: Off Wrap: Off", v.Indicator().GetText(true)) diff --git a/internal/view/pod.go b/internal/view/pod.go index e8f6f5b2..c8b12780 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -49,7 +49,7 @@ func (p *Pod) portForwardIndicator(data render.TableData) render.TableData { } } - return data + return decorateCpuMemHeaderRows(p.App(), data) } func (p *Pod) bindDangerousKeys(aa ui.KeyActions) { diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index 64d86469..8e18c48e 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -18,12 +18,12 @@ import ( func computeFilename(cluster, ns, title, path string) (string, error) { now := time.Now().UnixNano() - dir := filepath.Join(config.K9sDumpDir, cluster) + dir := filepath.Join(config.K9sDumpDir, sanitizeFilename(cluster)) if err := ensureDir(dir); err != nil { return "", err } - name := title + "-" + strings.Replace(path, "/", "-", -1) + name := title + "-" + sanitizeFilename(path) if path == "" { name = title } diff --git a/internal/view/yaml.go b/internal/view/yaml.go index 90ff4258..6831490d 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -61,13 +61,13 @@ func enableRegion(str string) string { } func saveYAML(cluster, name, data string) (string, error) { - dir := filepath.Join(config.K9sDumpDir, cluster) + dir := filepath.Join(config.K9sDumpDir, sanitizeFilename(cluster)) if err := ensureDir(dir); err != nil { return "", err } now := time.Now().UnixNano() - fName := fmt.Sprintf("%s-%d.yml", strings.Replace(name, "/", "-", -1), now) + fName := fmt.Sprintf("%s-%d.yml", sanitizeFilename(name), now) path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY diff --git a/plugins/README.md b/plugins/README.md index af9331bc..7ee0f973 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -2,9 +2,10 @@ K9s plugins extend the tool to provide additonal functionality via actions to further help you observe or administer your Kubernetes clusters. -| Plugin-Name | Description | Available on Views | Shortcut | Kubectl plugin, external dependencies | -|-----------------|--------------------------------|--------------------|----------|-------------------------------------------| -| log_stern.yml | View resource logs using stern | pods | Ctrl-l | | -| log_jq.yml | View resource logs using jq | pods | Ctrl-j | kubetcl-plugins/kubectl-jq | -| job_suspend.yml | Suspends a running cronjob | cronjobs | Ctrl-s | | -| dive.yml | Dive image layers | containers | d | [Dive](https://github.com/wagoodman/dive) | +| Plugin-Name | Description | Available on Views | Shortcut | Kubectl plugin, external dependencies | +|-----------------|----------------------------------|--------------------|----------|---------------------------------------------------------------------------------------| +| log_stern.yml | View resource logs using stern | pods | Ctrl-l | | +| log_jq.yml | View resource logs using jq | pods | Ctrl-j | kubetcl-plugins/kubectl-jq | +| job_suspend.yml | Suspends a running cronjob | cronjobs | Ctrl-s | | +| dive.yml | Dive image layers | containers | d | [Dive](https://github.com/wagoodman/dive) | +| get-all.yml | get all resources in a namespace | all | g | [Krew](https://krew.sigs.k8s.io/), [ketall](https://github.com/corneliusweig/ketall/) | diff --git a/plugins/get-all.yml b/plugins/get-all.yml new file mode 100644 index 00000000..20001d0e --- /dev/null +++ b/plugins/get-all.yml @@ -0,0 +1,13 @@ +plugin: + #get all resources in a namespace using the krew get-all plugin + get-all: + shortCut: g + confirm: false + description: get-all + scopes: + - all + command: sh + background: false + args: + - -c + - "kubectl get-all -n $NAMESPACE | less" \ No newline at end of file