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