github.com/hernad/nomad@v1.6.112/command/job_run.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/hernad/nomad/api"
    16  	"github.com/hernad/nomad/helper/pointer"
    17  	"github.com/posener/complete"
    18  )
    19  
    20  var (
    21  	// enforceIndexRegex is a regular expression which extracts the enforcement error
    22  	enforceIndexRegex = regexp.MustCompile(`\((Enforcing job modify index.*)\)`)
    23  )
    24  
    25  type JobRunCommand struct {
    26  	Meta
    27  	JobGetter
    28  }
    29  
    30  func (c *JobRunCommand) Help() string {
    31  	helpText := `
    32  Usage: nomad job run [options] <path>
    33  Alias: nomad run
    34  
    35    Starts running a new job or updates an existing job using
    36    the specification located at <path>. This is the main command
    37    used to interact with Nomad.
    38  
    39    If the supplied path is "-", the jobfile is read from stdin. Otherwise
    40    it is read from the file at the supplied path or downloaded and
    41    read from URL specified.
    42  
    43    Upon successful job submission, this command will immediately
    44    enter an interactive monitor. This is useful to watch Nomad's
    45    internals make scheduling decisions and place the submitted work
    46    onto nodes. The monitor will end once job placement is done. It
    47    is safe to exit the monitor early using ctrl+c.
    48  
    49    On successful job submission and scheduling, exit code 0 will be
    50    returned. If there are job placement issues encountered
    51    (unsatisfiable constraints, resource exhaustion, etc), then the
    52    exit code will be 2. Any other errors, including client connection
    53    issues or internal errors, are indicated by exit code 1.
    54  
    55    If the job has specified the region, the -region flag and NOMAD_REGION
    56    environment variable are overridden and the job's region is used.
    57  
    58    The run command will set the consul_token of the job based on the following
    59    precedence, going from highest to lowest: the -consul-token flag, the
    60    $CONSUL_HTTP_TOKEN environment variable and finally the value in the job file.
    61  
    62    The run command will set the vault_token of the job based on the following
    63    precedence, going from highest to lowest: the -vault-token flag, the
    64    $VAULT_TOKEN environment variable and finally the value in the job file.
    65  
    66    When ACLs are enabled, this command requires a token with the 'submit-job'
    67    capability for the job's namespace. Jobs that mount CSI volumes require a
    68    token with the 'csi-mount-volume' capability for the volume's
    69    namespace. Jobs that mount host volumes require a token with the
    70    'host_volume' capability for that volume.
    71  
    72  General Options:
    73  
    74    ` + generalOptionsUsage(usageOptsDefault) + `
    75  
    76  Run Options:
    77  
    78    -check-index
    79      If set, the job is only registered or updated if the passed
    80      job modify index matches the server side version. If a check-index value of
    81      zero is passed, the job is only registered if it does not yet exist. If a
    82      non-zero value is passed, it ensures that the job is being updated from a
    83      known state. The use of this flag is most common in conjunction with plan
    84      command.
    85  
    86    -detach
    87      Return immediately instead of entering monitor mode. After job submission,
    88      the evaluation ID will be printed to the screen, which can be used to
    89      examine the evaluation using the eval-status command.
    90  
    91    -eval-priority
    92      Override the priority of the evaluations produced as a result of this job
    93      submission. By default, this is set to the priority of the job.
    94  
    95    -json
    96      Parses the job file as JSON. If the outer object has a Job field, such as
    97      from "nomad job inspect" or "nomad run -output", the value of the field is
    98      used as the job.
    99  
   100    -hcl1
   101      Parses the job file as HCLv1. Takes precedence over "-hcl2-strict".
   102  
   103    -hcl2-strict
   104      Whether an error should be produced from the HCL2 parser where a variable
   105      has been supplied which is not defined within the root variables. Defaults
   106      to true, but ignored if "-hcl1" is also defined.
   107  
   108    -output
   109      Output the JSON that would be submitted to the HTTP API without submitting
   110      the job.
   111  
   112    -policy-override
   113      Sets the flag to force override any soft mandatory Sentinel policies.
   114  
   115    -preserve-counts
   116      If set, the existing task group counts will be preserved when updating a job.
   117  
   118    -consul-token
   119      If set, the passed Consul token is stored in the job before sending to the
   120      Nomad servers. This allows passing the Consul token without storing it in
   121      the job file. This overrides the token found in $CONSUL_HTTP_TOKEN environment
   122      variable and that found in the job.
   123  
   124    -vault-token
   125      Used to validate if the user submitting the job has permission to run the job
   126      according to its Vault policies. A Vault token must be supplied if the vault
   127      block allow_unauthenticated is disabled in the Nomad server configuration.
   128      If the -vault-token flag is set, the passed Vault token is added to the jobspec
   129      before sending to the Nomad servers. This allows passing the Vault token
   130      without storing it in the job file. This overrides the token found in the
   131      $VAULT_TOKEN environment variable and the vault_token field in the job file.
   132      This token is cleared from the job after validating and cannot be used within
   133      the job executing environment. Use the vault block when templating in a job
   134      with a Vault token.
   135  
   136    -vault-namespace
   137      If set, the passed Vault namespace is stored in the job before sending to the
   138      Nomad servers.
   139  
   140    -var 'key=value'
   141      Variable for template, can be used multiple times.
   142  
   143    -var-file=path
   144      Path to HCL2 file containing user variables.
   145  
   146    -verbose
   147      Display full information.
   148  `
   149  	return strings.TrimSpace(helpText)
   150  }
   151  
   152  func (c *JobRunCommand) Synopsis() string {
   153  	return "Run a new job or update an existing job"
   154  }
   155  
   156  func (c *JobRunCommand) AutocompleteFlags() complete.Flags {
   157  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
   158  		complete.Flags{
   159  			"-check-index":     complete.PredictNothing,
   160  			"-detach":          complete.PredictNothing,
   161  			"-verbose":         complete.PredictNothing,
   162  			"-consul-token":    complete.PredictNothing,
   163  			"-vault-token":     complete.PredictAnything,
   164  			"-vault-namespace": complete.PredictAnything,
   165  			"-output":          complete.PredictNothing,
   166  			"-policy-override": complete.PredictNothing,
   167  			"-preserve-counts": complete.PredictNothing,
   168  			"-json":            complete.PredictNothing,
   169  			"-hcl1":            complete.PredictNothing,
   170  			"-hcl2-strict":     complete.PredictNothing,
   171  			"-var":             complete.PredictAnything,
   172  			"-var-file":        complete.PredictFiles("*.var"),
   173  			"-eval-priority":   complete.PredictNothing,
   174  		})
   175  }
   176  
   177  func (c *JobRunCommand) AutocompleteArgs() complete.Predictor {
   178  	return complete.PredictOr(
   179  		complete.PredictFiles("*.nomad"),
   180  		complete.PredictFiles("*.hcl"),
   181  		complete.PredictFiles("*.json"),
   182  	)
   183  }
   184  
   185  func (c *JobRunCommand) Name() string { return "job run" }
   186  
   187  func (c *JobRunCommand) Run(args []string) int {
   188  	var detach, verbose, output, override, preserveCounts bool
   189  	var checkIndexStr, consulToken, consulNamespace, vaultToken, vaultNamespace string
   190  	var evalPriority int
   191  
   192  	flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient)
   193  	flagSet.Usage = func() { c.Ui.Output(c.Help()) }
   194  	flagSet.BoolVar(&detach, "detach", false, "")
   195  	flagSet.BoolVar(&verbose, "verbose", false, "")
   196  	flagSet.BoolVar(&output, "output", false, "")
   197  	flagSet.BoolVar(&override, "policy-override", false, "")
   198  	flagSet.BoolVar(&preserveCounts, "preserve-counts", false, "")
   199  	flagSet.BoolVar(&c.JobGetter.JSON, "json", false, "")
   200  	flagSet.BoolVar(&c.JobGetter.HCL1, "hcl1", false, "")
   201  	flagSet.BoolVar(&c.JobGetter.Strict, "hcl2-strict", true, "")
   202  	flagSet.StringVar(&checkIndexStr, "check-index", "", "")
   203  	flagSet.StringVar(&consulToken, "consul-token", "", "")
   204  	flagSet.StringVar(&consulNamespace, "consul-namespace", "", "")
   205  	flagSet.StringVar(&vaultToken, "vault-token", "", "")
   206  	flagSet.StringVar(&vaultNamespace, "vault-namespace", "", "")
   207  	flagSet.Var(&c.JobGetter.Vars, "var", "")
   208  	flagSet.Var(&c.JobGetter.VarFiles, "var-file", "")
   209  	flagSet.IntVar(&evalPriority, "eval-priority", 0, "")
   210  
   211  	if err := flagSet.Parse(args); err != nil {
   212  		return 1
   213  	}
   214  
   215  	// Truncate the id unless full length is requested
   216  	length := shortId
   217  	if verbose {
   218  		length = fullId
   219  	}
   220  
   221  	// Check that we got exactly one argument
   222  	args = flagSet.Args()
   223  	if len(args) != 1 {
   224  		c.Ui.Error("This command takes one argument: <path>")
   225  		c.Ui.Error(commandErrorText(c))
   226  		return 1
   227  	}
   228  
   229  	if c.JobGetter.HCL1 {
   230  		c.JobGetter.Strict = false
   231  	}
   232  
   233  	if err := c.JobGetter.Validate(); err != nil {
   234  		c.Ui.Error(fmt.Sprintf("Invalid job options: %s", err))
   235  		return 1
   236  	}
   237  
   238  	// Get Job struct from Jobfile
   239  	sub, job, err := c.JobGetter.Get(args[0])
   240  	if err != nil {
   241  		c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err))
   242  		return 1
   243  	}
   244  
   245  	// Get the HTTP client
   246  	client, err := c.Meta.Client()
   247  	if err != nil {
   248  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   249  		return 1
   250  	}
   251  
   252  	// Force the region to be that of the job.
   253  	if r := job.Region; r != nil {
   254  		client.SetRegion(*r)
   255  	}
   256  
   257  	// Force the namespace to be that of the job.
   258  	if n := job.Namespace; n != nil {
   259  		client.SetNamespace(*n)
   260  	}
   261  
   262  	// Check if the job is periodic or is a parameterized job
   263  	periodic := job.IsPeriodic()
   264  	paramjob := job.IsParameterized()
   265  	multiregion := job.IsMultiregion()
   266  
   267  	// Parse the Consul token
   268  	if consulToken == "" {
   269  		// Check the environment variable
   270  		consulToken = os.Getenv("CONSUL_HTTP_TOKEN")
   271  	}
   272  
   273  	if consulToken != "" {
   274  		job.ConsulToken = pointer.Of(consulToken)
   275  	}
   276  
   277  	if consulNamespace != "" {
   278  		job.ConsulNamespace = pointer.Of(consulNamespace)
   279  	}
   280  
   281  	// Parse the Vault token
   282  	if vaultToken == "" {
   283  		// Check the environment variable
   284  		vaultToken = os.Getenv("VAULT_TOKEN")
   285  	}
   286  
   287  	if vaultToken != "" {
   288  		job.VaultToken = pointer.Of(vaultToken)
   289  	}
   290  
   291  	if vaultNamespace != "" {
   292  		job.VaultNamespace = pointer.Of(vaultNamespace)
   293  	}
   294  
   295  	if output {
   296  		req := struct {
   297  			Job *api.Job
   298  		}{
   299  			Job: job,
   300  		}
   301  		buf, err := json.MarshalIndent(req, "", "    ")
   302  		if err != nil {
   303  			c.Ui.Error(fmt.Sprintf("Error converting job: %s", err))
   304  			return 1
   305  		}
   306  
   307  		c.Ui.Output(string(buf))
   308  		return 0
   309  	}
   310  
   311  	// Parse the check-index
   312  	checkIndex, enforce, err := parseCheckIndex(checkIndexStr)
   313  	if err != nil {
   314  		c.Ui.Error(fmt.Sprintf("Error parsing check-index value %q: %v", checkIndexStr, err))
   315  		return 1
   316  	}
   317  
   318  	// Set the register options
   319  	opts := &api.RegisterOptions{
   320  		PolicyOverride: override,
   321  		PreserveCounts: preserveCounts,
   322  		EvalPriority:   evalPriority,
   323  		Submission:     sub,
   324  	}
   325  	if enforce {
   326  		opts.EnforceIndex = true
   327  		opts.ModifyIndex = checkIndex
   328  	}
   329  
   330  	// Submit the job
   331  	resp, _, err := client.Jobs().RegisterOpts(job, opts, nil)
   332  	if err != nil {
   333  		if strings.Contains(err.Error(), api.RegisterEnforceIndexErrPrefix) {
   334  			// Format the error specially if the error is due to index
   335  			// enforcement
   336  			matches := enforceIndexRegex.FindStringSubmatch(err.Error())
   337  			if len(matches) == 2 {
   338  				c.Ui.Error(matches[1]) // The matched group
   339  				c.Ui.Error("Job not updated")
   340  				return 1
   341  			}
   342  		}
   343  
   344  		c.Ui.Error(fmt.Sprintf("Error submitting job: %s", err))
   345  		return 1
   346  	}
   347  
   348  	// Print any warnings if there are any
   349  	if resp.Warnings != "" {
   350  		c.Ui.Output(
   351  			c.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", resp.Warnings)))
   352  	}
   353  
   354  	evalID := resp.EvalID
   355  
   356  	// Check if we should enter monitor mode
   357  	if detach || periodic || paramjob || multiregion {
   358  		c.Ui.Output("Job registration successful")
   359  		if periodic && !paramjob {
   360  			loc, err := job.Periodic.GetLocation()
   361  			if err == nil {
   362  				now := time.Now().In(loc)
   363  				next, err := job.Periodic.Next(now)
   364  				if err != nil {
   365  					c.Ui.Error(fmt.Sprintf("Error determining next launch time: %v", err))
   366  				} else {
   367  					c.Ui.Output(fmt.Sprintf("Approximate next launch time: %s (%s from now)",
   368  						formatTime(next), formatTimeDifference(now, next, time.Second)))
   369  				}
   370  			}
   371  		} else if !paramjob {
   372  			c.Ui.Output("Evaluation ID: " + evalID)
   373  		}
   374  
   375  		return 0
   376  	}
   377  
   378  	// Detach was not specified, so start monitoring
   379  	mon := newMonitor(c.Ui, client, length)
   380  	return mon.monitor(evalID)
   381  
   382  }
   383  
   384  // parseCheckIndex parses the check-index flag and returns the index, whether it
   385  // was set and potentially an error during parsing.
   386  func parseCheckIndex(input string) (uint64, bool, error) {
   387  	if input == "" {
   388  		return 0, false, nil
   389  	}
   390  
   391  	u, err := strconv.ParseUint(input, 10, 64)
   392  	return u, true, err
   393  }