add screen dump view + fix filtering

mine
derailed 2019-05-30 15:27:49 -06:00
parent dafc4376c8
commit 0163f9b5de
14 changed files with 334 additions and 53 deletions

View File

@ -0,0 +1,24 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# 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)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import (
const (
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

View File

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

View File

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

View File

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

246
internal/views/dump.go Normal file
View File

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

View File

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

View File

@ -26,7 +26,6 @@ type forwardView struct {
*tview.Pages
app *appView
current igniter
cancel context.CancelFunc
bench *benchmark
}
@ -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")
}

View File

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

View File

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

View File

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

View File

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