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  }