diff --git a/change_logs/release_0.7.1.md b/change_logs/release_0.7.1.md new file mode 100644 index 00000000..c7dbefbe --- /dev/null +++ b/change_logs/release_0.7.1.md @@ -0,0 +1,24 @@ + + +# Release v0.7.1 + +## Notes + +Thank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + + +--- + +## Resolved Bugs/Features + ++ [Issue #200](https://github.com/derailed/k9s/issues/200) + +--- + + © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/info.go b/cmd/info.go index 5f1ea94d..70fcbde2 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -21,11 +21,12 @@ func infoCmd() *cobra.Command { } func printInfo() { - const secFmt = "%-15s " + const sectionFmt = "%-15s " printLogo(printer.ColorCyan) - printTuple(secFmt, "Configuration", config.K9sConfigFile) - printTuple(secFmt, "Logs", config.K9sLogs) + printTuple(sectionFmt, "Configuration", config.K9sConfigFile) + printTuple(sectionFmt, "Logs", config.K9sLogs) + printTuple(sectionFmt, "Screen Dumps", config.K9sDumpDir) } func printLogo(color int) { diff --git a/internal/config/config.go b/internal/config/config.go index f3967aaf..afa93f9c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,6 +24,8 @@ var ( K9sConfigFile = filepath.Join(K9sHome, "config.yml") // K9sLogs represents K9s log. K9sLogs = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser())) + // K9sDumpDir represents a directory where K9s screen dumps will be persisted. + K9sDumpDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", MustK9sUser())) ) type ( diff --git a/internal/views/app.go b/internal/views/app.go index b9fa516a..d3462763 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -16,8 +16,9 @@ import ( ) const ( - splashTime = 1 - devMode = "dev" + splashTime = 1 + devMode = "dev" + clusterRefresh = time.Duration(15 * time.Second) ) type ( @@ -154,6 +155,20 @@ func (a *appView) Init(v string, rate int, flags *genericclioptions.ConfigFlags) a.SetRoot(a.pages, true) } +func (a *appView) clusterUpdater(ctx context.Context) { + for { + select { + case <-ctx.Done(): + log.Debug().Msg("Cluster updater canceled!") + return + case <-time.After(clusterRefresh): + a.QueueUpdateDraw(func() { + a.clusterInfoView.refresh() + }) + } + } +} + func (a *appView) startInformer() { if a.stopCh != nil { close(a.stopCh) @@ -229,6 +244,10 @@ func (a *appView) stylesUpdater(ctx context.Context) error { // Run starts the application loop func (a *appView) Run() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go a.clusterUpdater(ctx) + // Only enable skin updater while in dev mode. if a.version == devMode && a.hasSkins { var ctx context.Context diff --git a/internal/views/bench.go b/internal/views/bench.go index 44f4d5ad..2783fd3f 100644 --- a/internal/views/bench.go +++ b/internal/views/bench.go @@ -36,7 +36,6 @@ type benchView struct { *tview.Pages app *appView - current igniter cancel context.CancelFunc selectedItem string selectedRow int @@ -48,7 +47,6 @@ func newBenchView(_ string, app *appView, _ resource.List) resourceViewer { Pages: tview.NewPages(), actions: make(keyActions), app: app, - current: app.content.GetPrimitive("main").(igniter), } tv := newTableView(app, benchTitle) @@ -119,7 +117,6 @@ func (v *benchView) selChanged(r, c int) { } v.selectedRow = r v.selectedItem = strings.TrimSpace(tv.GetCell(r, 7).Text) - v.getTV().cmdBuff.setActive(false) } func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -209,7 +206,6 @@ func (v *benchView) hydrate() resource.TableData { } dir := filepath.Join(K9sBenchDir, v.app.config.K9s.CurrentCluster) - log.Debug().Msgf("----> DIR %s", dir) ff, err := ioutil.ReadDir(dir) if err != nil { log.Error().Err(err).Msg("Reading bench dir") diff --git a/internal/views/colorer.go b/internal/views/colorer.go index 0aa39366..3145a27b 100644 --- a/internal/views/colorer.go +++ b/internal/views/colorer.go @@ -33,6 +33,10 @@ func forwardColorer(string, *resource.RowEvent) tcell.Color { return tcell.ColorSkyblue } +func dumpColorer(ns string, r *resource.RowEvent) tcell.Color { + return tcell.ColorNavajoWhite +} + func benchColorer(ns string, r *resource.RowEvent) tcell.Color { c := tcell.ColorPaleGreen diff --git a/internal/views/command.go b/internal/views/command.go index cc9c031d..08e59940 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -62,12 +62,6 @@ func (c *command) run(cmd string) bool { case cmd == "?", cmd == "help": c.app.inject(newHelpView(c.app)) return true - case cmd == "pf": - c.app.inject(newForwardView("", c.app, nil)) - return true - case cmd == "be": - c.app.inject(newBenchView("", c.app, nil)) - return true case cmd == "alias": c.app.inject(newAliasView(c.app)) return true diff --git a/internal/views/dump.go b/internal/views/dump.go new file mode 100644 index 00000000..08d14fa1 --- /dev/null +++ b/internal/views/dump.go @@ -0,0 +1,246 @@ +package views + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/resource" + "github.com/derailed/tview" + "github.com/fsnotify/fsnotify" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" +) + +const ( + dumpTitle = "Screen Dumps" + dumpTitleFmt = " [mediumvioletred::b]%s([fuchsia::b]%d[fuchsia::-])[mediumvioletred::-] " +) + +var ( + dumpHeader = resource.Row{"NAME", "AGE"} +) + +type dumpView struct { + *tview.Pages + + app *appView + cancel context.CancelFunc + selectedItem string + selectedRow int + actions keyActions +} + +func newDumpView(_ string, app *appView, _ resource.List) resourceViewer { + v := dumpView{ + Pages: tview.NewPages(), + actions: make(keyActions), + app: app, + } + + tv := newTableView(app, dumpTitle) + { + tv.SetSelectionChangedFunc(v.selChanged) + tv.SetBorderFocusColor(tcell.ColorSteelBlue) + tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone) + tv.colorerFn = dumpColorer + tv.currentNS = "" + } + v.AddPage("table", tv, true, true) + + details := newDetailsView(app, v.backCmd) + v.AddPage("details", details, true, false) + v.registerActions() + + return &v +} + +func (v *dumpView) setEnterFn(enterFn) {} +func (v *dumpView) setColorerFn(colorerFn) {} +func (v *dumpView) setDecorateFn(decorateFn) {} +func (v *dumpView) setExtraActionsFn(actionsFn) {} + +// Init the view. +func (v *dumpView) init(ctx context.Context, _ string) { + if err := v.watchDumpDir(ctx); err != nil { + log.Error().Err(err).Msg("Dumpdir watch failed!") + v.app.flash().errf("Unable to watch dumpmarks directory %s", err) + } + + tv := v.getTV() + v.refresh() + tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+1, true + tv.refresh() + tv.Select(1, 0) + v.app.SetFocus(tv) +} + +func (v *dumpView) refresh() { + tv := v.getTV() + tv.update(v.hydrate()) + tv.resetTitle() + v.selChanged(v.selectedRow, 0) +} + +func (v *dumpView) registerActions() { + v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) + v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, true) + v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) + + vu := v.getTV() + vu.setActions(v.actions) + v.app.setHints(vu.hints()) +} + +func (v *dumpView) getTitle() string { + return dumpTitle +} + +func (v *dumpView) selChanged(r, c int) { + log.Debug().Msgf("Selection changed %d:%c", r, c) + tv := v.getTV() + if r == 0 || tv.GetCell(r, 0) == nil { + v.selectedItem = "" + return + } + v.selectedRow = r + v.selectedItem = strings.TrimSpace(tv.GetCell(r, 0).Text) +} + +func (v *dumpView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + tv := v.getTV() + tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+col, asc + tv.refresh() + + return nil + } +} + +func (v *dumpView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.getTV().cmdBuff.isActive() { + return v.getTV().filterCmd(evt) + } + sel := v.selectedItem + if sel == "" { + return nil + } + + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + if !run(true, v.app, filepath.Join(dir, sel)) { + log.Error().Msg("Failed to launch editor") + v.app.flash().err(errors.New("Failed to launch editor")) + } + + return nil +} + +func (v *dumpView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := v.selectedItem + if sel == "" { + return nil + } + + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + showModal(v.Pages, fmt.Sprintf("Deleting `%s are you sure?", sel), "table", func() { + if err := os.Remove(filepath.Join(dir, sel)); err != nil { + v.app.flash().errf("Unable to delete file %s", err) + log.Error().Err(err).Msg("Delete failed") + return + } + v.refresh() + v.app.flash().infof("ScreenDump file %s deleted!", sel) + }) + + return nil +} + +func (v *dumpView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + if v.cancel != nil { + v.cancel() + } + v.SwitchToPage("table") + return nil +} + +func (v *dumpView) hints() hints { + return v.CurrentPage().Item.(hinter).hints() +} + +func (v *dumpView) hydrate() resource.TableData { + data := resource.TableData{ + Header: dumpHeader, + Rows: make(resource.RowEvents, 10), + Namespace: resource.NotNamespaced, + } + + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + ff, err := ioutil.ReadDir(dir) + if err != nil { + log.Error().Err(err).Msg("Reading dump dir") + v.app.flash().errf("Unable to read dump directory %s", err) + } + + for _, f := range ff { + fields := resource.Row{f.Name(), time.Since(f.ModTime()).String()} + data.Rows[f.Name()] = &resource.RowEvent{ + Action: resource.New, + Fields: fields, + Deltas: fields, + } + } + + return data +} + +func (v *dumpView) resetTitle() { + v.SetTitle(fmt.Sprintf(dumpTitleFmt, dumpTitle, v.getTV().GetRowCount()-1)) +} + +func (v *dumpView) watchDumpDir(ctx context.Context) error { + w, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + go func() { + for { + select { + case evt := <-w.Events: + log.Debug().Msgf("Dump event %#v", evt) + v.app.QueueUpdateDraw(func() { + v.refresh() + }) + case err := <-w.Errors: + log.Info().Err(err).Msg("Dir Watcher failed") + return + case <-ctx.Done(): + log.Debug().Msg("!!!! FS WATCHER DONE!!") + w.Close() + return + } + } + }() + + return w.Add(filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster)) +} + +func (v *dumpView) getTV() *tableView { + if vu, ok := v.GetPrimitive("table").(*tableView); ok { + return vu + } + return nil +} + +func (v *dumpView) getDetails() *detailsView { + if vu, ok := v.GetPrimitive("details").(*detailsView); ok { + return vu + } + return nil +} diff --git a/internal/views/exec.go b/internal/views/exec.go index 1bfdbc1b..84cfae5f 100644 --- a/internal/views/exec.go +++ b/internal/views/exec.go @@ -37,6 +37,21 @@ func runK(clear bool, app *appView, args ...string) bool { }) } +func run(clear bool, app *appView, args ...string) bool { + bin, err := exec.LookPath(os.Getenv("EDITOR")) + if err != nil { + log.Error().Msgf("Unable to find editor command in path %v", err) + return false + } + + return app.Suspend(func() { + if err := execute(clear, bin, args...); err != nil { + log.Error().Msgf("Command exited: %T %v %v", err, err, args) + app.flash().errf("Command exited: %v", err) + } + }) +} + func execute(clear bool, bin string, args ...string) error { if clear { clearScreen() diff --git a/internal/views/forward.go b/internal/views/forward.go index 2614405a..7ddc90f5 100644 --- a/internal/views/forward.go +++ b/internal/views/forward.go @@ -25,10 +25,9 @@ const ( type forwardView struct { *tview.Pages - app *appView - current igniter - cancel context.CancelFunc - bench *benchmark + app *appView + cancel context.CancelFunc + bench *benchmark } var _ resourceViewer = &forwardView{} @@ -45,8 +44,6 @@ func newForwardView(ns string, app *appView, list resource.List) resourceViewer tv.colorerFn = forwardColorer tv.currentNS = "" v.AddPage("table", tv, true, true) - - v.current = app.content.GetPrimitive("main").(igniter) v.registerActions() return &v @@ -248,7 +245,7 @@ func (v *forwardView) backCmd(evt *tcell.EventKey) *tcell.EventKey { if tv.cmdBuff.isActive() { tv.cmdBuff.reset() } else { - v.app.inject(v.current) + v.app.inject(v.app.content.GetPrimitive("main").(igniter)) } return nil @@ -362,7 +359,5 @@ func watchFS(ctx context.Context, app *appView, dir, file string, cb func()) err } func benchConfig(cluster string) string { - path := filepath.Join(config.K9sHome, config.K9sBench+"-"+cluster+".yml") - log.Debug().Msgf("!!!!!!!!!!!!!! Loading bench config from: %s", path) - return path + return filepath.Join(config.K9sHome, config.K9sBench+"-"+cluster+".yml") } diff --git a/internal/views/log.go b/internal/views/log.go index 313cbd0d..d4c9c2b7 100644 --- a/internal/views/log.go +++ b/internal/views/log.go @@ -117,7 +117,8 @@ func (v *logView) update() { // Actions... func (v *logView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if err := os.MkdirAll(K9sDump, 0744); err != nil { + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + if err := os.MkdirAll(dir, 0744); err != nil { log.Error().Err(err).Msgf("Mkdir K9s dump") return nil } @@ -125,7 +126,7 @@ func (v *logView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { now := time.Now().UnixNano() fName := fmt.Sprintf("%s-%d.log", strings.Replace(v.path, "/", "-", -1), now) - path := filepath.Join(K9sDump, fName) + path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY file, err := os.OpenFile(path, mod, 0644) defer func() { diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 438634f3..8810835c 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -303,6 +303,11 @@ func resourceViews(c k8s.Connection) map[string]resCmd { api: "", viewFn: newBenchView, }, + "sd": { + title: "ScreenDumps", + api: "", + viewFn: newDumpView, + }, } rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"}) diff --git a/internal/views/resource.go b/internal/views/resource.go index 4c5af133..6ef428ea 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -17,10 +17,7 @@ import ( "github.com/rs/zerolog/log" ) -const ( - noSelection = "" - clusterRefresh = time.Duration(15 * time.Second) -) +const noSelection = "" type updatable interface { restartUpdates() @@ -124,20 +121,6 @@ func (v *resourceView) init(ctx context.Context, ns string) { } func (v *resourceView) update(ctx context.Context) { - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - log.Debug().Msgf("%s cluster updater canceled!", v.list.GetName()) - return - case <-time.After(clusterRefresh): - v.app.QueueUpdateDraw(func() { - v.app.clusterInfoView.refresh() - }) - } - } - }(ctx) - go func(ctx context.Context) { for { select { @@ -164,7 +147,6 @@ func (v *resourceView) getTitle() string { func (v *resourceView) selChanged(r, c int) { v.selectedRow = r v.selectItem(r, c) - v.getTV().cmdBuff.setActive(false) } func (v *resourceView) getSelectedItem() string { diff --git a/internal/views/table.go b/internal/views/table.go index 7660ddbe..4c1ceba6 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -89,9 +89,7 @@ func newTableView(app *appView, title string) *tableView { } func (v *tableView) bindKeys() { - v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, false) v.actions[tcell.KeyCtrlS] = newKeyAction("Save", v.saveCmd, true) - v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false) v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false) v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false) @@ -100,6 +98,7 @@ func (v *tableView) bindKeys() { v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false) v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false) + v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, false) v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true) v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true) } @@ -139,16 +138,14 @@ func (v *tableView) setSelection() { } } -// K9sDump represents a directory where K9s artifacts will be persisted. -var K9sDump = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", config.MustK9sUser())) - const ( fullFmat = "%s-%s-%d.csv" noNSFmat = "%s-%d.csv" ) func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if err := os.MkdirAll(K9sDump, 0744); err != nil { + dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster) + if err := os.MkdirAll(dir, 0744); err != nil { log.Error().Err(err).Msgf("Mkdir K9s dump") return nil } @@ -162,7 +159,7 @@ func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { fName = fmt.Sprintf(noNSFmat, v.baseTitle, now) } - path := filepath.Join(K9sDump, fName) + path := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY file, err := os.OpenFile(path, mod, 0644) defer func() {