diff --git a/change_logs/release_0.5.2.md b/change_logs/release_0.5.2.md new file mode 100644 index 00000000..2e66ca62 --- /dev/null +++ b/change_logs/release_0.5.2.md @@ -0,0 +1,30 @@ + + +# Release v0.5.2 + +## 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. + +Thank you so much for your support and awesome suggestions to make K9s better!! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Change Logs + + +--- + +## Resolved Bugs + ++ [Issue #171](https://github.com/derailed/k9s/issues/171) ++ [Issue #173](https://github.com/derailed/k9s/issues/173) ++ [Issue #174](https://github.com/derailed/k9s/issues/174) + +--- + + © 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 97e3eb1e..ec881558 100644 --- a/internal/k8s/api.go +++ b/internal/k8s/api.go @@ -135,7 +135,11 @@ func (a *APIClient) IsNamespaced(res string) bool { // SupportsResource checks for resource supported version against the server. func (a *APIClient) SupportsResource(group string) bool { - list, _ := a.DialOrDie().Discovery().ServerPreferredResources() + list, err := a.DialOrDie().Discovery().ServerPreferredResources() + if err != nil { + log.Debug().Err(err).Msg("Unable to dial api server") + return false + } for _, l := range list { log.Debug().Msgf(">>> Group %s", l.GroupVersion) if l.GroupVersion == group { diff --git a/internal/resource/base.go b/internal/resource/base.go index 65f9d901..980ba465 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -115,8 +115,6 @@ func (b *Base) Describe(kind, pa string, flags *genericclioptions.ConfigFlags) ( return "", err } - log.Debug().Msgf("Describer %#v", d) - return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true}) } diff --git a/internal/resource/container.go b/internal/resource/container.go index a187c8e4..20eb202c 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -179,6 +179,7 @@ func (r *Container) Fields(ns string) Row { } cs = &c } + if cs == nil { for _, c := range r.pod.Status.InitContainerStatuses { if c.Name != i.Name { @@ -188,12 +189,17 @@ func (r *Container) Fields(ns string) Row { } } + ready, state, restarts := "false", MissingValue, "0" + if cs != nil { + ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount)) + } + return append(ff, i.Name, i.Image, - boolToStr(cs.Ready), - toState(cs.State), - strconv.Itoa(int(cs.RestartCount)), + ready, + state, + restarts, probe(i.LivenessProbe), probe(i.ReadinessProbe), cpu, diff --git a/internal/resource/pod.go b/internal/resource/pod.go index 218c8b64..124384a1 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" + "k8s.io/kubernetes/pkg/util/node" ) const ( @@ -212,7 +213,7 @@ func (r *Pod) Fields(ns string) Row { return append(ff, i.ObjectMeta.Name, strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)), - r.phase(i.Status), + r.phase(i), strconv.Itoa(rc), ToMillicore(r.metrics.CurrentCPU), ToMi(r.metrics.CurrentMEM), @@ -251,21 +252,76 @@ func (r *Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { return } -func (*Pod) phase(s v1.PodStatus) string { - status := "Pending" - for _, cs := range s.ContainerStatuses { +func isSet(s *string) bool { + return s != nil && *s != "" +} + +func (*Pod) phase(po *v1.Pod) string { + status := string(po.Status.Phase) + if po.Status.Reason != "" { + if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason { + return "Unknown" + } + status = po.Status.Reason + } + + var init bool + for i, cs := range po.Status.InitContainerStatuses { switch { - case cs.State.Running != nil: - status = "Running" - case cs.State.Waiting != nil: - status = cs.State.Waiting.Reason case cs.State.Terminated != nil: - status = "Terminating" - if len(cs.State.Terminated.Reason) != 0 { - status = cs.State.Terminated.Reason + if cs.State.Terminated.ExitCode == 0 { + continue } + if cs.State.Terminated.Reason != "" { + status = "Init:" + cs.State.Terminated.Reason + init = true + break + } + + if cs.State.Terminated.Signal != 0 { + status = fmt.Sprintf("Init:Signal:%d", cs.State.Terminated.Signal) + } else { + status = fmt.Sprintf("Init:ExitCode:%d", cs.State.Terminated.ExitCode) + } + case cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing": + status = "Init:" + cs.State.Waiting.Reason + default: + status = fmt.Sprintf("Init:%d/%d", i, len(po.Spec.InitContainers)) + } + init = true + break + } + + if init { + return status + } + + var running bool + for i := len(po.Status.ContainerStatuses) - 1; i >= 0; i-- { + cs := po.Status.ContainerStatuses[i] + switch { + case cs.State.Waiting != nil && cs.State.Waiting.Reason != "": + status = cs.State.Waiting.Reason + case cs.State.Terminated != nil && cs.State.Terminated.Reason != "": + status = cs.State.Terminated.Reason + case cs.State.Terminated != nil: + if cs.State.Terminated.Signal != 0 { + status = fmt.Sprintf("Signal:%d", cs.State.Terminated.Signal) + } else { + status = fmt.Sprintf("ExitCode:%d", cs.State.Terminated.ExitCode) + } + case cs.Ready && cs.State.Running != nil: + running = true } } - return status + if status == "Completed" && running { + status = "Running" + } + + if po.DeletionTimestamp == nil { + return status + } + + return "Terminated" } diff --git a/internal/resource/pod_int_test.go b/internal/resource/pod_int_test.go index 1cd2a43e..784d8535 100644 --- a/internal/resource/pod_int_test.go +++ b/internal/resource/pod_int_test.go @@ -2,9 +2,11 @@ package resource import ( "testing" + "time" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestPodStatuses(t *testing.T) { @@ -49,69 +51,132 @@ func TestPodStatuses(t *testing.T) { func TestPodPhase(t *testing.T) { uu := []struct { - s v1.PodStatus + p *v1.Pod e string }{ - { - v1.PodStatus{ - ContainerStatuses: []v1.ContainerStatus{ - { - Name: "c1", - State: v1.ContainerState{ - Running: &v1.ContainerStateRunning{}, - }, - }, - }, - }, - "Running", - }, - { - v1.PodStatus{ - ContainerStatuses: []v1.ContainerStatus{ - { - Name: "c1", - State: v1.ContainerState{ - Waiting: &v1.ContainerStateWaiting{ - Reason: "blee", - }, - }, - }, - }, - }, - "blee", - }, - { - v1.PodStatus{ - ContainerStatuses: []v1.ContainerStatus{ - { - Name: "c1", - State: v1.ContainerState{ - Terminated: &v1.ContainerStateTerminated{}, - }, - }, - }, - }, - "Terminating", - }, - { - v1.PodStatus{ - ContainerStatuses: []v1.ContainerStatus{ - { - Name: "c1", - State: v1.ContainerState{ - Terminated: &v1.ContainerStateTerminated{ - Reason: "blee", - }, - }, - }, - }, - }, - "blee", - }, + {makePodStatus("p1", v1.PodRunning, ""), "Running"}, + {makePodStatus("p1", v1.PodRunning, "Evicted"), "Evicted"}, + {makePodStatus("p1", v1.PodPending, ""), "Pending"}, + {makePodStatus("p1", v1.PodSucceeded, ""), "Succeeded"}, + {makePodStatus("p1", v1.PodFailed, ""), "Failed"}, + {makePodStatus("p1", v1.PodUnknown, ""), "Unknown"}, + {makePodCoInitTerminated("p1"), "Init:OOMKilled"}, + {makePodCoInitWaiting("p1", ""), "Init:0/1"}, + {makePodCoInitWaiting("p1", "Waiting"), "Init:Waiting"}, + {makePodCoInitWaiting("p1", "PodInitializing"), "Init:0/1"}, + {makePodCoWaiting("p1", "Waiting"), "Waiting"}, + {makePodCoWaiting("p1", ""), ""}, + {makePodCoTerminated("p1", "OOMKilled", 0, true), "Terminated"}, + {makePodCoTerminated("p1", "OOMKilled", 0, false), "OOMKilled"}, + {makePodCoTerminated("p1", "", 0, true), "Terminated"}, + {makePodCoTerminated("p1", "", 0, false), "ExitCode:1"}, + {makePodCoTerminated("p1", "", 1, true), "Terminated"}, + {makePodCoTerminated("p1", "", 1, false), "Signal:1"}, } var p Pod for _, u := range uu { - assert.Equal(t, u.e, p.phase(u.s)) + assert.Equal(t, u.e, p.phase(u.p)) + } +} + +func makePodStatus(n string, phase v1.PodPhase, reason string) *v1.Pod { + po := makePod(n) + po.Status = v1.PodStatus{ + Phase: phase, + Reason: reason, + } + + return po +} + +func makePodCoInitTerminated(n string) *v1.Pod { + po := makePod(n) + + po.Status.InitContainerStatuses = []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "OOMKilled", + ExitCode: 1, + }, + }, + }, + } + + return po +} + +func makePodCoInitWaiting(n, reason string) *v1.Pod { + po := makePod(n) + + po.Status.InitContainerStatuses = []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: reason, + }, + }, + }, + } + + return po +} + +func makePodCoTerminated(n, reason string, signal int32, deleted bool) *v1.Pod { + po := makePod(n) + + if deleted { + po.DeletionTimestamp = &metav1.Time{time.Now()} + } + po.Status.ContainerStatuses = []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: reason, + Signal: signal, + ExitCode: 1, + }, + }, + }, + } + + return po +} + +func makePodCoWaiting(n, reason string) *v1.Pod { + po := makePod(n) + + po.Status.ContainerStatuses = []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: reason, + }, + }, + }, + } + + return po +} + +func makePod(n string) *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + Namespace: "default", + }, + Spec: v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "ic1", + }, + }, + Containers: []v1.Container{ + { + Name: "c1", + }, + }, + }, } } diff --git a/internal/views/app.go b/internal/views/app.go index a7ffde30..13527e23 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "time" + "sync" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/k8s" @@ -60,6 +61,7 @@ type ( cmdBuff *cmdBuff cmdView *cmdView actions keyActions + mx sync.Mutex } ) @@ -143,6 +145,9 @@ func (a *appView) Run() { } func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey { + a.mx.Lock() + defer a.mx.Unlock() + key := evt.Key() if key == tcell.KeyRune { if a.cmdBuff.isActive() { diff --git a/internal/views/details.go b/internal/views/details.go index 0b790f47..6ff92d7b 100644 --- a/internal/views/details.go +++ b/internal/views/details.go @@ -5,6 +5,7 @@ import ( "regexp" "strconv" "strings" + "sync" "github.com/derailed/tview" "github.com/gdamore/tcell" @@ -24,6 +25,7 @@ type detailsView struct { cmdBuff *cmdBuff backFn actionHandler numSelections int + mx sync.Mutex } func newDetailsView(app *appView, backFn actionHandler) *detailsView { @@ -63,6 +65,9 @@ func (v *detailsView) setCategory(n string) { } func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { + v.mx.Lock() + defer v.mx.Unlock() + key := evt.Key() if key == tcell.KeyRune { if v.cmdBuff.isActive() { diff --git a/internal/views/logs.go b/internal/views/logs.go index 004d1f17..6c818d31 100644 --- a/internal/views/logs.go +++ b/internal/views/logs.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strconv" + "sync" "time" "github.com/derailed/k9s/internal/resource" @@ -28,6 +29,7 @@ type logsView struct { cancelFunc context.CancelFunc autoScroll bool showPrevious bool + mx sync.Mutex } func newLogsView(pview string, parent loggable) *logsView { @@ -148,6 +150,9 @@ func (v *logsView) load(i int) { } func (v *logsView) doLoad(path, co string) error { + v.mx.Lock() + defer v.mx.Unlock() + v.stop() c := make(chan string) diff --git a/internal/views/menu.go b/internal/views/menu.go index 36aa84a1..7538235f 100644 --- a/internal/views/menu.go +++ b/internal/views/menu.go @@ -6,6 +6,7 @@ import ( "sort" "strconv" "strings" + "sync" "github.com/derailed/k9s/internal/resource" "github.com/derailed/tview" @@ -77,7 +78,7 @@ func newKeyAction(d string, a actionHandler, display bool) keyAction { } func newMenuView() *menuView { - v := menuView{tview.NewTable()} + v := menuView{Table: tview.NewTable()} return &v } @@ -106,9 +107,14 @@ func (a keyActions) toHints() hints { // ----------------------------------------------------------------------------- type menuView struct { *tview.Table + + mx sync.Mutex } func (v *menuView) populateMenu(hh hints) { + v.mx.Lock() + defer v.mx.Unlock() + v.Clear() sort.Sort(hh) diff --git a/internal/views/pod.go b/internal/views/pod.go index 3cfb9b76..04385c95 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -25,7 +25,7 @@ type loggable interface { } func newPodView(t string, app *appView, list resource.List) resourceViewer { - v := podView{newResourceView(t, app, list).(*resourceView)} + v := podView{resourceView: newResourceView(t, app, list).(*resourceView)} { v.extraActionsFn = v.extraActions v.enterFn = v.listContainers diff --git a/internal/views/resource.go b/internal/views/resource.go index f90c719c..a034c043 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -70,11 +70,6 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie details := newDetailsView(app, v.backCmd) v.AddPage("details", details, true, false) - confirm := tview.NewModal(). - AddButtons([]string{"Cancel", "OK"}). - SetTextColor(tcell.ColorFuchsia) - v.AddPage("confirm", confirm, false, false) - return &v } @@ -180,9 +175,7 @@ func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { } sel := v.getSelectedItem() - confirm := v.GetPrimitive("confirm").(*tview.Modal) - confirm.SetText(fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel)) - confirm.SetDoneFunc(func(_ int, button string) { + v.showModal(fmt.Sprintf("Delete %s %s?", v.list.GetName(), sel), func(_ int, button string) { if button == "OK" { v.getTV().setDeleted() v.app.flash(flashInfo, fmt.Sprintf("Deleting %s %s", v.list.GetName(), sel)) @@ -192,13 +185,27 @@ func (v *resourceView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { v.refresh() } } - v.switchPage(v.list.GetName()) + v.dismissModal() }) - v.SwitchToPage("confirm") return nil } +func (v *resourceView) showModal(msg string, done func(int, string)) { + confirm := tview.NewModal(). + AddButtons([]string{"Cancel", "OK"}). + SetTextColor(tcell.ColorFuchsia). + SetText(msg). + SetDoneFunc(done) + v.AddPage("confirm", confirm, false, false) + v.ShowPage("confirm") +} + +func (v *resourceView) dismissModal() { + v.RemovePage("confirm") + v.switchPage(v.list.GetName()) +} + func (v *resourceView) defaultEnter(app *appView, ns, resource, selection string) { sel := v.getSelectedItem() yaml, err := v.list.Resource().Describe(v.title, sel, v.app.flags) @@ -270,6 +277,7 @@ func (v *resourceView) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { i, _ := strconv.Atoi(string(evt.Rune())) ns := v.namespaces[i] v.doSwitchNamespace(ns) + return nil } @@ -284,6 +292,7 @@ func (v *resourceView) doSwitchNamespace(ns string) { v.list.SetNamespace(v.selectedNS) } v.update.Unlock() + v.refresh() v.selectItem(0, 0) v.getTV().resetTitle() diff --git a/internal/views/rs.go b/internal/views/rs.go index a8b69cab..27c60abe 100644 --- a/internal/views/rs.go +++ b/internal/views/rs.go @@ -7,7 +7,6 @@ import ( "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" - "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" @@ -85,17 +84,14 @@ func (v *replicaSetView) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - confirm := v.GetPrimitive("confirm").(*tview.Modal) - confirm.SetText(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), v.selectedItem)) - confirm.SetDoneFunc(func(_ int, button string) { + v.showModal(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), v.selectedItem), func(_ int, button string) { if button == "OK" { v.app.flash(flashInfo, fmt.Sprintf("Rolling back %s %s", v.list.GetName(), v.selectedItem)) rollback(v.app, v.selectedItem) v.refresh() } - v.switchPage(v.list.GetName()) + v.dismissModal() }) - v.SwitchToPage("confirm") return nil } diff --git a/internal/views/table.go b/internal/views/table.go index 38c5ef26..c06db966 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -3,6 +3,7 @@ package views import ( "fmt" "regexp" + "runtime" "sort" "strings" "sync" @@ -111,6 +112,7 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey { if a, ok := v.actions[key]; ok { log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key]) + log.Debug().Msgf("Go Routine %d", runtime.NumGoroutine()) return a.action(evt) }