github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/job_scale.go (about)

     1  package command
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/nomad/api"
    10  	"github.com/mitchellh/cli"
    11  	"github.com/posener/complete"
    12  )
    13  
    14  // Ensure JobScaleCommand satisfies the cli.Command interface.
    15  var _ cli.Command = &JobScaleCommand{}
    16  
    17  // JobScaleCommand implements cli.Command.
    18  type JobScaleCommand struct {
    19  	Meta
    20  }
    21  
    22  // Help satisfies the cli.Command Help function.
    23  func (j *JobScaleCommand) Help() string {
    24  	helpText := `
    25  Usage: nomad job scale [options] <job> [<group>] <count>
    26  
    27    Perform a scaling action by altering the count within a job group.
    28  
    29    Upon successful job submission, this command will immediately
    30    enter an interactive monitor. This is useful to watch Nomad's
    31    internals make scheduling decisions and place the submitted work
    32    onto nodes. The monitor will end once job placement is done. It
    33    is safe to exit the monitor early using ctrl+c.
    34  
    35    When ACLs are enabled, this command requires a token with the 'scale-job'
    36    capability for the job's namespace.
    37  
    38  General Options:
    39  
    40    ` + generalOptionsUsage(usageOptsDefault) + `
    41  
    42  Scale Options:
    43  
    44    -detach
    45      Return immediately instead of entering monitor mode. After job scaling,
    46      the evaluation ID will be printed to the screen, which can be used to
    47      examine the evaluation using the eval-status command.
    48  
    49    -verbose
    50      Display full information.
    51  `
    52  	return strings.TrimSpace(helpText)
    53  }
    54  
    55  // Synopsis satisfies the cli.Command Synopsis function.
    56  func (j *JobScaleCommand) Synopsis() string {
    57  	return "Change the count of a Nomad job group"
    58  }
    59  
    60  func (j *JobScaleCommand) AutocompleteFlags() complete.Flags {
    61  	return mergeAutocompleteFlags(j.Meta.AutocompleteFlags(FlagSetClient),
    62  		complete.Flags{
    63  			"-detach":  complete.PredictNothing,
    64  			"-verbose": complete.PredictNothing,
    65  		})
    66  }
    67  
    68  // Name returns the name of this command.
    69  func (j *JobScaleCommand) Name() string { return "job scale" }
    70  
    71  // Run satisfies the cli.Command Run function.
    72  func (j *JobScaleCommand) Run(args []string) int {
    73  	var detach, verbose bool
    74  
    75  	flags := j.Meta.FlagSet(j.Name(), FlagSetClient)
    76  	flags.Usage = func() { j.Ui.Output(j.Help()) }
    77  	flags.BoolVar(&detach, "detach", false, "")
    78  	flags.BoolVar(&verbose, "verbose", false, "")
    79  	if err := flags.Parse(args); err != nil {
    80  		return 1
    81  	}
    82  
    83  	var jobString, countString, groupString string
    84  	args = flags.Args()
    85  
    86  	// It is possible to specify either 2 or 3 arguments. Check and assign the
    87  	// args so they can be validate later on.
    88  	if numArgs := len(args); numArgs < 2 || numArgs > 3 {
    89  		j.Ui.Error("Command requires at least two arguments and no more than three")
    90  		return 1
    91  	} else if numArgs == 3 {
    92  		groupString = args[1]
    93  		countString = args[2]
    94  	} else {
    95  		countString = args[1]
    96  	}
    97  	jobString = args[0]
    98  
    99  	// Convert the count string arg to an int as required by the API.
   100  	count, err := strconv.Atoi(countString)
   101  	if err != nil {
   102  		j.Ui.Error(fmt.Sprintf("Failed to convert count string to int: %s", err))
   103  		return 1
   104  	}
   105  
   106  	// Get the HTTP client.
   107  	client, err := j.Meta.Client()
   108  	if err != nil {
   109  		j.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   110  		return 1
   111  	}
   112  
   113  	// Detail the job so we can perform addition checks before submitting the
   114  	// scaling request.
   115  	job, _, err := client.Jobs().ScaleStatus(jobString, nil)
   116  	if err != nil {
   117  		j.Ui.Error(fmt.Sprintf("Error querying job: %v", err))
   118  		return 1
   119  	}
   120  
   121  	if err := j.performGroupCheck(job.TaskGroups, &groupString); err != nil {
   122  		j.Ui.Error(err.Error())
   123  		return 1
   124  	}
   125  
   126  	// This is our default message added to scaling submissions.
   127  	msg := "submitted using the Nomad CLI"
   128  
   129  	// Perform the scaling action.
   130  	resp, _, err := client.Jobs().Scale(jobString, groupString, &count, msg, false, nil, nil)
   131  	if err != nil {
   132  		j.Ui.Error(fmt.Sprintf("Error submitting scaling request: %s", err))
   133  		return 1
   134  	}
   135  
   136  	// Print any warnings if we have some.
   137  	if resp.Warnings != "" {
   138  		j.Ui.Output(
   139  			j.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", resp.Warnings)))
   140  	}
   141  
   142  	// If we are to detach, log the evaluation ID and exit.
   143  	if detach {
   144  		j.Ui.Output("Evaluation ID: " + resp.EvalID)
   145  		return 0
   146  	}
   147  
   148  	// Truncate the ID unless full length is requested.
   149  	length := shortId
   150  	if verbose {
   151  		length = fullId
   152  	}
   153  
   154  	// Create and monitor the evaluation.
   155  	mon := newMonitor(j.Ui, client, length)
   156  	return mon.monitor(resp.EvalID)
   157  }
   158  
   159  // performGroupCheck performs logic to ensure the user specified the correct
   160  // group argument.
   161  func (j *JobScaleCommand) performGroupCheck(groups map[string]api.TaskGroupScaleStatus, group *string) error {
   162  
   163  	// If the job contains multiple groups and the user did not supply a task
   164  	// group, return an error.
   165  	if len(groups) > 1 && *group == "" {
   166  		return errors.New("Group name required")
   167  	}
   168  
   169  	// We have to iterate the map to have any idea what task groups we are
   170  	// dealing with.
   171  	for groupName := range groups {
   172  
   173  		// If the job has a single task group, and the user did not supply a
   174  		// task group, it is assumed we scale the only group in the job.
   175  		if len(groups) == 1 && *group == "" {
   176  			*group = groupName
   177  			return nil
   178  		}
   179  
   180  		// If we found a match, return.
   181  		if groupName == *group {
   182  			return nil
   183  		}
   184  	}
   185  
   186  	// If we got here, we didn't find a match and therefore return an error.
   187  	return fmt.Errorf("Group %v not found within job", *group)
   188  }