github.com/manicqin/nomad@v0.9.5/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  = 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.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  // * follow: A boolean of whether to follow the file, defaults to true.
   198  // * offset: The offset to start streaming data at, defaults to zero.
   199  // * origin: Either "start" or "end" and defines from where the offset is
   200  //           applied. Defaults to "start".
   201  func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   202  	var allocID, path string
   203  	var err error
   204  
   205  	q := req.URL.Query()
   206  
   207  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" {
   208  		return nil, allocIDNotPresentErr
   209  	}
   210  
   211  	if path = q.Get("path"); path == "" {
   212  		return nil, fileNameNotPresentErr
   213  	}
   214  
   215  	follow := true
   216  	if followStr := q.Get("follow"); followStr != "" {
   217  		if follow, err = strconv.ParseBool(followStr); err != nil {
   218  			return nil, fmt.Errorf("failed to parse follow field to boolean: %v", err)
   219  		}
   220  	}
   221  
   222  	var offset int64
   223  	offsetString := q.Get("offset")
   224  	if offsetString != "" {
   225  		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
   226  			return nil, fmt.Errorf("error parsing offset: %v", err)
   227  		}
   228  	}
   229  
   230  	origin := q.Get("origin")
   231  	switch origin {
   232  	case "start", "end":
   233  	case "":
   234  		origin = "start"
   235  	default:
   236  		return nil, invalidOrigin
   237  	}
   238  
   239  	// Create the request arguments
   240  	fsReq := &cstructs.FsStreamRequest{
   241  		AllocID: allocID,
   242  		Path:    path,
   243  		Origin:  origin,
   244  		Offset:  offset,
   245  		Follow:  follow,
   246  	}
   247  	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
   248  
   249  	// Make the request
   250  	return s.fsStreamImpl(resp, req, "FileSystem.Stream", fsReq, fsReq.AllocID)
   251  }
   252  
   253  // Logs streams the content of a log blocking on EOF. The parameters are:
   254  // * task: task name to stream logs for.
   255  // * type: stdout/stderr to stream.
   256  // * follow: A boolean of whether to follow the logs.
   257  // * offset: The offset to start streaming data at, defaults to zero.
   258  // * origin: Either "start" or "end" and defines from where the offset is
   259  //           applied. Defaults to "start".
   260  func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   261  	var allocID, task, logType string
   262  	var plain, follow bool
   263  	var err error
   264  
   265  	q := req.URL.Query()
   266  	if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" {
   267  		return nil, allocIDNotPresentErr
   268  	}
   269  
   270  	if task = q.Get("task"); task == "" {
   271  		return nil, taskNotPresentErr
   272  	}
   273  
   274  	if followStr := q.Get("follow"); followStr != "" {
   275  		if follow, err = strconv.ParseBool(followStr); err != nil {
   276  			return nil, CodedError(400, fmt.Sprintf("failed to parse follow field to boolean: %v", err))
   277  		}
   278  	}
   279  
   280  	if plainStr := q.Get("plain"); plainStr != "" {
   281  		if plain, err = strconv.ParseBool(plainStr); err != nil {
   282  			return nil, CodedError(400, fmt.Sprintf("failed to parse plain field to boolean: %v", err))
   283  		}
   284  	}
   285  
   286  	logType = q.Get("type")
   287  	switch logType {
   288  	case "stdout", "stderr":
   289  	default:
   290  		return nil, logTypeNotPresentErr
   291  	}
   292  
   293  	var offset int64
   294  	offsetString := q.Get("offset")
   295  	if offsetString != "" {
   296  		var err error
   297  		if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
   298  			return nil, CodedError(400, fmt.Sprintf("error parsing offset: %v", err))
   299  		}
   300  	}
   301  
   302  	origin := q.Get("origin")
   303  	switch origin {
   304  	case "start", "end":
   305  	case "":
   306  		origin = "start"
   307  	default:
   308  		return nil, invalidOrigin
   309  	}
   310  
   311  	// Create the request arguments
   312  	fsReq := &cstructs.FsLogsRequest{
   313  		AllocID:   allocID,
   314  		Task:      task,
   315  		LogType:   logType,
   316  		Offset:    offset,
   317  		Origin:    origin,
   318  		PlainText: plain,
   319  		Follow:    follow,
   320  	}
   321  	s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
   322  
   323  	// Make the request
   324  	return s.fsStreamImpl(resp, req, "FileSystem.Logs", fsReq, fsReq.AllocID)
   325  }
   326  
   327  // fsStreamImpl is used to make a streaming filesystem call that serializes the
   328  // args and then expects a stream of StreamErrWrapper results where the payload
   329  // is copied to the response body.
   330  func (s *HTTPServer) fsStreamImpl(resp http.ResponseWriter,
   331  	req *http.Request, method string, args interface{}, allocID string) (interface{}, error) {
   332  
   333  	// Get the correct handler
   334  	localClient, remoteClient, localServer := s.rpcHandlerForAlloc(allocID)
   335  	var handler structs.StreamingRpcHandler
   336  	var handlerErr error
   337  	if localClient {
   338  		handler, handlerErr = s.agent.Client().StreamingRpcHandler(method)
   339  	} else if remoteClient {
   340  		handler, handlerErr = s.agent.Client().RemoteStreamingRpcHandler(method)
   341  	} else if localServer {
   342  		handler, handlerErr = s.agent.Server().StreamingRpcHandler(method)
   343  	}
   344  
   345  	if handlerErr != nil {
   346  		return nil, CodedError(500, handlerErr.Error())
   347  	}
   348  
   349  	// Create a pipe connecting the (possibly remote) handler to the http response
   350  	httpPipe, handlerPipe := net.Pipe()
   351  	decoder := codec.NewDecoder(httpPipe, structs.MsgpackHandle)
   352  	encoder := codec.NewEncoder(httpPipe, structs.MsgpackHandle)
   353  
   354  	// Create a goroutine that closes the pipe if the connection closes.
   355  	ctx, cancel := context.WithCancel(req.Context())
   356  	go func() {
   357  		<-ctx.Done()
   358  		httpPipe.Close()
   359  	}()
   360  
   361  	// Create an output that gets flushed on every write
   362  	output := ioutils.NewWriteFlusher(resp)
   363  
   364  	// Create a channel that decodes the results
   365  	errCh := make(chan HTTPCodedError)
   366  	go func() {
   367  		defer cancel()
   368  
   369  		// Send the request
   370  		if err := encoder.Encode(args); err != nil {
   371  			errCh <- CodedError(500, err.Error())
   372  			return
   373  		}
   374  
   375  		for {
   376  			select {
   377  			case <-ctx.Done():
   378  				errCh <- nil
   379  				return
   380  			default:
   381  			}
   382  
   383  			var res cstructs.StreamErrWrapper
   384  			if err := decoder.Decode(&res); err != nil {
   385  				errCh <- CodedError(500, err.Error())
   386  				return
   387  			}
   388  			decoder.Reset(httpPipe)
   389  
   390  			if err := res.Error; err != nil {
   391  				code := 500
   392  				if err.Code != nil {
   393  					code = int(*err.Code)
   394  				}
   395  
   396  				errCh <- CodedError(code, err.Error())
   397  				return
   398  			}
   399  
   400  			if _, err := io.Copy(output, bytes.NewReader(res.Payload)); err != nil {
   401  				errCh <- CodedError(500, err.Error())
   402  				return
   403  			}
   404  		}
   405  	}()
   406  
   407  	handler(handlerPipe)
   408  	cancel()
   409  	codedErr := <-errCh
   410  
   411  	// Ignore EOF and ErrClosedPipe errors.
   412  	if codedErr != nil &&
   413  		(codedErr == io.EOF ||
   414  			strings.Contains(codedErr.Error(), "closed") ||
   415  			strings.Contains(codedErr.Error(), "EOF")) {
   416  		codedErr = nil
   417  	}
   418  	return nil, codedErr
   419  }