diff --git a/go.mod b/go.mod index e258aa64..160a7575 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,14 @@ require ( github.com/rs/zerolog v1.12.0 github.com/spf13/cobra v0.0.3 github.com/stretchr/testify v1.2.2 + golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f // indirect gopkg.in/yaml.v2 v2.2.2 k8s.io/api v0.0.0-20190202010724-74b699b93c15 k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467 k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a k8s.io/client-go v10.0.0+incompatible k8s.io/klog v0.2.0 // indirect + k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c // indirect k8s.io/kubernetes v1.13.3 k8s.io/metrics v0.0.0-20181121073115-d8618695b08f sigs.k8s.io/structured-merge-diff v0.0.0-20190404181321-646549c5a231 // indirect diff --git a/go.sum b/go.sum index 74f6390b..5b6aaf3f 100644 --- a/go.sum +++ b/go.sum @@ -240,7 +240,10 @@ golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -361,6 +364,8 @@ k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668 h1:M80qeWaBNOX2Uc4plRHcb6k+3YE5VWMaJXKZo+tX9aU= k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c h1:kJCzg2vGCzah5icgkKR7O1Dzn0NA2iGlym27sb0ZfGE= +k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kubernetes v1.13.3 h1:46t44D87wKtdKFgr/lXM60K8xPrW0wO67Woof3Vsv6E= k8s.io/kubernetes v1.13.3/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/metrics v0.0.0-20181121073115-d8618695b08f h1:HyUoIBzks9xTaSnMJ6kv/SSmwaQQccokuiriu2cV0aA= diff --git a/internal/k8s/base.go b/internal/k8s/base.go index 7de33b52..cf849dbe 100644 --- a/internal/k8s/base.go +++ b/internal/k8s/base.go @@ -14,3 +14,7 @@ func (b *base) SetFieldSelector(s string) { func (b *base) SetLabelSelector(s string) { b.labelSelector = s } + +func (b *base) HasSelectors() bool { + return b.labelSelector != "" || b.fieldSelector != "" +} diff --git a/internal/k8s/pod.go b/internal/k8s/pod.go index ed94bf8d..ad3cd4f6 100644 --- a/internal/k8s/pod.go +++ b/internal/k8s/pod.go @@ -30,7 +30,6 @@ func (p *Pod) List(ns string) (Collection, error) { LabelSelector: p.labelSelector, FieldSelector: p.fieldSelector, } - // FieldSelector: "spec.nodeName=gke-k9s-default-pool-0fa2fb89-lbtf", rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts) if err != nil { diff --git a/internal/resource/base.go b/internal/resource/base.go index 9b7985cd..65f9d901 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -22,6 +22,7 @@ type ( Delete(ns string, name string) error SetLabelSelector(string) SetFieldSelector(string) + HasSelectors() bool } // Connection represents a Kubenetes apiserver connection. @@ -47,6 +48,11 @@ func NewBase(c Connection, r Cruder) *Base { return &Base{Connection: c, Resource: r} } +// HasSelectors returns true if field or label selectors are set. +func (b *Base) HasSelectors() bool { + return b.Resource.HasSelectors() +} + // SetFieldSelector refines query results via selector. func (b *Base) SetFieldSelector(s string) { b.Resource.SetFieldSelector(s) @@ -99,6 +105,7 @@ func (b *Base) Describe(kind, pa string, flags *genericclioptions.ConfigFlags) ( mapping, err := k8s.RestMapping.Find(kind) if err != nil { + log.Debug().Msgf("Unable to find mapper for %s %s", kind, pa) return "", err } diff --git a/internal/resource/list.go b/internal/resource/list.go index 93145172..89ab91e0 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -65,8 +65,11 @@ type ( Reconcile() error GetName() string Access(flag int) bool + GetAccess() int + SetAccess(int) SetFieldSelector(string) SetLabelSelector(string) + HasSelectors() bool } // Columnar tracks resources that can be diplayed in a tabular fashion. @@ -97,6 +100,7 @@ type ( Header(ns string) Row SetFieldSelector(string) SetLabelSelector(string) + HasSelectors() bool } list struct { @@ -122,6 +126,10 @@ func NewList(ns, name string, res Resource, verbs int) *list { } } +func (l *list) HasSelectors() bool { + return l.resource.HasSelectors() +} + // SetFieldSelector narrows down resource query given fields selection. func (l *list) SetFieldSelector(s string) { l.resource.SetFieldSelector(s) @@ -137,6 +145,16 @@ func (l *list) Access(f int) bool { return l.verbs&f == f } +// Access check access control on a given resource. +func (l *list) GetAccess() int { + return l.verbs +} + +// Access check access control on a given resource. +func (l *list) SetAccess(f int) { + l.verbs = f +} + // Namespaced checks if k8s resource is namespaced. func (l *list) Namespaced() bool { return l.namespace != NotNamespaced diff --git a/internal/resource/mock_cruder_test.go b/internal/resource/mock_cruder_test.go index 1d160356..0e2bbb0d 100644 --- a/internal/resource/mock_cruder_test.go +++ b/internal/resource/mock_cruder_test.go @@ -52,6 +52,21 @@ func (mock *MockCruder) Get(_param0 string, _param1 string) (interface{}, error) return ret0, ret1 } +func (mock *MockCruder) HasSelectors() bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockCruder().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("HasSelectors", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockCruder) List(_param0 string) (k8s.Collection, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockCruder().") @@ -186,6 +201,23 @@ func (c *Cruder_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []st return } +func (verifier *VerifierCruder) HasSelectors() *Cruder_HasSelectors_OngoingVerification { + params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasSelectors", params, verifier.timeout) + return &Cruder_HasSelectors_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type Cruder_HasSelectors_OngoingVerification struct { + mock *MockCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *Cruder_HasSelectors_OngoingVerification) GetCapturedArguments() { +} + +func (c *Cruder_HasSelectors_OngoingVerification) GetAllCapturedArguments() { +} + func (verifier *VerifierCruder) List(_param0 string) *Cruder_List_OngoingVerification { params := []pegomock.Param{_param0} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout) diff --git a/internal/resource/mock_switchablecruder_test.go b/internal/resource/mock_switchablecruder_test.go index ba757a67..917c28d1 100644 --- a/internal/resource/mock_switchablecruder_test.go +++ b/internal/resource/mock_switchablecruder_test.go @@ -52,6 +52,21 @@ func (mock *MockSwitchableCruder) Get(_param0 string, _param1 string) (interface return ret0, ret1 } +func (mock *MockSwitchableCruder) HasSelectors() bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("HasSelectors", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockSwitchableCruder) List(_param0 string) (k8s.Collection, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().") @@ -216,6 +231,23 @@ func (c *SwitchableCruder_Get_OngoingVerification) GetAllCapturedArguments() (_p return } +func (verifier *VerifierSwitchableCruder) HasSelectors() *SwitchableCruder_HasSelectors_OngoingVerification { + params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasSelectors", params, verifier.timeout) + return &SwitchableCruder_HasSelectors_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type SwitchableCruder_HasSelectors_OngoingVerification struct { + mock *MockSwitchableCruder + methodInvocations []pegomock.MethodInvocation +} + +func (c *SwitchableCruder_HasSelectors_OngoingVerification) GetCapturedArguments() { +} + +func (c *SwitchableCruder_HasSelectors_OngoingVerification) GetAllCapturedArguments() { +} + func (verifier *VerifierSwitchableCruder) List(_param0 string) *SwitchableCruder_List_OngoingVerification { params := []pegomock.Param{_param0} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout) diff --git a/internal/views/app.go b/internal/views/app.go index 504a056a..0e5bfe49 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -29,12 +29,15 @@ type ( keyboard(evt *tcell.EventKey) *tcell.EventKey } + actionsFn func(keyActions) + resourceViewer interface { igniter setEnterFn(enterFn) setColorerFn(colorerFn) setDecorateFn(decorateFn) + setExtraActionsFn(actionsFn) } appView struct { diff --git a/internal/views/container.go b/internal/views/container.go index 08307d15..0d91ece2 100644 --- a/internal/views/container.go +++ b/internal/views/container.go @@ -94,7 +94,7 @@ func (v *containerView) shellIn(path, co string) { } args = append(args, "--", "sh") log.Debug().Msgf("Shell args %v", args) - runK(v.app, args...) + runK(true, v.app, args...) } func (v *containerView) extraActions(aa keyActions) { @@ -103,7 +103,9 @@ func (v *containerView) extraActions(aa keyActions) { aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false) aa[KeyP] = newKeyAction("Previous", v.backCmd, false) - aa[tcell.KeyEnter] = newKeyAction("Enter", v.logsCmd, false) + aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false) + aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true) + aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true) } func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/views/dp.go b/internal/views/dp.go new file mode 100644 index 00000000..e5f93af9 --- /dev/null +++ b/internal/views/dp.go @@ -0,0 +1,72 @@ +package views + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type deployView struct { + *resourceView +} + +func newDeployView(t string, app *appView, list resource.List) resourceViewer { + v := deployView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("deploy") + } + + return &v +} + +func (v *deployView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *deployView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + d := k8s.NewDeployment(v.app.conn()) + dep, err := d.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching Deployment %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + dp := dep.(*v1.Deployment) + + sel, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for Deployment %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "Deployment", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *deployView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} diff --git a/internal/views/ds.go b/internal/views/ds.go new file mode 100644 index 00000000..fcd10924 --- /dev/null +++ b/internal/views/ds.go @@ -0,0 +1,72 @@ +package views + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + extv1beta1 "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type daemonSetView struct { + *resourceView +} + +func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer { + v := daemonSetView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("ds") + } + + return &v +} + +func (v *daemonSetView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *daemonSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + d := k8s.NewDaemonSet(v.app.conn()) + dset, err := d.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching DeaemonSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + ds := dset.(*extv1beta1.DaemonSet) + + sel, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *daemonSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} diff --git a/internal/views/exec.go b/internal/views/exec.go index c1e9e8cd..8201177c 100644 --- a/internal/views/exec.go +++ b/internal/views/exec.go @@ -13,7 +13,7 @@ import ( "github.com/rs/zerolog/log" ) -func runK(app *appView, args ...string) bool { +func runK(clear bool, app *appView, args ...string) bool { bin, err := exec.LookPath("kubectl") if err != nil { log.Error().Msgf("Unable to find kubeclt command in path %v", err) @@ -24,21 +24,23 @@ func runK(app *appView, args ...string) bool { last := len(args) - 1 if args[last] == "sh" { args[last] = "bash" - if err := execute(bin, args...); err != nil { + if err := execute(clear, bin, args...); err != nil { args[last] = "sh" } else { return } } - if err := execute(bin, args...); err != nil { + if err := execute(clear, bin, args...); err != nil { log.Error().Msgf("Command exited: %T %v %v", err, err, args) app.flash(flashErr, "Command exited:", err.Error()) } }) } -func execute(bin string, args ...string) error { - clearScreen() +func execute(clear bool, bin string, args ...string) error { + if clear { + clearScreen() + } log.Debug().Msgf("Running command > %s %s", bin, strings.Join(args, " ")) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -64,5 +66,6 @@ func execute(bin string, args ...string) error { } func clearScreen() { + log.Debug().Msg("Clearing screen...") fmt.Print("\033[H\033[2J") } diff --git a/internal/views/help.go b/internal/views/help.go index c3112ce0..6127f235 100644 --- a/internal/views/help.go +++ b/internal/views/help.go @@ -96,7 +96,7 @@ func (v *helpView) init(_ context.Context, _ string) { views := []helpItem{ {"?", "Help"}, - {"a", "Aliases view"}, + {"Ctrl-a", "Aliases view"}, } fmt.Fprintf(v, "️️\n😱 [aqua::b]%s\n", "Help") for _, h := range views { @@ -106,7 +106,7 @@ func (v *helpView) init(_ context.Context, _ string) { } func (v *helpView) printHelp(key, desc string) { - fmt.Fprintf(v, "[pink::b]%9s [white::]%s\n", key, desc) + fmt.Fprintf(v, "[dodgerblue::b]%9s [white::]%s\n", key, desc) } func (v *helpView) hints() hints { diff --git a/internal/views/job.go b/internal/views/job.go index 772b1ee8..647e8bde 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -1,9 +1,12 @@ package views import ( + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type jobView struct { @@ -31,6 +34,9 @@ func newJobView(t string, app *appView, list resource.List) resourceViewer { // Protocol... +func (v *jobView) setExtraActionsFn(f actionsFn) { +} + func (v *jobView) appView() *appView { return v.app } @@ -99,4 +105,37 @@ func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) { func (v *jobView) extraActions(aa keyActions) { aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *jobView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + j := k8s.NewJob(v.app.conn()) + job, err := j.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching Job %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + jo := job.(*batchv1.Job) + + sel, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for Job %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "Job", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *jobView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil } diff --git a/internal/views/no.go b/internal/views/no.go index 8a250d7e..0f1d7294 100644 --- a/internal/views/no.go +++ b/internal/views/no.go @@ -1,6 +1,9 @@ package views import ( + "fmt" + + "github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/resource" "github.com/gdamore/tcell" ) @@ -22,6 +25,7 @@ func newNodeView(t string, app *appView, list resource.List) resourceViewer { func (v *nodeView) extraActions(aa keyActions) { aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true) aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) } func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -33,3 +37,34 @@ func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcel return nil } } + +func (v *nodeView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + showPods(v.app, "", "Node", v.selectedItem, "", "spec.nodeName="+v.selectedItem, v.backCmd) + + return nil +} + +func (v *nodeView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} + +func showPods(app *appView, ns, res, selected, labelSel, fieldSel string, b actionHandler) { + mx := k8s.NewMetricsServer(app.conn()) + list := resource.NewPodList(app.conn(), mx, ns) + + list.SetLabelSelector(labelSel) + list.SetFieldSelector(fieldSel) + + title := fmt.Sprintf("%s:%s Pods", res, selected) + pv := newPodView(title, app, list) + pv.setExtraActionsFn(func(aa keyActions) { + aa[tcell.KeyEsc] = newKeyAction("Back", b, true) + }) + app.inject(pv) +} diff --git a/internal/views/pod.go b/internal/views/pod.go index 29fc7a00..9c46015a 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -166,7 +166,7 @@ func (v *podView) shellIn(path, co string) { } args = append(args, "--", "sh") log.Debug().Msgf("Shell args %v", args) - runK(v.app, args...) + runK(true, v.app, args...) } func (v *podView) extraActions(aa keyActions) { diff --git a/internal/views/registrar.go b/internal/views/registrar.go index df542276..570def4c 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -153,14 +153,14 @@ func resourceViews(c k8s.Connection) map[string]resCmd { "ds": { title: "DaemonSets", api: "", - viewFn: newResourceView, + viewFn: newDaemonSetView, listFn: resource.NewDaemonSetList, colorerFn: dpColorer, }, "dp": { title: "Deployments", api: "apps", - viewFn: newResourceView, + viewFn: newDeployView, listFn: resource.NewDeploymentList, colorerFn: dpColorer, }, @@ -255,7 +255,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { "rs": { title: "ReplicaSets", api: "apps", - viewFn: newResourceView, + viewFn: newReplicaSetView, listFn: resource.NewReplicaSetList, colorerFn: rsColorer, }, @@ -275,7 +275,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd { "sts": { title: "StatefulSets", api: "apps", - viewFn: newResourceView, + viewFn: newStatefulSetView, listFn: resource.NewStatefulSetList, colorerFn: stsColorer, }, diff --git a/internal/views/resource.go b/internal/views/resource.go index 35135e9d..c6fef247 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -47,6 +47,7 @@ type ( selectedFn func() string decorateFn decorateFn colorerFn colorerFn + actions keyActions } ) @@ -54,6 +55,7 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie v := resourceView{ app: app, title: title, + actions: make(keyActions), list: list, selectedNS: list.GetNamespace(), Pages: tview.NewPages(), @@ -103,6 +105,10 @@ func (v *resourceView) init(ctx context.Context, ns string) { } } +func (v *resourceView) setExtraActionsFn(f actionsFn) { + f(v.actions) +} + func (v *resourceView) getTitle() string { return v.title } @@ -251,7 +257,7 @@ func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey { args = append(args, "-n", ns) args = append(args, "--context", v.app.config.K9s.CurrentContext) args = append(args, po) - runK(v.app, args...) + runK(true, v.app, args...) return evt } @@ -369,8 +375,7 @@ func (v *resourceView) refreshActions() { } var nn []interface{} - aa := make(keyActions) - if k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") { + if !v.list.HasSelectors() && k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") { var err error nn, err = k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces) if err != nil { @@ -387,35 +392,35 @@ func (v *resourceView) refreshActions() { if v.list.Access(resource.NamespaceAccess) { v.namespaces = make(map[int]string, config.MaxFavoritesNS) for i, n := range v.app.config.FavNamespaces() { - aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true) + v.actions[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true) v.namespaces[i] = n } } } - aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false) + v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false) - aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false) - aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false) - aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) + v.actions[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false) + v.actions[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false) + v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) if v.list.Access(resource.EditAccess) { - aa[KeyE] = newKeyAction("Edit", v.editCmd, true) + v.actions[KeyE] = newKeyAction("Edit", v.editCmd, true) } if v.list.Access(resource.DeleteAccess) { - aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) + v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true) } if v.list.Access(resource.ViewAccess) { - aa[KeyY] = newKeyAction("YAML", v.viewCmd, true) + v.actions[KeyY] = newKeyAction("YAML", v.viewCmd, true) } if v.list.Access(resource.DescribeAccess) { - aa[KeyD] = newKeyAction("Describe", v.describeCmd, true) + v.actions[KeyD] = newKeyAction("Describe", v.describeCmd, true) } if v.extraActionsFn != nil { - v.extraActionsFn(aa) + v.extraActionsFn(v.actions) } t := v.getTV() - t.setActions(aa) + t.setActions(v.actions) v.app.setHints(t.hints()) } diff --git a/internal/views/rs.go b/internal/views/rs.go new file mode 100644 index 00000000..5e8fbb5e --- /dev/null +++ b/internal/views/rs.go @@ -0,0 +1,170 @@ +package views + +import ( + "fmt" + "strconv" + "strings" + + "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" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/kubectl" +) + +type replicaSetView struct { + *resourceView +} + +func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer { + v := replicaSetView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("rs") + } + + return &v +} + +func (v *replicaSetView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true) + aa[tcell.KeyCtrlB] = newKeyAction("Rollback", v.rollbackCmd, true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *replicaSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *replicaSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + rset := k8s.NewReplicaSet(v.app.conn()) + r, err := rset.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching ReplicaSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + rs := r.(*v1.ReplicaSet) + + sel, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "ReplicaSet", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *replicaSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} + +func (v *replicaSetView) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + 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) { + if button == "OK" { + v.app.flash(flashInfo, fmt.Sprintf("Rolling back %s %s", v.list.GetName(), v.selectedItem)) + if !rollback(v.app, v.selectedItem) { + v.app.flash(flashErr, "Rollback failed!") + } else { + v.refresh() + } + } + v.switchPage(v.list.GetName()) + }) + v.SwitchToPage("confirm") + + return nil +} + +func rollback(app *appView, selectedItem string) bool { + ns, n := namespaced(selectedItem) + rset := k8s.NewReplicaSet(app.conn()) + r, err := rset.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching ReplicaSet %s", selectedItem) + app.flash(flashErr, err.Error()) + return false + } + rs := r.(*v1.ReplicaSet) + + var ctrlName, ctrlKind, ctrlAPI string + for _, ref := range rs.ObjectMeta.OwnerReferences { + if ref.Controller != nil && *ref.Controller { + ctrlAPI, ctrlKind, ctrlName = ref.APIVersion, ref.Kind, ref.Name + break + } + } + if ctrlName == "" || ctrlKind == "" || ctrlAPI == "" { + app.flash(flashErr, "Unable to find controller for ReplicaSet %s", selectedItem) + return false + } + + revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"] + if rs.Status.Replicas != 0 { + app.flash(flashWarn, "Can not rollback the current replica!") + return false + } + + dpr := k8s.NewDeployment(app.conn()) + dep, err := dpr.Get(ns, ctrlName) + if err != nil { + log.Error().Err(err).Msgf("Fetching Deployment %s", selectedItem) + app.flash(flashErr, err.Error()) + return false + } + dp := dep.(*appsv1.Deployment) + + vers, err := strconv.Atoi(revision) + if err != nil { + log.Error().Err(err).Msg("Revision conversion failed") + return false + } + + tokens := strings.Split(ctrlAPI, "/") + group := ctrlAPI + if len(tokens) == 2 { + group = tokens[0] + } + rb, err := kubectl.RollbackerFor(schema.GroupKind{group, ctrlKind}, app.conn().DialOrDie()) + if err != nil { + log.Error().Err(err).Msg("No rollbacker") + return false + } + + res, err := rb.Rollback(dp, map[string]string{}, int64(vers), false) + if err != nil { + log.Error().Err(err).Msg("Rollback failed") + return false + } + log.Debug().Msgf("Version %s %s", revision, res) + app.flash(flashInfo, fmt.Sprintf("Version %s %s", revision, res)) + + return true +} diff --git a/internal/views/sts.go b/internal/views/sts.go new file mode 100644 index 00000000..889c3b00 --- /dev/null +++ b/internal/views/sts.go @@ -0,0 +1,72 @@ +package views + +import ( + "github.com/derailed/k9s/internal/k8s" + "github.com/derailed/k9s/internal/resource" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type statefulSetView struct { + *resourceView +} + +func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer { + v := statefulSetView{newResourceView(t, app, list).(*resourceView)} + { + v.extraActionsFn = v.extraActions + v.switchPage("sts") + } + + return &v +} + +func (v *statefulSetView) extraActions(aa keyActions) { + aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(1, false), true) + aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(2, false), true) + aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true) +} + +func (v *statefulSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil + } +} + +func (v *statefulSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { + if !v.rowSelected() { + return evt + } + + ns, n := namespaced(v.selectedItem) + d := k8s.NewStatefulSet(v.app.conn()) + s, err := d.Get(ns, n) + if err != nil { + log.Error().Err(err).Msgf("Fetching StatefulSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + sts := s.(*v1.StatefulSet) + + sel, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector) + if err != nil { + log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", v.selectedItem) + v.app.flash(flashErr, err.Error()) + return evt + } + showPods(v.app, "", "StatefulSet", v.selectedItem, sel.String(), "", v.backCmd) + + return nil +} + +func (v *statefulSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + v.app.inject(v) + + return nil +} diff --git a/internal/views/subject.go b/internal/views/subject.go index 8288f40b..882ec356 100644 --- a/internal/views/subject.go +++ b/internal/views/subject.go @@ -83,6 +83,9 @@ func (v *subjectView) init(c context.Context, _ string) { v.app.SetFocus(v) } +func (v *subjectView) setExtraActionsFn(f actionsFn) { +} + func (v *subjectView) setColorerFn(f colorerFn) {} func (v *subjectView) setEnterFn(f enterFn) {} func (v *subjectView) setDecorateFn(f decorateFn) {}