Repository: derailed/k9s

Issue:
Add option to disable Ctrl-k (delete without confirmation)
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="100" height="auto"/>

<br/>
<br/>
<br/>


**Is your feature request related to a problem? Please describe.**
N/A

**Describe the solution you'd like**
An option to disable the "delete resource without confirmation" (`Ctrl-k`) command.

**Describe alternatives you've considered**
Read only RBAC roles (we use this, but there are times when admin privileges are required and it would be a bummer to fat finger a delete).  Learning to type (so far unsuccessful).

**Additional context**
I can't type and it's only a matter of time until I accidentally delete a resource.


Discussion:
@shrinandj: I loved trying out K9S. But the moment I saw the Ctrl-k option that would delete without confirmation, I deleted it. Would love to know if there is an option to disable this.@vimalk78: Btw, i would like to know what is the differece between Delete `Ctrl-d` and  Kill `Ctrl-k` ?@RothAndrew: > Btw, i would like to know what is the differece between Delete `Ctrl-d` and Kill `Ctrl-k` ?

Ctrl-d brings up a confirmation dialogue. Ctrl-k deletes without confirmation@derailed: @charlievieth @shrinandj @vimalk78 @RothAndrew  Thank you all for your feedback! 

To be clear the only place in K9s that offers the kill switch is on the pod views everywhere else deletes are fronted by a  confirm dialog. I get this makes some folks nervous but power users wanted this feature. To boot, in k8s land pods die/reincarnate all the time so minor offense imho. If there is still contentions here, I will be happy to offer a config to disable it.@charlievieth: I haven't accidentally deleted anything since I started using k9s. So for me this is less of an issue than initially feared. I'm happy to close this but will let the will let the others who commented chime in first.

Also, thanks for this project - its awesome and I use it daily!@derailed: @charlievieth @RothAndrew @shrinandj  @vimalk78. K9s v0.14.0 now offers a --readonly flag to disable potential destructive commands. Thanks to @joscha-alisch! you can either pass the flag via the cli or add a readOnly: true line to your $HOME/.k9s/config.yml@brandonwittwer: A different set of power users that use slack a lot would like to not accidentally let an errant Ctrl+K land in K9s and immediately disrupt the delicate balance of a zookeeper cluster stateful set.  I vote /reopen and add some kind of config that allows disabling the ctrl+k shortcut.
 @derailed @derailed: @brandonwittwer You can use the cli flag --readonly or readOnly: true in the k9s config file.@brandonwittwer: I want to do lots of non-read-only things.  I'd imagine that --readonly would remove all modification abilities.  Ctrl+k nearness to Shift + ; has also started to thwart me.  My fingers have learned too many keyboard shortcuts and I get key-tourettes.  In the end, I don't understand the need for two commands that do the same thing, one without a confirmation box and another with!  configs please!  I'd be willing to offer the pull-request as a commandline switch "--careful" 🍬 
@tshirtman: https://github.com/derailed/k9s#hotkey-support maybe allowing to override standard hotkeys would be a generic way to solve the issue?@Aracki: @tshirtman I've added hotkey for `ctrl-k` but it seems "Kill" command has precedence somehow.

I see "HotKeys load failed!" message, it seems related to https://github.com/derailed/k9s/issues/472@buckleyGI: My 2 cents. The 2 commands create confusion (and kill isn't k8s jargon).
Good that it only works on pods but it frightened me a bit when accidentally hitting the shortcut.
--readonly doesn't cover my use case as @brandonwittwer words it.
I would make the kill an opt in.
Now that I know it's not a big deal. But for newcomers it does.@b-abadie: This issue really is a massive drawback from an otherwise awesome tool : a simple mistake in what the current active window is, and "let's switch slack channel to see this new message" becomes "let's disrupt production"...

I know k8s is (or should be) meant to stop and restart workload easily. I know there is a --readonly flag. But my k9s windows usually are read-write because I need to, and I could do without the adrenaline rush of "OMG what did I do what did I just kill"...

If at any point comes an option to disable this behavior (or a way to overload the hotkey), I would upgrade and disable in a heartbeat (and most of my colleagues would too).@buckleyGI: @derailed I tried overriding the ctrl-k command as well but it doesn't work on the latest version v0.40.10
Can we add an option to disable this command? Readonly is too restrictive as other stated. Some of use just want the confirmation as ctrl-d does.
I switch to tmux and for some reason killed several pods today. In general, It shouldn't matter as you say but sometimes it does if contains some state that you want to keep alive (if debugging for example).
Thank you!@mscroggi: @derailed I too tried overriding the binding like @buckleyGI   I too am very nervous with the ctrl-k binding.   I have to be able to get shell access so readonly mode wont work..   In testing I managed to kill a pod accidentally. :-(

A few ideas to work around:
1. override or disable the binding as mentioned. 
2. allow for toggling readonly mode in the UI.  K9s could be started readonly and toggled to read/write when needed and set back. 
3. allow picking extra features to allow in readonly mode.   Meaning I could start readonly but still have shell access without the delete or kill functions.   I am not sure how feasible that idea is.





Solution patch:
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/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 a9f8cc86..e50c0458 100644
--- a/internal/view/browser.go
+++ b/internal/view/browser.go
@@ -355,7 +355,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
 }
@@ -370,11 +370,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 18f1684e..8a6d1357 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),



Test patch:
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