open-nomad/command/agent/fs_endpoint.go

572 lines
14 KiB
Go
Raw Normal View History

package agent
2017-09-11 22:40:27 +00:00
//go:generate codecgen -d 101 -o fs_endpoint.generated.go fs_endpoint.go
2016-12-10 01:33:10 +00:00
import (
"bytes"
2018-01-21 01:19:55 +00:00
"context"
"fmt"
2016-01-14 21:35:42 +00:00
"io"
2018-01-21 01:19:55 +00:00
"net"
"net/http"
2016-01-13 06:06:42 +00:00
"strconv"
"strings"
2016-07-06 00:08:58 +00:00
2016-07-06 03:48:25 +00:00
"github.com/docker/docker/pkg/ioutils"
2018-02-01 01:35:21 +00:00
"github.com/hashicorp/nomad/acl"
2018-01-21 01:19:55 +00:00
cstructs "github.com/hashicorp/nomad/client/structs"
"github.com/hashicorp/nomad/nomad/structs"
2016-07-06 00:08:58 +00:00
"github.com/ugorji/go/codec"
)
2016-01-13 19:49:39 +00:00
var (
allocIDNotPresentErr = fmt.Errorf("must provide a valid alloc id")
fileNameNotPresentErr = fmt.Errorf("must provide a file name")
2016-07-18 16:48:29 +00:00
taskNotPresentErr = fmt.Errorf("must provide task name")
logTypeNotPresentErr = fmt.Errorf("must provide log type (stdout/stderr)")
clientNotRunning = fmt.Errorf("node is not running a Nomad Client")
2016-07-06 00:08:58 +00:00
invalidOrigin = fmt.Errorf("origin must be start or end")
)
func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var secret string
var namespace string
2018-02-01 01:35:21 +00:00
s.parseToken(req, &secret)
parseNamespace(req, &namespace)
2018-02-01 01:35:21 +00:00
var aclObj *acl.ACL
if s.agent.client != nil {
var err error
aclObj, err = s.agent.Client().ResolveToken(secret)
if err != nil {
return nil, err
}
}
path := strings.TrimPrefix(req.URL.Path, "/v1/client/fs/")
switch {
case strings.HasPrefix(path, "ls/"):
return s.DirectoryListRequest(resp, req)
case strings.HasPrefix(path, "stat/"):
return s.FileStatRequest(resp, req)
case strings.HasPrefix(path, "readat/"):
2018-02-01 01:35:21 +00:00
if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
return nil, structs.ErrPermissionDenied
}
return s.FileReadAtRequest(resp, req)
2016-03-28 18:06:22 +00:00
case strings.HasPrefix(path, "cat/"):
2018-02-01 01:35:21 +00:00
if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
return nil, structs.ErrPermissionDenied
}
return s.FileCatRequest(resp, req)
2018-01-21 01:19:55 +00:00
//case strings.HasPrefix(path, "stream/"):
//if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
//return nil, structs.ErrPermissionDenied
//}
//return s.Stream(resp, req)
2016-07-18 16:48:29 +00:00
case strings.HasPrefix(path, "logs/"):
return s.Logs(resp, req)
default:
return nil, CodedError(404, ErrInvalidMethod)
}
}
func (s *HTTPServer) DirectoryListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
2016-01-13 06:25:12 +00:00
var allocID, path string
if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/ls/"); allocID == "" {
2016-01-13 19:49:39 +00:00
return nil, allocIDNotPresentErr
}
2016-01-13 06:25:12 +00:00
if path = req.URL.Query().Get("path"); path == "" {
path = "/"
}
2016-01-14 21:35:42 +00:00
fs, err := s.agent.client.GetAllocFS(allocID)
if err != nil {
return nil, err
}
return fs.List(path)
}
func (s *HTTPServer) FileStatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
2016-01-13 06:25:12 +00:00
var allocID, path string
if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stat/"); allocID == "" {
2016-01-13 19:49:39 +00:00
return nil, allocIDNotPresentErr
2016-01-12 23:25:51 +00:00
}
if path = req.URL.Query().Get("path"); path == "" {
2016-01-13 19:49:39 +00:00
return nil, fileNameNotPresentErr
2016-01-12 23:25:51 +00:00
}
2016-01-14 21:35:42 +00:00
fs, err := s.agent.client.GetAllocFS(allocID)
if err != nil {
return nil, err
}
return fs.Stat(path)
}
func (s *HTTPServer) FileReadAtRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
2016-01-13 06:25:12 +00:00
var allocID, path string
var offset, limit int64
var err error
2016-01-13 06:06:42 +00:00
2016-01-13 06:25:12 +00:00
q := req.URL.Query()
if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/readat/"); allocID == "" {
2016-01-13 19:49:39 +00:00
return nil, allocIDNotPresentErr
2016-01-13 06:06:42 +00:00
}
2016-01-13 06:25:12 +00:00
if path = q.Get("path"); path == "" {
2016-01-13 19:49:39 +00:00
return nil, fileNameNotPresentErr
2016-01-13 06:06:42 +00:00
}
2016-01-13 06:25:12 +00:00
if offset, err = strconv.ParseInt(q.Get("offset"), 10, 64); err != nil {
return nil, fmt.Errorf("error parsing offset: %v", err)
2016-01-13 06:06:42 +00:00
}
// Parse the limit
if limitStr := q.Get("limit"); limitStr != "" {
if limit, err = strconv.ParseInt(limitStr, 10, 64); err != nil {
return nil, fmt.Errorf("error parsing limit: %v", err)
}
2016-01-13 06:06:42 +00:00
}
2016-01-14 21:35:42 +00:00
fs, err := s.agent.client.GetAllocFS(allocID)
if err != nil {
return nil, err
}
2016-07-12 23:01:33 +00:00
rc, err := fs.ReadAt(path, offset)
if limit > 0 {
2016-07-12 23:01:33 +00:00
rc = &ReadCloserWrapper{
Reader: io.LimitReader(rc, limit),
Closer: rc,
}
}
2016-01-14 21:35:42 +00:00
if err != nil {
2016-01-13 06:06:42 +00:00
return nil, err
}
io.Copy(resp, rc)
2016-07-12 23:01:33 +00:00
return nil, rc.Close()
}
// ReadCloserWrapper wraps a LimitReader so that a file is closed once it has been
// read
type ReadCloserWrapper struct {
io.Reader
io.Closer
}
2016-03-28 18:06:22 +00:00
func (s *HTTPServer) FileCatRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var allocID, path string
var err error
q := req.URL.Query()
if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/cat/"); allocID == "" {
return nil, allocIDNotPresentErr
}
if path = q.Get("path"); path == "" {
return nil, fileNameNotPresentErr
}
fs, err := s.agent.client.GetAllocFS(allocID)
if err != nil {
return nil, err
}
fileInfo, err := fs.Stat(path)
if err != nil {
return nil, err
}
if fileInfo.IsDir {
2016-04-19 01:53:05 +00:00
return nil, fmt.Errorf("file %q is a directory", path)
2016-03-28 18:06:22 +00:00
}
2016-07-06 00:08:58 +00:00
r, err := fs.ReadAt(path, int64(0))
2016-06-16 21:32:07 +00:00
if err != nil {
return nil, err
}
2016-03-28 18:06:22 +00:00
io.Copy(resp, r)
2016-07-12 23:01:33 +00:00
return nil, r.Close()
2016-03-28 18:06:22 +00:00
}
2016-07-06 00:08:58 +00:00
2018-01-21 01:19:55 +00:00
/*
2016-07-07 15:15:22 +00:00
// Stream streams the content of a file blocking on EOF.
// The parameters are:
// * path: path to file to stream.
// * offset: The offset to start streaming data at, defaults to zero.
// * origin: Either "start" or "end" and defines from where the offset is
// applied. Defaults to "start".
2016-07-06 00:08:58 +00:00
func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var allocID, path string
var err error
q := req.URL.Query()
if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/stream/"); allocID == "" {
return nil, allocIDNotPresentErr
}
if path = q.Get("path"); path == "" {
return nil, fileNameNotPresentErr
}
var offset int64
offsetString := q.Get("offset")
if offsetString != "" {
var err error
if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
return nil, fmt.Errorf("error parsing offset: %v", err)
}
}
origin := q.Get("origin")
switch origin {
case "start", "end":
case "":
origin = "start"
default:
return nil, invalidOrigin
}
fs, err := s.agent.client.GetAllocFS(allocID)
if err != nil {
return nil, err
}
fileInfo, err := fs.Stat(path)
if err != nil {
return nil, err
}
if fileInfo.IsDir {
return nil, fmt.Errorf("file %q is a directory", path)
}
// If offsetting from the end subtract from the size
if origin == "end" {
offset = fileInfo.Size - offset
}
2016-07-06 03:48:25 +00:00
// Create an output that gets flushed on every write
output := ioutils.NewWriteFlusher(resp)
2016-07-18 16:48:29 +00:00
// Create the framer
2018-01-21 01:19:55 +00:00
framer := sframer.NewStreamFramer(output, false, streamHeartbeatRate, streamBatchWindow, streamFrameSize)
2016-07-18 16:48:29 +00:00
framer.Run()
defer framer.Destroy()
2016-07-22 22:07:11 +00:00
err = s.stream(offset, path, fs, framer, nil)
2016-07-22 22:01:56 +00:00
if err != nil && err != syscall.EPIPE {
return nil, err
}
return nil, nil
2016-07-07 15:15:22 +00:00
}
// parseFramerErr takes an error and returns an error. The error will
// potentially change if it was caused by the connection being closed.
func parseFramerErr(err error) error {
if err == nil {
return nil
}
2017-12-01 05:30:20 +00:00
errMsg := err.Error()
if strings.Contains(errMsg, io.ErrClosedPipe.Error()) {
// The pipe check is for tests
return syscall.EPIPE
}
// The connection was closed by our peer
2017-12-01 05:30:20 +00:00
if strings.Contains(errMsg, syscall.EPIPE.Error()) || strings.Contains(errMsg, syscall.ECONNRESET.Error()) {
return syscall.EPIPE
}
// Windows version of ECONNRESET
//XXX(schmichael) I could find no existing error or constant to
// compare this against.
if strings.Contains(errMsg, "forcibly closed") {
return syscall.EPIPE
}
return err
}
2016-07-22 22:01:56 +00:00
// stream is the internal method to stream the content of a file. eofCancelCh is
// used to cancel the stream if triggered while at EOF. If the connection is
// broken an EPIPE error is returned
2016-07-18 16:48:29 +00:00
func (s *HTTPServer) stream(offset int64, path string,
2018-01-21 01:19:55 +00:00
fs allocdir.AllocDirFS, framer *sframer.StreamFramer,
2016-07-18 16:48:29 +00:00
eofCancelCh chan error) error {
2016-07-06 00:08:58 +00:00
// Get the reader
f, err := fs.ReadAt(path, offset)
if err != nil {
2016-07-07 15:15:22 +00:00
return err
2016-07-06 00:08:58 +00:00
}
defer f.Close()
// Create a tomb to cancel watch events
t := tomb.Tomb{}
defer func() {
t.Kill(nil)
t.Done()
}()
2016-07-06 00:08:58 +00:00
// Create a variable to allow setting the last event
var lastEvent string
// Only create the file change watcher once. But we need to do it after we
// read and reach EOF.
var changes *watch.FileChanges
2016-07-06 00:08:58 +00:00
// Start streaming the data
2016-07-10 17:55:52 +00:00
data := make([]byte, streamFrameSize)
2016-07-06 00:08:58 +00:00
OUTER:
for {
// Read up to the max frame size
n, readErr := f.Read(data)
2016-07-06 00:08:58 +00:00
// Update the offset
offset += int64(n)
// Return non-EOF errors
if readErr != nil && readErr != io.EOF {
return readErr
2016-07-06 00:08:58 +00:00
}
// Send the frame
2017-05-11 20:05:53 +00:00
if n != 0 || lastEvent != "" {
if err := framer.Send(path, lastEvent, data[:n], offset); err != nil {
2016-12-09 18:49:39 +00:00
return parseFramerErr(err)
}
}
// Clear the last event
if lastEvent != "" {
lastEvent = ""
2016-07-06 00:08:58 +00:00
}
// Just keep reading
if readErr == nil {
2016-07-06 00:08:58 +00:00
continue
}
// If EOF is hit, wait for a change to the file
if changes == nil {
changes, err = fs.ChangeEvents(path, offset, &t)
if err != nil {
return err
}
2016-07-06 00:08:58 +00:00
}
for {
select {
case <-changes.Modified:
continue OUTER
case <-changes.Deleted:
2016-12-09 22:44:50 +00:00
return parseFramerErr(framer.Send(path, deleteEvent, nil, offset))
2016-07-06 00:08:58 +00:00
case <-changes.Truncated:
// Close the current reader
if err := f.Close(); err != nil {
2016-07-07 15:15:22 +00:00
return err
2016-07-06 00:08:58 +00:00
}
// Get a new reader at offset zero
offset = 0
var err error
f, err = fs.ReadAt(path, offset)
if err != nil {
2016-07-07 15:15:22 +00:00
return err
2016-07-06 00:08:58 +00:00
}
defer f.Close()
// Store the last event
lastEvent = truncateEvent
continue OUTER
case <-framer.ExitCh():
return parseFramerErr(framer.Err())
2016-07-20 17:18:05 +00:00
case err, ok := <-eofCancelCh:
if !ok {
return nil
}
2016-07-18 16:48:29 +00:00
return err
2016-07-06 00:08:58 +00:00
}
}
}
}
2018-01-21 01:19:55 +00:00
*/
2016-07-18 16:48:29 +00:00
// Logs streams the content of a log blocking on EOF. The parameters are:
// * task: task name to stream logs for.
// * type: stdout/stderr to stream.
2016-07-20 17:18:05 +00:00
// * follow: A boolean of whether to follow the logs.
2016-07-18 16:48:29 +00:00
// * offset: The offset to start streaming data at, defaults to zero.
// * origin: Either "start" or "end" and defines from where the offset is
// applied. Defaults to "start".
func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var allocID, task, logType string
2017-01-13 21:12:36 +00:00
var plain, follow bool
2016-07-18 16:48:29 +00:00
var err error
q := req.URL.Query()
if allocID = strings.TrimPrefix(req.URL.Path, "/v1/client/fs/logs/"); allocID == "" {
return nil, allocIDNotPresentErr
}
if task = q.Get("task"); task == "" {
return nil, taskNotPresentErr
}
2017-01-24 00:58:53 +00:00
if followStr := q.Get("follow"); followStr != "" {
2017-01-13 21:12:36 +00:00
if follow, err = strconv.ParseBool(followStr); err != nil {
return nil, fmt.Errorf("Failed to parse follow field to boolean: %v", err)
}
}
2017-01-24 00:58:53 +00:00
if plainStr := q.Get("plain"); plainStr != "" {
2017-01-13 21:12:36 +00:00
if plain, err = strconv.ParseBool(plainStr); err != nil {
return nil, fmt.Errorf("Failed to parse plain field to boolean: %v", err)
}
2016-07-20 17:18:05 +00:00
}
2016-07-18 16:48:29 +00:00
logType = q.Get("type")
switch logType {
case "stdout", "stderr":
default:
return nil, logTypeNotPresentErr
}
var offset int64
offsetString := q.Get("offset")
if offsetString != "" {
var err error
if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
return nil, fmt.Errorf("error parsing offset: %v", err)
}
}
origin := q.Get("origin")
switch origin {
case "start", "end":
case "":
origin = "start"
default:
return nil, invalidOrigin
}
2018-01-21 01:19:55 +00:00
// Create an output that gets flushed on every write
output := ioutils.NewWriteFlusher(resp)
2016-07-18 16:48:29 +00:00
2018-02-01 01:35:21 +00:00
localClient := s.agent.Client()
localServer := s.agent.Server()
// See if the local client can handle the request.
localAlloc := false
if localClient != nil {
_, err := localClient.GetClientAlloc(allocID)
if err == nil {
localAlloc = true
}
}
// Only use the client RPC to server if we don't have a server and the local
// client can't handle the call.
useClientRPC := localClient != nil && !localAlloc && localServer == nil
// Use the server as a last case.
useServerRPC := localServer != nil
// Get the correct handler
var handler structs.StreamingRpcHandler
var handlerErr error
if localAlloc {
handler, handlerErr = localClient.StreamingRpcHandler("FileSystem.Logs")
} else if useClientRPC {
handler, handlerErr = localClient.RemoteStreamingRpcHandler("FileSystem.Logs")
} else if useServerRPC {
handler, handlerErr = localServer.StreamingRpcHandler("FileSystem.Logs")
}
if handlerErr != nil {
return nil, CodedError(500, handlerErr.Error())
}
2018-01-21 01:19:55 +00:00
// Create the request arguments
fsReq := &cstructs.FsLogsRequest{
AllocID: allocID,
Task: task,
LogType: logType,
Offset: offset,
Origin: origin,
PlainText: plain,
Follow: follow,
}
2018-02-01 01:35:21 +00:00
s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
2018-01-21 01:19:55 +00:00
p1, p2 := net.Pipe()
decoder := codec.NewDecoder(p1, structs.MsgpackHandle)
encoder := codec.NewEncoder(p1, structs.MsgpackHandle)
2018-01-21 01:19:55 +00:00
// Create a goroutine that closes the pipe if the connection closes.
ctx, cancel := context.WithCancel(req.Context())
go func() {
<-ctx.Done()
p1.Close()
2016-07-18 16:48:29 +00:00
}()
2018-01-21 01:19:55 +00:00
// Create a channel that decodes the results
errCh := make(chan HTTPCodedError)
2016-07-20 21:14:54 +00:00
go func() {
2018-01-21 01:19:55 +00:00
// Send the request
if err := encoder.Encode(fsReq); err != nil {
errCh <- CodedError(500, err.Error())
cancel()
return
}
2016-07-20 21:14:54 +00:00
for {
select {
2018-01-21 01:19:55 +00:00
case <-ctx.Done():
errCh <- nil
cancel()
return
2018-01-21 01:19:55 +00:00
default:
}
var res cstructs.StreamErrWrapper
if err := decoder.Decode(&res); err != nil {
2018-02-01 01:35:21 +00:00
errCh <- CodedError(500, err.Error())
2018-01-21 01:19:55 +00:00
cancel()
2016-07-20 21:14:54 +00:00
return
2018-01-21 01:19:55 +00:00
}
2016-07-20 21:14:54 +00:00
2018-01-21 01:19:55 +00:00
if err := res.Error; err != nil {
if err.Code != nil {
errCh <- CodedError(int(*err.Code), err.Error())
cancel()
2016-07-20 21:14:54 +00:00
return
}
}
2016-07-18 16:48:29 +00:00
2018-02-01 01:35:21 +00:00
if _, err := io.Copy(output, bytes.NewBuffer(res.Payload)); err != nil {
errCh <- CodedError(500, err.Error())
2018-01-21 01:19:55 +00:00
cancel()
return
2016-07-19 22:58:02 +00:00
}
}
2018-01-21 01:19:55 +00:00
}()
2016-07-18 16:48:29 +00:00
2018-01-21 01:19:55 +00:00
handler(p2)
cancel()
codedErr := <-errCh
2018-02-01 01:35:21 +00:00
if codedErr != nil &&
(codedErr == io.EOF ||
strings.Contains(codedErr.Error(), "closed") ||
strings.Contains(codedErr.Error(), "EOF")) {
2018-01-21 01:19:55 +00:00
codedErr = nil
}
return nil, codedErr
2016-07-18 16:48:29 +00:00
}