1package command
2
3import (
4	"fmt"
5	"strconv"
6	"strings"
7	"time"
8
9	"github.com/hashicorp/nomad/api"
10	"github.com/hashicorp/nomad/api/contexts"
11	"github.com/posener/complete"
12	"github.com/ryanuber/columnize"
13)
14
15type JobHistoryCommand struct {
16	Meta
17	formatter DataFormatter
18}
19
20func (c *JobHistoryCommand) Help() string {
21	helpText := `
22Usage: nomad job history [options] <job>
23
24  History is used to display the known versions of a particular job. The command
25  can display the diff between job versions and can be useful for understanding
26  the changes that occurred to the job as well as deciding job versions to revert
27  to.
28
29General Options:
30
31  ` + generalOptionsUsage() + `
32
33History Options:
34
35  -p
36    Display the difference between each job and its predecessor.
37
38  -full
39    Display the full job definition for each version.
40
41  -version <job version>
42    Display only the history for the given job version.
43
44  -json
45    Output the job versions in a JSON format.
46
47  -t
48    Format and display the job versions using a Go template.
49`
50	return strings.TrimSpace(helpText)
51}
52
53func (c *JobHistoryCommand) Synopsis() string {
54	return "Display all tracked versions of a job"
55}
56
57func (c *JobHistoryCommand) AutocompleteFlags() complete.Flags {
58	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
59		complete.Flags{
60			"-p":       complete.PredictNothing,
61			"-full":    complete.PredictNothing,
62			"-version": complete.PredictAnything,
63			"-json":    complete.PredictNothing,
64			"-t":       complete.PredictAnything,
65		})
66}
67
68func (c *JobHistoryCommand) AutocompleteArgs() complete.Predictor {
69	return complete.PredictFunc(func(a complete.Args) []string {
70		client, err := c.Meta.Client()
71		if err != nil {
72			return nil
73		}
74
75		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Jobs, nil)
76		if err != nil {
77			return []string{}
78		}
79		return resp.Matches[contexts.Jobs]
80	})
81}
82
83func (c *JobHistoryCommand) Name() string { return "job history" }
84
85func (c *JobHistoryCommand) Run(args []string) int {
86	var json, diff, full bool
87	var tmpl, versionStr string
88
89	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
90	flags.Usage = func() { c.Ui.Output(c.Help()) }
91	flags.BoolVar(&diff, "p", false, "")
92	flags.BoolVar(&full, "full", false, "")
93	flags.BoolVar(&json, "json", false, "")
94	flags.StringVar(&versionStr, "version", "", "")
95	flags.StringVar(&tmpl, "t", "", "")
96
97	if err := flags.Parse(args); err != nil {
98		return 1
99	}
100
101	// Check that we got exactly one node
102	args = flags.Args()
103	if l := len(args); l < 1 || l > 2 {
104		c.Ui.Error("This command takes one argument: <job>")
105		c.Ui.Error(commandErrorText(c))
106		return 1
107	}
108
109	if (json || len(tmpl) != 0) && (diff || full) {
110		c.Ui.Error("-json and -t are exclusive with -p and -full")
111		return 1
112	}
113
114	// Get the HTTP client
115	client, err := c.Meta.Client()
116	if err != nil {
117		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
118		return 1
119	}
120
121	jobID := args[0]
122
123	// Check if the job exists
124	jobs, _, err := client.Jobs().PrefixList(jobID)
125	if err != nil {
126		c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err))
127		return 1
128	}
129	if len(jobs) == 0 {
130		c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
131		return 1
132	}
133	if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
134		c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
135		return 1
136	}
137
138	// Prefix lookup matched a single job
139	versions, diffs, _, err := client.Jobs().Versions(jobs[0].ID, diff, nil)
140	if err != nil {
141		c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err))
142		return 1
143	}
144
145	f, err := DataFormat("json", "")
146	if err != nil {
147		c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
148		return 1
149	}
150	c.formatter = f
151
152	if versionStr != "" {
153		version, _, err := parseVersion(versionStr)
154		if err != nil {
155			c.Ui.Error(fmt.Sprintf("Error parsing version value %q: %v", versionStr, err))
156			return 1
157		}
158
159		var job *api.Job
160		var diff *api.JobDiff
161		var nextVersion uint64
162		for i, v := range versions {
163			if *v.Version != version {
164				continue
165			}
166
167			job = v
168			if i+1 <= len(diffs) {
169				diff = diffs[i]
170				nextVersion = *versions[i+1].Version
171			}
172		}
173
174		if json || len(tmpl) > 0 {
175			out, err := Format(json, tmpl, job)
176			if err != nil {
177				c.Ui.Error(err.Error())
178				return 1
179			}
180
181			c.Ui.Output(out)
182			return 0
183		}
184
185		if err := c.formatJobVersion(job, diff, nextVersion, full); err != nil {
186			c.Ui.Error(err.Error())
187			return 1
188		}
189
190	} else {
191		if json || len(tmpl) > 0 {
192			out, err := Format(json, tmpl, versions)
193			if err != nil {
194				c.Ui.Error(err.Error())
195				return 1
196			}
197
198			c.Ui.Output(out)
199			return 0
200		}
201
202		if err := c.formatJobVersions(versions, diffs, full); err != nil {
203			c.Ui.Error(err.Error())
204			return 1
205		}
206	}
207
208	return 0
209}
210
211// parseVersion parses the version flag and returns the index, whether it
212// was set and potentially an error during parsing.
213func parseVersion(input string) (uint64, bool, error) {
214	if input == "" {
215		return 0, false, nil
216	}
217
218	u, err := strconv.ParseUint(input, 10, 64)
219	return u, true, err
220}
221
222func (c *JobHistoryCommand) formatJobVersions(versions []*api.Job, diffs []*api.JobDiff, full bool) error {
223	vLen := len(versions)
224	dLen := len(diffs)
225	if dLen != 0 && vLen != dLen+1 {
226		return fmt.Errorf("Number of job versions %d doesn't match number of diffs %d", vLen, dLen)
227	}
228
229	for i, version := range versions {
230		var diff *api.JobDiff
231		var nextVersion uint64
232		if i+1 <= dLen {
233			diff = diffs[i]
234			nextVersion = *versions[i+1].Version
235		}
236
237		if err := c.formatJobVersion(version, diff, nextVersion, full); err != nil {
238			return err
239		}
240
241		// Insert a blank
242		if i != vLen-1 {
243			c.Ui.Output("")
244		}
245	}
246
247	return nil
248}
249
250func (c *JobHistoryCommand) formatJobVersion(job *api.Job, diff *api.JobDiff, nextVersion uint64, full bool) error {
251	if job == nil {
252		return fmt.Errorf("Error printing job history for non-existing job or job version")
253	}
254
255	basic := []string{
256		fmt.Sprintf("Version|%d", *job.Version),
257		fmt.Sprintf("Stable|%v", *job.Stable),
258		fmt.Sprintf("Submit Date|%v", formatTime(time.Unix(0, *job.SubmitTime))),
259	}
260
261	if diff != nil {
262		//diffStr := fmt.Sprintf("Difference between version %d and %d:", *job.Version, nextVersion)
263		basic = append(basic, fmt.Sprintf("Diff|\n%s", strings.TrimSpace(formatJobDiff(diff, false))))
264	}
265
266	if full {
267		out, err := c.formatter.TransformData(job)
268		if err != nil {
269			return fmt.Errorf("Error formatting the data: %s", err)
270		}
271
272		basic = append(basic, fmt.Sprintf("Full|JSON Job:\n%s", out))
273	}
274
275	columnConf := columnize.DefaultConfig()
276	columnConf.Glue = " = "
277	columnConf.NoTrim = true
278	output := columnize.Format(basic, columnConf)
279
280	c.Ui.Output(c.Colorize().Color(output))
281	return nil
282}
283