add label select + bugz fixes

mine
derailed 2019-06-13 19:41:18 -06:00
parent 15a9885d16
commit 127414b5e5
12 changed files with 154 additions and 29 deletions

View File

@ -67,8 +67,7 @@ snapcraft:
By leveraging a terminal UI, you can easily traverse Kubernetes resources By leveraging a terminal UI, you can easily traverse Kubernetes resources
and view the state of you clusters in a single powerful session. and view the state of you clusters in a single powerful session.
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
publish: false publish: true
# publish: true
replacements: replacements:
amd64: 64-bit amd64: 64-bit
386: 32-bit 386: 32-bit

View File

@ -0,0 +1,31 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.7.7
## 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
### Labels Filters
K9s now provides an affordance to filter Kubernetes resources by label (Feature #233. Thank you [Chad Hanley](https://github.com/cchanley2003)). In order to enable filtering by labels, enter the filter mode via `/` on any resource table and enter your label filter via `-l app=fred,env=prod` + `<ENTER>`.
---
## Resolved Bugs/Features
+ [Feature #233](https://github.com/derailed/k9s/issues/233)
+ [Issue #232](https://github.com/derailed/k9s/issues/232)
+ [Issue #230](https://github.com/derailed/k9s/issues/230)
+ [Issue #229](https://github.com/derailed/k9s/issues/229)
+ [Issue #226](https://github.com/derailed/k9s/issues/226) Thank you for the excellent PR [Yves Blusseau](https://github.com/JrCs)
---
<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

@ -117,7 +117,7 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) (bool, e
sar.Spec.ResourceAttributes.Verb = v sar.Spec.ResourceAttributes.Verb = v
resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar) resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("CanIAccess") log.Error().Err(err).Msgf("CanIAccess")
return false, err return false, err
} }
log.Debug().Msgf("CHECKING ACCESS group:%q|resource:%q|namespace:%q|name:%q, verb:%s access:%t -- %s", gr.Group, gr.Resource, ns, name, v, resp.Status.Allowed, resp.Status.Reason) log.Debug().Msgf("CHECKING ACCESS group:%q|resource:%q|namespace:%q|name:%q, verb:%s access:%t -- %s", gr.Group, gr.Resource, ns, name, v, resp.Status.Allowed, resp.Status.Reason)

View File

@ -8,7 +8,6 @@ import (
"sync" "sync"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
) )
@ -175,8 +174,6 @@ func (r *Container) Fields(ns string) Row {
} }
} }
log.Debug().Msgf("Container %s %v", i.Name, cs.Name)
ready, state, restarts := "false", MissingValue, "0" ready, state, restarts := "false", MissingValue, "0"
if cs != nil { if cs != nil {
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount)) ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))

View File

@ -54,13 +54,11 @@ func (v *logResourceView) getSelection() string {
func (v *logResourceView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *logResourceView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey {
v.showLogs(true) v.showLogs(true)
return nil return nil
} }
func (v *logResourceView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *logResourceView) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
v.showLogs(false) v.showLogs(false)
return nil return nil
} }

View File

@ -15,7 +15,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
const containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])" const (
containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
shellCheck = "command -v bash >/dev/null && exec bash || exec sh"
)
type podView struct { type podView struct {
*resourceView *resourceView
@ -241,5 +244,5 @@ func computeShellArgs(path, co, context string, kcfg *string) []string {
args = append(args, "-c", co) args = append(args, "-c", co)
} }
return append(args, "--", "sh", "-c", "command -v bash >/dev/null && exec bash || exec sh") return append(args, "--", "sh", "-c", shellCheck)
} }

View File

@ -19,28 +19,28 @@ func TestComputeShellArgs(t *testing.T) {
"c1", "c1",
"ctx1", "ctx1",
&config, &config,
"exec -it --context ctx1 -n fred blee --kubeconfig coolConfig -c c1 -- sh -c command -v bash >/dev/null && exec bash || exec sh", "exec -it --context ctx1 -n fred blee --kubeconfig coolConfig -c c1 -- sh -c " + shellCheck,
}, },
"noconfig": { "noconfig": {
"fred/blee", "fred/blee",
"c1", "c1",
"ctx1", "ctx1",
nil, nil,
"exec -it --context ctx1 -n fred blee -c c1 -- sh -c command -v bash >/dev/null && exec bash || exec sh", "exec -it --context ctx1 -n fred blee -c c1 -- sh -c " + shellCheck,
}, },
"emptyConfig": { "emptyConfig": {
"fred/blee", "fred/blee",
"c1", "c1",
"ctx1", "ctx1",
&empty, &empty,
"exec -it --context ctx1 -n fred blee -c c1 -- sh -c command -v bash >/dev/null && exec bash || exec sh", "exec -it --context ctx1 -n fred blee -c c1 -- sh -c " + shellCheck,
}, },
"singleContainer": { "singleContainer": {
"fred/blee", "fred/blee",
"", "",
"ctx1", "ctx1",
&empty, &empty,
"exec -it --context ctx1 -n fred blee -- sh -c command -v bash >/dev/null && exec bash || exec sh", "exec -it --context ctx1 -n fred blee -- sh -c " + shellCheck,
}, },
} }

View File

@ -46,6 +46,11 @@ type resourceView struct {
parentCtx context.Context parentCtx context.Context
} }
func (v *resourceView) filterResource(sel string) {
v.list.SetLabelSelector(sel)
v.refresh()
}
func newResourceView(title string, app *appView, list resource.List) resourceViewer { func newResourceView(title string, app *appView, list resource.List) resourceViewer {
v := resourceView{ v := resourceView{
app: app, app: app,
@ -58,6 +63,8 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie
tv := newTableView(app, v.title) tv := newTableView(app, v.title)
tv.SetSelectionChangedFunc(v.selChanged) tv.SetSelectionChangedFunc(v.selChanged)
tv.filterChanged(v.filterResource)
v.AddPage(v.list.GetName(), tv, true, true) v.AddPage(v.list.GetName(), tv, true, true)
details := newDetailsView(app, v.backCmd) details := newDetailsView(app, v.backCmd)
@ -114,7 +121,10 @@ func (v *resourceView) init(ctx context.Context, ns string) {
v.app.clusterInfoView.refresh() v.app.clusterInfoView.refresh()
v.refresh() v.refresh()
if tv, ok := v.CurrentPage().Item.(*tableView); ok { if tv, ok := v.CurrentPage().Item.(*tableView); ok {
tv.Select(1, 0) r, _ := tv.GetSelection()
if r == 0 && tv.GetRowCount() > 0 {
tv.Select(1, 0)
}
} }
} }
@ -151,7 +161,6 @@ func (v *resourceView) getSelectedItem() string {
if v.selectedFn != nil { if v.selectedFn != nil {
return v.selectedFn() return v.selectedFn()
} }
return v.selectedItem return v.selectedItem
} }
@ -238,15 +247,12 @@ func (v *resourceView) showDelete(msg string, done func(bool, bool)) {
SetLabelColor(tcell.ColorAqua). SetLabelColor(tcell.ColorAqua).
SetFieldTextColor(tcell.ColorOrange) SetFieldTextColor(tcell.ColorOrange)
f.AddCheckbox("Cascade:", cascade, func(checked bool) { f.AddCheckbox("Cascade:", cascade, func(checked bool) {
log.Debug().Msgf("Cascade changed: %t", checked)
cascade = checked cascade = checked
}) })
f.AddCheckbox("Force:", force, func(checked bool) { f.AddCheckbox("Force:", force, func(checked bool) {
log.Debug().Msgf("Force changed: %t", checked)
force = checked force = checked
}) })
f.AddButton("Cancel", func() { f.AddButton("Cancel", func() {
v.app.flash().info("Canceled!!")
v.dismissModal() v.dismissModal()
}) })
f.AddButton("OK", func() { f.AddButton("OK", func() {
@ -292,9 +298,7 @@ func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt return evt
} }
log.Debug().Msgf("Selected Item %v-%v-%v", v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
v.defaultEnter(v.list.GetNamespace(), v.list.GetName(), v.selectedItem) v.defaultEnter(v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
return nil return nil
} }

View File

@ -51,6 +51,13 @@ func (s groupSorter) Less(i, j int) bool {
// Helpers... // Helpers...
func less(asc bool, c1, c2 string) bool { func less(asc bool, c1, c2 string) bool {
if c1 == resource.NAValue && c2 != resource.NAValue {
return false
}
if c1 != resource.NAValue && c2 == resource.NAValue {
return true
}
if o, ok := isMetricSort(asc, c1, c2); ok { if o, ok := isMetricSort(asc, c1, c2); ok {
return o return o
} }

View File

@ -73,6 +73,26 @@ func TestRowSort(t *testing.T) {
resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}}, resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}},
resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}}, resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}},
}, },
{
true,
resource.Rows{resource.Row{"n/a"}, resource.Row{"31m"}},
resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}},
},
{
true,
resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}},
resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}},
},
{
false,
resource.Rows{resource.Row{"n/a"}, resource.Row{"31m"}},
resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}},
},
{
false,
resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}},
resource.Rows{resource.Row{"31m"}, resource.Row{"n/a"}},
},
} }
for _, u := range uu { for _, u := range uu {

View File

@ -20,11 +20,12 @@ import (
) )
const ( const (
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] " titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]] "
searchFmt = "<[filter:bg:b]/%s[fg:bg:]> " searchFmt = "<[filter:bg:b]/%s[fg:bg:]> "
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
descIndicator = "↓" descIndicator = "↓"
ascIndicator = "↑" ascIndicator = "↑"
labelSelIndicator = "-l"
) )
var ( var (
@ -54,8 +55,8 @@ type (
cleanseFn cleanseFn cleanseFn cleanseFn
data resource.TableData data resource.TableData
cmdBuff *cmdBuff cmdBuff *cmdBuff
sortBuff *cmdBuff
sortCol sortColumn sortCol sortColumn
filterFn func(string)
} }
) )
@ -89,6 +90,10 @@ func newTableView(app *appView, title string) *tableView {
return &v return &v
} }
func (v *tableView) filterChanged(fn func(string)) {
v.filterFn = fn
}
func (v *tableView) bindKeys() { func (v *tableView) bindKeys() {
v.actions[tcell.KeyCtrlS] = newKeyAction("Save", v.saveCmd, true) v.actions[tcell.KeyCtrlS] = newKeyAction("Save", v.saveCmd, true)
v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false) v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false)
@ -191,6 +196,11 @@ func (v *tableView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey { func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() { if v.cmdBuff.isActive() {
v.cmdBuff.setActive(false) v.cmdBuff.setActive(false)
cmd := v.cmdBuff.String()
if isLabelSelector(cmd) && v.filterFn != nil {
v.filterFn(trimLabelSelector(cmd))
return nil
}
v.refresh() v.refresh()
return nil return nil
} }
@ -210,6 +220,9 @@ func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() { if !v.cmdBuff.empty() {
v.app.flash().info("Clearing filter...") v.app.flash().info("Clearing filter...")
} }
if isLabelSelector(v.cmdBuff.String()) {
v.filterFn("")
}
v.cmdBuff.reset() v.cmdBuff.reset()
v.refresh() v.refresh()
@ -257,6 +270,9 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
v.app.flash().info("Filter mode activated.") v.app.flash().info("Filter mode activated.")
if isLabelSelector(v.cmdBuff.String()) {
return nil
}
v.cmdBuff.reset() v.cmdBuff.reset()
v.cmdBuff.setActive(true) v.cmdBuff.setActive(true)
@ -308,7 +324,7 @@ func (v *tableView) update(data resource.TableData) {
} }
func (v *tableView) filtered() resource.TableData { func (v *tableView) filtered() resource.TableData {
if v.cmdBuff.empty() { if v.cmdBuff.empty() || isLabelSelector(v.cmdBuff.String()) {
return v.data return v.data
} }
@ -496,7 +512,11 @@ func (v *tableView) resetTitle() {
} }
if !v.cmdBuff.isActive() && !v.cmdBuff.empty() { if !v.cmdBuff.isActive() && !v.cmdBuff.empty() {
title += skinTitle(fmt.Sprintf(searchFmt, v.cmdBuff), v.app.styles.Style) cmd := v.cmdBuff.String()
if isLabelSelector(cmd) {
cmd = trimLabelSelector(cmd)
}
title += skinTitle(fmt.Sprintf(searchFmt, cmd), v.app.styles.Style)
} }
v.SetTitle(title) v.SetTitle(title)
} }
@ -523,3 +543,16 @@ func (v *tableView) active(b bool) {
} }
v.SetBorderColor(tcell.ColorDodgerBlue) v.SetBorderColor(tcell.ColorDodgerBlue)
} }
var labelCmd = regexp.MustCompile(`\A\-l`)
func isLabelSelector(s string) bool {
if s == "" {
return false
}
return labelCmd.MatchString(s)
}
func trimLabelSelector(s string) string {
return strings.TrimSpace(s[2:])
}

View File

@ -9,6 +9,39 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIsSelector(t *testing.T) {
uu := map[string]struct {
sel string
e bool
}{
"cool": {"-l app=fred,env=blee", true},
"noMode": {"app=fred,env=blee", false},
"noSpace": {"-lapp=fred,env=blee", true},
"wrongLabel": {"-f app=fred,env=blee", false},
}
for k, u := range uu {
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, isLabelSelector(u.sel))
})
}
}
func TestTrimLabelSelector(t *testing.T) {
uu := map[string]struct {
sel, e string
}{
"cool": {"-l app=fred,env=blee", "app=fred,env=blee"},
"noSpace": {"-lapp=fred,env=blee", "app=fred,env=blee"},
}
for k, u := range uu {
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, trimLabelSelector(u.sel))
})
}
}
func TestTVSortRows(t *testing.T) { func TestTVSortRows(t *testing.T) {
uu := []struct { uu := []struct {
rows resource.RowEvents rows resource.RowEvents