github.com/quite/nomad@v0.8.6/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 }