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