checkpoint

mine
derailed 2019-11-13 23:14:58 -07:00
parent 05558c89fa
commit 2c57888bac
61 changed files with 1122 additions and 743 deletions

301
.golangci.yml Normal file
View File

@ -0,0 +1,301 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 1m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
# build-tags:
# - mytag
# which dirs to skip: issues from them won't be reported;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but default dirs are skipped independently
# from this option's value (see skip-dirs-use-default).
# skip-dirs:
# - src/external_libs
# - autogenerated_by_my_lib
# default is true. Enables skipping of directories:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs-use-default: true
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
# skip-files:
# - ".*\\.my\\.go$"
# - lib/bad.go
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
# to go.mod are needed. This setting is most useful to check that go.mod does
# not need updates, such as in a continuous integration and testing system.
# If invoked with -mod=vendor, the go command assumes that the vendor
# directory holds the correct copies of dependencies and ignores
# the dependency descriptions in go.mod.
# modules-download-mode: readonly|release|vendor
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# all available settings of specific linters
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: true
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
# [deprecated] comma-separated list of pairs of the form pkg:regex
# the regex is used to ignore names within pkg. (default "fmt:.*").
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
# ignore: fmt:.*,io/ioutil:^Read.*
# path to a file containing a list of functions to exclude from checking
# see https://github.com/kisielk/errcheck#excluding-functions for details
# exclude: /path/to/file.txt
funlen:
lines: 60
statements: 40
govet:
# report about shadowed variables
check-shadowing: true
# settings per analyzer
settings:
printf: # analyzer name, run `go tool vet help` to see all analyzers
funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
# enable or disable analyzers by name
enable:
- atomicalign
enable-all: false
disable:
- shadow
disable-all: false
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
goimports:
# put imports beginning with prefix after 3rd-party packages;
# it's a comma-separated list of prefixes
local-prefixes: github.com/org/project
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10
gocognit:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
depguard:
list-type: blacklist
include-go-root: false
packages:
- github.com/sirupsen/logrus
packages-with-error-messages:
# specify an error message to output when a blacklisted package is used
github.com/sirupsen/logrus: "logging is allowed only by logutils.Log"
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
ignore-words:
- someword
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 120
# tab width in spaces. Default to 1.
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks';
# See https://go-critic.github.io/overview#checks-overview
# To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
# By default list of stable checks is used.
enabled-checks:
- rangeValCopy
# Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
disabled-checks:
- regexpMust
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
enabled-tags:
- performance
settings: # settings passed to gocritic
captLocal: # must be valid enabled check name
paramsOnly: true
rangeValCopy:
sizeThreshold: 32
godox:
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
# might be left in the code accidentally and should be resolved before merging
keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
- NOTE
- OPTIMIZE # marks code that should be optimized before merging
- HACK # marks hack-arounds that should be removed before merging
dogsled:
# checks assignments with too many blank identifiers; default is 2
max-blank-identifiers: 2
whitespace:
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
wsl:
# If true append is only allowed to be cuddled if appending value is
# matching variables, fields or types on line above. Default is true.
strict-append: true
# Allow calls and assignments to be cuddled as long as the lines have any
# matching variables, fields or types. Default is true.
allow-assign-and-call: true
# Allow multiline assignments to be cuddled. Default is true.
allow-multiline-assign: true
# Allow declarations (var) to be cuddled.
allow-cuddle-declarations: false
# Allow trailing comments in ending of blocks
allow-trailing-comment: false
# Force newlines in end of case at this limit (0 = never).
force-case-trailing-whitespace: 0
linters:
enable:
- megacheck
- govet
disable:
- maligned
- prealloc
disable-all: false
presets:
- bugs
- unused
fast: false
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- abcdef
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via "nolint" comments.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some staticcheck messages
- linters:
- staticcheck
text: "SA9003:"
# Exclude lll issues for long lines with go:generate
- linters:
- lll
source: "^//go:generate "
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: false
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
# Show only new issues created after git revision `REV`
new-from-rev: REV
# Show only new issues created in git patch with set file path.
new-from-patch: path/to/patch/file

View File

@ -22,7 +22,6 @@ This is a maintenance release to mainly resolve outstanding issues and bugs.
Added two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node view.
---
## Resolved Bugs

View File

@ -12,6 +12,11 @@ type MenuHint struct {
Visible bool
}
// IsBlank checks if menu hint is a place holder.
func (m MenuHint) IsBlank() bool {
return m.Mnemonic == "" && m.Description == "" && m.Visible == false
}
// MenuHints represents a collection of hints.
type MenuHints []MenuHint

View File

@ -74,14 +74,9 @@ func (s *Stack) RemoveListener(l StackListener) {
// AddListener registers a stack listener.
func (s *Stack) AddListener(l StackListener) {
s.listeners = append(s.listeners, l)
log.Debug().Msgf("Stack Add listener %#v", s.components)
s.DumpStack()
if s.Empty() {
log.Debug().Msgf("Stack is empty!")
} else {
log.Debug().Msgf("TOP is %s", s.Top().Name())
}
if !s.Empty() {
l.StackTop(s.Top())
}
}
// Dump prints out the stack.
@ -114,7 +109,7 @@ func (s *Stack) Pop() (Component, bool) {
c.Stop()
if top := s.Top(); top != nil {
log.Debug().Msgf("Calling Restart on %s", top.Name())
log.Debug().Msgf("Calling Start on %s", top.Name())
top.Start()
}
@ -131,6 +126,15 @@ func (s *Stack) IsLast() bool {
return len(s.components) == 1
}
// Previous returns the previous component if any.
func (s *Stack) Previous() Component {
if s.IsLast() {
return s.Top()
}
return s.components[len(s.components)-2]
}
// Top returns the top most item or nil if the stack is empty.
func (s *Stack) Top() Component {
if s.Empty() {

View File

@ -64,11 +64,6 @@ func (r *Container) Marshal(path string) (string, error) {
return "", nil
}
// // PodLogs tail logs for all containers in a running Pod.
// func (r *Container) PodLogs(ctx context.Context, c chan<- string, ns, n string, lines int64, prev bool) error {
// return nil
// }
// Logs tails a given container logs
func (r *Container) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
res, ok := r.Resource.(k8s.Loggable)

View File

@ -55,7 +55,7 @@ type (
func NewPodList(c Connection, ns string) List {
return NewList(
ns,
"po",
"pods",
NewPod(c),
AllVerbsAccess|DescribeAccess,
)

View File

@ -2,7 +2,6 @@ package ui
import (
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
@ -13,7 +12,6 @@ type App struct {
Configurator
Main *Pages
Hint *model.Hint
actions KeyActions
@ -28,7 +26,6 @@ func NewApp() *App {
actions: make(KeyActions),
Main: NewPages(),
cmdBuff: NewCmdBuff(':', CommandBuff),
Hint: model.NewHint(),
}
a.RefreshStyles()
@ -50,8 +47,6 @@ func (a *App) Init() {
a.SetInputCapture(a.keyboard)
a.cmdBuff.AddListener(a.Cmd())
a.SetRoot(a.Main, true)
a.Hint.AddListener(a.Menu())
}
// Conn returns an api server connection.

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview"
"github.com/rs/zerolog/log"
)
// Crumbs represents user breadcrumbs.
@ -35,14 +34,12 @@ func NewCrumbs(styles *config.Styles) *Crumbs {
// StackPushed indicates a new item was added.
func (v *Crumbs) StackPushed(c model.Component) {
v.stack.Push(c)
log.Debug().Msgf(">>> PUSH %v", v.stack.Flatten())
v.refresh(v.stack.Flatten())
}
// StackPopped indicates an item was deleted
func (v *Crumbs) StackPopped(_, _ model.Component) {
v.stack.Pop()
log.Debug().Msgf("<<< POP %v", v.stack.Flatten())
v.refresh(v.stack.Flatten())
}

View File

@ -17,6 +17,7 @@ import (
const (
menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s "
maxRows = 7
chopWidth = 20
)
var menuRX = regexp.MustCompile(`\d`)
@ -36,9 +37,16 @@ func NewMenu(styles *config.Styles) *Menu {
return &v
}
// HintsChanged updates the menu based on hints changing.
func (v *Menu) HintsChanged(hh model.MenuHints) {
v.HydrateMenu(hh)
func (v *Menu) StackPushed(c model.Component) {
v.HydrateMenu(c.Hints())
}
func (v *Menu) StackPopped(o, n model.Component) {
v.HydrateMenu(n.Hints())
}
func (v *Menu) StackTop(t model.Component) {
v.HydrateMenu(t.Hints())
}
// HydrateMenu populate menu ui from hints.
@ -58,17 +66,34 @@ func (v *Menu) HydrateMenu(hh model.MenuHints) {
}
}
func (v *Menu) hasDigits(hh model.MenuHints) bool {
for _, h := range hh {
if !h.Visible {
continue
}
if menuRX.MatchString(h.Mnemonic) {
return true
}
}
return false
}
func (v *Menu) buildMenuTable(hh model.MenuHints) [][]string {
table := make([]model.MenuHints, maxRows+1)
colCount := (len(hh) / maxRows) + 1
if v.hasDigits(hh) {
colCount++
}
for row := 0; row < maxRows; row++ {
table[row] = make(model.MenuHints, colCount+1)
table[row] = make(model.MenuHints, colCount)
}
var row, col int
firstCmd := true
maxKeys := make([]int, colCount+1)
maxKeys := make([]int, colCount)
for _, h := range hh {
if !h.Visible {
continue
@ -76,6 +101,9 @@ func (v *Menu) buildMenuTable(hh model.MenuHints) [][]string {
isDigit := menuRX.MatchString(h.Mnemonic)
if !isDigit && firstCmd {
row, col, firstCmd = 0, col+1, false
if table[0][0].IsBlank() {
col = 0
}
}
if maxKeys[col] < len(h.Mnemonic) {
maxKeys[col] = len(h.Mnemonic)
@ -142,7 +170,7 @@ func formatNSMenu(i int, name string, styles config.Frame) string {
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1)
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
return fmt.Sprintf(fmat, i, Truncate(name, 14))
return fmt.Sprintf(fmat, i, Truncate(name, chopWidth))
}
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {

View File

@ -60,6 +60,7 @@ func NewTable(title string) *Table {
}
func (t *Table) Init(ctx context.Context) {
log.Debug().Msgf("UI Table INIT %q", t.baseTitle)
t.styles = ctx.Value(KeyStyles).(*config.Styles)
t.SetFixed(1, 0)
@ -78,7 +79,6 @@ func (t *Table) Init(ctx context.Context) {
t.SetSelectionChangedFunc(t.selChanged)
t.SetInputCapture(t.keyboard)
}
// SendKey sends an keyboard event (testing only!).

View File

@ -55,7 +55,7 @@ func (a *Alias) registerActions() {
a.RmAction(tcell.KeyCtrlS)
a.AddActions(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Goto Resource", a.gotoCmd, true),
tcell.KeyEscape: ui.NewKeyAction("Reset", a.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", a.activateCmd, false),
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.SortColCmd(0), false),
@ -82,6 +82,7 @@ func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if r != 0 {
s := ui.TrimCell(a.Table.Table, r, 1)
tokens := strings.Split(s, ",")
a.app.Content.Pop()
a.app.gotoResource(tokens[0], true)
return nil
}

View File

@ -19,7 +19,6 @@ import (
const (
splashTime = 1
devMode = "dev"
clusterRefresh = time.Duration(5 * time.Second)
indicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
)
@ -27,8 +26,6 @@ const (
// ActionsFunc augments Keybindinga.
type ActionsFunc func(ui.KeyActions)
type focusHandler func(tview.Primitive)
type forwarder interface {
Start(path, co string, ports []string) (*portforward.PortForwarder, error)
Stop()
@ -86,7 +83,9 @@ func (a *App) ActiveView() model.Component {
}
func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey {
if !a.Content.IsLast() {
a.Content.Pop()
}
return nil
}
@ -95,6 +94,7 @@ func (a *App) Init(version string, rate int) {
ctx := context.WithValue(context.Background(), ui.KeyApp, a)
a.Content.Init(ctx)
a.Content.Stack.AddListener(a.Crumbs())
a.Content.Stack.AddListener(a.Menu())
a.version = version
a.CmdBuff().AddListener(a)
@ -122,15 +122,6 @@ func (a *App) Init(version string, rate int) {
a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
// ctx := context.WithValue(context.Background(), ui.KeyApp, a)
// a.Content.Init(ctx)
// d := NewDetails(a, nil)
// d.SetText("Fuck!!")
// a.Content.Push(d)
// d = NewDetails(a, nil)
// d.SetText("Shit!!")
// a.Content.Push(d)
main.AddItem(a.indicator(), 1, 1, false)
main.AddItem(a.Content, 0, 10, true)
main.AddItem(a.Crumbs(), 2, 1, false)
@ -138,31 +129,12 @@ func (a *App) Init(version string, rate int) {
a.toggleHeader(!a.Config.K9s.GetHeadless())
}
// func (a *App) StackPushed(c model.Component) {
// ctx := context.WithValue(context.Background(), ui.KeyApp, a)
// ctx, a.cancelFn = context.WithCancel(context.Background())
// c.Init(ctx)
// a.Frame().AddPage(c.Name(), c, true, true)
// a.SetFocus(c)
// a.setHints(c.Hints())
// }
// func (a *App) StackPopped(o, c model.Component) {
// a.Frame().RemovePage(o.Name())
// if c != nil {
// a.StackPushed(c)
// }
// }
// func (a *App) StackTop(model.Component) {
// }
// Changed indicates the buffer was changed.
func (a *App) BufferChanged(s string) {}
// Active indicates the buff activity changed.
func (a *App) BufferActive(state bool, _ ui.BufferKind) {
log.Debug().Msgf("App Buffer Activated!")
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
if !ok {
return
@ -258,7 +230,9 @@ func (a *App) switchNS(ns string) bool {
log.Debug().Msgf("Namespace did not change %s", ns)
return true
}
a.Config.SetActiveNamespace(ns)
if err := a.Config.SetActiveNamespace(ns); err != nil {
log.Error().Err(err).Msg("Config Set NS failed!")
}
return a.startInformer(ns)
}
@ -276,7 +250,9 @@ func (a *App) switchCtx(ctx string, load bool) error {
}
a.startInformer(ns)
a.Config.Reset()
a.Config.Save()
if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
a.Flash().Infof("Switching context to %s", ctx)
if load {
a.gotoResource("po", true)

View File

@ -12,7 +12,6 @@ import (
"time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/perf"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
@ -23,7 +22,6 @@ import (
const (
benchTitle = "Benchmarks"
benchTitleFmt = " [seagreen::b]%s([fuchsia::b]%d[fuchsia::-])[seagreen::-] "
)
var (
@ -42,13 +40,14 @@ type Bench struct {
cancelFn context.CancelFunc
}
func NewBench(title, gvr string, _ resource.List) ResourceViewer {
// NewBench returns a new viewer.
func NewBench(_, _ string, _ resource.List) ResourceViewer {
return &Bench{
MasterDetail: NewMasterDetail(title),
MasterDetail: NewMasterDetail(benchTitle, ""),
}
}
// Init the view.
// Init initializes the viewer.
func (b *Bench) Init(ctx context.Context) {
b.MasterDetail.Init(ctx)
b.keyBindings()
@ -101,7 +100,7 @@ func (b *Bench) refresh() {
func (b *Bench) keyBindings() {
aa := ui.KeyActions{
ui.KeyP: ui.NewKeyAction("Previous", b.app.PrevCmd, false),
tcell.KeyEsc: ui.NewKeyAction("Back", b.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Enter", b.enterCmd, false),
tcell.KeyCtrlD: ui.NewKeyAction("Delete", b.deleteCmd, false),
}
@ -112,16 +111,6 @@ func (b *Bench) getTitle() string {
return benchTitle
}
func (b *Bench) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
tv := b.masterPage()
tv.SetSortCol(tv.NameColIndex()+col, 0, asc)
tv.Refresh()
return nil
}
}
func (b *Bench) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
if b.masterPage().SearchBuff().IsActive() {
return b.masterPage().filterCmd(evt)
@ -172,14 +161,6 @@ func (b *Bench) benchFile() string {
return ui.TrimCell(b.masterPage().Table, r, 7)
}
func (b *Bench) Hints() model.MenuHints {
if h, ok := b.CurrentPage().Item.(model.Hinter); ok {
return h.Hints()
}
return nil
}
func (b *Bench) hydrate() resource.TableData {
ff, err := loadBenchDir(b.app.Config)
if err != nil {

View File

@ -60,6 +60,8 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color {
case "Completed":
return ui.CompletedColor
case "Running":
case "Terminating":
return ui.KillColor
default:
c = ui.ErrColor
}

View File

@ -100,7 +100,10 @@ func (c *command) run(cmd string) bool {
switch cmds[0] {
case "ctx", "context", "contexts":
if len(cmds) == 2 {
c.app.switchCtx(cmds[1], true)
if err := c.app.switchCtx(cmds[1], true); err != nil {
log.Error().Err(err).Msg("Context switch failed!")
return false
}
return true
}
view := c.componentFor(gvr, v)

View File

@ -1,6 +1,7 @@
package view
import (
"context"
"errors"
"fmt"
"strings"
@ -25,13 +26,19 @@ func NewContainer(title string, list resource.List, path string) ResourceViewer
LogResource: NewLogResource(title, "", list),
}
c.path = &path
return &c
}
// Init initializes the viewer.
func (c *Container) Init(ctx context.Context) {
c.envFn = c.k9sEnv
c.containerFn = c.selectedContainer
c.extraActionsFn = c.extraActions
c.enterFn = c.viewLogs
c.colorerFn = containerColorer
return &c
c.LogResource.Init(ctx)
}
// Start starts the component.
@ -49,8 +56,6 @@ func (c *Container) extraActions(aa ui.KeyActions) {
aa[ui.KeyShiftF] = ui.NewKeyAction("PortForward", c.portFwdCmd, true)
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", c.prevLogsCmd, true)
aa[ui.KeyS] = ui.NewKeyAction("Shell", c.shellCmd, true)
aa[tcell.KeyEscape] = ui.NewKeyAction("Back", c.backCmd, false)
aa[ui.KeyP] = ui.NewKeyAction("Previous", c.backCmd, false)
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort CPU", c.sortColCmd(6, false), false)
aa[ui.KeyShiftM] = ui.NewKeyAction("Sort MEM", c.sortColCmd(7, false), false)
aa[ui.KeyShiftX] = ui.NewKeyAction("Sort CPU%", c.sortColCmd(8, false), false)
@ -169,7 +174,3 @@ func (c *Container) runForward(pf *k8s.PortForward, f *portforward.PortForwarder
pf.SetActive(false)
})
}
func (c *Container) backCmd(evt *tcell.EventKey) *tcell.EventKey {
return c.app.PrevCmd(evt)
}

View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestContainerNew(t *testing.T) {
po := view.NewContainer("Container", resource.NewContainerList(nil, nil), "fred/blee")
po.Init(makeCtx())
assert.Equal(t, "containers", po.Name())
assert.Equal(t, 22, len(po.Hints()))
}

View File

@ -53,7 +53,9 @@ func (c *Context) useContext(name string) error {
return err
}
c.app.switchCtx(name, false)
if err := c.app.switchCtx(name, false); err != nil {
return err
}
c.refresh()
if tv, ok := c.GetPrimitive("ctx").(*Table); ok {
tv.Select(1, 0)

View File

@ -18,7 +18,7 @@ import (
const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] "
// Details presents a generic text viewer.
// Details represents a generic text viewer.
type Details struct {
*tview.TextView
@ -40,6 +40,7 @@ func NewDetails(app *App, backFn ui.ActionHandler) *Details {
}
}
// Init initializes the viewer.
func (d *Details) Init(ctx context.Context) {
d.app = ctx.Value(ui.KeyApp).(*App)
@ -63,8 +64,13 @@ func (d *Details) Init(ctx context.Context) {
})
}
// Name returns the component name.
func (d *Details) Name() string { return "details" }
// Start starts the view updater.
func (d *Details) Start() {}
// Stop terminates the updater.
func (d *Details) Stop() {}
func (d *Details) bindKeys() {
@ -219,10 +225,7 @@ func (d *Details) setActions(aa ui.KeyActions) {
// Hints fetch mmemonic and hints
func (d *Details) Hints() model.MenuHints {
if d.actions != nil {
return d.actions.Hints()
}
return nil
}
func (d *Details) refreshTitle() {

View File

@ -20,10 +20,6 @@ const (
helpTitleFmt = " [aqua::b]%s "
)
type helpItem struct {
key, description string
}
// Help presents a help viewer.
type Help struct {
*ui.Table
@ -49,13 +45,16 @@ func (v *Help) Init(ctx context.Context) {
v.SetBorderPadding(0, 0, 1, 1)
v.SetInputCapture(v.keyboard)
v.bindKeys()
v.build(v.app.Hint.Peek())
v.build(v.app.Content.Previous().Hints())
}
func (v *Help) Name() string { return helpTitle }
func (v *Help) Start() {}
func (v *Help) Stop() {}
func (v *Help) Hints() model.MenuHints { return v.actions.Hints() }
func (v *Help) Hints() model.MenuHints {
log.Debug().Msgf("Help Hints %#v", v.actions.Hints())
return v.actions.Hints()
}
func (v *Help) bindKeys() {
v.actions = ui.KeyActions{
@ -165,10 +164,6 @@ func (v *Help) showGeneral() model.MenuHints {
Mnemonic: "Shift-i",
Description: "Invert Sort",
},
{
Mnemonic: "p",
Description: "Previous View",
},
{
Mnemonic: ":q",
Description: "Quit",

View File

@ -1,29 +1,35 @@
package view_test
// import (
// "testing"
import (
"testing"
// "github.com/derailed/k9s/internal/config"
// "github.com/derailed/k9s/internal/model"
// "github.com/stretchr/testify/assert"
// v1 "k8s.io/api/core/v1"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// func newNS(n string) v1.Namespace {
// return v1.Namespace{ObjectMeta: metav1.ObjectMeta{
// Name: n,
// }}
// }
func newNS(n string) v1.Namespace {
return v1.Namespace{ObjectMeta: metav1.ObjectMeta{
Name: n,
}}
}
// func TestHelpNew(t *testing.T) {
// a := view.NewApp(config.NewConfig(ks{}))
// v := view.NewHelp()
// ctx := context.WithValue(ui.KeyApp, app)
// v.Init(ctx)
func TestHelpNew(t *testing.T) {
ctx := makeCtx()
// app.SetHints(model.MenuHints{{Mnemonic: "blee", Description: "duh"}})
app := ctx.Value(ui.KeyApp).(*view.App)
po := view.NewPod("Pod", "blee", resource.NewPodList(nil, ""))
po.Init(ctx)
app.Content.Push(po)
// assert.Equal(t, "<blee>", v.GetCell(1, 0).Text)
// assert.Equal(t, "duh", v.GetCell(1, 1).Text)
// }
v := view.NewHelp()
v.Init(ctx)
assert.Equal(t, 32, v.GetRowCount())
assert.Equal(t, 10, v.GetColumnCount())
assert.Equal(t, "<esc>", v.GetCell(1, 0).Text)
assert.Equal(t, "Back", v.GetCell(1, 1).Text)
}

View File

@ -24,7 +24,7 @@ func (j *Job) extraActions(aa ui.KeyActions) {
j.LogResource.extraActions(aa)
}
func (j *Job) showPods(app *App, ns, res, sel string) {
func (j *Job) showPods(app *App, _, res, sel string) {
ns, n := namespaced(sel)
job, err := k8s.NewJob(app.Conn()).Get(ns, n)
if err != nil {

View File

@ -6,7 +6,6 @@ import (
"os"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/derailed/k9s/internal/config"
@ -17,44 +16,34 @@ import (
"github.com/rs/zerolog/log"
)
type logFrame struct {
// Log represents a generic log viewer.
type Log struct {
*tview.Flex
app *App
actions ui.KeyActions
backFn ui.ActionHandler
logs *Details
scrollIndicator *AutoScrollIndicator
ansiWriter io.Writer
path string
}
func newLogFrame(app *App, backFn ui.ActionHandler) *logFrame {
f := logFrame{
// NewLog returns a new viewer.
func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log {
l := Log{
Flex: tview.NewFlex(),
app: app,
backFn: backFn,
actions: make(ui.KeyActions),
}
f.SetBorder(true)
f.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor))
f.SetBorderPadding(0, 0, 1, 1)
f.SetDirection(tview.FlexRow)
l.SetBorder(true)
l.SetBackgroundColor(config.AsColor(app.Styles.Views().Log.BgColor))
l.SetBorderPadding(0, 0, 1, 1)
l.SetDirection(tview.FlexRow)
return &f
}
type Log struct {
*logFrame
logs *Details
status *statusView
ansiWriter io.Writer
autoScroll int32
path string
}
func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log {
l := Log{
logFrame: newLogFrame(app, backFn),
autoScroll: 1,
}
l.scrollIndicator = NewAutoScrollIndicator(app.Styles)
l.AddItem(l.scrollIndicator, 1, 1, false)
l.logs = NewDetails(app, backFn)
{
@ -67,8 +56,6 @@ func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log {
l.logs.SetMaxBuffer(app.Config.K9s.LogBufferSize)
}
l.ansiWriter = tview.ANSIWriter(l.logs, app.Styles.Views().Log.FgColor, app.Styles.Views().Log.BgColor)
l.status = newStatusView(app.Styles)
l.AddItem(l.status, 1, 1, false)
l.AddItem(l.logs, 0, 1, true)
l.bindKeys()
@ -77,16 +64,26 @@ func NewLog(_ string, app *App, backFn ui.ActionHandler) *Log {
return &l
}
// Logs return the viewer logs.
func (l *Log) Logs() *Details {
return l.logs
}
// ScrollIndicator returns the scroll mode viewer.
func (l *Log) ScrollIndicator() *AutoScrollIndicator {
return l.scrollIndicator
}
func (l *Log) bindKeys() {
l.actions = ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", l.backCmd, true),
ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleScrollCmd, true),
ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true),
ui.KeyG: ui.NewKeyAction("Top", l.topCmd, false),
ui.KeyShiftG: ui.NewKeyAction("Bottom", l.bottomCmd, false),
ui.KeyF: ui.NewKeyAction("Up", l.pageUpCmd, false),
ui.KeyB: ui.NewKeyAction("Down", l.pageDownCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true),
}
}
@ -124,32 +121,24 @@ func (l *Log) log(lines string) {
log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount())
}
func (l *Log) flush(index int, buff []string) {
if index == 0 {
// Flush write logs to viewer.
func (l *Log) Flush(index int, buff []string) {
if index == 0 || !l.scrollIndicator.AutoScroll() {
return
}
if atomic.LoadInt32(&l.autoScroll) == 1 {
l.log(strings.Join(buff[:index], "\n"))
l.app.QueueUpdateDraw(func() {
l.updateIndicator()
l.scrollIndicator.Refresh()
l.logs.ScrollToEnd()
})
}
}
func (l *Log) updateIndicator() {
status := "Off"
if l.autoScroll == 1 {
status = "On"
}
l.status.update([]string{fmt.Sprintf("Autoscroll: %s", status)})
}
// ----------------------------------------------------------------------------
// Actions...
func (l *Log) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
// SaveCmd dumps the logs to file.
func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey {
if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.path, l.logs.GetText(true)); err != nil {
l.app.Flash().Err(err)
} else {
@ -183,28 +172,18 @@ func saveData(cluster, name, data string) (string, error) {
log.Error().Err(err).Msgf("LogFile create %s", path)
return "", nil
}
if _, err := fmt.Fprintf(file, data); err != nil {
if _, err := file.Write([]byte(data)); err != nil {
return "", err
}
return path, nil
}
func (l *Log) toggleScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
if atomic.LoadInt32(&l.autoScroll) == 0 {
atomic.StoreInt32(&l.autoScroll, 1)
} else {
atomic.StoreInt32(&l.autoScroll, 0)
}
if atomic.LoadInt32(&l.autoScroll) == 1 {
l.app.Flash().Info("Autoscroll is on.")
l.logs.ScrollToEnd()
} else {
l.logs.LineUp()
l.app.Flash().Info("Autoscroll is off.")
}
l.updateIndicator()
// ToggleAutoScrollCmd toggles auto scrolling of logs.
func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {
l.scrollIndicator.ToggleAutoScroll()
l.scrollIndicator.Refresh()
return nil
}

View File

@ -6,6 +6,7 @@ import (
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
// ContainerFn returns the active container name.
@ -85,8 +86,9 @@ func (l *LogResource) showLogs(prev bool) {
}
func (l *LogResource) backCmd(evt *tcell.EventKey) *tcell.EventKey {
// Reset namespace to what it was
l.app.Config.SetActiveNamespace(l.list.GetNamespace())
if err := l.app.Config.SetActiveNamespace(l.list.GetNamespace()); err != nil {
log.Error().Err(err).Msg("Config NS set failed!")
}
l.app.inject(l)
return nil

View File

@ -1,81 +1,81 @@
package view_test
// import (
// "bytes"
// "fmt"
// "testing"
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
// "github.com/derailed/k9s/internal/config"
// "github.com/derailed/k9s/internal/view"
// "github.com/derailed/tview"
// "github.com/stretchr/testify/assert"
// )
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/view"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
)
// func TestAnsi(t *testing.T) {
// buff := bytes.NewBufferString("")
// w := tview.ANSIWriter(buff, "white", "black")
// fmt.Fprintf(w, "[YELLOW] ok")
// assert.Equal(t, "[YELLOW] ok", buff.String())
func TestAnsi(t *testing.T) {
buff := bytes.NewBufferString("")
w := tview.ANSIWriter(buff, "white", "black")
fmt.Fprintf(w, "[YELLOW] ok")
assert.Equal(t, "[YELLOW] ok", buff.String())
// v := tview.NewTextView()
// v.SetDynamicColors(true)
// aw := tview.ANSIWriter(v, "white", "black")
// s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]"
// fmt.Fprintf(aw, s)
// assert.Equal(t, s+"\n", v.GetText(false))
// }
v := tview.NewTextView()
v.SetDynamicColors(true)
aw := tview.ANSIWriter(v, "white", "black")
s := "[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]"
fmt.Fprintf(aw, s)
assert.Equal(t, s+"\n", v.GetText(false))
}
// func TestLogFlush(t *testing.T) {
// v := view.NewLog("Logs", NewApp(config.NewConfig(ks{})), nil)
// v.flush(2, []string{"blee", "bozo"})
func TestLogFlush(t *testing.T) {
v := view.NewLog("Logs", makeApp(), nil)
v.Flush(2, []string{"blee", "bozo"})
// v.toggleScrollCmd(nil)
// assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true))
// assert.Equal(t, " Autoscroll: Off ", v.status.GetText(true))
// v.toggleScrollCmd(nil)
// assert.Equal(t, " Autoscroll: On ", v.status.GetText(true))
// }
v.ToggleAutoScrollCmd(nil)
assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true))
assert.Equal(t, " Autoscroll: Off ", v.ScrollIndicator().GetText(true))
v.ToggleAutoScrollCmd(nil)
assert.Equal(t, " Autoscroll: On ", v.ScrollIndicator().GetText(true))
assert.Equal(t, 8, len(v.Hints()))
}
// func TestLogViewSave(t *testing.T) {
// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil)
// v.flush(2, []string{"blee", "bozo"})
// v.path = "k9s-test"
// dir := filepath.Join(config.K9sDumpDir, v.app.Config.K9s.CurrentCluster)
// c1, _ := ioutil.ReadDir(dir)
// v.saveCmd(nil)
// c2, _ := ioutil.ReadDir(dir)
// assert.Equal(t, len(c2), len(c1)+1)
// }
func TestLogViewSave(t *testing.T) {
app := makeApp()
v := view.NewLog("Logs", app, nil)
v.Flush(2, []string{"blee", "bozo"})
config.K9sDumpDir = "/tmp"
dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir)
v.SaveCmd(nil)
c2, _ := ioutil.ReadDir(dir)
assert.Equal(t, len(c2), len(c1)+1)
}
// func TestLogViewNav(t *testing.T) {
// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil)
// var buff []string
// v.autoScroll = 1
// for i := 0; i < 100; i++ {
// buff = append(buff, fmt.Sprintf("line-%d\n", i))
// }
// v.flush(100, buff)
func TestLogViewNav(t *testing.T) {
v := view.NewLog("Logs", makeApp(), nil)
var buff []string
for i := 0; i < 100; i++ {
buff = append(buff, fmt.Sprintf("line-%d\n", i))
}
v.Flush(100, buff)
v.ToggleAutoScrollCmd(nil)
// v.topCmd(nil)
// r, _ := v.logs.GetScrollOffset()
// assert.Equal(t, 0, r)
// v.pageDownCmd(nil)
// r, _ = v.logs.GetScrollOffset()
// assert.Equal(t, 0, r)
// v.pageUpCmd(nil)
// r, _ = v.logs.GetScrollOffset()
// assert.Equal(t, 0, r)
// v.bottomCmd(nil)
// r, _ = v.logs.GetScrollOffset()
// assert.Equal(t, 0, r)
// }
r, _ := v.Logs().GetScrollOffset()
assert.Equal(t, -1, r)
}
// func TestLogViewClear(t *testing.T) {
// v := newLogView("Logs", NewApp(config.NewConfig(ks{})), nil)
// v.flush(2, []string{"blee", "bozo"})
func TestLogViewClear(t *testing.T) {
v := view.NewLog("Logs", makeApp(), nil)
v.Flush(2, []string{"blee", "bozo"})
// v.toggleScrollCmd(nil)
// assert.Equal(t, "blee\nbozo\n", v.logs.GetText(true))
// v.clearCmd(nil)
// assert.Equal(t, "", v.logs.GetText(true))
// }
v.ToggleAutoScrollCmd(nil)
assert.Equal(t, "blee\nbozo\n", v.Logs().GetText(true))
v.Logs().Clear()
assert.Equal(t, "", v.Logs().GetText(true))
}
// Helpers...
func makeApp() *view.App {
return view.NewApp(config.NewConfig(ks{}))
}

View File

@ -14,31 +14,24 @@ import (
const (
logBuffSize = 100
flushTimeout = 200 * time.Millisecond
FlushTimeout = 200 * time.Millisecond
logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) "
logFmt = " Logs([fg:bg:]%s) "
)
type (
masterView interface {
backFn() ui.ActionHandler
App() *App
}
// Logs presents a collection of logs.
Logs struct {
// Logs presents a collection of logs.
type Logs struct {
*ui.Pages
app *App
parent loggable
parent Loggable
actions ui.KeyActions
cancelFunc context.CancelFunc
}
)
}
// NewLogs returns a new logs viewer.
func NewLogs(title string, parent loggable) *Logs {
func NewLogs(title string, parent Loggable) *Logs {
return &Logs{
Pages: ui.NewPages(),
parent: parent,
@ -55,7 +48,7 @@ func (l *Logs) Name() string { return "logs" }
// Protocol...
func (l *Logs) reload(co string, parent loggable, prevLogs bool) {
func (l *Logs) reload(co string, parent Loggable, prevLogs bool) {
l.parent = parent
l.deletePage()
l.AddPage("logs", NewLog(co, l.app, l.backCmd), true, true)
@ -152,7 +145,7 @@ func updateLogs(ctx context.Context, c <-chan string, l *Log, buffSize int) {
case line, ok := <-c:
if !ok {
log.Debug().Msgf("Closed channel detected. Bailing out...")
l.flush(index, buff)
l.Flush(index, buff)
return
}
if index < buffSize {
@ -160,12 +153,12 @@ func updateLogs(ctx context.Context, c <-chan string, l *Log, buffSize int) {
index++
continue
}
l.flush(index, buff)
l.Flush(index, buff)
index = 0
buff[index] = line
index++
case <-time.After(flushTimeout):
l.flush(index, buff)
case <-time.After(FlushTimeout):
l.Flush(index, buff)
index = 0
case <-ctx.Done():
return

View File

@ -3,7 +3,11 @@ package view
import (
"context"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
// MasterDetail presents a master-detail viewer.
@ -18,23 +22,42 @@ type MasterDetail struct {
}
// NewMasterDetail returns a new master-detail viewer.
func NewMasterDetail(title string) *MasterDetail {
func NewMasterDetail(title, ns string) *MasterDetail {
return &MasterDetail{
PageStack: NewPageStack(),
title: title,
currentNS: ns,
}
}
// Init initializes the viewer.
func (m *MasterDetail) Init(ctx context.Context) {
log.Debug().Msgf("\t>>>MasterDetail init %q", m.title)
app := ctx.Value(ui.KeyApp).(*App)
if m.currentNS != resource.NotNamespaced {
m.currentNS = app.Config.ActiveNamespace()
}
m.PageStack.Init(ctx)
m.AddListener(app.Menu())
t := NewTable(m.title)
t.Init(ctx)
m.Push(t)
m.details = NewDetails(m.app, nil)
m.details = NewDetails(m.app, func(evt *tcell.EventKey) *tcell.EventKey {
m.Pop()
return nil
})
m.details.Init(ctx)
log.Debug().Msgf("\t<<<<MasterDetail INIT DONE!!")
}
// Hints returns the current viewer hints
func (m *MasterDetail) Hints() model.MenuHints {
if c, ok := m.Top().(model.Hinter); ok {
return c.Hints()
}
return nil
}
func (m *MasterDetail) setExtraActionsFn(f ActionsFunc) {
@ -63,14 +86,46 @@ func (m *MasterDetail) detailsPage() *Details {
return m.details
}
func (m *MasterDetail) isMaster() bool {
p := m.CurrentPage()
if p == nil {
return false
}
log.Debug().Msgf("!!!!!Checking MASTER %q vs %q -- %t", p.Name, m.title, p.Name == m.title)
return p.Name == m.title
}
// ----------------------------------------------------------------------------
// Actions...
func (m *MasterDetail) defaultActions(aa ui.KeyActions) {
aa[ui.KeyHelp] = ui.NewKeyAction("Help", noopCmd, false)
aa[ui.KeyP] = ui.NewKeyAction("Previous", m.app.PrevCmd, false)
aa[tcell.KeyEsc] = ui.NewKeyAction("Back", m.backCmd, false)
if m.extraActionsFn != nil {
m.extraActionsFn(aa)
}
}
func (m *MasterDetail) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if !m.isMaster() {
return m.app.PrevCmd(evt)
}
if m.masterPage().resetCmd(evt) != nil {
return m.app.PrevCmd(evt)
}
return nil
}
func (m *MasterDetail) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t := m.masterPage()
t.SetSortCol(t.NameColIndex()+col, 0, asc)
t.Refresh()
return nil
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
// Node represents a node view.
@ -29,16 +30,6 @@ func (n *Node) extraActions(aa ui.KeyActions) {
aa[ui.KeyShiftZ] = ui.NewKeyAction("Sort MEM%", n.sortColCmd(10, false), false)
}
func (n *Node) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t := n.masterPage()
t.SetSortCol(t.NameColIndex()+col, 0, asc)
t.Refresh()
return nil
}
}
func (n *Node) showPods(app *App, _, _, sel string) {
showPods(app, "", "", "spec.nodeName="+sel, n.backCmd)
}
@ -59,9 +50,12 @@ func showPods(app *App, ns, labelSel, fieldSel string, a ui.ActionHandler) {
v := NewPod("Pod", "v1/pods", list)
v.setColorerFn(podColorer)
v.masterPage().AddActions(ui.KeyActions{
tcell.KeyEsc: ui.NewKeyAction("Back", a, true),
})
app.Config.SetActiveNamespace(ns)
// BOZO!!
// v.masterPage().AddActions(ui.KeyActions{
// tcell.KeyEsc: ui.NewKeyAction("Back", a, true),
// })
if err := app.Config.SetActiveNamespace(ns); err != nil {
log.Error().Err(err).Msg("Config NS set failed!")
}
app.inject(v)
}

View File

@ -7,12 +7,12 @@ import (
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
const (
favNSIndicator = "+"
defaultNSIndicator = "(*)"
deltaNSIndicator = "(𝜟)"
)
var nsCleanser = regexp.MustCompile(`(\w+)[+|(*)|(𝜟)]*`)
@ -59,7 +59,9 @@ func (n *Namespace) useNamespace(ns string) {
} else {
n.app.Flash().Infof("Namespace %s is now active!", ns)
}
n.app.Config.Save()
if err := n.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config file save failed!")
}
n.app.startInformer(ns)
}
@ -79,11 +81,11 @@ func (n *Namespace) decorate(data resource.TableData) resource.TableData {
}
for k, r := range data.Rows {
if config.InList(n.app.Config.FavNamespaces(), k) {
r.Fields[0] += "+"
r.Fields[0] += favNSIndicator
r.Action = resource.Unchanged
}
if n.app.Config.ActiveNamespace() == k {
r.Fields[0] += "(*)"
r.Fields[0] += defaultNSIndicator
r.Action = resource.Unchanged
}
}

View File

@ -5,7 +5,6 @@ import (
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/rs/zerolog/log"
)
type PageStack struct {
@ -22,28 +21,14 @@ func NewPageStack() *PageStack {
func (p *PageStack) Init(ctx context.Context) {
p.app = ctx.Value(ui.KeyApp).(*App)
p.Pages.SetChangedFunc(func() {
log.Debug().Msgf(">>>>>PS CHNGED<<<<<")
p.DumpStack()
active := p.CurrentPage()
if active == nil {
return
}
c := active.Item.(model.Component)
log.Debug().Msgf("-------Page activated %#v", active)
p.app.Hint.SetHints(c.Hints())
})
p.Pages.SetTitle("Fuck!")
p.Stack.AddListener(p)
}
func (p *PageStack) StackPushed(c model.Component) {
ctx := context.WithValue(context.Background(), ui.KeyApp, p.app)
c.Init(ctx)
c.Start()
p.app.SetFocus(c)
p.app.Hint.SetHints(c.Hints())
}
func (p *PageStack) StackPopped(o, top model.Component) {
@ -57,5 +42,4 @@ func (p *PageStack) StackTop(top model.Component) {
}
top.Start()
p.app.SetFocus(top)
p.app.Hint.SetHints(top.Hints())
}

View File

@ -1,6 +1,7 @@
package view
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/model"
@ -18,12 +19,14 @@ const (
shellCheck = "command -v bash >/dev/null && exec bash || exec sh"
)
type loggable interface {
type Loggable interface {
getSelection() string
getList() resource.List
Pop() (model.Component, bool)
}
var _ Loggable = &Pod{}
// Pod represents a pod viewer.
type Pod struct {
*Resource
@ -34,20 +37,23 @@ type Pod struct {
// NewPod returns a new viewer.
func NewPod(title, gvr string, list resource.List) ResourceViewer {
p := Pod{
return &Pod{
Resource: NewResource(title, gvr, list),
}
}
// Init initializes the viewer.
func (p *Pod) Init(ctx context.Context) {
p.extraActionsFn = p.extraActions
p.enterFn = p.listContainers
p.Resource.Init(ctx)
p.picker = newSelectList(&p)
p.picker = newSelectList(p)
p.picker.setActions(ui.KeyActions{
tcell.KeyEscape: {Description: "Back", Action: p.backCmd, Visible: true},
})
p.logs = NewLogs(list.GetName(), &p)
return &p
p.logs = NewLogs(p.list.GetName(), p)
p.logs.Init(ctx)
}
func (p *Pod) extraActions(aa ui.KeyActions) {
@ -143,9 +149,8 @@ func (p *Pod) viewLogs(prev bool) bool {
return true
}
func (p *Pod) showLogs(path, co string, parent loggable, prev bool) {
l := p.GetPrimitive("logs").(*Logs)
l.reload(co, parent, prev)
func (p *Pod) showLogs(path, co string, parent Loggable, prev bool) {
p.logs.reload(co, parent, prev)
p.Push(p.logs)
}
@ -180,16 +185,6 @@ func (p *Pod) shellIn(path, co string) {
p.Start()
}
func (p *Pod) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t := p.masterPage()
t.SetSortCol(t.NameColIndex()+col, 0, asc)
t.Refresh()
return nil
}
}
// ----------------------------------------------------------------------------
// Helpers...

View File

@ -1,15 +1,27 @@
package view_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestPodNew(t *testing.T) {
po := view.NewPod("test", "blee", resource.NewPodList(nil, ""))
po := view.NewPod("Pod", "blee", resource.NewPodList(nil, ""))
po.Init(makeCtx())
assert.Equal(t, "po", po.Name())
assert.Equal(t, "pods", po.Name())
assert.Equal(t, 31, len(po.Hints()))
}
// Helpers...
func makeCtx() context.Context {
cfg := config.NewConfig(ks{})
return context.WithValue(context.Background(), ui.KeyApp, view.NewApp(cfg))
}

View File

@ -5,7 +5,6 @@ import (
"fmt"
"time"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
@ -85,9 +84,8 @@ func (p *Policy) bindKeys() {
p.RmAction(ui.KeyShiftA)
p.AddActions(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Reset", p.resetCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", p.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", p.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", p.app.PrevCmd, false),
ui.KeyShiftS: ui.NewKeyAction("Sort Namespace", p.SortColCmd(0), false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.SortColCmd(1), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.SortColCmd(2), false),
@ -130,10 +128,6 @@ func (p *Policy) backCmd(evt *tcell.EventKey) *tcell.EventKey {
return p.app.PrevCmd(evt)
}
func (p *Policy) Hints() model.MenuHints {
return p.Hints()
}
func (p *Policy) reconcile() (resource.TableData, error) {
var table resource.TableData

View File

@ -19,14 +19,14 @@ import (
)
const (
forwardTitle = "Port Forwards"
forwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] "
portForwardTitle = "PortForwards"
portForwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] "
promptPage = "prompt"
)
// PortForward presents active portforward viewer.
type PortForward struct {
*ui.Pages
*MasterDetail
cancelFn context.CancelFunc
bench *perf.Benchmark
@ -34,27 +34,26 @@ type PortForward struct {
}
// NewPortForward returns a new viewer.
func NewPortForward(title, _ string, list resource.List) ResourceViewer {
func NewPortForward(title, gvr string, list resource.List) ResourceViewer {
return &PortForward{
Pages: ui.NewPages(),
MasterDetail: NewMasterDetail(portForwardTitle, ""),
}
}
// Init the view.
func (p *PortForward) Init(ctx context.Context) {
p.app = ctx.Value(ui.KeyApp).(*App)
p.MasterDetail.Init(ctx)
p.registerActions()
tv := NewTable(forwardTitle)
tv.Init(ctx)
tv := p.masterPage()
tv.SetBorderFocusColor(tcell.ColorDodgerBlue)
tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDodgerBlue, tcell.AttrNone)
tv.SetColorerFn(forwardColorer)
tv.SetActiveNS("")
tv.SetSortCol(tv.NameColIndex()+6, 0, true)
tv.Select(1, 0)
p.Push(tv)
p.registerActions()
p.Start()
p.refresh()
}
@ -71,11 +70,7 @@ func (p *PortForward) Start() {
func (p *PortForward) Stop() {}
func (p *PortForward) Name() string {
return "portForwards"
}
func (p *PortForward) masterPage() *Table {
return p.GetPrimitive("table").(*Table)
return portForwardTitle
}
func (p *PortForward) setEnterFn(enterFn) {}
@ -83,13 +78,6 @@ func (p *PortForward) setColorerFn(ui.ColorerFunc) {}
func (p *PortForward) setDecorateFn(decorateFn) {}
func (p *PortForward) setExtraActionsFn(ActionsFunc) {}
func (p *PortForward) getTV() *Table {
if vu, ok := p.GetPrimitive("table").(*Table); ok {
return vu
}
return nil
}
func (p *PortForward) reload() {
path := ui.BenchConfig(p.app.Config.K9s.CurrentCluster)
log.Debug().Msgf("Reloading Config %s", path)
@ -100,38 +88,28 @@ func (p *PortForward) reload() {
}
func (p *PortForward) refresh() {
tv := p.getTV()
tv := p.masterPage()
tv.Update(p.hydrate())
p.app.SetFocus(tv)
tv.UpdateTitle()
}
func (p *PortForward) registerActions() {
tv := p.getTV()
tv := p.masterPage()
tv.AddActions(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoBenchCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Benchmarks", p.gotoBenchCmd, true),
tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true),
tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true),
tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true),
ui.KeySlash: ui.NewKeyAction("Filter", tv.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", p.app.PrevCmd, false),
tcell.KeyEsc: ui.NewKeyAction("Back", p.app.PrevCmd, false),
ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.sortColCmd(2, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.sortColCmd(4, true), false),
})
}
func (p *PortForward) getTitle() string {
return forwardTitle
}
func (p *PortForward) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
tv := p.getTV()
tv.SetSortCol(tv.NameColIndex()+col, 0, asc)
p.refresh()
return nil
}
return portForwardTitle
}
func (p *PortForward) gotoBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
@ -162,7 +140,7 @@ func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
tv := p.getTV()
tv := p.masterPage()
r, _ := tv.GetSelection()
cfg, co := defaultConfig(), ui.TrimCell(tv.Table, r, 2)
if b, ok := p.app.Bench.Benchmarks.Containers[containerID(sel, co)]; ok {
@ -205,7 +183,7 @@ func (p *PortForward) runBenchmark() {
}
func (p *PortForward) getSelectedItem() string {
tv := p.getTV()
tv := p.masterPage()
r, _ := tv.GetSelection()
if r == 0 {
return ""
@ -217,7 +195,7 @@ func (p *PortForward) getSelectedItem() string {
}
func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
tv := p.getTV()
tv := p.masterPage()
if !tv.SearchBuff().Empty() {
tv.SearchBuff().Reset()
return nil
@ -238,7 +216,7 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
delete(p.app.forwarders, sel)
log.Debug().Msgf("PortForwards after delete: %#v", p.app.forwarders)
p.getTV().Update(p.hydrate())
p.masterPage().Update(p.hydrate())
p.app.Flash().Infof("PortForward %s deleted!", sel)
})
@ -250,7 +228,7 @@ func (p *PortForward) backCmd(evt *tcell.EventKey) *tcell.EventKey {
p.cancelFn()
}
tv := p.getTV()
tv := p.masterPage()
if tv.SearchBuff().IsActive() {
tv.SearchBuff().Reset()
} else {
@ -260,10 +238,6 @@ func (p *PortForward) backCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (p *PortForward) Hints() model.MenuHints {
return p.getTV().Hints()
}
func (p *PortForward) hydrate() resource.TableData {
data := initHeader(len(p.app.forwarders))
dc, dn := p.app.Bench.Benchmarks.Defaults.C, p.app.Bench.Benchmarks.Defaults.N
@ -293,7 +267,7 @@ func (p *PortForward) hydrate() resource.TableData {
}
func (p *PortForward) resetTitle() {
p.SetTitle(fmt.Sprintf(forwardTitleFmt, forwardTitle, p.getTV().GetRowCount()-1))
p.SetTitle(fmt.Sprintf(portForwardTitleFmt, portForwardTitle, p.masterPage().GetRowCount()-1))
}
// ----------------------------------------------------------------------------
@ -354,7 +328,6 @@ func showModal(p *ui.Pages, msg, back string, ok func()) {
func dismissModal(p *ui.Pages, page string) {
p.RemovePage(promptPage)
p.SwitchToPage(page)
}
func watchFS(ctx context.Context, app *App, dir, file string, cb func()) error {

View File

@ -0,0 +1,16 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestPortForwardNew(t *testing.T) {
po := view.NewPortForward("", "", nil)
po.Init(makeCtx())
assert.Equal(t, "PortForwards", po.Name())
assert.Equal(t, 15, len(po.Hints()))
}

View File

@ -10,15 +10,6 @@ type portSelector struct {
ok, cancel func()
}
func newSelector(title, port string, okFn, cancelFn func()) *portSelector {
return &portSelector{
title: title,
port: port,
ok: okFn,
cancel: cancelFn,
}
}
func (p *portSelector) show(app *App) {
f := tview.NewForm()
f.SetItemPadding(0)

View File

@ -15,11 +15,11 @@ import (
)
const (
clusterRole roleKind = iota
role
ClusterRole roleKind = iota
Role
all = "*"
rbacTitle = "RBAC"
rbacTitle = "Rbac"
rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
)
@ -49,15 +49,6 @@ var (
"delete",
}
httpVerbs = []string{
"get",
"post",
"put",
"patch",
"delete",
"options",
}
httpTok8sVerbs = map[string]string{
"post": "create",
"put": "update",
@ -66,8 +57,8 @@ var (
type roleKind = int8
// RBAC presents an RBAC policy viewer.
type RBAC struct {
// Rbac presents an RBAC policy viewer.
type Rbac struct {
*Table
app *App
@ -77,24 +68,24 @@ type RBAC struct {
cache resource.RowEvents
}
// NewRBAC returns a new viewer.
func NewRBAC(app *App, ns, name string, kind roleKind) *RBAC {
r := RBAC{
// NewRbac returns a new viewer.
func NewRbac(app *App, ns, name string, kind roleKind) *Rbac {
r := Rbac{
app: app,
roleName: name,
roleType: kind,
}
r.Table = NewTable(r.getTitle())
r.SetActiveNS(ns)
r.SetColorerFn(rbacColorer)
r.bindKeys()
return &r
}
// Init initializes the view.
func (r *RBAC) Init(ctx context.Context) {
func (r *Rbac) Init(ctx context.Context) {
r.SetActiveNS(r.app.Config.ActiveNamespace())
r.SetColorerFn(rbacColorer)
r.Table.Init(ctx)
r.bindKeys()
r.Start()
r.SetSortCol(1, len(rbacHeader), true)
@ -102,7 +93,11 @@ func (r *RBAC) Init(ctx context.Context) {
}
// Start watches for viewer updates
func (r *RBAC) Start() {
func (r *Rbac) Start() {
if r.app.Conn() == nil {
return
}
r.Stop()
var ctx context.Context
@ -123,33 +118,35 @@ func (r *RBAC) Start() {
}
// Stop terminates the viewer updater.
func (r *RBAC) Stop() {
func (r *Rbac) Stop() {
if r.cancelFn != nil {
r.cancelFn()
}
}
// Name returns the component name.
func (r *RBAC) Name() string {
func (r *Rbac) Name() string {
return rbacTitle
}
func (r *RBAC) bindKeys() {
func (r *Rbac) bindKeys() {
r.RmAction(ui.KeyShiftA)
r.AddActions(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Reset", r.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", r.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", r.app.PrevCmd, false),
ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", r.SortColCmd(1), false),
})
}
func (r *RBAC) getTitle() string {
func (r *Rbac) getTitle() string {
return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, r.roleName), r.app.Styles.Frame())
}
func (r *RBAC) refresh() {
func (r *Rbac) refresh() {
if r.app.Conn() == nil {
return
}
data, err := r.reconcile(r.ActiveNS(), r.roleName, r.roleType)
if err != nil {
log.Error().Err(err).Msgf("Refresh for %s:%d", r.roleName, r.roleType)
@ -158,7 +155,8 @@ func (r *RBAC) refresh() {
r.Update(data)
}
func (r *RBAC) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
func (r *Rbac) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("!!!YO!!!!")
if !r.SearchBuff().Empty() {
r.SearchBuff().Reset()
return nil
@ -167,7 +165,8 @@ func (r *RBAC) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
return r.backCmd(evt)
}
func (r *RBAC) backCmd(evt *tcell.EventKey) *tcell.EventKey {
func (r *Rbac) backCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("!!!!RBAC back!!!")
if r.cancelFn != nil {
r.cancelFn()
}
@ -180,7 +179,7 @@ func (r *RBAC) backCmd(evt *tcell.EventKey) *tcell.EventKey {
return r.app.PrevCmd(evt)
}
func (r *RBAC) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
func (r *Rbac) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
var table resource.TableData
evts, err := r.rowEvents(ns, name, kind)
@ -191,28 +190,28 @@ func (r *RBAC) reconcile(ns, name string, kind roleKind) (resource.TableData, er
return buildTable(r, evts), nil
}
func (r *RBAC) header() resource.Row {
func (r *Rbac) header() resource.Row {
return rbacHeader
}
func (r *RBAC) getCache() resource.RowEvents {
func (r *Rbac) getCache() resource.RowEvents {
return r.cache
}
func (r *RBAC) setCache(evts resource.RowEvents) {
func (r *Rbac) setCache(evts resource.RowEvents) {
r.cache = evts
}
func (r *RBAC) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
func (r *Rbac) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
var (
evts resource.RowEvents
err error
)
switch kind {
case clusterRole:
case ClusterRole:
evts, err = r.clusterPolicies(name)
case role:
case Role:
evts, err = r.namespacedPolicies(name)
default:
return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind)
@ -225,7 +224,7 @@ func (r *RBAC) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, er
return evts, nil
}
func (r *RBAC) clusterPolicies(name string) (resource.RowEvents, error) {
func (r *Rbac) clusterPolicies(name string) (resource.RowEvents, error) {
cr, err := r.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{})
if err != nil {
return nil, err
@ -234,7 +233,7 @@ func (r *RBAC) clusterPolicies(name string) (resource.RowEvents, error) {
return r.parseRules(cr.Rules), nil
}
func (r *RBAC) namespacedPolicies(path string) (resource.RowEvents, error) {
func (r *Rbac) namespacedPolicies(path string) (resource.RowEvents, error) {
ns, na := namespaced(path)
cr, err := r.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{})
if err != nil {
@ -244,7 +243,7 @@ func (r *RBAC) namespacedPolicies(path string) (resource.RowEvents, error) {
return r.parseRules(cr.Rules), nil
}
func (r *RBAC) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
func (r *Rbac) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
m := make(resource.RowEvents, len(rules))
for _, r := range rules {
for _, grp := range r.APIGroups {

View File

@ -0,0 +1,115 @@
package view
import (
"testing"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
rbacv1 "k8s.io/api/rbac/v1"
)
func TestHasVerb(t *testing.T) {
uu := []struct {
vv []string
v string
e bool
}{
{[]string{"*"}, "get", true},
{[]string{"get", "list", "watch"}, "watch", true},
{[]string{"get", "dope", "list"}, "watch", false},
{[]string{"get"}, "get", true},
{[]string{"post"}, "create", true},
{[]string{"put"}, "update", true},
{[]string{"list", "deletecollection"}, "deletecollection", true},
}
for _, u := range uu {
assert.Equal(t, u.e, hasVerb(u.vv, u.v))
}
}
func TestAsVerbs(t *testing.T) {
ok, nok := toVerbIcon(true), toVerbIcon(false)
uu := []struct {
vv []string
e resource.Row
}{
{[]string{"*"}, resource.Row{ok, ok, ok, ok, ok, ok, ok, ok, ""}},
{[]string{"get", "list", "patch"}, resource.Row{ok, ok, nok, nok, nok, ok, nok, nok, ""}},
{[]string{"get", "list", "deletecollection", "post"}, resource.Row{ok, ok, ok, nok, ok, nok, nok, nok, ""}},
{[]string{"get", "list", "blee"}, resource.Row{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}},
}
for _, u := range uu {
assert.Equal(t, u.e, asVerbs(u.vv...))
}
}
func TestParseRules(t *testing.T) {
ok, nok := toVerbIcon(true), toVerbIcon(false)
_ = nok
uu := []struct {
pp []rbacv1.PolicyRule
e map[string]resource.Row
}{
{
[]rbacv1.PolicyRule{
{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}},
},
map[string]resource.Row{
"*.*": {"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""},
},
},
{
[]rbacv1.PolicyRule{
{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}},
},
map[string]resource.Row{
"*.*": {"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""},
},
},
{
[]rbacv1.PolicyRule{
{APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}},
},
map[string]resource.Row{
"*": {"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
},
},
{
[]rbacv1.PolicyRule{
{APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}},
},
map[string]resource.Row{
"pods": {"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
"pods/fred": {"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
},
},
{
[]rbacv1.PolicyRule{
{APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}},
},
map[string]resource.Row{
"/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""},
},
},
{
[]rbacv1.PolicyRule{
{APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}},
},
map[string]resource.Row{
"/fred": {"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""},
},
},
}
var v Rbac
for _, u := range uu {
evts := v.parseRules(u.pp)
for k, v := range u.e {
assert.Equal(t, v, evts[k].Fields)
}
}
}

View File

@ -1,12 +1,22 @@
package view
package view_test
// import (
// "testing"
import (
"testing"
// "github.com/derailed/k9s/internal/resource"
// "github.com/stretchr/testify/assert"
// rbacv1 "k8s.io/api/rbac/v1"
// )
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestRbacNew(t *testing.T) {
cfg := config.NewConfig(ks{})
app := view.NewApp(cfg)
v := view.NewRbac(app, "", "fred", view.ClusterRole)
v.Init(makeCtx())
assert.Equal(t, "Rbac", v.Name())
assert.Equal(t, 10, len(v.Hints()))
}
// func TestHasVerb(t *testing.T) {
// uu := []struct {
@ -105,7 +115,7 @@ package view
// },
// }
// var v rbacView
// var v view.Rbac
// for _, u := range uu {
// evts := v.parseRules(u.pp)
// for k, v := range u.e {

View File

@ -78,11 +78,11 @@ func allCRDs(c k8s.Connection, vv viewers) {
}
func showRBAC(app *App, ns, resource, selection string) {
kind := clusterRole
kind := ClusterRole
if resource == "role" {
kind = role
kind = Role
}
app.inject(NewRBAC(app, ns, selection, kind))
app.inject(NewRbac(app, ns, selection, kind))
}
func showCRD(app *App, ns, resource, selection string) {
@ -98,7 +98,7 @@ func showClusterRole(app *App, ns, resource, selection string) {
app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection)
return
}
app.inject(NewRBAC(app, ns, crb.RoleRef.Name, clusterRole))
app.inject(NewRbac(app, ns, crb.RoleRef.Name, ClusterRole))
}
func showRole(app *App, _, resource, selection string) {
@ -108,7 +108,7 @@ func showRole(app *App, _, resource, selection string) {
app.Flash().Errf("Unable to retrieve rolebindings for %s", selection)
return
}
app.inject(NewRBAC(app, ns, fqn(ns, rb.RoleRef.Name), role))
app.inject(NewRbac(app, ns, fqn(ns, rb.RoleRef.Name), Role))
}
func showSAPolicy(app *App, _, _, selection string) {

View File

@ -9,7 +9,6 @@ import (
"github.com/atotto/clipboard"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
@ -37,7 +36,7 @@ type Resource struct {
// NewResource returns a new viewer.
func NewResource(title, gvr string, list resource.List) *Resource {
return &Resource{
MasterDetail: NewMasterDetail(title),
MasterDetail: NewMasterDetail(title, list.GetNamespace()),
list: list,
gvr: gvr,
}
@ -49,19 +48,23 @@ func (r *Resource) Init(ctx context.Context) {
r.envFn = r.defaultK9sEnv
table := r.masterPage()
{
table.setFilterFn(r.filterResource)
colorer := ui.DefaultColorer
if r.colorerFn != nil {
colorer = r.colorerFn
}
table.SetColorerFn(colorer)
}
r.refresh()
{
row, _ := table.GetSelection()
if row == 0 && table.GetRowCount() > 0 {
table.Select(1, 0)
}
r.DumpPages()
r.refresh()
}
log.Debug().Msgf("<<<< RESOURCE INIT")
}
// Start initializes updates.
@ -84,18 +87,6 @@ func (r *Resource) Name() string {
return r.list.GetName()
}
// Hints returns the current viewer hints
func (r *Resource) Hints() model.MenuHints {
if r.CurrentPage() == nil {
return nil
}
if c, ok := r.CurrentPage().Item.(model.Hinter); ok {
return c.Hints()
}
return nil
}
func (r *Resource) setColorerFn(f ui.ColorerFunc) {
r.colorerFn = f
}
@ -131,19 +122,6 @@ func (r *Resource) backCmd(*tcell.EventKey) *tcell.EventKey {
return nil
}
func (r *Resource) switchPage1(p string) {
log.Debug().Msgf("Switching page to %s", p)
if _, ok := r.CurrentPage().Item.(*Table); ok {
r.Stop()
}
r.SwitchToPage(p)
if _, ok := r.CurrentPage().Item.(*Table); ok {
r.Start()
}
}
// ----------------------------------------------------------------------------
// Actions...
@ -210,9 +188,7 @@ func (r *Resource) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
}
}
r.refresh()
}, func() {
r.Pop()
})
}, func() {})
return nil
}
@ -283,7 +259,7 @@ func (r *Resource) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
details.SetTextColor(r.app.Styles.FgColor())
details.SetText(colorizeYAML(r.app.Styles.Views().Yaml, raw))
details.ScrollToBeginning()
r.app.Content.Push(details)
r.showDetails()
return nil
}
@ -313,6 +289,7 @@ func (r *Resource) editCmd(evt *tcell.EventKey) *tcell.EventKey {
func (r *Resource) setNamespace(ns string) {
if r.list.Namespaced() {
r.currentNS = ns
r.list.SetNamespace(ns)
}
}
@ -335,32 +312,34 @@ func (r *Resource) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
r.masterPage().SelectRow(1, true)
r.app.CmdBuff().Reset()
if err := r.app.Config.SetActiveNamespace(r.currentNS); err != nil {
log.Error().Err(err).Msg("Config save NS failed!")
}
if err := r.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
r.app.Config.Save()
return nil
}
func (r *Resource) refresh() {
if r.CurrentPage() == nil {
return
}
if _, ok := r.CurrentPage().Item.(*Table); !ok {
if _, ok := r.Top().(*Table); !ok {
return
}
r.refreshActions()
if r.list.Namespaced() {
r.list.SetNamespace(r.currentNS)
}
if r.app.Conn() != nil {
if err := r.list.Reconcile(r.app.informer, r.path); err != nil {
r.app.Flash().Err(err)
}
}
data := r.list.Data()
if r.decorateFn != nil {
data = r.decorateFn(data)
}
r.refreshActions()
r.masterPage().Update(data)
}

View File

@ -40,9 +40,7 @@ func (r *RestartableResource) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
} else {
r.app.Flash().Infof("Rollout restart in progress for `%s...", sel)
}
}, func() {
r.showMaster()
})
}, func() {})
return nil
}

View File

@ -41,17 +41,7 @@ func (r *ReplicaSet) extraActions(aa ui.KeyActions) {
aa[tcell.KeyCtrlB] = ui.NewKeyAction("Rollback", r.rollbackCmd, true)
}
func (r *ReplicaSet) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t := r.masterPage()
t.SetSortCol(t.NameColIndex()+col, 0, asc)
t.Refresh()
return nil
}
}
func (r *ReplicaSet) showPods(app *App, ns, res, sel string) {
func (r *ReplicaSet) showPods(app *App, _, res, sel string) {
ns, n := namespaced(sel)
s, err := k8s.NewReplicaSet(app.Conn()).Get(ns, n)
if err != nil {
@ -174,7 +164,7 @@ func rollback(Conn k8s.Connection, selectedItem string) (string, error) {
if err != nil {
return "", err
}
rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{apiGroup, kind}, Conn.DialOrDie())
rb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{Group: apiGroup, Kind: kind}, Conn.DialOrDie())
if err != nil {
return "", err
}

View File

@ -35,24 +35,27 @@ type ScreenDump struct {
app *App
}
func NewScreenDump(title, _ string, _ resource.List) ResourceViewer {
func NewScreenDump(_, _ string, _ resource.List) ResourceViewer {
return &ScreenDump{
MasterDetail: NewMasterDetail(title),
MasterDetail: NewMasterDetail(dumpTitle, ""),
}
}
// Init initializes the viewer.
func (s *ScreenDump) Init(ctx context.Context) {
s.app = ctx.Value(ui.KeyApp).(*App)
s.MasterDetail.Init(ctx)
s.registerActions()
table := s.masterPage()
{
table.SetBorderFocusColor(tcell.ColorSteelBlue)
table.SetSelectedStyle(tcell.ColorWhite, tcell.ColorRoyalBlue, tcell.AttrNone)
table.SetColorerFn(dumpColorer)
table.SetActiveNS(resource.AllNamespaces)
table.SetSortCol(table.NameColIndex()+1, 0, true)
table.SetSortCol(table.NameColIndex(), 0, true)
table.SelectRow(1, true)
}
s.Start()
s.refresh()
}
@ -90,28 +93,18 @@ func (s *ScreenDump) refresh() {
}
func (s *ScreenDump) registerActions() {
aa := ui.KeyActions{
ui.KeyP: ui.NewKeyAction("Previous", s.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Enter", s.enterCmd, true),
s.masterPage().AddActions(ui.KeyActions{
tcell.KeyEsc: ui.NewKeyAction("Back", s.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("View", s.enterCmd, true),
tcell.KeyCtrlD: ui.NewKeyAction("Delete", s.deleteCmd, true),
tcell.KeyCtrlS: ui.NewKeyAction("Save", noopCmd, false),
}
s.masterPage().AddActions(aa)
})
}
func (s *ScreenDump) getTitle() string {
return dumpTitle
}
func (s *ScreenDump) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
tv := s.masterPage()
tv.SetSortCol(tv.NameColIndex()+col, 0, asc)
tv.Refresh()
return nil
}
}
func (s *ScreenDump) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msg("Dump enter!")
tv := s.masterPage()
@ -160,7 +153,14 @@ func (s *ScreenDump) backCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (s *ScreenDump) Hints() model.MenuHints {
return s.Hints()
if s.CurrentPage() == nil {
return nil
}
if c, ok := s.CurrentPage().Item.(model.Hinter); ok {
return c.Hints()
}
return nil
}
func (s *ScreenDump) hydrate() resource.TableData {

View File

@ -0,0 +1,16 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestScreenDumpNew(t *testing.T) {
po := view.NewScreenDump("fred", "blee", nil)
po.Init(makeCtx())
assert.Equal(t, "Screen Dumps", po.Name())
assert.Equal(t, 11, len(po.Hints()))
}

View File

@ -0,0 +1,57 @@
package view
import (
"fmt"
"sync/atomic"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
)
// AutoScrollIndicator represents a log autoscroll status indicator.
type AutoScrollIndicator struct {
*tview.TextView
styles *config.Styles
scrollStatus int32
}
// NewAutoScrollIndicator returns a new indicator.
func NewAutoScrollIndicator(styles *config.Styles) *AutoScrollIndicator {
a := AutoScrollIndicator{
styles: styles,
TextView: tview.NewTextView(),
scrollStatus: 1,
}
a.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor))
a.SetTextAlign(tview.AlignRight)
a.SetDynamicColors(true)
return &a
}
func (a *AutoScrollIndicator) AutoScroll() bool {
return atomic.LoadInt32(&a.scrollStatus) == 1
}
func (a *AutoScrollIndicator) ToggleAutoScroll() {
var val int32 = 1
if a.AutoScroll() {
val = 0
}
atomic.StoreInt32(&a.scrollStatus, val)
}
func (a *AutoScrollIndicator) Refresh() {
autoScroll := "Off"
if a.AutoScroll() {
autoScroll = "On"
}
a.update("Autoscroll: " + autoScroll)
}
func (a *AutoScrollIndicator) update(status string) {
a.Clear()
fg, bg := a.styles.Frame().Crumb.FgColor, a.styles.Frame().Crumb.ActiveColor
fmt.Fprintf(a, "[%s:%s:b] %-15s ", fg, bg, status)
}

View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestScrollIndicatorRefresg(t *testing.T) {
defaults, _ := config.NewStyles("")
v := view.NewAutoScrollIndicator(defaults)
v.Refresh()
assert.Equal(t, "[black:orange:b] Autoscroll: On \n", v.GetText(false))
}

View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestSecretNew(t *testing.T) {
s := view.NewSecret("secrets", "", resource.NewSecretList(nil, ""))
s.Init(makeCtx())
assert.Equal(t, "secrets", s.Name())
assert.Equal(t, 19, len(s.Hints()))
}

View File

@ -13,11 +13,11 @@ import (
type selectList struct {
*tview.List
parent loggable
parent Loggable
actions ui.KeyActions
}
func newSelectList(parent loggable) *selectList {
func newSelectList(parent Loggable) *selectList {
v := selectList{List: tview.NewList(), actions: ui.KeyActions{}}
{
v.parent = parent

View File

@ -1,35 +0,0 @@
package view
import (
"fmt"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/tview"
)
type statusView struct {
*tview.TextView
styles *config.Styles
}
func newStatusView(styles *config.Styles) *statusView {
v := statusView{styles: styles, TextView: tview.NewTextView()}
{
v.SetBackgroundColor(config.AsColor(styles.Views().Log.BgColor))
v.SetTextAlign(tview.AlignRight)
v.SetDynamicColors(true)
}
return &v
}
func (v *statusView) update(status []string) {
v.Clear()
last, bgColor := len(status)-1, v.styles.Frame().Crumb.BgColor
for i, c := range status {
if i == last {
bgColor = v.styles.Frame().Crumb.ActiveColor
}
fmt.Fprintf(v, "[%s:%s:b] %-15s ", v.styles.Frame().Crumb.FgColor, bgColor, c)
}
}

View File

@ -1,16 +0,0 @@
package view
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
func TestNewStatus(t *testing.T) {
defaults, _ := config.NewStyles("")
v := newStatusView(defaults)
v.update([]string{"blee", "duh"})
assert.Equal(t, "[black:aqua:b] blee [black:orange:b] duh \n", v.GetText(false))
}

View File

@ -36,7 +36,7 @@ func (s *StatefulSet) extraActions(aa ui.KeyActions) {
aa[ui.KeyShiftC] = ui.NewKeyAction("Sort Current", s.sortColCmd(2, false), false)
}
func (s *StatefulSet) showPods(app *App, ns, res, sel string) {
func (s *StatefulSet) showPods(app *App, _, res, sel string) {
ns, n := namespaced(sel)
st, err := k8s.NewStatefulSet(app.Conn()).Get(ns, n)
if err != nil {

View File

@ -1,46 +0,0 @@
package view
import (
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
type styles struct {
color tcell.Color
attrs tcell.AttrMask
align int
}
func stylesFor(app *App, res string, col int) styles {
switch res {
case "pod":
return podStyles(app, col)
default:
return defaultStyles(app, col)
}
}
func podStyles(app *App, col int) styles {
st := styles{
color: ui.StdColor,
attrs: tcell.AttrReverse,
align: tview.AlignLeft,
}
switch col {
case 5, 6, 7, 8:
st.align = tview.AlignLeft
st.color = tcell.ColorGreen
}
return st
}
func defaultStyles(app *App, col int) styles {
return styles{
color: tcell.ColorRed,
attrs: tcell.AttrReverse,
align: tview.AlignLeft,
}
}

View File

@ -96,9 +96,8 @@ func (s *Subject) bindKeys() {
s.AddActions(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Policies", s.policyCmd, true),
tcell.KeyEscape: ui.NewKeyAction("Reset", s.resetCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", s.resetCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter", s.activateCmd, false),
ui.KeyP: ui.NewKeyAction("Previous", s.app.PrevCmd, false),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.SortColCmd(1), false),
})
}

View File

@ -1,6 +1,7 @@
package view
import (
"context"
"errors"
"fmt"
"strings"
@ -16,6 +17,7 @@ import (
v1 "k8s.io/api/core/v1"
)
// Service represents a service viewer.
type Service struct {
*Resource
@ -23,15 +25,21 @@ type Service struct {
logs *Logs
}
// NewService returns a new viewer.
func NewService(title, gvr string, list resource.List) ResourceViewer {
s := Service{
return &Service{
Resource: NewResource(title, gvr, list),
}
}
// Init initializes the viewer.
func (s *Service) Init(ctx context.Context) {
s.extraActionsFn = s.extraActions
s.enterFn = s.showPods
s.logs = NewLogs(list.GetName(), &s)
s.Resource.Init(ctx)
return &s
s.logs = NewLogs(s.list.GetName(), s)
s.logs.Init(ctx)
}
// Protocol...
@ -51,17 +59,7 @@ func (s *Service) extraActions(aa ui.KeyActions) {
aa[ui.KeyShiftT] = ui.NewKeyAction("Sort Type", s.sortColCmd(1, false), false)
}
func (s *Service) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t := s.masterPage()
t.SetSortCol(t.NameColIndex()+col, 0, asc)
t.Refresh()
return nil
}
}
func (s *Service) showPods(app *App, ns, res, sel string) {
func (s *Service) showPods(app *App, _, res, sel string) {
ns, n := namespaced(sel)
svc, err := k8s.NewService(app.Conn()).Get(ns, n)
if err != nil {
@ -79,8 +77,7 @@ func (s *Service) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
l := s.GetPrimitive("logs").(*Logs)
l.reload("", s, false)
s.logs.reload("", s, false)
s.Push(s.logs)
return nil
@ -177,6 +174,10 @@ func (s *Service) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error {
if cfg.HTTP.Host == "" {
return fmt.Errorf("Invalid benchmark host %q", cfg.HTTP.Host)
}
var err error
base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path
if s.bench, err = perf.NewBenchmark(base, cfg); err != nil {

17
internal/view/svc_test.go Normal file
View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestServiceNew(t *testing.T) {
s := view.NewService("service", "", resource.NewServiceList(nil, ""))
s.Init(makeCtx())
assert.Equal(t, "svc", s.Name())
assert.Equal(t, 22, len(s.Hints()))
}

View File

@ -5,6 +5,7 @@ import (
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
type Table struct {
@ -21,6 +22,8 @@ func NewTable(title string) *Table {
}
func (t *Table) Init(ctx context.Context) {
log.Debug().Msgf("VIEW Table INIT %q", t.GetBaseTitle())
t.app = ctx.Value(ui.KeyApp).(*App)
ctx = context.WithValue(ctx, ui.KeyStyles, t.app.Styles)
@ -102,12 +105,15 @@ func (t *Table) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !t.SearchBuff().Empty() {
t.app.Flash().Info("Clearing filter...")
log.Debug().Msgf("Table filter reset!")
if t.SearchBuff().Empty() {
return evt
}
if ui.IsLabelSelector(t.SearchBuff().String()) {
t.filterFn("")
}
t.app.Flash().Info("Clearing filter...")
t.SearchBuff().Reset()
t.Refresh()
@ -115,6 +121,7 @@ func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("Table filter activated!")
if t.app.InCmdMode() {
return evt
}

View File

@ -45,9 +45,13 @@ func saveTable(cluster, name string, data resource.TableData) (string, error) {
}
w := csv.NewWriter(file)
w.Write(data.Header)
if err := w.Write(data.Header); err != nil {
return "", err
}
for _, r := range data.Rows {
w.Write(r.Fields)
if err := w.Write(r.Fields); err != nil {
return "", err
}
}
w.Flush()
if err := w.Error(); err != nil {
@ -67,53 +71,3 @@ func skinTitle(fmat string, style config.Frame) string {
return fmat
}
func sortRows(evts resource.RowEvents, sortFn ui.SortFn, sortCol ui.SortColumn, keys []string) {
rows := make(resource.Rows, 0, len(evts))
for k, r := range evts {
rows = append(rows, append(r.Fields, k))
}
sortFn(rows, sortCol)
for i, r := range rows {
keys[i] = r[len(r)-1]
}
}
// func defaultSort(rows resource.Rows, sortCol ui.SortColumn) {
// t := rowSorter{rows: rows, index: sortCol.index, asc: sortCol.asc}
// sort.Sort(t)
// }
// func sortAllRows(col ui.SortColumn, rows resource.RowEvents, sortFn ui.SortFn) (resource.Row, map[string]resource.Row) {
// keys := make([]string, len(rows))
// sortRows(rows, sortFn, col, keys)
// sec := make(map[string]resource.Row, len(rows))
// for _, k := range keys {
// grp := rows[k].Fields[col.index]
// sec[grp] = append(sec[grp], k)
// }
// // Performs secondary to sort by name for each groups.
// prim := make(resource.Row, 0, len(sec))
// for k, v := range sec {
// sort.Strings(v)
// prim = append(prim, k)
// }
// sort.Sort(groupSorter{prim, col.asc})
// return prim, sec
// }
// func sortIndicator(col ui.SortColumn, style config.Table, index int, name string) string {
// if col.index != index {
// return name
// }
// order := descIndicator
// if col.asc {
// order = ascIndicator
// }
// return fmt.Sprintf("%s[%s::]%s[::]", name, style.Header.SorterColor, order)
// }

View File

@ -78,7 +78,7 @@ func saveYAML(cluster, name, data string) (string, error) {
log.Error().Err(err).Msgf("YAML create %s", path)
return "", nil
}
if _, err := fmt.Fprintf(file, data); err != nil {
if _, err := file.Write([]byte(data)); err != nil {
return "", err
}

View File

@ -131,7 +131,6 @@ func (i *Informer) List(res, ns string, opts metav1.ListOptions) (k8s.Collection
// Get a resource by name.
func (i *Informer) Get(res, fqn string, opts metav1.GetOptions) (interface{}, error) {
if i == nil {
panic("blee")
return nil, errors.New("Invalid Get informer")
}