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 }