From 6852f21dddda3abaf1a673f9e02e95c0c99de3d7 Mon Sep 17 00:00:00 2001 From: Dave May Date: Tue, 12 Oct 2021 20:01:54 -0400 Subject: [PATCH] cli: Improved autocomplete support for job dispatch and operator debug (#11270) * Add autocomplete to nomad job dispatch * Add autocomplete to nomad operator debug * Update incorrect comment * Update test to verify autocomplete * Add changelog * Apply lint suggestions * Create dynamic slices instead of specific length * Align style across predictors --- .changelog/11270.txt | 3 ++ command/job_dispatch.go | 14 ++++-- command/job_dispatch_test.go | 23 +++++++--- command/operator_debug.go | 84 +++++++++++++++++++++++++++++++++--- 4 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 .changelog/11270.txt diff --git a/.changelog/11270.txt b/.changelog/11270.txt new file mode 100644 index 000000000..964ac391b --- /dev/null +++ b/.changelog/11270.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Improved autocomplete support for job dispatch and operator debug +``` diff --git a/command/job_dispatch.go b/command/job_dispatch.go index 022c24a51..68e9863ab 100644 --- a/command/job_dispatch.go +++ b/command/job_dispatch.go @@ -6,7 +6,6 @@ import ( "os" "strings" - "github.com/hashicorp/nomad/api/contexts" flaghelper "github.com/hashicorp/nomad/helper/flags" "github.com/posener/complete" ) @@ -75,11 +74,20 @@ func (c *JobDispatchCommand) AutocompleteArgs() complete.Predictor { return nil } - resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Jobs, nil) + resp, _, err := client.Jobs().PrefixList(a.Last) if err != nil { return []string{} } - return resp.Matches[contexts.Jobs] + + // filter by parameterized jobs + matches := make([]string, 0, len(resp)) + for _, job := range resp { + if job.ParameterizedJob { + matches = append(matches, job.ID) + } + } + return matches + }) } diff --git a/command/job_dispatch_test.go b/command/job_dispatch_test.go index c1a4e4060..37e4a3926 100644 --- a/command/job_dispatch_test.go +++ b/command/job_dispatch_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/nomad/nomad/mock" "github.com/mitchellh/cli" "github.com/posener/complete" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJobDispatchCommand_Implements(t *testing.T) { @@ -50,7 +50,6 @@ func TestJobDispatchCommand_Fails(t *testing.T) { } func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) { - assert := assert.New(t) t.Parallel() srv, _, url := testServer(t, true, nil) @@ -62,13 +61,27 @@ func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) { // Create a fake job state := srv.Agent.Server().State() j := mock.Job() - assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 1000, j)) + require.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, j)) prefix := j.ID[:len(j.ID)-5] args := complete.Args{Last: prefix} predictor := cmd.AutocompleteArgs() + // No parameterized jobs, should be 0 results res := predictor.Predict(args) - assert.Equal(1, len(res)) - assert.Equal(j.ID, res[0]) + require.Equal(t, 0, len(res)) + + // Create a fake parameterized job + j1 := mock.Job() + j1.ParameterizedJob = &structs.ParameterizedJobConfig{} + require.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 2000, j1)) + + prefix = j1.ID[:len(j1.ID)-5] + args = complete.Args{Last: prefix} + predictor = cmd.AutocompleteArgs() + + // Should return 1 parameterized job + res = predictor.Predict(args) + require.Equal(t, 1, len(res)) + require.Equal(t, j1.ID, res[0]) } diff --git a/command/operator_debug.go b/command/operator_debug.go index 97385bf3a..1646f062d 100644 --- a/command/operator_debug.go +++ b/command/operator_debug.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/api/contexts" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/structs" "github.com/posener/complete" @@ -179,12 +180,12 @@ func (c *OperatorDebugCommand) AutocompleteFlags() complete.Flags { complete.Flags{ "-duration": complete.PredictAnything, "-interval": complete.PredictAnything, - "-log-level": complete.PredictAnything, + "-log-level": complete.PredictSet("TRACE", "DEBUG", "INFO", "WARN", "ERROR"), "-max-nodes": complete.PredictAnything, - "-node-class": complete.PredictAnything, - "-node-id": complete.PredictAnything, - "-server-id": complete.PredictAnything, - "-output": complete.PredictAnything, + "-node-class": NodeClassPredictor(c.Client), + "-node-id": NodePredictor(c.Client), + "-server-id": ServerPredictor(c.Client), + "-output": complete.PredictDirs("*"), "-pprof-duration": complete.PredictAnything, "-consul-token": complete.PredictAnything, "-vault-token": complete.PredictAnything, @@ -195,6 +196,79 @@ func (c *OperatorDebugCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing } +// NodePredictor returns a client node predictor +func NodePredictor(factory ApiClientFactory) complete.Predictor { + return complete.PredictFunc(func(a complete.Args) []string { + client, err := factory() + if err != nil { + return nil + } + + resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Nodes, nil) + if err != nil { + return []string{} + } + return resp.Matches[contexts.Nodes] + }) +} + +// NodeClassPredictor returns a client node class predictor +// TODO: Consider API options for node class filtering +func NodeClassPredictor(factory ApiClientFactory) complete.Predictor { + return complete.PredictFunc(func(a complete.Args) []string { + client, err := factory() + if err != nil { + return nil + } + + nodes, _, err := client.Nodes().List(nil) // TODO: should be *api.QueryOptions that matches region + if err != nil { + return []string{} + } + + // Build map of unique node classes across all nodes + classes := make(map[string]bool) + for _, node := range nodes { + classes[node.NodeClass] = true + } + + // Iterate over node classes looking for match + filtered := []string{} + for class := range classes { + if strings.HasPrefix(class, a.Last) { + filtered = append(filtered, class) + } + } + + return filtered + }) +} + +// ServerPredictor returns a server member predictor +// TODO: Consider API options for server member filtering +func ServerPredictor(factory ApiClientFactory) complete.Predictor { + return complete.PredictFunc(func(a complete.Args) []string { + client, err := factory() + if err != nil { + return nil + } + members, err := client.Agent().Members() + if err != nil { + return []string{} + } + + // Iterate over server members looking for match + filtered := []string{} + for _, member := range members.Members { + if strings.HasPrefix(member.Name, a.Last) { + filtered = append(filtered, member.Name) + } + } + + return filtered + }) +} + func (c *OperatorDebugCommand) Name() string { return "debug" } func (c *OperatorDebugCommand) Run(args []string) int {