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