diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5c2ed2d21c..64058deb3213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ IMPROVEMENTS: * api: Added ?resources=true query parameter to /v1/nodes and /v1/allocations to include resource allocations in listings. [[GH-9055](https://github.com/hashicorp/nomad/issues/9055)] * api: Added ?task_states=false query parameter to /v1/allocations to remove TaskStates from listings. Defaults to being included as before. [[GH-9055](https://github.com/hashicorp/nomad/issues/9055)] * build: Updated to Go 1.15.4. [[GH-9305](https://github.com/hashicorp/nomad/issues/9305)] + * cli: Added autocompletion for `recommendation` commands [[GH-9317](https://github.com/hashicorp/nomad/issues/9317)] * cli: Added `scale` and `scaling-events` subcommands to the `job` command. [[GH-9023](https://github.com/hashicorp/nomad/pull/9023)] * cli: Added `scaling` command for interaction with the scaling API endpoint. [[GH-9025](https://github.com/hashicorp/nomad/pull/9025)] * client: Batch state store writes to reduce disk IO. [[GH-9093](https://github.com/hashicorp/nomad/issues/9093)] diff --git a/api/contexts/contexts.go b/api/contexts/contexts.go index ae40db3f81ba..ea4ea1309509 100644 --- a/api/contexts/contexts.go +++ b/api/contexts/contexts.go @@ -4,14 +4,15 @@ package contexts type Context string const ( - Allocs Context = "allocs" - Deployments Context = "deployment" - Evals Context = "evals" - Jobs Context = "jobs" - Nodes Context = "nodes" - Namespaces Context = "namespaces" - Quotas Context = "quotas" - Plugins Context = "plugins" - Volumes Context = "volumes" - All Context = "all" + Allocs Context = "allocs" + Deployments Context = "deployment" + Evals Context = "evals" + Jobs Context = "jobs" + Nodes Context = "nodes" + Namespaces Context = "namespaces" + Quotas Context = "quotas" + Recommendations Context = "recommendations" + Plugins Context = "plugins" + Volumes Context = "volumes" + All Context = "all" ) diff --git a/command/recommendation_apply.go b/command/recommendation_apply.go index 980746b4dc0f..6482013e4863 100644 --- a/command/recommendation_apply.go +++ b/command/recommendation_apply.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/hashicorp/nomad/api" + "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -15,6 +16,7 @@ var _ cli.Command = &RecommendationApplyCommand{} // RecommendationApplyCommand implements cli.Command. type RecommendationApplyCommand struct { Meta + RecommendationAutocompleteCommand } // Help satisfies the cli.Command Help function. diff --git a/command/recommendation_apply_test.go b/command/recommendation_apply_test.go index cc8f9e19cc1d..b2f051ff1eaa 100644 --- a/command/recommendation_apply_test.go +++ b/command/recommendation_apply_test.go @@ -4,10 +4,11 @@ import ( "fmt" "testing" - "github.com/hashicorp/nomad/api" - "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/testutil" ) func TestRecommendationApplyCommand_Run(t *testing.T) { @@ -83,3 +84,12 @@ func TestRecommendationApplyCommand_Run(t *testing.T) { require.NoError(err) require.Equal(1, *jobResp.TaskGroups[0].Tasks[0].Resources.CPU) } + +func TestRecommendationApplyCommand_AutocompleteArgs(t *testing.T) { + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := cli.NewMockUi() + cmd := RecommendationApplyCommand{Meta: Meta{Ui: ui, flagAddress: url}} + testRecommendationAutocompleteCommand(t, client, srv, ui, &cmd.RecommendationAutocompleteCommand) +} diff --git a/command/recommendation_dismiss.go b/command/recommendation_dismiss.go index bb362cd8b45b..66bc9b626517 100644 --- a/command/recommendation_dismiss.go +++ b/command/recommendation_dismiss.go @@ -6,14 +6,38 @@ import ( "github.com/mitchellh/cli" "github.com/posener/complete" + + "github.com/hashicorp/nomad/api/contexts" ) // Ensure RecommendationDismissCommand satisfies the cli.Command interface. var _ cli.Command = &RecommendationDismissCommand{} +// RecommendationAutocompleteCommand provides AutocompleteArgs for all +// recommendation commands that support prefix-search autocompletion +type RecommendationAutocompleteCommand struct { + Meta +} + +func (r *RecommendationAutocompleteCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictFunc(func(a complete.Args) []string { + client, err := r.Meta.Client() + if err != nil { + return nil + } + + resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Recommendations, nil) + if err != nil { + return []string{} + } + return resp.Matches[contexts.Recommendations] + }) +} + // RecommendationDismissCommand implements cli.Command. type RecommendationDismissCommand struct { Meta + RecommendationAutocompleteCommand } // Help satisfies the cli.Command Help function. diff --git a/command/recommendation_dismiss_test.go b/command/recommendation_dismiss_test.go index 77e8f3ba38c0..bcd10638953a 100644 --- a/command/recommendation_dismiss_test.go +++ b/command/recommendation_dismiss_test.go @@ -5,8 +5,11 @@ import ( "testing" "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/command/agent" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -80,3 +83,51 @@ func TestRecommendationDismissCommand_Run(t *testing.T) { require.Error(err, "not found") require.Nil(recInfo) } + +func TestRecommendationDismissCommand_AutocompleteArgs(t *testing.T) { + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := cli.NewMockUi() + cmd := &RecommendationDismissCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + testRecommendationAutocompleteCommand(t, client, srv, ui, &cmd.RecommendationAutocompleteCommand) +} + +func testRecommendationAutocompleteCommand(t *testing.T, client *api.Client, srv *agent.TestAgent, ui *cli.MockUi, cmd *RecommendationAutocompleteCommand) { + assert := assert.New(t) + t.Parallel() + + // Register a test job to write a recommendation against. + testJob := testJob("recommendation_autocomplete") + regResp, _, err := client.Jobs().Register(testJob, nil) + require.NoError(t, err) + registerCode := waitForSuccess(ui, client, fullId, t, regResp.EvalID) + require.Equal(t, 0, registerCode) + + // Write a recommendation. + rec := &api.Recommendation{ + JobID: *testJob.ID, + Group: *testJob.TaskGroups[0].Name, + Task: testJob.TaskGroups[0].Tasks[0].Name, + Resource: "CPU", + Value: 1050, + Meta: map[string]interface{}{"test-meta-entry": "test-meta-value"}, + Stats: map[string]float64{"p13": 1.13}, + } + rec, _, err = client.Recommendations().Upsert(rec, nil) + if srv.Enterprise { + require.NoError(t, err) + } else { + require.Error(t, err, "Nomad Enterprise only endpoint") + return + } + + prefix := rec.ID[:5] + args := complete.Args{Last: prefix} + predictor := cmd.AutocompleteArgs() + + res := predictor.Predict(args) + assert.Equal(1, len(res)) + assert.Equal(rec.ID, res[0]) +} diff --git a/command/recommendation_info.go b/command/recommendation_info.go index 42c822c02626..2105f1b70627 100644 --- a/command/recommendation_info.go +++ b/command/recommendation_info.go @@ -15,6 +15,7 @@ var _ cli.Command = &RecommendationInfoCommand{} // RecommendationInfoCommand implements cli.Command. type RecommendationInfoCommand struct { Meta + RecommendationAutocompleteCommand } // Help satisfies the cli.Command Help function. diff --git a/command/recommendation_info_test.go b/command/recommendation_info_test.go index b56bea86feca..29e564fe18df 100644 --- a/command/recommendation_info_test.go +++ b/command/recommendation_info_test.go @@ -4,10 +4,11 @@ import ( "fmt" "testing" - "github.com/hashicorp/nomad/api" - "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" "github.com/stretchr/testify/require" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/testutil" ) func TestRecommendationInfoCommand_Run(t *testing.T) { @@ -81,3 +82,12 @@ func TestRecommendationInfoCommand_Run(t *testing.T) { require.Contains(out, recResp.ID) } } + +func TestRecommendationInfoCommand_AutocompleteArgs(t *testing.T) { + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := cli.NewMockUi() + cmd := RecommendationInfoCommand{Meta: Meta{Ui: ui, flagAddress: url}} + testRecommendationAutocompleteCommand(t, client, srv, ui, &cmd.RecommendationAutocompleteCommand) +} diff --git a/command/recommendation_list_test.go b/command/recommendation_list_test.go index d09c67eb84e6..c7234bd7c50e 100644 --- a/command/recommendation_list_test.go +++ b/command/recommendation_list_test.go @@ -1,15 +1,14 @@ package command import ( - "fmt" "sort" "testing" - "github.com/hashicorp/nomad/api" - "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hashicorp/nomad/api" ) func TestRecommendationListCommand_Run(t *testing.T) { @@ -17,21 +16,6 @@ func TestRecommendationListCommand_Run(t *testing.T) { t.Parallel() srv, client, url := testServer(t, true, nil) defer srv.Shutdown() - testutil.WaitForResult(func() (bool, error) { - nodes, _, err := client.Nodes().List(nil) - if err != nil { - return false, err - } - if len(nodes) == 0 { - return false, fmt.Errorf("missing node") - } - if _, ok := nodes[0].Drivers["mock_driver"]; !ok { - return false, fmt.Errorf("mock_driver not ready") - } - return true, nil - }, func(err error) { - t.Fatalf("err: %s", err) - }) ui := cli.NewMockUi() cmd := &RecommendationListCommand{Meta: Meta{Ui: ui}} @@ -89,7 +73,7 @@ func TestRecommendationListCommand_Run(t *testing.T) { } } -func TestRecommendationList_Sort(t *testing.T) { +func TestRecommendationListCommand_Sort(t *testing.T) { testCases := []struct { inputRecommendationList []*api.Recommendation expectedOutputList []*api.Recommendation diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 58049ef70c18..95869e8cc2e3 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -193,16 +193,17 @@ var ( type Context string const ( - Allocs Context = "allocs" - Deployments Context = "deployment" - Evals Context = "evals" - Jobs Context = "jobs" - Nodes Context = "nodes" - Namespaces Context = "namespaces" - Quotas Context = "quotas" - All Context = "all" - Plugins Context = "plugins" - Volumes Context = "volumes" + Allocs Context = "allocs" + Deployments Context = "deployment" + Evals Context = "evals" + Jobs Context = "jobs" + Nodes Context = "nodes" + Namespaces Context = "namespaces" + Quotas Context = "quotas" + Recommendations Context = "recommendations" + All Context = "all" + Plugins Context = "plugins" + Volumes Context = "volumes" ) // NamespacedID is a tuple of an ID and a namespace diff --git a/vendor/github.com/hashicorp/nomad/api/contexts/contexts.go b/vendor/github.com/hashicorp/nomad/api/contexts/contexts.go index ae40db3f81ba..ea4ea1309509 100644 --- a/vendor/github.com/hashicorp/nomad/api/contexts/contexts.go +++ b/vendor/github.com/hashicorp/nomad/api/contexts/contexts.go @@ -4,14 +4,15 @@ package contexts type Context string const ( - Allocs Context = "allocs" - Deployments Context = "deployment" - Evals Context = "evals" - Jobs Context = "jobs" - Nodes Context = "nodes" - Namespaces Context = "namespaces" - Quotas Context = "quotas" - Plugins Context = "plugins" - Volumes Context = "volumes" - All Context = "all" + Allocs Context = "allocs" + Deployments Context = "deployment" + Evals Context = "evals" + Jobs Context = "jobs" + Nodes Context = "nodes" + Namespaces Context = "namespaces" + Quotas Context = "quotas" + Recommendations Context = "recommendations" + Plugins Context = "plugins" + Volumes Context = "volumes" + All Context = "all" )