From fb69b94d48cf76e7a7e8f87ecda55bb64882e3cc Mon Sep 17 00:00:00 2001 From: davemay99 Date: Tue, 21 Sep 2021 17:04:41 -0400 Subject: [PATCH 1/8] Add autocomplete to nomad job dispatch --- command/job_dispatch.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/command/job_dispatch.go b/command/job_dispatch.go index 022c24a51653..89d7f09780ac 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 nil + } + + // filter this by periodic jobs + matches := make([]string, 0, len(resp)) + for _, job := range resp { + if job.ParameterizedJob { + matches = append(matches, job.ID) + } } - return resp.Matches[contexts.Jobs] + return matches + }) } From 449fc5dbc3ffa6dd18764a0f94e54364c33f57fd Mon Sep 17 00:00:00 2001 From: davemay99 Date: Tue, 21 Sep 2021 17:05:22 -0400 Subject: [PATCH 2/8] Add autocomplete to nomad operator debug --- command/operator_debug.go | 77 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/command/operator_debug.go b/command/operator_debug.go index 97385bf3ad06..987806352015 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,72 @@ 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, _, _ := client.Nodes().List(nil) // TODO: should be *api.QueryOptions that matches region, namespace + + // 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 := make([]string, len(classes)) + 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 node class 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() + + unfiltered := make([]string, len(members.Members)) + filtered := unfiltered[:0] + + 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 { From bed5aaa9804cbbfe42a53408bb694a087c88ccc8 Mon Sep 17 00:00:00 2001 From: davemay99 Date: Wed, 29 Sep 2021 20:39:00 -0400 Subject: [PATCH 3/8] Update incorrect comment --- command/job_dispatch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/job_dispatch.go b/command/job_dispatch.go index 89d7f09780ac..e49d495ec12f 100644 --- a/command/job_dispatch.go +++ b/command/job_dispatch.go @@ -79,7 +79,7 @@ func (c *JobDispatchCommand) AutocompleteArgs() complete.Predictor { return nil } - // filter this by periodic jobs + // filter by parameterized jobs matches := make([]string, 0, len(resp)) for _, job := range resp { if job.ParameterizedJob { From d63f7e18af1d8e7f0e78107c90dd1e1fcb2290c0 Mon Sep 17 00:00:00 2001 From: davemay99 Date: Wed, 29 Sep 2021 20:58:28 -0400 Subject: [PATCH 4/8] Update test to verify autocomplete --- command/job_dispatch_test.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/command/job_dispatch_test.go b/command/job_dispatch_test.go index c1a4e406031b..37e4a39264b2 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]) } From bc6c485a75088519333efa88b3819386ed921451 Mon Sep 17 00:00:00 2001 From: davemay99 Date: Tue, 5 Oct 2021 23:35:16 -0400 Subject: [PATCH 5/8] Add changelog --- .changelog/11270.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/11270.txt diff --git a/.changelog/11270.txt b/.changelog/11270.txt new file mode 100644 index 000000000000..964ac391b166 --- /dev/null +++ b/.changelog/11270.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Improved autocomplete support for job dispatch and operator debug +``` From 0d4c7e6c6ac6290822a8f98fab4412a17759ceee Mon Sep 17 00:00:00 2001 From: davemay99 Date: Wed, 6 Oct 2021 09:44:20 -0400 Subject: [PATCH 6/8] Apply lint suggestions --- command/operator_debug.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/command/operator_debug.go b/command/operator_debug.go index 987806352015..245a7cda65eb 100644 --- a/command/operator_debug.go +++ b/command/operator_debug.go @@ -230,7 +230,7 @@ func NodeClassPredictor(factory ApiClientFactory) complete.Predictor { // Iterate over node classes looking for match filtered := make([]string, len(classes)) - for class, _ := range classes { + for class := range classes { if strings.HasPrefix(class, a.Last) { filtered = append(filtered, class) } @@ -249,6 +249,9 @@ func ServerPredictor(factory ApiClientFactory) complete.Predictor { return nil } members, err := client.Agent().Members() + if err != nil { + return []string{} + } unfiltered := make([]string, len(members.Members)) filtered := unfiltered[:0] From ce62ab3bb0a0711e8867e2f67b78c5232c54a41f Mon Sep 17 00:00:00 2001 From: davemay99 Date: Tue, 12 Oct 2021 17:31:42 -0400 Subject: [PATCH 7/8] Create dynamic slices instead of specific length --- command/operator_debug.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/command/operator_debug.go b/command/operator_debug.go index 245a7cda65eb..b7980d769d50 100644 --- a/command/operator_debug.go +++ b/command/operator_debug.go @@ -229,7 +229,7 @@ func NodeClassPredictor(factory ApiClientFactory) complete.Predictor { } // Iterate over node classes looking for match - filtered := make([]string, len(classes)) + filtered := []string{} for class := range classes { if strings.HasPrefix(class, a.Last) { filtered = append(filtered, class) @@ -253,8 +253,7 @@ func ServerPredictor(factory ApiClientFactory) complete.Predictor { return []string{} } - unfiltered := make([]string, len(members.Members)) - filtered := unfiltered[:0] + filtered := []string{} for _, member := range members.Members { if strings.HasPrefix(member.Name, a.Last) { From 0feff66521a5d32ef753d409b789b892942d9419 Mon Sep 17 00:00:00 2001 From: davemay99 Date: Tue, 12 Oct 2021 17:54:05 -0400 Subject: [PATCH 8/8] Align style across predictors --- command/job_dispatch.go | 2 +- command/operator_debug.go | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/command/job_dispatch.go b/command/job_dispatch.go index e49d495ec12f..68e9863ab384 100644 --- a/command/job_dispatch.go +++ b/command/job_dispatch.go @@ -76,7 +76,7 @@ func (c *JobDispatchCommand) AutocompleteArgs() complete.Predictor { resp, _, err := client.Jobs().PrefixList(a.Last) if err != nil { - return nil + return []string{} } // filter by parameterized jobs diff --git a/command/operator_debug.go b/command/operator_debug.go index b7980d769d50..1646f062d8f2 100644 --- a/command/operator_debug.go +++ b/command/operator_debug.go @@ -220,7 +220,11 @@ func NodeClassPredictor(factory ApiClientFactory) complete.Predictor { if err != nil { return nil } - nodes, _, _ := client.Nodes().List(nil) // TODO: should be *api.QueryOptions that matches region, namespace + + 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) @@ -241,7 +245,7 @@ func NodeClassPredictor(factory ApiClientFactory) complete.Predictor { } // ServerPredictor returns a server member predictor -// TODO: Consider API options for node class filtering +// 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() @@ -253,13 +257,14 @@ func ServerPredictor(factory ApiClientFactory) complete.Predictor { 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 }) }