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  }