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