github.com/quite/nomad@v0.8.6/command/agent/fs_endpoint.go (about)

     1  package agent
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/docker/docker/pkg/ioutils"
    14  	cstructs "github.com/hashicorp/nomad/client/structs"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  	"github.com/ugorji/go/codec"
    17  )
    18  
    19  var (
    20  	allocIDNotPresentErr  = fmt.Errorf("must provide a valid alloc id")
    21  	fileNameNotPresentErr = fmt.Errorf("must provide a file name")
    22  	taskNotPresentErr     = fmt.Errorf("must provide task name")
    23  	logTypeNotPresentErr  = fmt.Errorf("must provide log type (stdout/stderr)")
    24  	clientNotRunning      = fmt.Errorf("node is not running a Nomad Client")
    25  	invalidOrigin         = fmt.Errorf("origin must be start or end")
    26  )
    27  
    28  func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    29  	path := strings.TrimPrefix(req.URL.Path, "/v1/client/fs/")
    30  	switch {
    31  	case strings.HasPrefix(path, "ls/"):
    32  		return s.DirectoryListRequest(resp, req)
    33  	case strings.HasPrefix(path, "stat/"):
    34  		return s.FileStatRequest(resp, req)
    35  	case strings.HasPrefix(path, "readat/"):
    36  		return s.FileReadAtRequest(resp, req)
    37  	case strings.HasPrefix(path, "cat/"):
    38  		return s.FileCatRequest(resp, req)
    39  	case strings.HasPrefix(path, "stream/"):
    40  		return s.Stream(resp, req)
    41  	case strings.HasPrefix(path, "logs/"):
    42  		return s.Logs(resp, req)
    43  	default:
    44  		return nil, CodedError(404, ErrInvalidMethod)
    45  	}
    46  }
    47  
    48  func (s *HTTPServer) DirectoryListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    49  	var allocID, path string
    50  
    51  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/ls/"); allocID == "" {
    52  		return nil, allocIDNotPresentErr
    53  	}
    54  	if path = req.URL.Query().Get("path"); path == "" {
    55  		path = "/"
    56  	}
    57  
    58  	// Create the request
    59  	args := &cstructs.FsListRequest{
    60  		AllocID: allocID,
    61  		Path:    path,
    62  	}
    63  	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)
    64  
    65  	// Make the RPC
    66  	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)
    67  
    68  	var reply cstructs.FsListResponse
    69  	var rpcErr error
    70  	if localClient {
    71  		rpcErr = s.agent.Client().ClientRPC("FileSystem.List", &args, &reply)
    72  	} else if remoteClient {
    73  		rpcErr = s.agent.Client().RPC("FileSystem.List", &args, &reply)
    74  	} else if localServer {
    75  		rpcErr = s.agent.Server().RPC("FileSystem.List", &args, &reply)
    76  	}
    77  
    78  	if rpcErr != nil {
    79  		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) {
    80  			rpcErr = CodedError(404, rpcErr.Error())
    81  		}
    82  
    83  		return nil, rpcErr
    84  	}
    85  
    86  	return reply.Files, nil
    87  }
    88  
    89  func (s *HTTPServer) FileStatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    90  	var allocID, path string
    91  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stat/"); allocID == "" {
    92  		return nil, allocIDNotPresentErr
    93  	}
    94  	if path = req.URL.Query().Get("path"); path == "" {
    95  		return nil, fileNameNotPresentErr
    96  	}
    97  
    98  	// Create the request
    99  	args := &cstructs.FsStatRequest{
   100  		AllocID: allocID,
   101  		Path:    path,
   102  	}
   103  	s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)
   104  
   105  	// Make the RPC
   106  	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)
   107  
   108  	var reply cstructs.FsStatResponse
   109  	var rpcErr error
   110  	if localClient {
   111  		rpcErr = s.agent.Client().ClientRPC("FileSystem.Stat", &args, &reply)
   112  	} else if remoteClient {
   113  		rpcErr = s.agent.Client().RPC("FileSystem.Stat", &args, &reply)
   114  	} else if localServer {
   115  		rpcErr = s.agent.Server().RPC("FileSystem.Stat", &args, &reply)
   116  	}
   117  
   118  	if rpcErr != nil {
   119  		if structs.IsErrNoNodeConn(rpcErr) || structs.IsErrUnknownAllocation(rpcErr) {
   120  			rpcErr = CodedError(404, rpcErr.Error())
   121  		}
   122  
   123  		return nil, rpcErr
   124  	}
   125  
   126  	return reply.Info, nil
   127  }
   128  
   129  func (s *HTTPServer) FileReadAtRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   130  	var allocID, path string
   131  	var offset, limit int64
   132  	var err error
   133  
   134  	q := req.URL.Query()
   135  
   136  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/readat/"); allocID == "" {
   137  		return nil, allocIDNotPresentErr
   138  	}
   139  	if path = q.Get("path"); path == "" {
   140  		return nil, fileNameNotPresentErr
   141  	}
   142  
   143  	if offset, err = strconv.ParseInt(q.Get("offset"), 10, 64); err != nil {
   144  		return nil, fmt.Errorf("error parsing offset: %v", err)
   145  	}
   146  
   147  	// Parse the limit
   148  	if limitStr := q.Get("limit"); limitStr != "" {
   149  		if limit, err = strconv.ParseInt(limitStr, 10, 64); err != nil {
   150  			return nil, fmt.Errorf("error parsing limit: %v", err)
   151  		}
   152  	}
   153  
   154  	// Create the request arguments
   155  	fsReq := &cstructs.FsStreamRequest{
   156  		AllocID:   allocID,
   157  		Path:      path,
   158  		Offset:    offset,
   159  		Origin:    "start",
   160  		Limit:     limit,
   161  		PlainText: true,
   162  	}
   163  	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
   164  
   165  	// Make the request
   166  	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
   167  }
   168  
   169  func (s *HTTPServer) FileCatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   170  	var allocID, path string
   171  
   172  	q := req.URL.Query()
   173  
   174  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/cat/"); allocID == "" {
   175  		return nil, allocIDNotPresentErr
   176  	}
   177  	if path = q.Get("path"); path == "" {
   178  		return nil, fileNameNotPresentErr
   179  	}
   180  
   181  	// Create the request arguments
   182  	fsReq := &cstructs.FsStreamRequest{
   183  		AllocID:   allocID,
   184  		Path:      path,
   185  		Origin:    "start",
   186  		PlainText: true,
   187  	}
   188  	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
   189  
   190  	// Make the request
   191  	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
   192  }
   193  
   194  // Stream streams the content of a file blocking on EOF.
   195  // The parameters are:
   196  // * path: path to file to stream.
   197  // * offset: The offset to start streaming data at, defaults to zero.
   198  // * origin: Either "start" or "end" and defines from where the offset is
   199  //           applied. Defaults to "start".
   200  func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   201  	var allocID, path string
   202  
   203  	q := req.URL.Query()
   204  
   205  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" {
   206  		return nil, allocIDNotPresentErr
   207  	}
   208  
   209  	if path = q.Get("path"); path == "" {
   210  		return nil, fileNameNotPresentErr
   211  	}
   212  
   213  	var offset int64
   214  	offsetString := q.Get("offset")
   215  	if offsetString != "" {
   216  		var err error
   217  		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
   218  			return nil, fmt.Errorf("error parsing offset: %v", err)
   219  		}
   220  	}
   221  
   222  	origin := q.Get("origin")
   223  	switch origin {
   224  	case "start", "end":
   225  	case "":
   226  		origin = "start"
   227  	default:
   228  		return nil, invalidOrigin
   229  	}
   230  
   231  	// Create the request arguments
   232  	fsReq := &cstructs.FsStreamRequest{
   233  		AllocID: allocID,
   234  		Path:    path,
   235  		Origin:  origin,
   236  		Offset:  offset,
   237  		Follow:  true,
   238  	}
   239  	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
   240  
   241  	// Make the request
   242  	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
   243  }
   244  
   245  // Logs streams the content of a log blocking on EOF. The parameters are:
   246  // * task: task name to stream logs for.
   247  // * type: stdout/stderr to stream.
   248  // * follow: A boolean of whether to follow the logs.
   249  // * offset: The offset to start streaming data at, defaults to zero.
   250  // * origin: Either "start" or "end" and defines from where the offset is
   251  //           applied. Defaults to "start".
   252  func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   253  	var allocID, task, logType string
   254  	var plain, follow bool
   255  	var err error
   256  
   257  	q := req.URL.Query()
   258  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" {
   259  		return nil, allocIDNotPresentErr
   260  	}
   261  
   262  	if task = q.Get("task"); task == "" {
   263  		return nil, taskNotPresentErr
   264  	}
   265  
   266  	if followStr := q.Get("follow"); followStr != "" {
   267  		if follow, err = strconv.ParseBool(followStr); err != nil {
   268  			return nil, fmt.Errorf("Failed to parse follow field to boolean: %v", err)
   269  		}
   270  	}
   271  
   272  	if plainStr := q.Get("plain"); plainStr != "" {
   273  		if plain, err = strconv.ParseBool(plainStr); err != nil {
   274  			return nil, fmt.Errorf("Failed to parse plain field to boolean: %v", err)
   275  		}
   276  	}
   277  
   278  	logType = q.Get("type")
   279  	switch logType {
   280  	case "stdout", "stderr":
   281  	default:
   282  		return nil, logTypeNotPresentErr
   283  	}
   284  
   285  	var offset int64
   286  	offsetString := q.Get("offset")
   287  	if offsetString != "" {
   288  		var err error
   289  		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
   290  			return nil, fmt.Errorf("error parsing offset: %v", err)
   291  		}
   292  	}
   293  
   294  	origin := q.Get("origin")
   295  	switch origin {
   296  	case "start", "end":
   297  	case "":
   298  		origin = "start"
   299  	default:
   300  		return nil, invalidOrigin
   301  	}
   302  
   303  	// Create the request arguments
   304  	fsReq := &cstructs.FsLogsRequest{
   305  		AllocID:   allocID,
   306  		Task:      task,
   307  		LogType:   logType,
   308  		Offset:    offset,
   309  		Origin:    origin,
   310  		PlainText: plain,
   311  		Follow:    follow,
   312  	}
   313  	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
   314  
   315  	// Make the request
   316  	return s.fsStreamImpl(resp, req, "FileSystem.Logs", fsReq, fsReq.AllocID)
   317  }
   318  
   319  // fsStreamImpl is used to make a streaming filesystem call that serializes the
   320  // args and then expects a stream of StreamErrWrapper results where the payload
   321  // is copied to the response body.
   322  func (s *HTTPServer) fsStreamImpl(resp http.ResponseWriter,
   323  	req *http.Request, method string, args interface{}, allocID string) (interface{}, error) {
   324  
   325  	// Get the correct handler
   326  	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)
   327  	var handler structs.StreamingRpcHandler
   328  	var handlerErr error
   329  	if localClient {
   330  		handler, handlerErr = s.agent.Client().StreamingRpcHandler(method)
   331  	} else if remoteClient {
   332  		handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method)
   333  	} else if localServer {
   334  		handler, handlerErr = s.agent.Server().StreamingRpcHandler(method)
   335  	}
   336  
   337  	if handlerErr != nil {
   338  		return nil, CodedError(500, handlerErr.Error())
   339  	}
   340  
   341  	// Create a pipe connecting the (possibly remote) handler to the http response
   342  	httpPipe, handlerPipe := net.Pipe()
   343  	decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle)
   344  	encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle)
   345  
   346  	// Create a goroutine that closes the pipe if the connection closes.
   347  	ctx, cancel := context.WithCancel(req.Context())
   348  	go func() {
   349  		<-ctx.Done()
   350  		httpPipe.Close()
   351  	}()
   352  
   353  	// Create an output that gets flushed on every write
   354  	output := ioutils.NewWriteFlusher(resp)
   355  
   356  	// Create a channel that decodes the results
   357  	errCh := make(chan HTTPCodedError)
   358  	go func() {
   359  		defer cancel()
   360  
   361  		// Send the request
   362  		if err := encoder.Encode(args); err != nil {
   363  			errCh <- CodedError(500, err.Error())
   364  			return
   365  		}
   366  
   367  		for {
   368  			select {
   369  			case <-ctx.Done():
   370  				errCh <- nil
   371  				return
   372  			default:
   373  			}
   374  
   375  			var res cstructs.StreamErrWrapper
   376  			if err := decoder.Decode(&res); err != nil {
   377  				errCh <- CodedError(500, err.Error())
   378  				return
   379  			}
   380  			decoder.Reset(httpPipe)
   381  
   382  			if err := res.Error; err != nil {
   383  				if err.Code != nil {
   384  					errCh <- CodedError(int(*err.Code), err.Error())
   385  					return
   386  				}
   387  			}
   388  
   389  			if _, err := io.Copy(output, bytes.NewReader(res.Payload)); err != nil {
   390  				errCh <- CodedError(500, err.Error())
   391  				return
   392  			}
   393  		}
   394  	}()
   395  
   396  	handler(handlerPipe)
   397  	cancel()
   398  	codedErr := <-errCh
   399  
   400  	// Ignore EOF and ErrClosedPipe errors.
   401  	if codedErr != nil &&
   402  		(codedErr == io.EOF ||
   403  			strings.Contains(codedErr.Error(), "closed") ||
   404  			strings.Contains(codedErr.Error(), "EOF")) {
   405  		codedErr = nil
   406  	}
   407  	return nil, codedErr
   408  }