github.com/manicqin/nomad@v0.9.5/command/helpers.go (about)

     1  package command
     2  
     3  import (
     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.
    25  const 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.
    29  func 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.
    39  func 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
    48  func formatListWithSpaces(in []string) string {
    49  	columnConf := columnize.DefaultConfig()
    50  	return columnize.Format(in, columnConf)
    51  }
    52  
    53  // Limits the length of the string.
    54  func 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.
    64  func 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.
    74  func wrapAtLength(s string) string {
    75  	return wrapAtLengthWithPadding(s, 0)
    76  }
    77  
    78  // formatTime formats the time to string based on RFC822
    79  func 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.
    89  func 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
    97  func 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.
   103  func 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
   118  func 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.
   225  func 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
   245  func 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
   263  type 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.
   283  func 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  
   293  func (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  
   341  READ:
   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  
   380  type 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.
   386  func (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.
   444  func 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.
   457  func 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.
   466  func 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.
   475  type uiErrorWriter struct {
   476  	ui  cli.Ui
   477  	buf bytes.Buffer
   478  }
   479  
   480  func (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  
   502  func (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  }