From 9b453f7155a6226ebe44a44ddccf0cc9f9401fbd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jul 2023 09:23:53 -0400 Subject: [PATCH 1/4] chore: detect nil cmd handlers Prevent nil panic dereferences on cmd handlers. Add a unit test to prevent future mistakes --- cli/clitest/handlers.go | 24 ++++++++++++++++++++++++ cli/root.go | 5 +++++ cli/root_test.go | 8 ++++++++ enterprise/cli/provisionerdaemons.go | 3 +++ enterprise/cli/root_test.go | 8 ++++++++ 5 files changed, 48 insertions(+) create mode 100644 cli/clitest/handlers.go diff --git a/cli/clitest/handlers.go b/cli/clitest/handlers.go new file mode 100644 index 0000000000000..29dc71fb70868 --- /dev/null +++ b/cli/clitest/handlers.go @@ -0,0 +1,24 @@ +package clitest + +import ( + "testing" + + "github.com/coder/coder/cli/clibase" +) + +// HandlersOK asserts that all commands have a handler. +// Without a handler, the command has no default behavior. Even for +// non-root commands (like 'groups' or 'users'), a handler is required. +// These handlers are likely just the 'help' handler, but this must be +// explictly set. +func HandlersOK(t *testing.T, cmd *clibase.Cmd) { + cmd.Walk(func(cmd *clibase.Cmd) { + if cmd.Handler == nil { + // If you see this error, make the Handler a helper invoker. + // Handler: func(inv *clibase.Invocation) error { + // return inv.Command.HelpHandler(inv) + // }, + t.Errorf("command %q has no handler, change to a helper invoker using: 'inv.Command.HelpHandler(inv)'", cmd.Name()) + } + }) +} diff --git a/cli/root.go b/cli/root.go index 9b63bebe4d65a..8f859a051cbfc 100644 --- a/cli/root.go +++ b/cli/root.go @@ -260,6 +260,11 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) { // Add a wrapper to every command to enable debugging options. cmd.Walk(func(cmd *clibase.Cmd) { h := cmd.Handler + if h == nil { + // We should never have a nil handler, but if we do, do not + // wrap it. Wrapping it just hides a nil pointer dereference. + return + } cmd.Handler = func(i *clibase.Invocation) error { if !debugOptions { return h(i) diff --git a/cli/root_test.go b/cli/root_test.go index 163c9590f89ba..c8c8f0937ba28 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -181,3 +181,11 @@ func TestDERPHeaders(t *testing.T) { require.Greater(t, atomic.LoadInt64(&derpCalled), int64(0), "expected /derp to be called at least once") } + +func TestHandlersOK(t *testing.T) { + var root cli.RootCmd + cmd, err := root.Command(root.Core()) + require.NoError(t, err) + + clitest.HandlersOK(t, cmd) +} diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 03892fa37f65c..f3dfc2ba367d7 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -27,6 +27,9 @@ func (r *RootCmd) provisionerDaemons() *clibase.Cmd { cmd := &clibase.Cmd{ Use: "provisionerd", Short: "Manage provisioner daemons", + Handler: func(inv *clibase.Invocation) error { + return inv.Command.HelpHandler(inv) + }, Children: []*clibase.Cmd{ r.provisionerDaemonStart(), }, diff --git a/enterprise/cli/root_test.go b/enterprise/cli/root_test.go index 20c94041be0e0..3c37dc6292d5c 100644 --- a/enterprise/cli/root_test.go +++ b/enterprise/cli/root_test.go @@ -17,3 +17,11 @@ func newCLI(t *testing.T, args ...string) (*clibase.Invocation, config.Root) { require.NoError(t, err) return clitest.NewWithCommand(t, cmd, args...) } + +func TestEnterpriseHandlersOK(t *testing.T) { + var root cli.RootCmd + cmd, err := root.Command(root.EnterpriseSubcommands()) + require.NoError(t, err) + + clitest.HandlersOK(t, cmd) +} From 9fa88f84599653c470546359dc69e0771443ff61 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jul 2023 09:29:56 -0400 Subject: [PATCH 2/4] Fix typo --- cli/clitest/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/clitest/handlers.go b/cli/clitest/handlers.go index 29dc71fb70868..5151bc6c0ed6c 100644 --- a/cli/clitest/handlers.go +++ b/cli/clitest/handlers.go @@ -10,7 +10,7 @@ import ( // Without a handler, the command has no default behavior. Even for // non-root commands (like 'groups' or 'users'), a handler is required. // These handlers are likely just the 'help' handler, but this must be -// explictly set. +// explicitly set. func HandlersOK(t *testing.T, cmd *clibase.Cmd) { cmd.Walk(func(cmd *clibase.Cmd) { if cmd.Handler == nil { From 7de7e777a8129836d507e0b5bc01bb0bbac31baa Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jul 2023 09:37:54 -0400 Subject: [PATCH 3/4] Parallelize tests --- cli/root_test.go | 2 ++ enterprise/cli/root_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cli/root_test.go b/cli/root_test.go index c8c8f0937ba28..c892701a3acbc 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -183,6 +183,8 @@ func TestDERPHeaders(t *testing.T) { } func TestHandlersOK(t *testing.T) { + t.Parallel() + var root cli.RootCmd cmd, err := root.Command(root.Core()) require.NoError(t, err) diff --git a/enterprise/cli/root_test.go b/enterprise/cli/root_test.go index 3c37dc6292d5c..bf44ad08ff921 100644 --- a/enterprise/cli/root_test.go +++ b/enterprise/cli/root_test.go @@ -19,6 +19,8 @@ func newCLI(t *testing.T, args ...string) (*clibase.Invocation, config.Root) { } func TestEnterpriseHandlersOK(t *testing.T) { + t.Parallel() + var root cli.RootCmd cmd, err := root.Command(root.EnterpriseSubcommands()) require.NoError(t, err) From 63073c4e07ae9fa24ef92ebb2367c14c216fc397 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 5 Jul 2023 11:04:45 -0400 Subject: [PATCH 4/4] Expand comment lanugage on the nil handler --- cli/root.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cli/root.go b/cli/root.go index 8f859a051cbfc..2513b4f68cf58 100644 --- a/cli/root.go +++ b/cli/root.go @@ -263,6 +263,13 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) { if h == nil { // We should never have a nil handler, but if we do, do not // wrap it. Wrapping it just hides a nil pointer dereference. + // If a nil handler exists, this is a developer bug. If no handler + // is required for a command such as command grouping (e.g. `users' + // and 'groups'), then the handler should be set to the helper + // function. + // func(inv *clibase.Invocation) error { + // return inv.Command.HelpHandler(inv) + // } return } cmd.Handler = func(i *clibase.Invocation) error {