Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add prefix-search and auto-completion for scaling policy info command #9964

Merged
merged 6 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/contexts/contexts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
Namespaces Context = "namespaces"
Quotas Context = "quotas"
Recommendations Context = "recommendations"
ScalingPolicies Context = "scaling_policy"
Plugins Context = "plugins"
Volumes Context = "volumes"
All Context = "all"
Expand Down
2 changes: 1 addition & 1 deletion command/alloc_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (c *AllocStatusCommand) Run(args []string) int {
}

// If args not specified but output format is specified, format and output the allocations data list
if len(args) == 0 && json || len(tmpl) > 0 {
if len(args) == 0 && (json || len(tmpl) > 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we only want to do this if there were no args... previous parsing would go into list mode with args if template was specified, which i think was not the intended behavior. same below for eval status and job inspect.

allocs, _, err := client.Allocations().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying allocations: %v", err))
Expand Down
2 changes: 1 addition & 1 deletion command/eval_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (c *EvalStatusCommand) Run(args []string) int {
}

// If args not specified but output format is specified, format and output the evaluations data list
if len(args) == 0 && json || len(tmpl) > 0 {
if len(args) == 0 && (json || len(tmpl) > 0) {
evals, _, err := client.Evaluations().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying evaluations: %v", err))
Expand Down
2 changes: 1 addition & 1 deletion command/job_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (c *JobInspectCommand) Run(args []string) int {
}

// If args not specified but output format is specified, format and output the jobs data list
if len(args) == 0 && json || len(tmpl) > 0 {
if len(args) == 0 && (json || len(tmpl) > 0) {
jobs, _, err := client.Jobs().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying jobs: %v", err))
Expand Down
91 changes: 75 additions & 16 deletions command/scaling_policy_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (

"github.com/mitchellh/cli"
"github.com/posener/complete"

"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/api/contexts"
)

// Ensure ScalingPolicyInfoCommand satisfies the cli.Command interface.
Expand Down Expand Up @@ -54,6 +57,21 @@ func (s *ScalingPolicyInfoCommand) AutocompleteFlags() complete.Flags {
})
}

func (c *ScalingPolicyInfoCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictFunc(func(a complete.Args) []string {
client, err := c.Meta.Client()
if err != nil {
return nil
}

resp, _, err := client.Search().PrefixSearch(a.Last, contexts.ScalingPolicies, nil)
if err != nil {
return []string{}
}
return resp.Matches[contexts.ScalingPolicies]
})
}

lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
// Name returns the name of this command.
func (s *ScalingPolicyInfoCommand) Name() string { return "scaling policy info" }

Expand All @@ -70,37 +88,75 @@ func (s *ScalingPolicyInfoCommand) Run(args []string) int {
return 1
}

if args = flags.Args(); len(args) != 1 {
s.Ui.Error("This command takes one argument: <policy_id>")
s.Ui.Error(commandErrorText(s))
// Get the HTTP client.
client, err := s.Meta.Client()
if err != nil {
s.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}

// Get the policy ID.
args = flags.Args()

// Formatted list mode if no policy ID
if len(args) == 0 && (json || len(tmpl) > 0) {
policies, _, err := client.Scaling().ListPolicies(nil)
if err != nil {
s.Ui.Error(fmt.Sprintf("Error listing scaling policies: %v", err))
return 1
}
out, err := Format(json, tmpl, policies)
if err != nil {
s.Ui.Error(err.Error())
return 1
}
s.Ui.Output(out)
return 0
}

if len(args) != 1 {
s.Ui.Error("This command takes one of the following argument conditions:")
s.Ui.Error(" * A single <policy_id>")
s.Ui.Error(" * No arguments, with output format specified")
s.Ui.Error(commandErrorText(s))
return 1
}
policyID := args[0]
if len(policyID) == 1 {
s.Ui.Error("Identifier must contain at least two characters.")
return 1
}

// Get the HTTP client.
client, err := s.Meta.Client()
policyID = sanitizeUUIDPrefix(policyID)
policies, _, err := client.Scaling().ListPolicies(&api.QueryOptions{
Prefix: policyID,
})
if err != nil {
s.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
s.Ui.Error(fmt.Sprintf("Error querying scaling policy: %v", err))
return 1
}
if len(policies) == 0 {
s.Ui.Error(fmt.Sprintf("No scaling policies with prefix or id %q found", policyID))
return 1
}
if len(policies) > 1 {
out := formatScalingPolicies(policies)
s.Ui.Output(fmt.Sprintf("Prefix matched multiple scaling policies\n\n%s", out))
cgbaker marked this conversation as resolved.
Show resolved Hide resolved
return 0
}

policy, _, err := client.Scaling().GetPolicy(policyID, nil)
policy, _, err := client.Scaling().GetPolicy(policies[0].ID, nil)
if err != nil {
s.Ui.Error(fmt.Sprintf("Error listing scaling policies: %s", err))
s.Ui.Error(fmt.Sprintf("Error querying scaling policy: %s", err))
return 1
}

// If the user has specified to output the policy as JSON or using a
// template then perform this action for the entire object and exit the
// command.
if json || len(tmpl) > 0 {
out, err := Format(json, tmpl, policy)
if err != nil {
s.Ui.Error(err.Error())
return 1
}

s.Ui.Output(out)
return 0
}
Expand All @@ -109,10 +165,13 @@ func (s *ScalingPolicyInfoCommand) Run(args []string) int {
// and therefore can only be made pretty to a certain extent. Do this
// before the rest of the formatting so any errors are clearly passed back
// to the CLI.
out, err := Format(true, "", policy.Policy)
if err != nil {
s.Ui.Error(err.Error())
return 1
out := "<empty>"
if len(policy.Policy) > 0 {
out, err = Format(true, "", policy.Policy)
if err != nil {
s.Ui.Error(err.Error())
return 1
}
}

info := []string{
Expand Down
8 changes: 4 additions & 4 deletions command/scaling_policy_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ func TestScalingPolicyInfoCommand_Run(t *testing.T) {
cmd := &ScalingPolicyInfoCommand{Meta: Meta{Ui: ui}}

// Calling without the policyID should result in an error.
if code := cmd.Run([]string{"-address=" + url}); code != 1 {
if code := cmd.Run([]string{"-address=" + url, "first", "second"}); code != 1 {
cgbaker marked this conversation as resolved.
Show resolved Hide resolved
t.Fatalf("expected cmd run exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "This command takes one argument: <policy_id>") {
if out := ui.ErrorWriter.String(); !strings.Contains(out, "This command takes one of the following argument conditions") {
t.Fatalf("expected argument error within output: %v", out)
}

// Perform an initial info, which should return zero results.
if code := cmd.Run([]string{"-address=" + url, "scaling_policy_info"}); code != 1 {
t.Fatalf("expected cmd run exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "404 (policy not found)") {
t.Fatalf("expected 404 not found within output: %v", out)
if out := ui.ErrorWriter.String(); !strings.Contains(out, `No scaling policies with prefix or id "scaling_policy_inf" found`) {
t.Fatalf("expected 'no policies found' within output: %v", out)
}

// Generate a test job.
Expand Down
27 changes: 15 additions & 12 deletions command/scaling_policy_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func (s *ScalingPolicyListCommand) Run(args []string) int {
if args = flags.Args(); len(args) > 0 {
s.Ui.Error("This command takes no arguments")
s.Ui.Error(commandErrorText(s))
return 1
}

// Get the HTTP client.
Expand All @@ -111,11 +112,6 @@ func (s *ScalingPolicyListCommand) Run(args []string) int {
return 1
}

if len(policies) == 0 {
s.Ui.Output("No policies found")
return 0
}

if json || len(tmpl) > 0 {
out, err := Format(json, tmpl, policies)
if err != nil {
Expand All @@ -126,23 +122,30 @@ func (s *ScalingPolicyListCommand) Run(args []string) int {
return 0
}

output := formatScalingPolicies(policies)
s.Ui.Output(output)
return 0
}

func formatScalingPolicies(stubs []*api.ScalingPolicyListStub) string {
if len(stubs) == 0 {
return "No policies found"
}

// Create the output table header.
output := []string{"ID|Enabled|Type|Target"}
policies := []string{"ID|Enabled|Type|Target"}

// Sort the list of policies based on their target.
sortedPolicies := scalingPolicyStubList{policies: policies}
sortedPolicies := scalingPolicyStubList{policies: stubs}
sort.Sort(sortedPolicies)

// Iterate the policies and add to the output.
for _, policy := range sortedPolicies.policies {
output = append(output, fmt.Sprintf(
policies = append(policies, fmt.Sprintf(
"%s|%v|%s|%s",
policy.ID, policy.Enabled, policy.Type, formatScalingPolicyTarget(policy.Target)))
cgbaker marked this conversation as resolved.
Show resolved Hide resolved
}

// Output.
s.Ui.Output(formatList(output))
return 0
return formatList(policies)
}

// scalingPolicyStubList is a wrapper around []*api.ScalingPolicyListStub that
Expand Down
5 changes: 5 additions & 0 deletions nomad/search_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
structs.Deployments,
structs.Plugins,
structs.Volumes,
structs.ScalingPolicies,
structs.Namespaces,
}
)
Expand Down Expand Up @@ -68,6 +69,8 @@ func (s *Search) getMatches(iter memdb.ResultIterator, prefix string) ([]string,
id = t.ID
case *structs.CSIVolume:
id = t.ID
case *structs.ScalingPolicy:
id = t.ID
case *structs.Namespace:
id = t.Name
default:
Expand Down Expand Up @@ -106,6 +109,8 @@ func getResourceIter(context structs.Context, aclObj *acl.ACL, namespace, prefix
return state.DeploymentsByIDPrefix(ws, namespace, prefix)
case structs.Plugins:
return state.CSIPluginsByIDPrefix(ws, prefix)
case structs.ScalingPolicies:
return state.ScalingPoliciesByIDPrefix(ws, namespace, prefix)
case structs.Volumes:
return state.CSIVolumesByIDPrefix(ws, namespace, prefix)
case structs.Namespaces:
Expand Down
5 changes: 5 additions & 0 deletions nomad/search_endpoint_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func searchContexts(aclObj *acl.ACL, namespace string, context structs.Context)
acl.NamespaceCapabilityListJobs,
acl.NamespaceCapabilityReadJob)
volRead := allowVolume(aclObj, namespace)
policyRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityListScalingPolicies)

// Filter contexts down to those the ACL grants access to
available := make([]structs.Context, 0, len(all))
Expand All @@ -111,6 +112,10 @@ func searchContexts(aclObj *acl.ACL, namespace string, context structs.Context)
if jobRead {
available = append(available, c)
}
case structs.ScalingPolicies:
if policyRead || jobRead {
available = append(available, c)
}
case structs.Namespaces:
if aclObj.AllowNamespace(namespace) {
available = append(available, c)
Expand Down
39 changes: 39 additions & 0 deletions nomad/search_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,42 @@ func TestSearch_PrefixSearch_Namespace_ACL(t *testing.T) {
assert.Len(resp.Matches[structs.Namespaces], 2)
}
}

func TestSearch_PrefixSearch_ScalingPolicy(t *testing.T) {
t.Parallel()
require := require.New(t)

s, cleanupS := TestServer(t, func(c *Config) {
c.NumSchedulers = 0
})
defer cleanupS()
codec := rpcClient(t, s)
testutil.WaitForLeader(t, s.RPC)

job, policy := mock.JobWithScalingPolicy()
prefix := policy.ID
state := s.fsm.State()

require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, jobIndex, job))

req := &structs.SearchRequest{
Prefix: prefix,
Context: structs.ScalingPolicies,
QueryOptions: structs.QueryOptions{
Region: "global",
Namespace: job.Namespace,
},
}

var resp structs.SearchResponse
require.NoError(msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
require.Equal(1, len(resp.Matches[structs.ScalingPolicies]))
require.Equal(policy.ID, resp.Matches[structs.ScalingPolicies][0])
require.Equal(uint64(jobIndex), resp.Index)

req.Context = structs.All
require.NoError(msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp))
require.Equal(1, len(resp.Matches[structs.ScalingPolicies]))
require.Equal(policy.ID, resp.Matches[structs.ScalingPolicies][0])
require.Equal(uint64(jobIndex), resp.Index)
}
1 change: 1 addition & 0 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ const (
Namespaces Context = "namespaces"
Quotas Context = "quotas"
Recommendations Context = "recommendations"
ScalingPolicies Context = "scaling_policy"
All Context = "all"
Plugins Context = "plugins"
Volumes Context = "volumes"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.