diff --git a/.goreleaser.yml b/.goreleaser.yml
index f74f5942..ef73cac4 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -67,8 +67,7 @@ snapcraft:
By leveraging a terminal UI, you can easily traverse Kubernetes resources
and view the state of you clusters in a single powerful session.
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
- publish: false
- # publish: true
+ publish: true
replacements:
amd64: 64-bit
386: 32-bit
diff --git a/change_logs/release_0.7.7.md b/change_logs/release_0.7.7.md
new file mode 100644
index 00000000..141c4b0f
--- /dev/null
+++ b/change_logs/release_0.7.7.md
@@ -0,0 +1,31 @@
+
+
+# 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` + ``.
+
+---
+
+## 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)
+
+---
+
+
© 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
diff --git a/internal/k8s/api.go b/internal/k8s/api.go
index 80e16744..0c2fdd84 100644
--- a/internal/k8s/api.go
+++ b/internal/k8s/api.go
@@ -117,7 +117,7 @@ func (a *APIClient) CanIAccess(ns, name, resURL string, verbs []string) (bool, e
sar.Spec.ResourceAttributes.Verb = v
resp, err = a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews().Create(sar)
if err != nil {
- log.Warn().Err(err).Msgf("CanIAccess")
+ log.Error().Err(err).Msgf("CanIAccess")
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)
diff --git a/internal/resource/container.go b/internal/resource/container.go
index 3fbe4e84..6253db89 100644
--- a/internal/resource/container.go
+++ b/internal/resource/container.go
@@ -8,7 +8,6 @@ import (
"sync"
"github.com/derailed/k9s/internal/k8s"
- "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
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"
if cs != nil {
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
diff --git a/internal/views/log_resource.go b/internal/views/log_resource.go
index 0285c720..f696302c 100644
--- a/internal/views/log_resource.go
+++ b/internal/views/log_resource.go
@@ -54,13 +54,11 @@ func (v *logResourceView) getSelection() string {
func (v *logResourceView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey {
v.showLogs(true)
-
return nil
}
func (v *logResourceView) logsCmd(evt *tcell.EventKey) *tcell.EventKey {
v.showLogs(false)
-
return nil
}
diff --git a/internal/views/pod.go b/internal/views/pod.go
index 140f835a..6d8ac977 100644
--- a/internal/views/pod.go
+++ b/internal/views/pod.go
@@ -15,7 +15,10 @@ import (
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 {
*resourceView
@@ -241,5 +244,5 @@ func computeShellArgs(path, co, context string, kcfg *string) []string {
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)
}
diff --git a/internal/views/pod_test.go b/internal/views/pod_test.go
index 2957e23d..ec917ee5 100644
--- a/internal/views/pod_test.go
+++ b/internal/views/pod_test.go
@@ -19,28 +19,28 @@ func TestComputeShellArgs(t *testing.T) {
"c1",
"ctx1",
&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": {
"fred/blee",
"c1",
"ctx1",
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": {
"fred/blee",
"c1",
"ctx1",
&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": {
"fred/blee",
"",
"ctx1",
&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,
},
}
diff --git a/internal/views/resource.go b/internal/views/resource.go
index cabf2765..9f7400d4 100644
--- a/internal/views/resource.go
+++ b/internal/views/resource.go
@@ -46,6 +46,11 @@ type resourceView struct {
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 {
v := resourceView{
app: app,
@@ -58,6 +63,8 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie
tv := newTableView(app, v.title)
tv.SetSelectionChangedFunc(v.selChanged)
+ tv.filterChanged(v.filterResource)
+
v.AddPage(v.list.GetName(), tv, true, true)
details := newDetailsView(app, v.backCmd)
@@ -114,7 +121,10 @@ func (v *resourceView) init(ctx context.Context, ns string) {
v.app.clusterInfoView.refresh()
v.refresh()
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 {
return v.selectedFn()
}
-
return v.selectedItem
}
@@ -238,15 +247,12 @@ func (v *resourceView) showDelete(msg string, done func(bool, bool)) {
SetLabelColor(tcell.ColorAqua).
SetFieldTextColor(tcell.ColorOrange)
f.AddCheckbox("Cascade:", cascade, func(checked bool) {
- log.Debug().Msgf("Cascade changed: %t", checked)
cascade = checked
})
f.AddCheckbox("Force:", force, func(checked bool) {
- log.Debug().Msgf("Force changed: %t", checked)
force = checked
})
f.AddButton("Cancel", func() {
- v.app.flash().info("Canceled!!")
v.dismissModal()
})
f.AddButton("OK", func() {
@@ -292,9 +298,7 @@ func (v *resourceView) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
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)
-
return nil
}
diff --git a/internal/views/sorter.go b/internal/views/sorter.go
index 3be693bd..cd9dd212 100644
--- a/internal/views/sorter.go
+++ b/internal/views/sorter.go
@@ -51,6 +51,13 @@ func (s groupSorter) Less(i, j int) bool {
// Helpers...
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 {
return o
}
diff --git a/internal/views/sorter_test.go b/internal/views/sorter_test.go
index 5d28f870..f894969b 100644
--- a/internal/views/sorter_test.go
+++ b/internal/views/sorter_test.go
@@ -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"}},
},
+ {
+ 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 {
diff --git a/internal/views/table.go b/internal/views/table.go
index 6ee84da3..7578622b 100644
--- a/internal/views/table.go
+++ b/internal/views/table.go
@@ -20,11 +20,12 @@ import (
)
const (
- titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[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:-] "
- descIndicator = "↓"
- ascIndicator = "↑"
+ titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[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:-] "
+ descIndicator = "↓"
+ ascIndicator = "↑"
+ labelSelIndicator = "-l"
)
var (
@@ -54,8 +55,8 @@ type (
cleanseFn cleanseFn
data resource.TableData
cmdBuff *cmdBuff
- sortBuff *cmdBuff
sortCol sortColumn
+ filterFn func(string)
}
)
@@ -89,6 +90,10 @@ func newTableView(app *appView, title string) *tableView {
return &v
}
+func (v *tableView) filterChanged(fn func(string)) {
+ v.filterFn = fn
+}
+
func (v *tableView) bindKeys() {
v.actions[tcell.KeyCtrlS] = newKeyAction("Save", v.saveCmd, true)
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 {
if v.cmdBuff.isActive() {
v.cmdBuff.setActive(false)
+ cmd := v.cmdBuff.String()
+ if isLabelSelector(cmd) && v.filterFn != nil {
+ v.filterFn(trimLabelSelector(cmd))
+ return nil
+ }
v.refresh()
return nil
}
@@ -210,6 +220,9 @@ func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() {
v.app.flash().info("Clearing filter...")
}
+ if isLabelSelector(v.cmdBuff.String()) {
+ v.filterFn("")
+ }
v.cmdBuff.reset()
v.refresh()
@@ -257,6 +270,9 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
}
v.app.flash().info("Filter mode activated.")
+ if isLabelSelector(v.cmdBuff.String()) {
+ return nil
+ }
v.cmdBuff.reset()
v.cmdBuff.setActive(true)
@@ -308,7 +324,7 @@ func (v *tableView) update(data resource.TableData) {
}
func (v *tableView) filtered() resource.TableData {
- if v.cmdBuff.empty() {
+ if v.cmdBuff.empty() || isLabelSelector(v.cmdBuff.String()) {
return v.data
}
@@ -496,7 +512,11 @@ func (v *tableView) resetTitle() {
}
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)
}
@@ -523,3 +543,16 @@ func (v *tableView) active(b bool) {
}
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:])
+}
diff --git a/internal/views/table_test.go b/internal/views/table_test.go
index e5dab9a7..2ddb482a 100644
--- a/internal/views/table_test.go
+++ b/internal/views/table_test.go
@@ -9,6 +9,39 @@ import (
"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) {
uu := []struct {
rows resource.RowEvents