diff --git a/Makefile b/Makefile index 586b71a99b..efa7fdf5d1 100644 --- a/Makefile +++ b/Makefile @@ -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.3 +VERSION ?= v0.23.4 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/change_logs/release_v0.23.4.md b/change_logs/release_v0.23.4.md new file mode 100644 index 0000000000..b784cec633 --- /dev/null +++ b/change_logs/release_v0.23.4.md @@ -0,0 +1,28 @@ + + +# Release v0.23.4 + +## 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) + +--- + +## Maintenance Release! + +--- + +## Resolved Issues/Features + +* [Issue #920](https://github.com/derailed/k9s/issues/920) Timestamp stopped working +* [Issue #663](https://github.com/derailed/k9s/issues/663) Perf issues in v0.23.X - Better?? + +## Resolved PRs + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/root.go b/cmd/root.go index e9a9323c78..786daec944 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -53,7 +53,7 @@ func init() { if err := flags.Set("stderrthreshold", "fatal"); err != nil { panic(err) } - if err := flags.Set("v", "0"); err != nil { + if err := flags.Set("v", "-1"); err != nil { panic(err) } if err := flags.Set("log_file", config.K9sLogs); err != nil { diff --git a/internal/config/ns.go b/internal/config/ns.go index c6544433cb..4542228955 100644 --- a/internal/config/ns.go +++ b/internal/config/ns.go @@ -47,7 +47,6 @@ func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { // SetActive set the active namespace. func (n *Namespace) SetActive(ns string, ks KubeSettings) error { - log.Debug().Msgf("Setting active ns %q", ns) n.Active = ns if ns != "" { n.addFavNS(ns) diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go index 834dd4162d..4a1c222389 100644 --- a/internal/model/fish_buff.go +++ b/internal/model/fish_buff.go @@ -101,23 +101,28 @@ func (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) { } // Notify publish suggestions to all listeners. -func (f *FishBuff) Notify() { +func (f *FishBuff) Notify(delete bool) { if f.suggestionFn == nil { return } + ss := f.suggestionFn(string(f.buff)) + if len(ss) == 1 && !delete { + f.SetText(string(string(f.buff) + ss[0])) + return + } f.fireSuggestionChanged(f.suggestionFn(string(f.buff))) } -// Add adds a new charater to the buffer. +// Add adds a new character to the buffer. func (f *FishBuff) Add(r rune) { f.CmdBuff.Add(r) - f.Notify() + f.Notify(false) } // Delete removes the last character from the buffer. func (f *FishBuff) Delete() { f.CmdBuff.Delete() - f.Notify() + f.Notify(true) } func (f *FishBuff) fireSuggestionChanged(ss []string) { @@ -127,9 +132,10 @@ func (f *FishBuff) fireSuggestionChanged(ss []string) { return } + text, sug := f.GetText(), ss[f.suggestionIndex] for _, l := range f.listeners { if listener, ok := l.(SuggestionListener); ok { - listener.SuggestionChanged(f.GetText(), ss[f.suggestionIndex]) + listener.SuggestionChanged(text, sug) } } } diff --git a/internal/model/fish_buff_test.go b/internal/model/fish_buff_test.go index c58a05a405..f57327f4ce 100644 --- a/internal/model/fish_buff_test.go +++ b/internal/model/fish_buff_test.go @@ -8,20 +8,37 @@ import ( "github.com/stretchr/testify/assert" ) +func TestFishExact(t *testing.T) { + m := mockSuggestionListener{} + + f := model.NewFishBuff(' ', model.FilterBuffer) + f.AddListener(&m) + f.SetSuggestionFn(func(text string) sort.StringSlice { + return sort.StringSlice{"lee"} + }) + f.Add('b') + f.SetActive(true) + + assert.True(t, m.active) + assert.Equal(t, 1, m.buff) + assert.Equal(t, 0, m.sugg) + assert.Equal(t, "blee", m.text) +} + func TestFishAdd(t *testing.T) { m := mockSuggestionListener{} f := model.NewFishBuff(' ', model.FilterBuffer) f.AddListener(&m) f.SetSuggestionFn(func(text string) sort.StringSlice { - return sort.StringSlice{"blee", "duh"} + return sort.StringSlice{"blee", "brew"} }) - f.Add('a') + f.Add('b') f.SetActive(true) + assert.True(t, m.active) assert.Equal(t, 1, m.buff) assert.Equal(t, 1, m.sugg) - assert.True(t, m.active) assert.Equal(t, "blee", m.suggestion) c, ok := f.CurrentSuggestion() @@ -30,7 +47,7 @@ func TestFishAdd(t *testing.T) { c, ok = f.NextSuggestion() assert.True(t, ok) - assert.Equal(t, "duh", c) + assert.Equal(t, "brew", c) c, ok = f.PrevSuggestion() assert.True(t, ok) @@ -70,9 +87,9 @@ func TestFishDelete(t *testing.T) { // Helpers... type mockSuggestionListener struct { - buff, sugg int - suggestion string - active bool + buff, sugg int + suggestion, text string + active bool } func (m *mockSuggestionListener) BufferChanged(s string) { @@ -80,6 +97,7 @@ func (m *mockSuggestionListener) BufferChanged(s string) { } func (m *mockSuggestionListener) BufferCompleted(s string) { + m.text = s } func (m *mockSuggestionListener) BufferActive(state bool, kind model.BufferKind) { diff --git a/internal/model/log.go b/internal/model/log.go index c9224254e2..14e1ca25cb 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -50,14 +50,6 @@ func NewLog(gvr client.GVR, opts dao.LogOptions, flushTimeout time.Duration) *Lo } } -// LogOptions returns the current log options. -func (l *Log) LogOptions() dao.LogOptions { - l.mx.RLock() - defer l.mx.RUnlock() - - return l.logOptions -} - // SinceSeconds returns since seconds option. func (l *Log) SinceSeconds() int64 { l.mx.RLock() @@ -66,14 +58,15 @@ func (l *Log) SinceSeconds() int64 { return l.logOptions.SinceSeconds } -// SetLogOptions updates logger options. -func (l *Log) SetLogOptions(opts dao.LogOptions) { - l.mx.Lock() - { - l.logOptions = opts - } - l.mx.Unlock() +// ToggleShowTimestamp toggles to logs timestamps. +func (l *Log) ToggleShowTimestamp(b bool) { + l.logOptions.ShowTimestamp = b + l.Refresh() +} +// SetSinceSeconds sets the logs retrieval time. +func (l *Log) SetSinceSeconds(i int64) { + l.logOptions.SinceSeconds = i l.Restart() } diff --git a/internal/render/container.go b/internal/render/container.go index c2f23aed72..d91f21fb03 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -154,15 +154,14 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (resources, if rList.Cpu() != nil { p[requestCPU] = percentMc(c.rCPU(), rList.Cpu()) } - if rList.Memory() != nil { - p[requestMEM] = percentMi(c.rMEM(), rList.Memory()) - } - if lList.Cpu() != nil { - p[limitCPU] = percentMc(c.lCPU(), lList.Cpu()) + p[limitCPU] = percentMc(c.rCPU(), lList.Cpu()) } if rList.Memory() != nil { - p[limitMEM] = percentMi(c.lMEM(), lList.Memory()) + p[requestMEM] = percentMi(c.rMEM(), rList.Memory()) + } + if lList.Memory() != nil { + p[limitMEM] = percentMi(c.rMEM(), lList.Memory()) } return c, p, r diff --git a/internal/render/container_test.go b/internal/render/container_test.go index 8241baf203..e75768c4b2 100644 --- a/internal/render/container_test.go +++ b/internal/render/container_test.go @@ -40,9 +40,9 @@ func TestContainer(t *testing.T) { "20:20", "100:100", "50", - "0", + "50", + "20", "20", - "0", "", "container is not ready", }, diff --git a/internal/render/helpers.go b/internal/render/helpers.go index d6cd6b37a0..1d6b792680 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -1,7 +1,6 @@ package render import ( - "fmt" "regexp" "sort" "strconv" @@ -120,7 +119,7 @@ func join(a []string, sep string) string { return a[0] } - var b []string + b := make([]string, 0, len(a)) for _, s := range a { if s != "" { b = append(b, s) @@ -258,28 +257,26 @@ func mapToIfc(m interface{}) (s string) { return } -func toMcPerc(v1, v2 *resource.Quantity) string { - m := v1.MilliValue() - return fmt.Sprintf("%s (%d%%)", toMc(m), client.ToPercentage(m, v2.MilliValue())) +func toMcPerc(v1, v2 int64) string { + return toMc(v1) + " (" + strconv.Itoa(client.ToPercentage(v1, v2)) + "%)" } -func toMiPerc(v1, v2 *resource.Quantity) string { - m := v1.Value() - return fmt.Sprintf("%s (%d%%)", toMi(m), client.ToPercentage(m, v2.Value())) +func toMiPerc(v1, v2 int64) string { + return toMi(v1) + " (" + strconv.Itoa(client.ToPercentage(v1, v2)) + "%)" } func toMc(v int64) string { if v == 0 { return ZeroValue } - return AsThousands(v) + return strconv.Itoa(int(v)) } func toMi(v int64) string { if v == 0 { return ZeroValue } - return AsThousands(client.ToMB(v)) + return strconv.Itoa(int(client.ToMB(v))) } func boolPtrToStr(b *bool) string { diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index a438e639df..6befcf87a3 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -368,7 +368,7 @@ func TestToMc(t *testing.T) { }{ {0, "0"}, {2, "2"}, - {1_000, "1,000"}, + {1_000, "1000"}, } for _, u := range uu { @@ -383,7 +383,7 @@ func TestToMi(t *testing.T) { }{ {0, "0"}, {2 * client.MegaByte, "2"}, - {1_000 * client.MegaByte, "1,000"}, + {1_000 * client.MegaByte, "1000"}, } for _, u := range uu { diff --git a/internal/render/node.go b/internal/render/node.go index b792f4e61c..e0df15d439 100644 --- a/internal/render/node.go +++ b/internal/render/node.go @@ -41,10 +41,10 @@ func (Node) Header(_ string) Header { HeaderColumn{Name: "INTERNAL-IP", Wide: true}, HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, HeaderColumn{Name: "PODS", Align: tview.AlignRight}, - HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, + HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "CPU/R", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "CPU/L", Align: tview.AlignRight, MX: true}, HeaderColumn{Name: "MEM/R", Align: tview.AlignRight, MX: true}, @@ -63,7 +63,6 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { if !ok { return fmt.Errorf("Expected *NodeAndMetrics, but got %T", o) } - meta, ok := oo.Raw.Object["metadata"].(map[string]interface{}) if !ok { return fmt.Errorf("Unable to extract meta") @@ -96,9 +95,8 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { tlm.Add(*lList.Memory()) } } - res := newResources(newResourceList(trc, trm), newResourceList(tlc, tlm)) statuses := make(sort.StringSlice, 10) - status(no.Status, no.Spec.Unschedulable, statuses) + status(no.Status.Conditions, no.Spec.Unschedulable, statuses) sort.Sort(statuses) roles := make(sort.StringSlice, 10) nodeRoles(&no, roles) @@ -114,16 +112,16 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { iIP, eIP, strconv.Itoa(len(oo.Pods)), + toMc(c.cpu), + toMi(c.mem), strconv.Itoa(p.rCPU()), strconv.Itoa(p.rMEM()), - toMc(c.rCPU().MilliValue()), - toMi(c.rMEM().Value()), - toMcPerc(res.rCPU(), a.rCPU()), - toMcPerc(res.lCPU(), a.rCPU()), - toMiPerc(res.rMEM(), a.rMEM()), - toMiPerc(res.lMEM(), a.rMEM()), - toMc(a.rCPU().MilliValue()), - toMi(a.rMEM().Value()), + toMcPerc(trc.MilliValue(), a.cpu), + toMcPerc(tlc.MilliValue(), a.cpu), + toMiPerc(trm.Value(), a.mem), + toMiPerc(tlm.Value(), a.mem), + toMc(a.cpu), + toMi(a.mem), mapToStr(no.Labels), asStatus(n.diagnose(statuses)), toAge(no.ObjectMeta.CreationTimestamp), @@ -177,19 +175,24 @@ func (n *NodeWithMetrics) DeepCopyObject() runtime.Object { return n } -func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (resources, percentages, resources) { - c, p, a := newResources(nil, nil), newPercentages(), newResources(no.Status.Allocatable, nil) +type metric struct { + cpu, mem int64 +} + +func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (metric, percentages, metric) { + c := metric{cpu: 0, mem: 0} + p := newPercentages() + a := metric{ + cpu: no.Status.Allocatable.Cpu().MilliValue(), + mem: no.Status.Allocatable.Memory().Value(), + } if mx == nil { return c, p, a } - c[requestCPU], c[requestMEM] = mx.Usage.Cpu(), mx.Usage.Memory() - if a.rCPU() != nil { - p[requestCPU] = percentMc(c.rCPU(), a.rCPU()) - } - if a.rMEM() != nil { - p[requestMEM] = percentMi(c.rMEM(), a.rMEM()) - } + c.cpu, c.mem = mx.Usage.Cpu().MilliValue(), mx.Usage.Memory().Value() + p[requestCPU] = client.ToPercentage(c.cpu, a.cpu) + p[requestMEM] = client.ToPercentage(c.mem, a.mem) return c, p, a } @@ -230,11 +233,11 @@ func getIPs(addrs []v1.NodeAddress) (iIP, eIP string) { return } -func status(status v1.NodeStatus, exempt bool, res []string) { +func status(conds []v1.NodeCondition, exempt bool, res []string) { var index int - conditions := make(map[v1.NodeConditionType]*v1.NodeCondition) - for n := range status.Conditions { - cond := status.Conditions[n] + conditions := make(map[v1.NodeConditionType]*v1.NodeCondition, len(conds)) + for n := range conds { + cond := conds[n] conditions[cond.Type] = &cond } diff --git a/internal/render/node_test.go b/internal/render/node_test.go index de878f2ebd..163963c2b3 100644 --- a/internal/render/node_test.go +++ b/internal/render/node_test.go @@ -12,7 +12,7 @@ import ( func TestNodeRender(t *testing.T) { pom := render.NodeWithMetrics{ Raw: load(t, "no"), - MX: makeNodeMX("n1", "10m", "10Mi"), + MX: makeNodeMX("n1", "10m", "20Mi"), } var no render.Node @@ -21,7 +21,7 @@ func TestNodeRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "minikube", r.ID) - e := render.Fields{"minikube", "Ready", "master", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "0", "0", "10", "10", "0 (0%)", "0 (0%)", "0 (0%)", "0 (0%)", "4,000", "7,874"} + e := render.Fields{"minikube", "Ready", "master", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "0 (0%)", "0 (0%)", "0 (0%)", "0 (0%)", "4000", "7874"} assert.Equal(t, e, r.Fields[:18]) } diff --git a/internal/render/pod.go b/internal/render/pod.go index c81f098281..7f466142ab 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -218,13 +218,12 @@ func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (resources, percen if rList.Cpu() != nil { p[requestCPU] = percentMc(c.rCPU(), rList.Cpu()) } - if rList.Memory() != nil { - p[requestMEM] = percentMi(c.rMEM(), rList.Memory()) - } - if lList.Cpu() != nil { p[limitCPU] = percentMc(c.rCPU(), lList.Cpu()) } + if rList.Memory() != nil { + p[requestMEM] = percentMi(c.rMEM(), rList.Memory()) + } if lList.Memory() != nil { p[limitMEM] = percentMi(c.rMEM(), lList.Memory()) } diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index 9ff081b552..7449f4918d 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -46,7 +46,7 @@ type PromptModel interface { ClearText(fire bool) // Notify notifies all listener of current suggestions. - Notify() + Notify(bool) // AddListener registers a command listener. AddListener(model.BuffWatcher) @@ -178,7 +178,7 @@ func (p *Prompt) InCmdMode() bool { func (p *Prompt) activate() { p.SetCursorIndex(len(p.model.GetText())) p.write(p.model.GetText(), "") - p.model.Notify() + p.model.Notify(false) } func (p *Prompt) update(s string) { diff --git a/internal/view/log.go b/internal/view/log.go index 84c6ebcd69..f3d3088355 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -98,6 +98,8 @@ func (l *Log) Init(ctx context.Context) (err error) { l.model.AddListener(l) l.updateTitle() + l.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime) + return nil } @@ -130,12 +132,12 @@ func (l *Log) LogChanged(lines [][]byte) { // BufferCompleted indicates input was accepted. func (l *Log) BufferCompleted(s string) { - l.model.Filter(l.logs.cmdBuff.GetText()) + l.model.Filter(s) l.updateTitle() } // BufferChanged indicates the buffer was changed. -func (l *Log) BufferChanged(s string) {} +func (l *Log) BufferChanged(string) {} // BufferActive indicates the buff activity changed. func (l *Log) BufferActive(state bool, k model.BufferKind) { @@ -284,9 +286,7 @@ func (l *Log) Flush(lines [][]byte) { func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - opts := l.model.LogOptions() - opts.SinceSeconds = int64(a) - l.model.SetLogOptions(opts) + l.model.SetSinceSeconds(int64(a)) l.updateTitle() return nil } @@ -305,7 +305,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { } // SaveCmd dumps the logs to file. -func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { +func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { if path, err := saveData(l.app.Config.K9s.CurrentCluster, l.model.GetPath(), l.logs.GetText(true)); err != nil { l.app.Flash().Err(err) } else { @@ -314,7 +314,7 @@ func (l *Log) SaveCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (l *Log) cpCmd(evt *tcell.EventKey) *tcell.EventKey { +func (l *Log) cpCmd(*tcell.EventKey) *tcell.EventKey { l.app.Flash().Info("Content copied to clipboard...") if err := clipboard.WriteAll(l.logs.GetText(true)); err != nil { l.app.Flash().Err(err) @@ -378,7 +378,7 @@ func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { } l.indicator.ToggleTimestamp() - l.model.Refresh() + l.model.ToggleShowTimestamp(l.indicator.showTime) return nil }