github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/command/helpers.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "strconv" 10 "time" 11 12 gg "github.com/hashicorp/go-getter" 13 "github.com/hashicorp/nomad/api" 14 "github.com/hashicorp/nomad/jobspec" 15 16 "github.com/ryanuber/columnize" 17 ) 18 19 // formatKV takes a set of strings and formats them into properly 20 // aligned k = v pairs using the columnize library. 21 func formatKV(in []string) string { 22 columnConf := columnize.DefaultConfig() 23 columnConf.Empty = "<none>" 24 columnConf.Glue = " = " 25 return columnize.Format(in, columnConf) 26 } 27 28 // formatList takes a set of strings and formats them into properly 29 // aligned output, replacing any blank fields with a placeholder 30 // for awk-ability. 31 func formatList(in []string) string { 32 columnConf := columnize.DefaultConfig() 33 columnConf.Empty = "<none>" 34 return columnize.Format(in, columnConf) 35 } 36 37 // formatListWithSpaces takes a set of strings and formats them into properly 38 // aligned output. It should be used sparingly since it doesn't replace empty 39 // values and hence not awk/sed friendly 40 func formatListWithSpaces(in []string) string { 41 columnConf := columnize.DefaultConfig() 42 return columnize.Format(in, columnConf) 43 } 44 45 // Limits the length of the string. 46 func limit(s string, length int) string { 47 if len(s) < length { 48 return s 49 } 50 51 return s[:length] 52 } 53 54 // formatTime formats the time to string based on RFC822 55 func formatTime(t time.Time) string { 56 if t.Unix() < 1 { 57 // It's more confusing to display the UNIX epoch or a zero value than nothing 58 return "" 59 } 60 return t.Format("01/02/06 15:04:05 MST") 61 } 62 63 // formatUnixNanoTime is a helper for formatting time for output. 64 func formatUnixNanoTime(nano int64) string { 65 t := time.Unix(0, nano) 66 return formatTime(t) 67 } 68 69 // formatTimeDifference takes two times and determines their duration difference 70 // truncating to a passed unit. 71 // E.g. formatTimeDifference(first=1m22s33ms, second=1m28s55ms, time.Second) -> 6s 72 func formatTimeDifference(first, second time.Time, d time.Duration) string { 73 return second.Truncate(d).Sub(first.Truncate(d)).String() 74 } 75 76 // getLocalNodeID returns the node ID of the local Nomad Client and an error if 77 // it couldn't be determined or the Agent is not running in Client mode. 78 func getLocalNodeID(client *api.Client) (string, error) { 79 info, err := client.Agent().Self() 80 if err != nil { 81 return "", fmt.Errorf("Error querying agent info: %s", err) 82 } 83 clientStats, ok := info.Stats["client"] 84 if !ok { 85 return "", fmt.Errorf("Nomad not running in client mode") 86 } 87 88 nodeID, ok := clientStats["node_id"] 89 if !ok { 90 return "", fmt.Errorf("Failed to determine node ID") 91 } 92 93 return nodeID, nil 94 } 95 96 // evalFailureStatus returns whether the evaluation has failures and a string to 97 // display when presenting users with whether there are failures for the eval 98 func evalFailureStatus(eval *api.Evaluation) (string, bool) { 99 if eval == nil { 100 return "", false 101 } 102 103 hasFailures := len(eval.FailedTGAllocs) != 0 104 text := strconv.FormatBool(hasFailures) 105 if eval.Status == "blocked" { 106 text = "N/A - In Progress" 107 } 108 109 return text, hasFailures 110 } 111 112 // LineLimitReader wraps another reader and provides `tail -n` like behavior. 113 // LineLimitReader buffers up to the searchLimit and returns `-n` number of 114 // lines. After those lines have been returned, LineLimitReader streams the 115 // underlying ReadCloser 116 type LineLimitReader struct { 117 io.ReadCloser 118 lines int 119 searchLimit int 120 121 timeLimit time.Duration 122 lastRead time.Time 123 124 buffer *bytes.Buffer 125 bufFiled bool 126 foundLines bool 127 } 128 129 // NewLineLimitReader takes the ReadCloser to wrap, the number of lines to find 130 // searching backwards in the first searchLimit bytes. timeLimit can optionally 131 // be specified by passing a non-zero duration. When set, the search for the 132 // last n lines is aborted if no data has been read in the duration. This 133 // can be used to flush what is had if no extra data is being received. When 134 // used, the underlying reader must not block forever and must periodically 135 // unblock even when no data has been read. 136 func NewLineLimitReader(r io.ReadCloser, lines, searchLimit int, timeLimit time.Duration) *LineLimitReader { 137 return &LineLimitReader{ 138 ReadCloser: r, 139 searchLimit: searchLimit, 140 timeLimit: timeLimit, 141 lines: lines, 142 buffer: bytes.NewBuffer(make([]byte, 0, searchLimit)), 143 } 144 } 145 146 func (l *LineLimitReader) Read(p []byte) (n int, err error) { 147 // Fill up the buffer so we can find the correct number of lines. 148 if !l.bufFiled { 149 b := make([]byte, len(p)) 150 n, err := l.ReadCloser.Read(b) 151 if n > 0 { 152 if _, err := l.buffer.Write(b[:n]); err != nil { 153 return 0, err 154 } 155 } 156 157 if err != nil { 158 if err != io.EOF { 159 return 0, err 160 } 161 162 l.bufFiled = true 163 goto READ 164 } 165 166 if l.buffer.Len() >= l.searchLimit { 167 l.bufFiled = true 168 goto READ 169 } 170 171 if l.timeLimit.Nanoseconds() > 0 { 172 if l.lastRead.IsZero() { 173 l.lastRead = time.Now() 174 return 0, nil 175 } 176 177 now := time.Now() 178 if n == 0 { 179 // We hit the limit 180 if l.lastRead.Add(l.timeLimit).Before(now) { 181 l.bufFiled = true 182 goto READ 183 } else { 184 return 0, nil 185 } 186 } else { 187 l.lastRead = now 188 } 189 } 190 191 return 0, nil 192 } 193 194 READ: 195 if l.bufFiled && l.buffer.Len() != 0 { 196 b := l.buffer.Bytes() 197 198 // Find the lines 199 if !l.foundLines { 200 found := 0 201 i := len(b) - 1 202 sep := byte('\n') 203 lastIndex := len(b) - 1 204 for ; found < l.lines && i >= 0; i-- { 205 if b[i] == sep { 206 lastIndex = i 207 208 // Skip the first one 209 if i != len(b)-1 { 210 found++ 211 } 212 } 213 } 214 215 // We found them all 216 if found == l.lines { 217 // Clear the buffer until the last index 218 l.buffer.Next(lastIndex + 1) 219 } 220 221 l.foundLines = true 222 } 223 224 // Read from the buffer 225 n := copy(p, l.buffer.Next(len(p))) 226 return n, nil 227 } 228 229 // Just stream from the underlying reader now 230 return l.ReadCloser.Read(p) 231 } 232 233 type JobGetter struct { 234 // The fields below can be overwritten for tests 235 testStdin io.Reader 236 } 237 238 // StructJob returns the Job struct from jobfile. 239 func (j *JobGetter) ApiJob(jpath string) (*api.Job, error) { 240 var jobfile io.Reader 241 switch jpath { 242 case "-": 243 if j.testStdin != nil { 244 jobfile = j.testStdin 245 } else { 246 jobfile = os.Stdin 247 } 248 default: 249 if len(jpath) == 0 { 250 return nil, fmt.Errorf("Error jobfile path has to be specified.") 251 } 252 253 job, err := ioutil.TempFile("", "jobfile") 254 if err != nil { 255 return nil, err 256 } 257 defer os.Remove(job.Name()) 258 259 if err := job.Close(); err != nil { 260 return nil, err 261 } 262 263 // Get the pwd 264 pwd, err := os.Getwd() 265 if err != nil { 266 return nil, err 267 } 268 269 client := &gg.Client{ 270 Src: jpath, 271 Pwd: pwd, 272 Dst: job.Name(), 273 } 274 275 if err := client.Get(); err != nil { 276 return nil, fmt.Errorf("Error getting jobfile from %q: %v", jpath, err) 277 } else { 278 file, err := os.Open(job.Name()) 279 defer file.Close() 280 if err != nil { 281 return nil, fmt.Errorf("Error opening file %q: %v", jpath, err) 282 } 283 jobfile = file 284 } 285 } 286 287 // Parse the JobFile 288 jobStruct, err := jobspec.Parse(jobfile) 289 if err != nil { 290 return nil, fmt.Errorf("Error parsing job file from %s: %v", jpath, err) 291 } 292 293 return jobStruct, nil 294 } 295 296 // COMPAT: Remove in 0.7.0 297 // Nomad 0.6.0 introduces the submit time field so CLI's interacting with 298 // older versions of Nomad would SEGFAULT as reported here: 299 // https://github.com/hashicorp/nomad/issues/2918 300 // getSubmitTime returns a submit time of the job converting to time.Time 301 func getSubmitTime(job *api.Job) time.Time { 302 if job.SubmitTime != nil { 303 return time.Unix(0, *job.SubmitTime) 304 } 305 306 return time.Time{} 307 } 308 309 // COMPAT: Remove in 0.7.0 310 // Nomad 0.6.0 introduces job Versions so CLI's interacting with 311 // older versions of Nomad would SEGFAULT as reported here: 312 // https://github.com/hashicorp/nomad/issues/2918 313 // getVersion returns a version of the job in safely. 314 func getVersion(job *api.Job) uint64 { 315 if job.Version != nil { 316 return *job.Version 317 } 318 319 return 0 320 }