diff --git a/README.md b/README.md index dfec5388..2d8e10a6 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ k9s info k9s -n mycoolns # Start K9s in an existing KubeConfig context k9s --context coolCtx +# Start K9s in readonly mode - with all modification commands disabled +k9s --readonly ``` ## Key Bindings @@ -150,6 +152,8 @@ K9s uses aliases to navigate most K8s resources. k9s: # Indicates api-server poll intervals. refreshRate: 2 + # Indicates whether modification commands like delete/kill/edit are disabled. Default is false + readOnly: false # Indicates log view maximum buffer size. Default 1k lines. logBufferSize: 200 # Indicates how many lines of logs to retrieve from the api-server. Default 200 lines. diff --git a/cmd/root.go b/cmd/root.go index 957cedc3..6429d980 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,6 +113,10 @@ func loadConfiguration() *config.Config { k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless) } + if k9sFlags.ReadOnly != nil { + k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly) + } + if k9sFlags.Command != nil { k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) } @@ -189,6 +193,12 @@ func initK9sFlags() { config.DefaultCommand, "Specify the default command to view when the application launches", ) + rootCmd.Flags().BoolVar( + k9sFlags.ReadOnly, + "readonly", + false, + "Disable all commands that modify the cluster", + ) } func initK8sFlags() { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2a9b0eb5..52a6c570 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -205,6 +205,7 @@ func TestConfigSaveFile(t *testing.T) { cfg.SetConnection(mc) assert.Nil(t, cfg.Load("test_assets/k9s.yml")) cfg.K9s.RefreshRate = 100 + cfg.K9s.ReadOnly = true cfg.K9s.LogBufferSize = 500 cfg.K9s.LogRequestSize = 100 cfg.K9s.CurrentContext = "blee" @@ -260,6 +261,7 @@ func TestSetup(t *testing.T) { var expectedConfig = `k9s: refreshRate: 100 headless: false + readOnly: true logBufferSize: 500 logRequestSize: 100 currentContext: blee @@ -300,6 +302,7 @@ var expectedConfig = `k9s: var resetConfig = `k9s: refreshRate: 2 headless: false + readOnly: false logBufferSize: 200 logRequestSize: 200 currentContext: blee diff --git a/internal/config/flags.go b/internal/config/flags.go index 07520b4a..1f79b940 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -18,6 +18,7 @@ type Flags struct { Headless *bool Command *string AllNamespaces *bool + ReadOnly *bool } // NewFlags returns new configuration flags. @@ -28,6 +29,7 @@ func NewFlags() *Flags { Headless: boolPtr(false), Command: strPtr(DefaultCommand), AllNamespaces: boolPtr(false), + ReadOnly: boolPtr(false), } } diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 7982bec8..ae439edf 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -6,12 +6,14 @@ const ( defaultRefreshRate = 2 defaultLogRequestSize = 200 defaultLogBufferSize = 1000 + defaultReadOnly = false ) // K9s tracks K9s configuration options. type K9s struct { RefreshRate int `yaml:"refreshRate"` Headless bool `yaml:"headless"` + ReadOnly bool `yaml:"readOnly"` LogBufferSize int `yaml:"logBufferSize"` LogRequestSize int `yaml:"logRequestSize"` CurrentContext string `yaml:"currentContext"` @@ -20,6 +22,7 @@ type K9s struct { Clusters map[string]*Cluster `yaml:"clusters,omitempty"` manualRefreshRate int manualHeadless *bool + manualReadOnly *bool manualCommand *string } @@ -27,6 +30,7 @@ type K9s struct { func NewK9s() *K9s { return &K9s{ RefreshRate: defaultRefreshRate, + ReadOnly: defaultReadOnly, LogBufferSize: defaultLogBufferSize, LogRequestSize: defaultLogRequestSize, Clusters: make(map[string]*Cluster), @@ -43,6 +47,11 @@ func (k *K9s) OverrideHeadless(b bool) { k.manualHeadless = &b } +// OverrideReadOnly set the readonly mode manually. +func (k *K9s) OverrideReadOnly(b bool) { + k.manualReadOnly = &b +} + // OverrideCommand set the command manually. func (k *K9s) OverrideCommand(cmd string) { k.manualCommand = &cmd @@ -68,6 +77,15 @@ func (k *K9s) GetRefreshRate() int { return rate } +// GetReadOnly returns the readonly setting. +func (k *K9s) GetReadOnly() bool { + readOnly := k.ReadOnly + if k.manualReadOnly != nil && *k.manualReadOnly { + readOnly = *k.manualReadOnly + } + return readOnly +} + // ActiveCluster returns the currently active cluster. func (k *K9s) ActiveCluster() *Cluster { if k.Clusters == nil { diff --git a/internal/view/browser.go b/internal/view/browser.go index 4e471f59..b83f9bfb 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -356,7 +356,7 @@ func (b *Browser) defaultContext() context.Context { ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.SearchBuff().String())) } ctx = context.WithValue(ctx, internal.KeyFields, "") - ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace((b.App().Config.ActiveNamespace()))) + ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) return ctx } @@ -371,11 +371,13 @@ func (b *Browser) refreshActions() { if b.app.ConOK() { b.namespaceActions(aa) - if client.Can(b.meta.Verbs, "edit") { - aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) - } - if client.Can(b.meta.Verbs, "delete") { - aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) + if !b.app.Config.K9s.GetReadOnly() { + if client.Can(b.meta.Verbs, "edit") { + aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) + } + if client.Can(b.meta.Verbs, "delete") { + aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) + } } } diff --git a/internal/view/container.go b/internal/view/container.go index 9b1c150a..9b25a00f 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -40,11 +40,21 @@ func NewContainer(gvr client.GVR) ResourceViewer { // Name returns the component name. func (c *Container) Name() string { return containerTitle } +func (c *Container) bindDangerousKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true), + }) +} + func (c *Container) bindKeys(aa ui.KeyActions) { aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace) + + if !c.App().Config.K9s.GetReadOnly() { + c.bindDangerousKeys(aa) + } + aa.Add(ui.KeyActions{ ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), - ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true), ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false), ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false), ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd(8, false), false), diff --git a/internal/view/pod.go b/internal/view/pod.go index 300feea9..cc03de98 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" @@ -36,10 +35,19 @@ func NewPod(gvr client.GVR) ResourceViewer { return &p } -func (p *Pod) bindKeys(aa ui.KeyActions) { +func (p *Pod) bindDangerousKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ tcell.KeyCtrlK: ui.NewKeyAction("Kill", p.killCmd, true), ui.KeyS: ui.NewKeyAction("Shell", p.shellCmd, true), + }) +} + +func (p *Pod) bindKeys(aa ui.KeyActions) { + if !p.App().Config.K9s.GetReadOnly() { + p.bindDangerousKeys(aa) + } + + aa.Add(ui.KeyActions{ ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(1, true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(2, true), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd(3, false), false),