From 316ca44667126f7d14624c5df5e9fd02eca20827 Mon Sep 17 00:00:00 2001 From: Chris Baker <1675087+cgbaker@users.noreply.github.com> Date: Wed, 11 Nov 2020 10:29:42 +0000 Subject: [PATCH] auto-complete for recommendations CLI, plus OSS components of recommendations prefix search --- api/contexts/contexts.go | 21 ++++----- command/recommendation_apply.go | 17 +++++++ command/recommendation_apply_test.go | 45 +++++++++++++++++++ command/recommendation_dismiss.go | 17 +++++++ command/recommendation_dismiss_test.go | 45 +++++++++++++++++++ command/recommendation_info.go | 17 +++++++ command/recommendation_info_test.go | 45 +++++++++++++++++++ command/recommendation_list_test.go | 22 ++------- nomad/structs/structs.go | 21 ++++----- .../hashicorp/nomad/api/contexts/contexts.go | 21 ++++----- 10 files changed, 222 insertions(+), 49 deletions(-) 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..ee6b316743c7 100644 --- a/command/recommendation_apply.go +++ b/command/recommendation_apply.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/api/contexts" + "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -60,6 +62,21 @@ func (r *RecommendationApplyCommand) AutocompleteFlags() complete.Flags { }) } +func (r *RecommendationApplyCommand) 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] + }) +} + // Name returns the name of this command. func (r *RecommendationApplyCommand) Name() string { return "recommendation apply" } diff --git a/command/recommendation_apply_test.go b/command/recommendation_apply_test.go index cc8f9e19cc1d..e8656b3d58a9 100644 --- a/command/recommendation_apply_test.go +++ b/command/recommendation_apply_test.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -83,3 +85,46 @@ 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) { + assert := assert.New(t) + t.Parallel() + + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + // Register a test job to write a recommendation against. + ui := cli.NewMockUi() + testJob := testJob("recommendation_list") + 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 + } + + cmd := &RecommendationApplyCommand{Meta: Meta{Ui: ui, flagAddress: url}} + 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_dismiss.go b/command/recommendation_dismiss.go index bb362cd8b45b..636ce898f040 100644 --- a/command/recommendation_dismiss.go +++ b/command/recommendation_dismiss.go @@ -6,6 +6,8 @@ import ( "github.com/mitchellh/cli" "github.com/posener/complete" + + "github.com/hashicorp/nomad/api/contexts" ) // Ensure RecommendationDismissCommand satisfies the cli.Command interface. @@ -39,6 +41,21 @@ func (r *RecommendationDismissCommand) AutocompleteFlags() complete.Flags { complete.Flags{}) } +func (r *RecommendationDismissCommand) 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] + }) +} + // Name returns the name of this command. func (r *RecommendationDismissCommand) Name() string { return "recommendation dismiss" } diff --git a/command/recommendation_dismiss_test.go b/command/recommendation_dismiss_test.go index 77e8f3ba38c0..ea4a5bc155b5 100644 --- a/command/recommendation_dismiss_test.go +++ b/command/recommendation_dismiss_test.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/nomad/api" "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 +82,46 @@ func TestRecommendationDismissCommand_Run(t *testing.T) { require.Error(err, "not found") require.Nil(recInfo) } + +func TestRecommendationDismissCommand_AutocompleteArgs(t *testing.T) { + assert := assert.New(t) + t.Parallel() + + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + // Register a test job to write a recommendation against. + ui := cli.NewMockUi() + testJob := testJob("recommendation_list") + 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 + } + + cmd := &RecommendationDismissCommand{Meta: Meta{Ui: ui, flagAddress: url}} + 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..9045445abfea 100644 --- a/command/recommendation_info.go +++ b/command/recommendation_info.go @@ -7,6 +7,8 @@ import ( "github.com/mitchellh/cli" "github.com/posener/complete" + + "github.com/hashicorp/nomad/api/contexts" ) // Ensure RecommendationInfoCommand satisfies the cli.Command interface. @@ -52,6 +54,21 @@ func (r *RecommendationInfoCommand) AutocompleteFlags() complete.Flags { }) } +func (r *RecommendationInfoCommand) 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] + }) +} + // Name returns the name of this command. func (r *RecommendationInfoCommand) Name() string { return "recommendation info" } diff --git a/command/recommendation_info_test.go b/command/recommendation_info_test.go index b56bea86feca..614d7ef8789a 100644 --- a/command/recommendation_info_test.go +++ b/command/recommendation_info_test.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -81,3 +83,46 @@ func TestRecommendationInfoCommand_Run(t *testing.T) { require.Contains(out, recResp.ID) } } + +func TestRecommendationInfoCommand_AutocompleteArgs(t *testing.T) { + assert := assert.New(t) + t.Parallel() + + srv, client, url := testServer(t, true, nil) + defer srv.Shutdown() + + // Register a test job to write a recommendation against. + ui := cli.NewMockUi() + testJob := testJob("recommendation_list") + 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 + } + + cmd := &RecommendationInfoCommand{Meta: Meta{Ui: ui, flagAddress: url}} + 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_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" )