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 }