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  }