add resource navigation + dp rollbacks

mine
derailed 2019-04-12 13:28:10 -06:00
parent bb42c39645
commit cd950e04c6
22 changed files with 604 additions and 29 deletions

2
go.mod
View File

@ -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

5
go.sum
View File

@ -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=

View File

@ -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 != ""
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

72
internal/views/dp.go Normal file
View File

@ -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
}

72
internal/views/ds.go Normal file
View File

@ -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
}

View File

@ -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 {
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")
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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,
},

View File

@ -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())
}

170
internal/views/rs.go Normal file
View File

@ -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
}

72
internal/views/sts.go Normal file
View File

@ -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
}

View File

@ -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) {}