Skip to content

Commit

Permalink
Merge pull request #9964 from hashicorp/f-9787-alloc-prefix-cli
Browse files Browse the repository at this point in the history
add prefix-search and auto-completion for `scaling policy info` command
  • Loading branch information
cgbaker committed Feb 4, 2021
2 parents e45a124 + 2a302c6 commit 415203d
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 44 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ FEATURES:
* **Terminating Gateways**: Adds built-in support for running Consul Connect terminating gateways [[GH-9829](https://github.com/hashicorp/nomad/pull/9829)]

IMPROVEMENTS:
* cli: Improved `scaling policy` commands with -verbose, auto-completion, and prefix-matching [[GH-9964](https://github.com/hashicorp/nomad/issues/9964)]
* consul/connect: Made handling of sidecar task container image URLs consistent with the `docker` task driver. [[GH-9580](https://github.com/hashicorp/nomad/issues/9580)]


BUG FIXES:
* consul: Fixed a bug where failing tasks with group services would only cause the allocation to restart once instead of respecting the `restart` field. [[GH-9869](https://github.com/hashicorp/nomad/issues/9869)]
* consul/connect: Fixed a bug where gateway proxy connection default timeout not set [[GH-9851](https://github.com/hashicorp/nomad/pull/9851)]
Expand Down
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) {
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
110 changes: 90 additions & 20 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 All @@ -32,6 +35,9 @@ General Options:
Policy Info Options:
-verbose
Display full information.
-json
Output the scaling policy in its JSON format.
Expand All @@ -49,58 +55,119 @@ func (s *ScalingPolicyInfoCommand) Synopsis() string {
func (s *ScalingPolicyInfoCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(s.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-verbose": complete.PredictNothing,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}

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]
})
}

// Name returns the name of this command.
func (s *ScalingPolicyInfoCommand) Name() string { return "scaling policy info" }

// Run satisfies the cli.Command Run function.
func (s *ScalingPolicyInfoCommand) Run(args []string) int {
var json bool
var json, verbose bool
var tmpl string

flags := s.Meta.FlagSet(s.Name(), FlagSetClient)
flags.Usage = func() { s.Ui.Output(s.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}

if args = flags.Args(); len(args) != 1 {
s.Ui.Error("This command takes one argument: <policy_id>")
s.Ui.Error(commandErrorText(s))
return 1
// Truncate the id unless full length is requested
length := shortId
if verbose {
length = fullId
}

// Get the policy ID.
policyID := args[0]

// Get the HTTP client.
client, err := s.Meta.Client()
if err != nil {
s.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}

policy, _, err := client.Scaling().GetPolicy(policyID, nil)
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
}

policyID = sanitizeUUIDPrefix(policyID)
policies, _, err := client.Scaling().ListPolicies(&api.QueryOptions{
Prefix: policyID,
})
if err != nil {
s.Ui.Error(fmt.Sprintf("Error listing scaling policies: %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, length)
s.Ui.Error(fmt.Sprintf("Prefix matched multiple scaling policies\n\n%s", out))
return 0
}

policy, _, err := client.Scaling().GetPolicy(policies[0].ID, nil)
if err != nil {
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,14 +176,17 @@ 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{
fmt.Sprintf("ID|%s", policy.ID),
fmt.Sprintf("ID|%s", limit(policy.ID, length)),
fmt.Sprintf("Enabled|%v", *policy.Enabled),
fmt.Sprintf("Target|%s", formatScalingPolicyTarget(policy.Target)),
fmt.Sprintf("Min|%v", *policy.Min),
Expand Down
22 changes: 19 additions & 3 deletions command/scaling_policy_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,24 @@ func TestScalingPolicyInfoCommand_Run(t *testing.T) {
if code := cmd.Run([]string{"-address=" + url}); code != 1 {
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)
}

// Calling with more than one argument should result in an error.
if code := cmd.Run([]string{"-address=" + url, "first", "second"}); code != 1 {
t.Fatalf("expected cmd run exit code 1, got: %d", code)
}
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 Expand Up @@ -85,4 +93,12 @@ func TestScalingPolicyInfoCommand_Run(t *testing.T) {
if out := ui.OutputWriter.String(); !strings.Contains(out, "Policy:") {
t.Fatalf("expected policy ID within output: %v", out)
}

prefix := policies[0].ID[:2]
if code := cmd.Run([]string{"-address=" + url, prefix}); code != 0 {
t.Fatalf("expected cmd run exit code 0, got: %d", code)
}
if out := ui.OutputWriter.String(); !strings.Contains(out, "Policy:") {
t.Fatalf("expected policy ID within output: %v", out)
}
}
53 changes: 35 additions & 18 deletions command/scaling_policy_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Policy Info Options:
-type
Filter scaling policies by type.
-verbose
Display full information.
-json
Output the scaling policy in its JSON format.
Expand All @@ -59,10 +62,11 @@ func (s *ScalingPolicyListCommand) Synopsis() string {
func (s *ScalingPolicyListCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(s.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-job": complete.PredictNothing,
"-type": complete.PredictNothing,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-verbose": complete.PredictNothing,
"-job": complete.PredictNothing,
"-type": complete.PredictNothing,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}

Expand All @@ -71,11 +75,12 @@ func (s *ScalingPolicyListCommand) Name() string { return "scaling policy list"

// Run satisfies the cli.Command Run function.
func (s *ScalingPolicyListCommand) Run(args []string) int {
var json bool
var json, verbose bool
var tmpl, policyType, job string

flags := s.Meta.FlagSet(s.Name(), FlagSetClient)
flags.Usage = func() { s.Ui.Output(s.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
flags.StringVar(&policyType, "type", "", "")
Expand All @@ -87,6 +92,13 @@ 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
}

// Truncate the id unless full length is requested
length := shortId
if verbose {
length = fullId
}

// Get the HTTP client.
Expand All @@ -111,11 +123,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 +133,33 @@ func (s *ScalingPolicyListCommand) Run(args []string) int {
return 0
}

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

func formatScalingPolicies(stubs []*api.ScalingPolicyListStub, uuidLength int) 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)))
limit(policy.ID, uuidLength),
policy.Enabled,
policy.Type,
formatScalingPolicyTarget(policy.Target)))
}

// 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
Loading

0 comments on commit 415203d

Please sign in to comment.