1package command
2
3import (
4	"bufio"
5	"bytes"
6	"fmt"
7	"io"
8	"io/ioutil"
9	"os"
10	"strconv"
11	"strings"
12	"time"
13
14	gg "github.com/hashicorp/go-getter"
15	"github.com/hashicorp/nomad/api"
16	"github.com/hashicorp/nomad/jobspec"
17	"github.com/kr/text"
18	"github.com/mitchellh/cli"
19	"github.com/posener/complete"
20
21	"github.com/ryanuber/columnize"
22)
23
24// maxLineLength is the maximum width of any line.
25const maxLineLength int = 78
26
27// formatKV takes a set of strings and formats them into properly
28// aligned k = v pairs using the columnize library.
29func formatKV(in []string) string {
30	columnConf := columnize.DefaultConfig()
31	columnConf.Empty = "<none>"
32	columnConf.Glue = " = "
33	return columnize.Format(in, columnConf)
34}
35
36// formatList takes a set of strings and formats them into properly
37// aligned output, replacing any blank fields with a placeholder
38// for awk-ability.
39func formatList(in []string) string {
40	columnConf := columnize.DefaultConfig()
41	columnConf.Empty = "<none>"
42	return columnize.Format(in, columnConf)
43}
44
45// formatListWithSpaces takes a set of strings and formats them into properly
46// aligned output. It should be used sparingly since it doesn't replace empty
47// values and hence not awk/sed friendly
48func formatListWithSpaces(in []string) string {
49	columnConf := columnize.DefaultConfig()
50	return columnize.Format(in, columnConf)
51}
52
53// Limits the length of the string.
54func limit(s string, length int) string {
55	if len(s) < length {
56		return s
57	}
58
59	return s[:length]
60}
61
62// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking
63// into account any provided left padding.
64func wrapAtLengthWithPadding(s string, pad int) string {
65	wrapped := text.Wrap(s, maxLineLength-pad)
66	lines := strings.Split(wrapped, "\n")
67	for i, line := range lines {
68		lines[i] = strings.Repeat(" ", pad) + line
69	}
70	return strings.Join(lines, "\n")
71}
72
73// wrapAtLength wraps the given text to maxLineLength.
74func wrapAtLength(s string) string {
75	return wrapAtLengthWithPadding(s, 0)
76}
77
78// formatTime formats the time to string based on RFC822
79func formatTime(t time.Time) string {
80	if t.Unix() < 1 {
81		// It's more confusing to display the UNIX epoch or a zero value than nothing
82		return ""
83	}
84	// Return ISO_8601 time format GH-3806
85	return t.Format("2006-01-02T15:04:05Z07:00")
86}
87
88// formatUnixNanoTime is a helper for formatting time for output.
89func formatUnixNanoTime(nano int64) string {
90	t := time.Unix(0, nano)
91	return formatTime(t)
92}
93
94// formatTimeDifference takes two times and determines their duration difference
95// truncating to a passed unit.
96// E.g. formatTimeDifference(first=1m22s33ms, second=1m28s55ms, time.Second) -> 6s
97func formatTimeDifference(first, second time.Time, d time.Duration) string {
98	return second.Truncate(d).Sub(first.Truncate(d)).String()
99}
100
101// fmtInt formats v into the tail of buf.
102// It returns the index where the output begins.
103func fmtInt(buf []byte, v uint64) int {
104	w := len(buf)
105	for v > 0 {
106		w--
107		buf[w] = byte(v%10) + '0'
108		v /= 10
109	}
110	return w
111}
112
113// prettyTimeDiff prints a human readable time difference.
114// It uses abbreviated forms for each period - s for seconds, m for minutes, h for hours,
115// d for days, mo for months, and y for years. Time difference is rounded to the nearest second,
116// and the top two least granular periods are returned. For example, if the time difference
117// is 10 months, 12 days, 3 hours and 2 seconds, the string "10mo12d" is returned. Zero values return the empty string
118func prettyTimeDiff(first, second time.Time) string {
119	// handle zero values
120	if first.IsZero() || first.UnixNano() == 0 {
121		return ""
122	}
123	// round to the nearest second
124	first = first.Round(time.Second)
125	second = second.Round(time.Second)
126
127	// calculate time difference in seconds
128	var d time.Duration
129	messageSuffix := "ago"
130	if second.Equal(first) || second.After(first) {
131		d = second.Sub(first)
132	} else {
133		d = first.Sub(second)
134		messageSuffix = "from now"
135	}
136
137	u := uint64(d.Seconds())
138
139	var buf [32]byte
140	w := len(buf)
141	secs := u % 60
142
143	// track indexes of various periods
144	var indexes []int
145
146	if secs > 0 {
147		w--
148		buf[w] = 's'
149		// u is now seconds
150		w = fmtInt(buf[:w], secs)
151		indexes = append(indexes, w)
152	}
153	u /= 60
154	// u is now minutes
155	if u > 0 {
156		mins := u % 60
157		if mins > 0 {
158			w--
159			buf[w] = 'm'
160			w = fmtInt(buf[:w], mins)
161			indexes = append(indexes, w)
162		}
163		u /= 60
164		// u is now hours
165		if u > 0 {
166			hrs := u % 24
167			if hrs > 0 {
168				w--
169				buf[w] = 'h'
170				w = fmtInt(buf[:w], hrs)
171				indexes = append(indexes, w)
172			}
173			u /= 24
174		}
175		// u is now days
176		if u > 0 {
177			days := u % 30
178			if days > 0 {
179				w--
180				buf[w] = 'd'
181				w = fmtInt(buf[:w], days)
182				indexes = append(indexes, w)
183			}
184			u /= 30
185		}
186		// u is now months
187		if u > 0 {
188			months := u % 12
189			if months > 0 {
190				w--
191				buf[w] = 'o'
192				w--
193				buf[w] = 'm'
194				w = fmtInt(buf[:w], months)
195				indexes = append(indexes, w)
196			}
197			u /= 12
198		}
199		// u is now years
200		if u > 0 {
201			w--
202			buf[w] = 'y'
203			w = fmtInt(buf[:w], u)
204			indexes = append(indexes, w)
205		}
206	}
207	start := w
208	end := len(buf)
209
210	// truncate to the first two periods
211	num_periods := len(indexes)
212	if num_periods > 2 {
213		end = indexes[num_periods-3]
214	}
215	if start == end { //edge case when time difference is less than a second
216		return "0s " + messageSuffix
217	} else {
218		return string(buf[start:end]) + " " + messageSuffix
219	}
220
221}
222
223// getLocalNodeID returns the node ID of the local Nomad Client and an error if
224// it couldn't be determined or the Agent is not running in Client mode.
225func getLocalNodeID(client *api.Client) (string, error) {
226	info, err := client.Agent().Self()
227	if err != nil {
228		return "", fmt.Errorf("Error querying agent info: %s", err)
229	}
230	clientStats, ok := info.Stats["client"]
231	if !ok {
232		return "", fmt.Errorf("Nomad not running in client mode")
233	}
234
235	nodeID, ok := clientStats["node_id"]
236	if !ok {
237		return "", fmt.Errorf("Failed to determine node ID")
238	}
239
240	return nodeID, nil
241}
242
243// evalFailureStatus returns whether the evaluation has failures and a string to
244// display when presenting users with whether there are failures for the eval
245func evalFailureStatus(eval *api.Evaluation) (string, bool) {
246	if eval == nil {
247		return "", false
248	}
249
250	hasFailures := len(eval.FailedTGAllocs) != 0
251	text := strconv.FormatBool(hasFailures)
252	if eval.Status == "blocked" {
253		text = "N/A - In Progress"
254	}
255
256	return text, hasFailures
257}
258
259// LineLimitReader wraps another reader and provides `tail -n` like behavior.
260// LineLimitReader buffers up to the searchLimit and returns `-n` number of
261// lines. After those lines have been returned, LineLimitReader streams the
262// underlying ReadCloser
263type LineLimitReader struct {
264	io.ReadCloser
265	lines       int
266	searchLimit int
267
268	timeLimit time.Duration
269	lastRead  time.Time
270
271	buffer     *bytes.Buffer
272	bufFiled   bool
273	foundLines bool
274}
275
276// NewLineLimitReader takes the ReadCloser to wrap, the number of lines to find
277// searching backwards in the first searchLimit bytes. timeLimit can optionally
278// be specified by passing a non-zero duration. When set, the search for the
279// last n lines is aborted if no data has been read in the duration. This
280// can be used to flush what is had if no extra data is being received. When
281// used, the underlying reader must not block forever and must periodically
282// unblock even when no data has been read.
283func NewLineLimitReader(r io.ReadCloser, lines, searchLimit int, timeLimit time.Duration) *LineLimitReader {
284	return &LineLimitReader{
285		ReadCloser:  r,
286		searchLimit: searchLimit,
287		timeLimit:   timeLimit,
288		lines:       lines,
289		buffer:      bytes.NewBuffer(make([]byte, 0, searchLimit)),
290	}
291}
292
293func (l *LineLimitReader) Read(p []byte) (n int, err error) {
294	// Fill up the buffer so we can find the correct number of lines.
295	if !l.bufFiled {
296		b := make([]byte, len(p))
297		n, err := l.ReadCloser.Read(b)
298		if n > 0 {
299			if _, err := l.buffer.Write(b[:n]); err != nil {
300				return 0, err
301			}
302		}
303
304		if err != nil {
305			if err != io.EOF {
306				return 0, err
307			}
308
309			l.bufFiled = true
310			goto READ
311		}
312
313		if l.buffer.Len() >= l.searchLimit {
314			l.bufFiled = true
315			goto READ
316		}
317
318		if l.timeLimit.Nanoseconds() > 0 {
319			if l.lastRead.IsZero() {
320				l.lastRead = time.Now()
321				return 0, nil
322			}
323
324			now := time.Now()
325			if n == 0 {
326				// We hit the limit
327				if l.lastRead.Add(l.timeLimit).Before(now) {
328					l.bufFiled = true
329					goto READ
330				} else {
331					return 0, nil
332				}
333			} else {
334				l.lastRead = now
335			}
336		}
337
338		return 0, nil
339	}
340
341READ:
342	if l.bufFiled && l.buffer.Len() != 0 {
343		b := l.buffer.Bytes()
344
345		// Find the lines
346		if !l.foundLines {
347			found := 0
348			i := len(b) - 1
349			sep := byte('\n')
350			lastIndex := len(b) - 1
351			for ; found < l.lines && i >= 0; i-- {
352				if b[i] == sep {
353					lastIndex = i
354
355					// Skip the first one
356					if i != len(b)-1 {
357						found++
358					}
359				}
360			}
361
362			// We found them all
363			if found == l.lines {
364				// Clear the buffer until the last index
365				l.buffer.Next(lastIndex + 1)
366			}
367
368			l.foundLines = true
369		}
370
371		// Read from the buffer
372		n := copy(p, l.buffer.Next(len(p)))
373		return n, nil
374	}
375
376	// Just stream from the underlying reader now
377	return l.ReadCloser.Read(p)
378}
379
380type JobGetter struct {
381	// The fields below can be overwritten for tests
382	testStdin io.Reader
383}
384
385// StructJob returns the Job struct from jobfile.
386func (j *JobGetter) ApiJob(jpath string) (*api.Job, error) {
387	var jobfile io.Reader
388	switch jpath {
389	case "-":
390		if j.testStdin != nil {
391			jobfile = j.testStdin
392		} else {
393			jobfile = os.Stdin
394		}
395	default:
396		if len(jpath) == 0 {
397			return nil, fmt.Errorf("Error jobfile path has to be specified.")
398		}
399
400		job, err := ioutil.TempFile("", "jobfile")
401		if err != nil {
402			return nil, err
403		}
404		defer os.Remove(job.Name())
405
406		if err := job.Close(); err != nil {
407			return nil, err
408		}
409
410		// Get the pwd
411		pwd, err := os.Getwd()
412		if err != nil {
413			return nil, err
414		}
415
416		client := &gg.Client{
417			Src: jpath,
418			Pwd: pwd,
419			Dst: job.Name(),
420		}
421
422		if err := client.Get(); err != nil {
423			return nil, fmt.Errorf("Error getting jobfile from %q: %v", jpath, err)
424		} else {
425			file, err := os.Open(job.Name())
426			if err != nil {
427				return nil, fmt.Errorf("Error opening file %q: %v", jpath, err)
428			}
429			defer file.Close()
430			jobfile = file
431		}
432	}
433
434	// Parse the JobFile
435	jobStruct, err := jobspec.Parse(jobfile)
436	if err != nil {
437		return nil, fmt.Errorf("Error parsing job file from %s: %v", jpath, err)
438	}
439
440	return jobStruct, nil
441}
442
443// mergeAutocompleteFlags is used to join multiple flag completion sets.
444func mergeAutocompleteFlags(flags ...complete.Flags) complete.Flags {
445	merged := make(map[string]complete.Predictor, len(flags))
446	for _, f := range flags {
447		for k, v := range f {
448			merged[k] = v
449		}
450	}
451	return merged
452}
453
454// sanitizeUUIDPrefix is used to sanitize a UUID prefix. The returned result
455// will be a truncated version of the prefix if the prefix would not be
456// queryable.
457func sanitizeUUIDPrefix(prefix string) string {
458	hyphens := strings.Count(prefix, "-")
459	length := len(prefix) - hyphens
460	remainder := length % 2
461	return prefix[:len(prefix)-remainder]
462}
463
464// commandErrorText is used to easily render the same messaging across commads
465// when an error is printed.
466func commandErrorText(cmd NamedCommand) string {
467	return fmt.Sprintf("For additional help try 'nomad %s -help'", cmd.Name())
468}
469
470// uiErrorWriter is a io.Writer that wraps underlying ui.ErrorWriter().
471// ui.ErrorWriter expects full lines as inputs and it emits its own line breaks.
472//
473// uiErrorWriter scans input for individual lines to pass to ui.ErrorWriter. If data
474// doesn't contain a new line, it buffers result until next new line or writer is closed.
475type uiErrorWriter struct {
476	ui  cli.Ui
477	buf bytes.Buffer
478}
479
480func (w *uiErrorWriter) Write(data []byte) (int, error) {
481	read := 0
482	for len(data) != 0 {
483		a, token, err := bufio.ScanLines(data, false)
484		if err != nil {
485			return read, err
486		}
487
488		if a == 0 {
489			r, err := w.buf.Write(data)
490			return read + r, err
491		}
492
493		w.ui.Error(w.buf.String() + string(token))
494		data = data[a:]
495		w.buf.Reset()
496		read += a
497	}
498
499	return read, nil
500}
501
502func (w *uiErrorWriter) Close() error {
503	// emit what's remaining
504	if w.buf.Len() != 0 {
505		w.ui.Error(w.buf.String())
506		w.buf.Reset()
507	}
508	return nil
509}
510