github.com/nir0s/nomad@v0.8.7-rc1/command/helpers.go (about)

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