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