github.com/mattyr/nomad@v0.3.3-0.20160919021406-3485a065154a/api/fs.go (about) 1 package api 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "strconv" 8 "sync" 9 "time" 10 ) 11 12 const ( 13 // OriginStart and OriginEnd are the available parameters for the origin 14 // argument when streaming a file. They respectively offset from the start 15 // and end of a file. 16 OriginStart = "start" 17 OriginEnd = "end" 18 ) 19 20 // AllocFileInfo holds information about a file inside the AllocDir 21 type AllocFileInfo struct { 22 Name string 23 IsDir bool 24 Size int64 25 FileMode string 26 ModTime time.Time 27 } 28 29 // StreamFrame is used to frame data of a file when streaming 30 type StreamFrame struct { 31 Offset int64 `json:",omitempty"` 32 Data []byte `json:",omitempty"` 33 File string `json:",omitempty"` 34 FileEvent string `json:",omitempty"` 35 } 36 37 // IsHeartbeat returns if the frame is a heartbeat frame 38 func (s *StreamFrame) IsHeartbeat() bool { 39 return len(s.Data) == 0 && s.FileEvent == "" && s.File == "" && s.Offset == 0 40 } 41 42 // AllocFS is used to introspect an allocation directory on a Nomad client 43 type AllocFS struct { 44 client *Client 45 } 46 47 // AllocFS returns an handle to the AllocFS endpoints 48 func (c *Client) AllocFS() *AllocFS { 49 return &AllocFS{client: c} 50 } 51 52 // getNodeClient returns a Client that will dial the node. If the QueryOptions 53 // is set, the function will ensure that it is initalized and that the Params 54 // field is valid. 55 func (a *AllocFS) getNodeClient(nodeHTTPAddr, allocID string, q **QueryOptions) (*Client, error) { 56 if nodeHTTPAddr == "" { 57 return nil, fmt.Errorf("http addr of the node where alloc %q is running is not advertised", allocID) 58 } 59 60 // Get an API client for the node 61 nodeClientConfig := &Config{ 62 Address: fmt.Sprintf("http://%s", nodeHTTPAddr), 63 Region: a.client.config.Region, 64 } 65 nodeClient, err := NewClient(nodeClientConfig) 66 if err != nil { 67 return nil, err 68 } 69 70 // Set the query params 71 if q == nil { 72 return nodeClient, nil 73 } 74 75 if *q == nil { 76 *q = &QueryOptions{} 77 } 78 if actQ := *q; actQ.Params == nil { 79 actQ.Params = make(map[string]string) 80 } 81 return nodeClient, nil 82 } 83 84 // List is used to list the files at a given path of an allocation directory 85 func (a *AllocFS) List(alloc *Allocation, path string, q *QueryOptions) ([]*AllocFileInfo, *QueryMeta, error) { 86 node, _, err := a.client.Nodes().Info(alloc.NodeID, &QueryOptions{}) 87 if err != nil { 88 return nil, nil, err 89 } 90 nodeClient, err := a.getNodeClient(node.HTTPAddr, alloc.ID, &q) 91 if err != nil { 92 return nil, nil, err 93 } 94 q.Params["path"] = path 95 96 var resp []*AllocFileInfo 97 qm, err := nodeClient.query(fmt.Sprintf("/v1/client/fs/ls/%s", alloc.ID), &resp, q) 98 if err != nil { 99 return nil, nil, err 100 } 101 102 return resp, qm, nil 103 } 104 105 // Stat is used to stat a file at a given path of an allocation directory 106 func (a *AllocFS) Stat(alloc *Allocation, path string, q *QueryOptions) (*AllocFileInfo, *QueryMeta, error) { 107 node, _, err := a.client.Nodes().Info(alloc.NodeID, &QueryOptions{}) 108 if err != nil { 109 return nil, nil, err 110 } 111 nodeClient, err := a.getNodeClient(node.HTTPAddr, alloc.ID, &q) 112 if err != nil { 113 return nil, nil, err 114 } 115 q.Params["path"] = path 116 117 var resp AllocFileInfo 118 qm, err := nodeClient.query(fmt.Sprintf("/v1/client/fs/stat/%s", alloc.ID), &resp, q) 119 if err != nil { 120 return nil, nil, err 121 } 122 return &resp, qm, nil 123 } 124 125 // ReadAt is used to read bytes at a given offset until limit at the given path 126 // in an allocation directory. If limit is <= 0, there is no limit. 127 func (a *AllocFS) ReadAt(alloc *Allocation, path string, offset int64, limit int64, q *QueryOptions) (io.ReadCloser, error) { 128 node, _, err := a.client.Nodes().Info(alloc.NodeID, &QueryOptions{}) 129 if err != nil { 130 return nil, err 131 } 132 133 nodeClient, err := a.getNodeClient(node.HTTPAddr, alloc.ID, &q) 134 if err != nil { 135 return nil, err 136 } 137 q.Params["path"] = path 138 q.Params["offset"] = strconv.FormatInt(offset, 10) 139 q.Params["limit"] = strconv.FormatInt(limit, 10) 140 141 r, err := nodeClient.rawQuery(fmt.Sprintf("/v1/client/fs/readat/%s", alloc.ID), q) 142 if err != nil { 143 return nil, err 144 } 145 return r, nil 146 } 147 148 // Cat is used to read contents of a file at the given path in an allocation 149 // directory 150 func (a *AllocFS) Cat(alloc *Allocation, path string, q *QueryOptions) (io.ReadCloser, error) { 151 node, _, err := a.client.Nodes().Info(alloc.NodeID, &QueryOptions{}) 152 if err != nil { 153 return nil, err 154 } 155 156 nodeClient, err := a.getNodeClient(node.HTTPAddr, alloc.ID, &q) 157 if err != nil { 158 return nil, err 159 } 160 q.Params["path"] = path 161 162 r, err := nodeClient.rawQuery(fmt.Sprintf("/v1/client/fs/cat/%s", alloc.ID), q) 163 if err != nil { 164 return nil, err 165 } 166 return r, nil 167 } 168 169 // Stream streams the content of a file blocking on EOF. 170 // The parameters are: 171 // * path: path to file to stream. 172 // * offset: The offset to start streaming data at. 173 // * origin: Either "start" or "end" and defines from where the offset is applied. 174 // * cancel: A channel that when closed, streaming will end. 175 // 176 // The return value is a channel that will emit StreamFrames as they are read. 177 func (a *AllocFS) Stream(alloc *Allocation, path, origin string, offset int64, 178 cancel <-chan struct{}, q *QueryOptions) (<-chan *StreamFrame, error) { 179 180 node, _, err := a.client.Nodes().Info(alloc.NodeID, q) 181 if err != nil { 182 return nil, err 183 } 184 185 nodeClient, err := a.getNodeClient(node.HTTPAddr, alloc.ID, &q) 186 if err != nil { 187 return nil, err 188 } 189 q.Params["path"] = path 190 q.Params["offset"] = strconv.FormatInt(offset, 10) 191 q.Params["origin"] = origin 192 193 r, err := nodeClient.rawQuery(fmt.Sprintf("/v1/client/fs/stream/%s", alloc.ID), q) 194 if err != nil { 195 return nil, err 196 } 197 198 // Create the output channel 199 frames := make(chan *StreamFrame, 10) 200 201 go func() { 202 // Close the body 203 defer r.Close() 204 205 // Create a decoder 206 dec := json.NewDecoder(r) 207 208 for { 209 // Check if we have been cancelled 210 select { 211 case <-cancel: 212 return 213 default: 214 } 215 216 // Decode the next frame 217 var frame StreamFrame 218 if err := dec.Decode(&frame); err != nil { 219 close(frames) 220 return 221 } 222 223 // Discard heartbeat frames 224 if frame.IsHeartbeat() { 225 continue 226 } 227 228 frames <- &frame 229 } 230 }() 231 232 return frames, nil 233 } 234 235 // Logs streams the content of a tasks logs blocking on EOF. 236 // The parameters are: 237 // * allocation: the allocation to stream from. 238 // * follow: Whether the logs should be followed. 239 // * task: the tasks name to stream logs for. 240 // * logType: Either "stdout" or "stderr" 241 // * origin: Either "start" or "end" and defines from where the offset is applied. 242 // * offset: The offset to start streaming data at. 243 // * cancel: A channel that when closed, streaming will end. 244 // 245 // The return value is a channel that will emit StreamFrames as they are read. 246 func (a *AllocFS) Logs(alloc *Allocation, follow bool, task, logType, origin string, 247 offset int64, cancel <-chan struct{}, q *QueryOptions) (<-chan *StreamFrame, error) { 248 249 node, _, err := a.client.Nodes().Info(alloc.NodeID, q) 250 if err != nil { 251 return nil, err 252 } 253 254 nodeClient, err := a.getNodeClient(node.HTTPAddr, alloc.ID, &q) 255 if err != nil { 256 return nil, err 257 } 258 q.Params["follow"] = strconv.FormatBool(follow) 259 q.Params["task"] = task 260 q.Params["type"] = logType 261 q.Params["origin"] = origin 262 q.Params["offset"] = strconv.FormatInt(offset, 10) 263 264 r, err := nodeClient.rawQuery(fmt.Sprintf("/v1/client/fs/logs/%s", alloc.ID), q) 265 if err != nil { 266 return nil, err 267 } 268 269 // Create the output channel 270 frames := make(chan *StreamFrame, 10) 271 272 go func() { 273 // Close the body 274 defer r.Close() 275 276 // Create a decoder 277 dec := json.NewDecoder(r) 278 279 for { 280 // Check if we have been cancelled 281 select { 282 case <-cancel: 283 return 284 default: 285 } 286 287 // Decode the next frame 288 var frame StreamFrame 289 if err := dec.Decode(&frame); err != nil { 290 close(frames) 291 return 292 } 293 294 // Discard heartbeat frames 295 if frame.IsHeartbeat() { 296 continue 297 } 298 299 frames <- &frame 300 } 301 }() 302 303 return frames, nil 304 } 305 306 // FrameReader is used to convert a stream of frames into a read closer. 307 type FrameReader struct { 308 frames <-chan *StreamFrame 309 cancelCh chan struct{} 310 311 closedLock sync.Mutex 312 closed bool 313 314 unblockTime time.Duration 315 316 frame *StreamFrame 317 frameOffset int 318 319 byteOffset int 320 } 321 322 // NewFrameReader takes a channel of frames and returns a FrameReader which 323 // implements io.ReadCloser 324 func NewFrameReader(frames <-chan *StreamFrame, cancelCh chan struct{}) *FrameReader { 325 return &FrameReader{ 326 frames: frames, 327 cancelCh: cancelCh, 328 } 329 } 330 331 // SetUnblockTime sets the time to unblock and return zero bytes read. If the 332 // duration is unset or is zero or less, the read will block til data is read. 333 func (f *FrameReader) SetUnblockTime(d time.Duration) { 334 f.unblockTime = d 335 } 336 337 // Offset returns the offset into the stream. 338 func (f *FrameReader) Offset() int { 339 return f.byteOffset 340 } 341 342 // Read reads the data of the incoming frames into the bytes buffer. Returns EOF 343 // when there are no more frames. 344 func (f *FrameReader) Read(p []byte) (n int, err error) { 345 f.closedLock.Lock() 346 closed := f.closed 347 f.closedLock.Unlock() 348 if closed { 349 return 0, io.EOF 350 } 351 352 if f.frame == nil { 353 var unblock <-chan time.Time 354 if f.unblockTime.Nanoseconds() > 0 { 355 unblock = time.After(f.unblockTime) 356 } 357 358 select { 359 case frame, ok := <-f.frames: 360 if !ok { 361 return 0, io.EOF 362 } 363 f.frame = frame 364 365 // Store the total offset into the file 366 f.byteOffset = int(f.frame.Offset) 367 case <-unblock: 368 return 0, nil 369 case <-f.cancelCh: 370 return 0, io.EOF 371 } 372 } 373 374 // Copy the data out of the frame and update our offset 375 n = copy(p, f.frame.Data[f.frameOffset:]) 376 f.frameOffset += n 377 378 // Clear the frame and its offset once we have read everything 379 if len(f.frame.Data) == f.frameOffset { 380 f.frame = nil 381 f.frameOffset = 0 382 } 383 384 return n, nil 385 } 386 387 // Close cancels the stream of frames 388 func (f *FrameReader) Close() error { 389 f.closedLock.Lock() 390 defer f.closedLock.Unlock() 391 if f.closed { 392 return nil 393 } 394 395 close(f.cancelCh) 396 f.closed = true 397 return nil 398 }