derailed 2020-10-30 12:52:15 -06:00
parent 77ffacc2e4
commit 5c1ff0ed7b
21 changed files with 260 additions and 121 deletions

View File

@ -3,7 +3,7 @@ PACKAGE := github.com/derailed/$(NAME)
GIT := $(shell git rev-parse --short HEAD)
SOURCE_DATE_EPOCH ?= $(shell date +%s)
DATE := $(shell date -u -d @${SOURCE_DATE_EPOCH} +%FT%T%Z)
VERSION ?= v0.23.1
VERSION ?= v0.23.2
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,35 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.23.2
## Notes
Thank you to all that contributed with flushing out issues and enhancements for 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 are as ever very much noted and appreciated!
If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
---
### Write Mode
K9s is writable by default, meaning you can interact with your cluster and make changes using one shot commands ie edit, delete, scale, etc... There `readOnly` config option that can be specified in the configuration or via a cli arg to override this behavior. In this drop, we're introducing a symmetrical command line arg aka `--write` that overrides a K9s session and make it writable tho the readOnly config option is set to true.
## Inverse Log Filtering
In the last drop, we've introduces reverse filters to filter out resources from table views. Now you will be able to apply inverse filtering on your log views as well via `/!fred`
---
## Resolved Issues/Features
* [Issue #906](https://github.com/derailed/k9s/issues/906) Print resources in pod view. With Feelings. Thanks Claudio!
* [Issue #889](https://github.com/derailed/k9s/issues/889) Disable readOnly config
* [Issue #564](https://github.com/derailed/k9s/issues/564) Invert filter mode on logs
## Resolved PRs
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -28,7 +28,6 @@ var (
version, commit, date = "dev", "dev", client.NA
k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags
demoMode = new(bool)
rootCmd = &cobra.Command{
Use: appName,
@ -40,7 +39,6 @@ var (
func init() {
rootCmd.AddCommand(versionCmd(), infoCmd())
initTransientFlags()
initK9sFlags()
initK8sFlags()
@ -105,28 +103,15 @@ func loadConfiguration() *config.Config {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
}
if demoMode != nil {
k9sCfg.SetDemoMode(*demoMode)
}
if *k9sFlags.RefreshRate != config.DefaultRefreshRate {
k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate)
}
if k9sFlags.Headless != nil {
k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless)
}
if k9sFlags.Crumbsless != nil {
k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless)
}
if k9sFlags.ReadOnly != nil {
k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly)
}
if k9sFlags.Command != nil {
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
}
k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless)
k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless)
k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly)
k9sCfg.K9s.OverrideWrite(*k9sFlags.Write)
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
if isBoolSet(k9sFlags.AllNamespaces) && k9sCfg.SetActiveNamespace(client.AllNamespaces) != nil {
log.Error().Msg("Setting active namespace")
@ -175,15 +160,6 @@ func parseLevel(level string) zerolog.Level {
}
}
func initTransientFlags() {
rootCmd.Flags().BoolVar(
demoMode,
"demo",
false,
"Enable demo mode to show keyboard commands",
)
}
func initK9sFlags() {
k9sFlags = config.NewFlags()
rootCmd.Flags().IntVarP(
@ -204,6 +180,12 @@ func initK9sFlags() {
false,
"Turn K9s header off",
)
rootCmd.Flags().BoolVar(
k9sFlags.Crumbsless,
"crumbsless",
false,
"Turn K9s crumbs off",
)
rootCmd.Flags().BoolVarP(
k9sFlags.AllNamespaces,
"all-namespaces", "A",
@ -220,13 +202,13 @@ func initK9sFlags() {
k9sFlags.ReadOnly,
"readonly",
false,
"Toggles readOnly mode by overriding configuration setting",
"Sets readOnly mode by overriding readOnly configuration setting",
)
rootCmd.Flags().BoolVar(
k9sFlags.Crumbsless,
"crumbsless",
k9sFlags.Write,
"write",
false,
"Turn K9s crumbs off",
"Sets write mode by overriding the readOnly configuration setting",
)
}

View File

@ -52,7 +52,6 @@ type (
K9s *K9s `yaml:"k9s"`
client client.Connection
settings KubeSettings
demoMode bool
}
)
@ -70,16 +69,6 @@ func NewConfig(ks KubeSettings) *Config {
return &Config{K9s: NewK9s(), settings: ks}
}
// DemoMode returns true if demo mode is active, false otherwise.
func (c *Config) DemoMode() bool {
return c.demoMode
}
// SetDemoMode sets the demo mode.
func (c *Config) SetDemoMode(b bool) {
c.demoMode = b
}
// Refine the configuration based on cli args.
func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error {
cfg, err := flags.ToRawKubeConfigLoader().RawConfig()

View File

@ -19,6 +19,7 @@ type Flags struct {
Command *string
AllNamespaces *bool
ReadOnly *bool
Write *bool
Crumbsless *bool
}
@ -31,6 +32,7 @@ func NewFlags() *Flags {
Command: strPtr(DefaultCommand),
AllNamespaces: boolPtr(false),
ReadOnly: boolPtr(false),
Write: boolPtr(false),
Crumbsless: boolPtr(false),
}
}

View File

@ -1,6 +1,8 @@
package config
import "github.com/derailed/k9s/internal/client"
import (
"github.com/derailed/k9s/internal/client"
)
const (
defaultRefreshRate = 2
@ -56,7 +58,17 @@ func (k *K9s) OverrideCrumbsless(b bool) {
// OverrideReadOnly set the readonly mode manually.
func (k *K9s) OverrideReadOnly(b bool) {
k.manualReadOnly = &b
if b {
k.manualReadOnly = &b
}
}
// OverrideWrite set the write mode manually.
func (k *K9s) OverrideWrite(b bool) {
if b {
flag := !b
k.manualReadOnly = &flag
}
}
// OverrideCommand set the command manually.
@ -100,6 +112,7 @@ func (k *K9s) IsReadOnly() bool {
if k.manualReadOnly != nil {
readOnly = *k.manualReadOnly
}
return readOnly
}

View File

@ -8,6 +8,57 @@ import (
"github.com/stretchr/testify/assert"
)
func TestIsReadOnly(t *testing.T) {
uu := map[string]struct {
config string
read, write bool
readOnly bool
}{
"writable": {
config: "k9s.yml",
},
"writable_read_override": {
config: "k9s.yml",
read: true,
readOnly: true,
},
"writable_write_override": {
config: "k9s.yml",
write: true,
},
"readonly": {
config: "k9s_readonly.yml",
readOnly: true,
},
"readonly_read_override": {
config: "k9s_readonly.yml",
read: true,
readOnly: true,
},
"readonly_write_override": {
config: "k9s_readonly.yml",
write: true,
},
"readonly_both_override": {
config: "k9s_readonly.yml",
read: true,
write: true,
},
}
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Nil(t, cfg.Load("testdata/"+u.config))
cfg.K9s.OverrideReadOnly(u.read)
cfg.K9s.OverrideWrite(u.write)
assert.Equal(t, u.readOnly, cfg.K9s.IsReadOnly())
})
}
}
func TestK9sValidate(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)

View File

@ -1,5 +1,6 @@
k9s:
refreshRate: 2
readOnly: false
logger:
tail: 200
buffer: 2000

View File

@ -0,0 +1,31 @@
k9s:
refreshRate: 2
readOnly: true
logger:
tail: 200
buffer: 2000
currentContext: minikube
currentCluster: minikube
clusters:
minikube:
namespace:
active: kube-system
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: ctx
fred:
namespace:
active: default
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: po

View File

@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"math"
"regexp"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
@ -13,6 +14,19 @@ import (
"k8s.io/cli-runtime/pkg/printers"
)
var (
inverseRx = regexp.MustCompile(`\A\!`)
fuzzyRx = regexp.MustCompile(`\A\-f`)
)
// IsInverseSelector checks if inverse char has been provided.
func IsInverseSelector(s string) bool {
if s == "" {
return false
}
return inverseRx.MatchString(s)
}
// IsFuzzySelector checks if filter is fuzzy or not.
func IsFuzzySelector(s string) bool {
if s == "" {

View File

@ -180,8 +180,6 @@ func (l LogItems) Filter(q string, showTime bool) ([]int, [][]int, error) {
return matches, indices, nil
}
var fuzzyRx = regexp.MustCompile(`\A\-f`)
func (l LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
q = strings.TrimSpace(q)
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
@ -195,22 +193,32 @@ func (l LogItems) fuzzyFilter(q string, showTime bool) ([]int, [][]int) {
}
func (l LogItems) filterLogs(q string, showTime bool) ([]int, [][]int, error) {
var invert bool
if IsInverseSelector(q) {
invert = true
q = q[1:]
}
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil, nil, err
}
matches, indices := make([]int, 0, len(l)), make([][]int, 0, 10)
for i, line := range l.Lines(showTime) {
if locs := rx.FindIndex(line); locs != nil {
matches = append(matches, i)
ii := make([]int, 0, 10)
for i := 0; i < len(locs); i += 2 {
for j := locs[i]; j < locs[i+1]; j++ {
ii = append(ii, j)
}
}
indices = append(indices, ii)
locs := rx.FindIndex(line)
if locs != nil && invert {
continue
}
if locs == nil && !invert {
continue
}
matches = append(matches, i)
ii := make([]int, 0, 10)
for i := 0; i < len(locs); i += 2 {
for j := locs[i]; j < locs[i+1]; j++ {
ii = append(ii, j)
}
}
indices = append(indices, ii)
}
return matches, indices, nil

View File

@ -38,7 +38,6 @@ type Log struct {
filter string
lastSent int
flushTimeout time.Duration
filtering bool
}
// NewLog returns a new model.
@ -176,28 +175,15 @@ func (l *Log) Filter(q string) {
defer l.mx.Unlock()
if len(q) == 0 {
l.filter, l.filtering = "", false
l.filter = ""
l.fireLogCleared()
l.fireLogBuffChanged(l.lines)
return
}
l.filter = q
// BOZO!! No needed since cmdbuff is now throttled!!
if l.filtering {
return
}
l.filtering = true
go func(l *Log) {
<-time.After(500 * time.Millisecond)
l.fireLogCleared()
l.fireLogBuffChanged(l.lines)
l.mx.Lock()
{
l.filtering = false
}
l.mx.Unlock()
}(l)
l.fireLogCleared()
l.fireLogBuffChanged(l.lines)
}
func (l *Log) load() error {

View File

@ -50,6 +50,10 @@ func TestLogFilter(t *testing.T) {
q: `pod-line-[1-3]{1}`,
e: 4,
},
"invert": {
q: `!pod-line-1`,
e: 8,
},
"fuzzy": {
q: `-f po-l1`,
e: 2,
@ -75,13 +79,13 @@ func TestLogFilter(t *testing.T) {
m.Notify()
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled)
assert.Equal(t, u.e, len(v.data))
m.ClearFilter()
assert.Equal(t, 2, v.dataCalled)
assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 3, v.clearCalled)
assert.Equal(t, 0, v.errCalled)
assert.Equal(t, size, len(v.data))
})
@ -195,7 +199,7 @@ func TestLogTimedout(t *testing.T) {
}
m.Notify()
assert.Equal(t, 1, v.dataCalled)
assert.Equal(t, 1, v.clearCalled)
assert.Equal(t, 2, v.clearCalled)
assert.Equal(t, 0, v.errCalled)
const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m"
assert.Equal(t, e, string(v.data[0]))

View File

@ -3,7 +3,6 @@ package model
import (
"context"
"fmt"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
@ -27,10 +26,6 @@ func NewPulseHealth(f dao.Factory) *PulseHealth {
// List returns a canned collection of resources health.
func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) {
defer func(t time.Time) {
log.Debug().Msgf("PulseHealthCheck %v", time.Since(t))
}(time.Now())
gvrs := []string{
"v1/pods",
"v1/events",

View File

@ -145,6 +145,11 @@ func (h Header) HasAge() bool {
return h.IndexOf(ageCol, true) != -1
}
// IsMetricsCol checks if given column index represents metrics.
func (h Header) IsMetricsCol(col int) bool {
return h[col].MX
}
// IsAgeCol checks if given column index is the age column.
func (h Header) IsAgeCol(col int) bool {
if !h.HasAge() || col >= len(h) {

View File

@ -1,6 +1,7 @@
package render
import (
"fmt"
"regexp"
"sort"
"strconv"
@ -259,30 +260,26 @@ func mapToIfc(m interface{}) (s string) {
func toMcPerc(v1, v2 *resource.Quantity) string {
m := v1.MilliValue()
return toMc(m) + " (" +
strconv.Itoa(client.ToPercentage(m, v2.MilliValue())) + "%)"
return fmt.Sprintf("%s (%d%%)", toMc(m), client.ToPercentage(m, v2.MilliValue()))
}
func toMiPerc(v1, v2 *resource.Quantity) string {
m := v1.Value()
return toMi(m) + " (" +
strconv.Itoa(client.ToPercentage(m, v2.Value())) + "%)"
return fmt.Sprintf("%s (%d%%)", toMi(m), client.ToPercentage(m, v2.Value()))
}
func toMc(v int64) string {
if v == 0 {
return ZeroValue
}
p := message.NewPrinter(language.English)
return p.Sprintf("%d", v)
return AsThousands(v)
}
func toMi(v int64) string {
if v == 0 {
return ZeroValue
}
p := message.NewPrinter(language.English)
return p.Sprintf("%d", client.ToMB(v))
return AsThousands(client.ToMB(v))
}
func boolPtrToStr(b *bool) string {

View File

@ -4,6 +4,7 @@ import (
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/fvbommel/sortorder"
@ -146,8 +147,14 @@ func (rr Rows) Find(id string) (int, bool) {
}
// Sort rows based on column index and order.
func (rr Rows) Sort(col int, asc bool) {
t := RowSorter{Rows: rr, Index: col, Asc: asc}
func (rr Rows) Sort(col int, asc, isNum, isDur bool) {
t := RowSorter{
Rows: rr,
Index: col,
IsNumber: isNum,
IsDuration: isDur,
Asc: asc,
}
sort.Sort(t)
}
@ -155,9 +162,10 @@ func (rr Rows) Sort(col int, asc bool) {
// RowSorter sorts rows.
type RowSorter struct {
Rows Rows
Index int
Asc bool
Rows Rows
Index int
IsNumber, IsDuration bool
Asc bool
}
func (s RowSorter) Len() int {
@ -169,7 +177,7 @@ func (s RowSorter) Swap(i, j int) {
}
func (s RowSorter) Less(i, j int) bool {
return Less(s.Asc, s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index])
return Less(s.Asc, s.IsNumber, s.IsDuration, s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index])
}
// ----------------------------------------------------------------------------
@ -185,8 +193,13 @@ func toAgeDuration(dur string) string {
}
// Less return true if c1 < c2.
func Less(asc bool, c1, c2 string) bool {
c1, c2 = toAgeDuration(c1), toAgeDuration(c2)
func Less(asc, isNumber, isDuration bool, c1, c2 string) bool {
if isNumber {
c1, c2 = strings.Replace(c1, ",", "", -1), strings.Replace(c2, ",", "", -1)
}
if isDuration {
c1, c2 = toAgeDuration(c1), toAgeDuration(c2)
}
b := sortorder.NaturalLess(c1, c2)
if asc {
return b

View File

@ -196,12 +196,19 @@ func (r RowEvents) FindIndex(id string) (int, bool) {
}
// Sort rows based on column index and order.
func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
func (r RowEvents) Sort(ns string, sortCol int, ageCol, numCol, asc bool) {
if sortCol == -1 {
return
}
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc}
t := RowEventSorter{
NS: ns,
Events: r,
Index: sortCol,
Asc: asc,
IsNumber: numCol,
IsDuration: ageCol,
}
sort.Sort(t)
iids, fields := map[string][]string{}, make(StringSet, 0, len(r))
@ -227,10 +234,12 @@ func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
// RowEventSorter sorts row events by a given colon.
type RowEventSorter struct {
Events RowEvents
Index int
NS string
Asc bool
Events RowEvents
Index int
NS string
IsNumber bool
IsDuration bool
Asc bool
}
func (r RowEventSorter) Len() int {
@ -243,7 +252,7 @@ func (r RowEventSorter) Swap(i, j int) {
func (r RowEventSorter) Less(i, j int) bool {
f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields
return Less(r.Asc, f1[r.Index], f2[r.Index])
return Less(r.Asc, r.IsNumber, r.IsDuration, f1[r.Index], f2[r.Index])
}
// ----------------------------------------------------------------------------

View File

@ -409,10 +409,10 @@ func TestRowEventsDelete(t *testing.T) {
func TestRowEventsSort(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
col int
age, asc bool
e render.RowEvents
re render.RowEvents
col int
age, num, asc bool
e render.RowEvents
}{
"age_time": {
re: render.RowEvents{
@ -483,7 +483,7 @@ func TestRowEventsSort(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
u.re.Sort("", u.col, u.age, u.asc)
u.re.Sort("", u.col, u.age, u.num, u.asc)
assert.Equal(t, u.e, u.re)
})
}

View File

@ -231,10 +231,10 @@ func TestRowsUpsert(t *testing.T) {
func TestRowsSortText(t *testing.T) {
uu := map[string]struct {
rows render.Rows
col int
asc bool
e render.Rows
rows render.Rows
col int
asc, num bool
e render.Rows
}{
"plainAsc": {
rows: render.Rows{
@ -266,6 +266,7 @@ func TestRowsSortText(t *testing.T) {
{Fields: []string{"1", "blee"}},
},
col: 0,
num: true,
asc: true,
e: render.Rows{
{Fields: []string{"1", "blee"}},
@ -278,6 +279,7 @@ func TestRowsSortText(t *testing.T) {
{Fields: []string{"1", "blee"}},
},
col: 0,
num: true,
asc: false,
e: render.Rows{
{Fields: []string{"10", "duh"}},
@ -301,7 +303,7 @@ func TestRowsSortText(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
u.rows.Sort(u.col, u.asc)
u.rows.Sort(u.col, u.asc, u.num, false)
assert.Equal(t, u.e, u.rows)
})
}
@ -342,7 +344,7 @@ func TestRowsSortDuration(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
u.rows.Sort(u.col, u.asc)
u.rows.Sort(u.col, u.asc, false, true)
assert.Equal(t, u.e, u.rows)
})
}
@ -369,13 +371,13 @@ func TestRowsSortMetrics(t *testing.T) {
},
"metricDesc": {
rows: render.Rows{
{Fields: []string{"10m", "100Mi"}},
{Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}},
},
col: 1,
asc: false,
e: render.Rows{
{Fields: []string{"10m", "100Mi"}},
{Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}},
},
},
@ -384,7 +386,7 @@ func TestRowsSortMetrics(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
u.rows.Sort(u.col, u.asc)
u.rows.Sort(u.col, u.asc, true, false)
assert.Equal(t, u.e, u.rows)
})
}

View File

@ -234,10 +234,12 @@ func (t *Table) doUpdate(data render.TableData) {
c.SetTextColor(fg)
col++
}
colIndex := custData.Header.IndexOf(t.sortCol.name, false)
custData.RowEvents.Sort(
custData.Namespace,
custData.Header.IndexOf(t.sortCol.name, false),
colIndex,
t.sortCol.name == "AGE",
data.Header.IsMetricsCol(colIndex),
t.sortCol.asc,
)