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
This commit is contained in:
Dave May 2021-10-12 20:01:54 -04:00 committed by GitHub
parent 2af0422bca
commit 305e8e98bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 13 deletions

3
.changelog/11270.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Improved autocomplete support for job dispatch and operator debug
```

View File

@ -6,7 +6,6 @@ import (
"os" "os"
"strings" "strings"
"github.com/hashicorp/nomad/api/contexts"
flaghelper "github.com/hashicorp/nomad/helper/flags" flaghelper "github.com/hashicorp/nomad/helper/flags"
"github.com/posener/complete" "github.com/posener/complete"
) )
@ -75,11 +74,20 @@ func (c *JobDispatchCommand) AutocompleteArgs() complete.Predictor {
return nil return nil
} }
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Jobs, nil) resp, _, err := client.Jobs().PrefixList(a.Last)
if err != nil { if err != nil {
return []string{} 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
}) })
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
func TestJobDispatchCommand_Implements(t *testing.T) { func TestJobDispatchCommand_Implements(t *testing.T) {
@ -50,7 +50,6 @@ func TestJobDispatchCommand_Fails(t *testing.T) {
} }
func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) { func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) {
assert := assert.New(t)
t.Parallel() t.Parallel()
srv, _, url := testServer(t, true, nil) srv, _, url := testServer(t, true, nil)
@ -62,13 +61,27 @@ func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) {
// Create a fake job // Create a fake job
state := srv.Agent.Server().State() state := srv.Agent.Server().State()
j := mock.Job() 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] prefix := j.ID[:len(j.ID)-5]
args := complete.Args{Last: prefix} args := complete.Args{Last: prefix}
predictor := cmd.AutocompleteArgs() predictor := cmd.AutocompleteArgs()
// No parameterized jobs, should be 0 results
res := predictor.Predict(args) res := predictor.Predict(args)
assert.Equal(1, len(res)) require.Equal(t, 0, len(res))
assert.Equal(j.ID, res[0])
// 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])
} }

View File

@ -21,6 +21,7 @@ import (
"github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/api/contexts"
"github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/posener/complete" "github.com/posener/complete"
@ -179,12 +180,12 @@ func (c *OperatorDebugCommand) AutocompleteFlags() complete.Flags {
complete.Flags{ complete.Flags{
"-duration": complete.PredictAnything, "-duration": complete.PredictAnything,
"-interval": complete.PredictAnything, "-interval": complete.PredictAnything,
"-log-level": complete.PredictAnything, "-log-level": complete.PredictSet("TRACE", "DEBUG", "INFO", "WARN", "ERROR"),
"-max-nodes": complete.PredictAnything, "-max-nodes": complete.PredictAnything,
"-node-class": complete.PredictAnything, "-node-class": NodeClassPredictor(c.Client),
"-node-id": complete.PredictAnything, "-node-id": NodePredictor(c.Client),
"-server-id": complete.PredictAnything, "-server-id": ServerPredictor(c.Client),
"-output": complete.PredictAnything, "-output": complete.PredictDirs("*"),
"-pprof-duration": complete.PredictAnything, "-pprof-duration": complete.PredictAnything,
"-consul-token": complete.PredictAnything, "-consul-token": complete.PredictAnything,
"-vault-token": complete.PredictAnything, "-vault-token": complete.PredictAnything,
@ -195,6 +196,79 @@ func (c *OperatorDebugCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing 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) Name() string { return "debug" }
func (c *OperatorDebugCommand) Run(args []string) int { func (c *OperatorDebugCommand) Run(args []string) int {