1package command
2
3import (
4	"encoding/json"
5	"fmt"
6	"os"
7	"regexp"
8	"strconv"
9	"strings"
10	"time"
11
12	"github.com/hashicorp/nomad/api"
13	"github.com/hashicorp/nomad/helper"
14	"github.com/posener/complete"
15)
16
17var (
18	// enforceIndexRegex is a regular expression which extracts the enforcement error
19	enforceIndexRegex = regexp.MustCompile(`\((Enforcing job modify index.*)\)`)
20)
21
22type JobRunCommand struct {
23	Meta
24	JobGetter
25}
26
27func (c *JobRunCommand) Help() string {
28	helpText := `
29Usage: nomad job run [options] <path>
30Alias: nomad run
31
32  Starts running a new job or updates an existing job using
33  the specification located at <path>. This is the main command
34  used to interact with Nomad.
35
36  If the supplied path is "-", the jobfile is read from stdin. Otherwise
37  it is read from the file at the supplied path or downloaded and
38  read from URL specified.
39
40  Upon successful job submission, this command will immediately
41  enter an interactive monitor. This is useful to watch Nomad's
42  internals make scheduling decisions and place the submitted work
43  onto nodes. The monitor will end once job placement is done. It
44  is safe to exit the monitor early using ctrl+c.
45
46  On successful job submission and scheduling, exit code 0 will be
47  returned. If there are job placement issues encountered
48  (unsatisfiable constraints, resource exhaustion, etc), then the
49  exit code will be 2. Any other errors, including client connection
50  issues or internal errors, are indicated by exit code 1.
51
52  If the job has specified the region, the -region flag and NOMAD_REGION
53  environment variable are overridden and the job's region is used.
54
55  The run command will set the vault_token of the job based on the following
56  precedence, going from highest to lowest: the -vault-token flag, the
57  $VAULT_TOKEN environment variable and finally the value in the job file.
58
59General Options:
60
61  ` + generalOptionsUsage() + `
62
63Run Options:
64
65  -check-index
66    If set, the job is only registered or updated if the passed
67    job modify index matches the server side version. If a check-index value of
68    zero is passed, the job is only registered if it does not yet exist. If a
69    non-zero value is passed, it ensures that the job is being updated from a
70    known state. The use of this flag is most common in conjunction with plan
71    command.
72
73  -detach
74    Return immediately instead of entering monitor mode. After job submission,
75    the evaluation ID will be printed to the screen, which can be used to
76    examine the evaluation using the eval-status command.
77
78  -output
79    Output the JSON that would be submitted to the HTTP API without submitting
80    the job.
81
82  -policy-override
83    Sets the flag to force override any soft mandatory Sentinel policies.
84
85  -vault-token
86    If set, the passed Vault token is stored in the job before sending to the
87    Nomad servers. This allows passing the Vault token without storing it in
88    the job file. This overrides the token found in $VAULT_TOKEN environment
89    variable and that found in the job.
90
91  -verbose
92    Display full information.
93`
94	return strings.TrimSpace(helpText)
95}
96
97func (c *JobRunCommand) Synopsis() string {
98	return "Run a new job or update an existing job"
99}
100
101func (c *JobRunCommand) AutocompleteFlags() complete.Flags {
102	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
103		complete.Flags{
104			"-check-index":     complete.PredictNothing,
105			"-detach":          complete.PredictNothing,
106			"-verbose":         complete.PredictNothing,
107			"-vault-token":     complete.PredictAnything,
108			"-output":          complete.PredictNothing,
109			"-policy-override": complete.PredictNothing,
110		})
111}
112
113func (c *JobRunCommand) AutocompleteArgs() complete.Predictor {
114	return complete.PredictOr(complete.PredictFiles("*.nomad"), complete.PredictFiles("*.hcl"))
115}
116
117func (c *JobRunCommand) Name() string { return "job run" }
118
119func (c *JobRunCommand) Run(args []string) int {
120	var detach, verbose, output, override bool
121	var checkIndexStr, vaultToken string
122
123	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
124	flags.Usage = func() { c.Ui.Output(c.Help()) }
125	flags.BoolVar(&detach, "detach", false, "")
126	flags.BoolVar(&verbose, "verbose", false, "")
127	flags.BoolVar(&output, "output", false, "")
128	flags.BoolVar(&override, "policy-override", false, "")
129	flags.StringVar(&checkIndexStr, "check-index", "", "")
130	flags.StringVar(&vaultToken, "vault-token", "", "")
131
132	if err := flags.Parse(args); err != nil {
133		return 1
134	}
135
136	// Truncate the id unless full length is requested
137	length := shortId
138	if verbose {
139		length = fullId
140	}
141
142	// Check that we got exactly one argument
143	args = flags.Args()
144	if len(args) != 1 {
145		c.Ui.Error("This command takes one argument: <path>")
146		c.Ui.Error(commandErrorText(c))
147		return 1
148	}
149
150	// Get Job struct from Jobfile
151	job, err := c.JobGetter.ApiJob(args[0])
152	if err != nil {
153		c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err))
154		return 1
155	}
156
157	// Get the HTTP client
158	client, err := c.Meta.Client()
159	if err != nil {
160		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
161		return 1
162	}
163
164	// Force the region to be that of the job.
165	if r := job.Region; r != nil {
166		client.SetRegion(*r)
167	}
168
169	// Force the namespace to be that of the job.
170	if n := job.Namespace; n != nil {
171		client.SetNamespace(*n)
172	}
173
174	// Check if the job is periodic or is a parameterized job
175	periodic := job.IsPeriodic()
176	paramjob := job.IsParameterized()
177
178	// Parse the Vault token
179	if vaultToken == "" {
180		// Check the environment variable
181		vaultToken = os.Getenv("VAULT_TOKEN")
182	}
183
184	if vaultToken != "" {
185		job.VaultToken = helper.StringToPtr(vaultToken)
186	}
187
188	if output {
189		req := api.RegisterJobRequest{Job: job}
190		buf, err := json.MarshalIndent(req, "", "    ")
191		if err != nil {
192			c.Ui.Error(fmt.Sprintf("Error converting job: %s", err))
193			return 1
194		}
195
196		c.Ui.Output(string(buf))
197		return 0
198	}
199
200	// Parse the check-index
201	checkIndex, enforce, err := parseCheckIndex(checkIndexStr)
202	if err != nil {
203		c.Ui.Error(fmt.Sprintf("Error parsing check-index value %q: %v", checkIndexStr, err))
204		return 1
205	}
206
207	// Set the register options
208	opts := &api.RegisterOptions{}
209	if enforce {
210		opts.EnforceIndex = true
211		opts.ModifyIndex = checkIndex
212	}
213	if override {
214		opts.PolicyOverride = true
215	}
216
217	// Submit the job
218	resp, _, err := client.Jobs().RegisterOpts(job, opts, nil)
219	if err != nil {
220		if strings.Contains(err.Error(), api.RegisterEnforceIndexErrPrefix) {
221			// Format the error specially if the error is due to index
222			// enforcement
223			matches := enforceIndexRegex.FindStringSubmatch(err.Error())
224			if len(matches) == 2 {
225				c.Ui.Error(matches[1]) // The matched group
226				c.Ui.Error("Job not updated")
227				return 1
228			}
229		}
230
231		c.Ui.Error(fmt.Sprintf("Error submitting job: %s", err))
232		return 1
233	}
234
235	// Print any warnings if there are any
236	if resp.Warnings != "" {
237		c.Ui.Output(
238			c.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", resp.Warnings)))
239	}
240
241	evalID := resp.EvalID
242
243	// Check if we should enter monitor mode
244	if detach || periodic || paramjob {
245		c.Ui.Output("Job registration successful")
246		if periodic && !paramjob {
247			loc, err := job.Periodic.GetLocation()
248			if err == nil {
249				now := time.Now().In(loc)
250				next, err := job.Periodic.Next(now)
251				if err != nil {
252					c.Ui.Error(fmt.Sprintf("Error determining next launch time: %v", err))
253				} else {
254					c.Ui.Output(fmt.Sprintf("Approximate next launch time: %s (%s from now)",
255						formatTime(next), formatTimeDifference(now, next, time.Second)))
256				}
257			}
258		} else if !paramjob {
259			c.Ui.Output("Evaluation ID: " + evalID)
260		}
261
262		return 0
263	}
264
265	// Detach was not specified, so start monitoring
266	mon := newMonitor(c.Ui, client, length)
267	return mon.monitor(evalID, false)
268
269}
270
271// parseCheckIndex parses the check-index flag and returns the index, whether it
272// was set and potentially an error during parsing.
273func parseCheckIndex(input string) (uint64, bool, error) {
274	if input == "" {
275		return 0, false, nil
276	}
277
278	u, err := strconv.ParseUint(input, 10, 64)
279	return u, true, err
280}
281