2016-01-12 23:03:53 +00:00
|
|
|
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
|
|
|
|
2016-01-12 23:03:53 +00:00
|
|
|
import (
|
2016-07-09 16:28:46 +00:00
|
|
|
"bytes"
|
2018-01-21 01:19:55 +00:00
|
|
|
"context"
|
2016-01-12 23:03:53 +00:00
|
|
|
"fmt"
|
2016-01-14 21:35:42 +00:00
|
|
|
"io"
|
2018-01-21 01:19:55 +00:00
|
|
|
"net"
|
2016-01-12 23:03:53 +00:00
|
|
|
"net/http"
|
2016-01-13 06:06:42 +00:00
|
|
|
"strconv"
|
2016-01-12 23:03:53 +00:00
|
|
|
"strings"
|
2016-07-06 00:08:58 +00:00
|
|
|
"time"
|
|
|
|
|
2016-07-06 03:48:25 +00:00
|
|
|
"github.com/docker/docker/pkg/ioutils"
|
2018-01-21 01:19:55 +00:00
|
|
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
2017-04-28 20:18:04 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2016-07-06 00:08:58 +00:00
|
|
|
"github.com/ugorji/go/codec"
|
2016-01-12 23:03:53 +00:00
|
|
|
)
|
|
|
|
|
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)")
|
2016-03-07 19:26:54 +00:00
|
|
|
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")
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2016-07-10 17:55:52 +00:00
|
|
|
// streamFrameSize is the maximum number of bytes to send in a single frame
|
|
|
|
streamFrameSize = 64 * 1024
|
2016-07-06 00:08:58 +00:00
|
|
|
|
|
|
|
// streamHeartbeatRate is the rate at which a heartbeat will occur to detect
|
|
|
|
// a closed connection without sending any additional data
|
2016-07-19 17:04:57 +00:00
|
|
|
streamHeartbeatRate = 1 * time.Second
|
2016-07-06 00:08:58 +00:00
|
|
|
|
2016-07-09 16:28:46 +00:00
|
|
|
// streamBatchWindow is the window in which file content is batched before
|
|
|
|
// being flushed if the frame size has not been hit.
|
|
|
|
streamBatchWindow = 200 * time.Millisecond
|
|
|
|
|
2016-07-20 21:14:54 +00:00
|
|
|
// nextLogCheckRate is the rate at which we check for a log entry greater
|
|
|
|
// than what we are watching for. This is to handle the case in which logs
|
|
|
|
// rotate faster than we can detect and we have to rely on a normal
|
|
|
|
// directory listing.
|
|
|
|
nextLogCheckRate = 100 * time.Millisecond
|
|
|
|
|
2016-07-20 20:06:05 +00:00
|
|
|
// deleteEvent and truncateEvent are the file events that can be sent in a
|
|
|
|
// StreamFrame
|
2016-07-06 00:08:58 +00:00
|
|
|
deleteEvent = "file deleted"
|
|
|
|
truncateEvent = "file truncated"
|
2016-07-20 20:06:05 +00:00
|
|
|
|
|
|
|
// OriginStart and OriginEnd are the available parameters for the origin
|
|
|
|
// argument when streaming a file. They respectively offset from the start
|
|
|
|
// and end of a file.
|
|
|
|
OriginStart = "start"
|
|
|
|
OriginEnd = "end"
|
2016-01-13 19:49:39 +00:00
|
|
|
)
|
|
|
|
|
2016-03-07 19:26:54 +00:00
|
|
|
func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2018-01-22 01:09:20 +00:00
|
|
|
//if s.agent.client == nil {
|
|
|
|
//return nil, clientNotRunning
|
|
|
|
//}
|
2016-03-07 19:26:54 +00:00
|
|
|
|
2017-10-05 00:01:32 +00:00
|
|
|
var secret string
|
|
|
|
s.parseToken(req, &secret)
|
|
|
|
|
|
|
|
var namespace string
|
|
|
|
parseNamespace(req, &namespace)
|
|
|
|
|
2018-01-22 01:09:20 +00:00
|
|
|
//aclObj, err := s.agent.Client().ResolveToken(secret)
|
|
|
|
//if err != nil {
|
|
|
|
//return nil, err
|
|
|
|
//}
|
2017-10-05 00:01:32 +00:00
|
|
|
|
2016-03-07 19:26:54 +00:00
|
|
|
path := strings.TrimPrefix(req.URL.Path, "/v1/client/fs/")
|
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(path, "ls/"):
|
2018-01-22 01:09:20 +00:00
|
|
|
//if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
|
|
|
|
//return nil, structs.ErrPermissionDenied
|
|
|
|
//}
|
2017-10-09 18:19:25 +00:00
|
|
|
return s.DirectoryListRequest(resp, req)
|
2016-03-07 19:26:54 +00:00
|
|
|
case strings.HasPrefix(path, "stat/"):
|
2018-01-22 01:09:20 +00:00
|
|
|
//if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
|
|
|
|
//return nil, structs.ErrPermissionDenied
|
|
|
|
//}
|
2017-10-09 18:19:25 +00:00
|
|
|
return s.FileStatRequest(resp, req)
|
2016-03-07 19:26:54 +00:00
|
|
|
case strings.HasPrefix(path, "readat/"):
|
2018-01-22 01:09:20 +00:00
|
|
|
//if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
|
|
|
|
//return nil, structs.ErrPermissionDenied
|
|
|
|
//}
|
2017-10-09 18:19:25 +00:00
|
|
|
return s.FileReadAtRequest(resp, req)
|
2016-03-28 18:06:22 +00:00
|
|
|
case strings.HasPrefix(path, "cat/"):
|
2018-01-22 01:09:20 +00:00
|
|
|
//if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS) {
|
|
|
|
//return nil, structs.ErrPermissionDenied
|
|
|
|
//}
|
2017-10-09 18:19:25 +00:00
|
|
|
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/"):
|
2017-10-05 00:01:32 +00:00
|
|
|
// Logs can be accessed with ReadFS or ReadLogs caps
|
2018-01-21 01:19:55 +00:00
|
|
|
//if aclObj != nil {
|
|
|
|
//readfs := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadFS)
|
|
|
|
//logs := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadLogs)
|
|
|
|
//if !readfs && !logs {
|
|
|
|
//return nil, structs.ErrPermissionDenied
|
|
|
|
//}
|
|
|
|
//}
|
2016-07-18 16:48:29 +00:00
|
|
|
return s.Logs(resp, req)
|
2016-03-07 19:26:54 +00:00
|
|
|
default:
|
|
|
|
return nil, CodedError(404, ErrInvalidMethod)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-12 23:03:53 +00:00
|
|
|
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-12 23:03:53 +00:00
|
|
|
}
|
2016-01-13 06:25:12 +00:00
|
|
|
if path = req.URL.Query().Get("path"); path == "" {
|
|
|
|
path = "/"
|
2016-01-12 23:03:53 +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.List(path)
|
2016-01-12 23:03:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
2016-01-13 21:21:03 +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)
|
2016-01-12 23:03:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
2016-07-07 21:04:36 +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-07-07 21:04:36 +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-07 21:04:36 +00:00
|
|
|
|
2016-07-12 23:01:33 +00:00
|
|
|
rc, err := fs.ReadAt(path, offset)
|
2016-07-07 21:04:36 +00:00
|
|
|
if limit > 0 {
|
2016-07-12 23:01:33 +00:00
|
|
|
rc = &ReadCloserWrapper{
|
|
|
|
Reader: io.LimitReader(rc, limit),
|
|
|
|
Closer: rc,
|
|
|
|
}
|
2016-07-07 21:04:36 +00:00
|
|
|
}
|
|
|
|
|
2016-01-14 21:35:42 +00:00
|
|
|
if err != nil {
|
2016-01-13 06:06:42 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2016-07-07 21:04:36 +00:00
|
|
|
|
|
|
|
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-01-12 23:03:53 +00:00
|
|
|
}
|
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-09 16:28:46 +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
|
|
|
}
|
|
|
|
|
2017-12-01 01:42:53 +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()) {
|
2017-12-01 01:42:53 +00:00
|
|
|
// 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") {
|
2017-12-01 01:42:53 +00:00
|
|
|
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{}
|
2016-07-09 16:28:46 +00:00
|
|
|
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
|
|
|
|
|
2016-07-07 21:04:36 +00:00
|
|
|
// 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
|
2016-07-09 16:28:46 +00:00
|
|
|
n, readErr := f.Read(data)
|
2016-07-06 00:08:58 +00:00
|
|
|
|
|
|
|
// Update the offset
|
|
|
|
offset += int64(n)
|
|
|
|
|
|
|
|
// Return non-EOF errors
|
2016-07-09 16:28:46 +00:00
|
|
|
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 != "" {
|
2016-07-09 16:28:46 +00:00
|
|
|
if err := framer.Send(path, lastEvent, data[:n], offset); err != nil {
|
2016-12-09 18:49:39 +00:00
|
|
|
return parseFramerErr(err)
|
2016-07-09 16:28:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear the last event
|
|
|
|
if lastEvent != "" {
|
|
|
|
lastEvent = ""
|
2016-07-06 00:08:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Just keep reading
|
2016-07-09 16:28:46 +00:00
|
|
|
if readErr == nil {
|
2016-07-06 00:08:58 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-07-09 16:28:46 +00:00
|
|
|
// If EOF is hit, wait for a change to the file
|
2016-07-07 21:04:36 +00:00
|
|
|
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
|
2016-07-09 16:28:46 +00:00
|
|
|
case <-framer.ExitCh():
|
2017-12-01 01:42:53 +00:00
|
|
|
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-01-21 01:19:55 +00:00
|
|
|
// TODO make work for both
|
|
|
|
// Get the client's handler
|
2018-01-22 01:09:20 +00:00
|
|
|
handler, err := s.agent.Server().StreamingRpcHandler("FileSystem.Logs")
|
2017-03-31 22:57:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
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,
|
2017-03-31 22:57:10 +00:00
|
|
|
}
|
2018-01-21 01:19:55 +00:00
|
|
|
s.parseToken(req, &fsReq.QueryOptions.AuthToken)
|
2017-03-31 22:57:10 +00:00
|
|
|
|
2018-01-21 01:19:55 +00:00
|
|
|
p1, p2 := net.Pipe()
|
|
|
|
decoder := codec.NewDecoder(p1, structs.MsgpackHandle)
|
|
|
|
encoder := codec.NewEncoder(p1, structs.MsgpackHandle)
|
2017-03-31 22:57:10 +00:00
|
|
|
|
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()
|
|
|
|
s.logger.Printf("--------- HTTP: Request finished. Closing pipes")
|
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()
|
2016-10-03 21:58:44 +00:00
|
|
|
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()
|
|
|
|
s.logger.Printf("--------- HTTP: Exitting frame copier")
|
2016-11-18 04:14:47 +00:00
|
|
|
return
|
2018-01-21 01:19:55 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
var res cstructs.StreamErrWrapper
|
|
|
|
if err := decoder.Decode(&res); err != nil {
|
|
|
|
//errCh <- CodedError(500, err.Error())
|
|
|
|
errCh <- CodedError(501, err.Error())
|
|
|
|
cancel()
|
2016-07-20 21:14:54 +00:00
|
|
|
return
|
2018-01-21 01:19:55 +00:00
|
|
|
}
|
|
|
|
s.logger.Printf("--------- HTTP: Decoded stream wrapper")
|
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-01-21 01:19:55 +00:00
|
|
|
s.logger.Printf("--------- HTTP: Copying payload of size: %d", len(res.Payload))
|
|
|
|
if n, err := io.Copy(output, bytes.NewBuffer(res.Payload)); err != nil {
|
|
|
|
//errCh <- CodedError(500, err.Error())
|
|
|
|
errCh <- CodedError(502, err.Error())
|
|
|
|
cancel()
|
|
|
|
return
|
2016-07-19 22:58:02 +00:00
|
|
|
} else {
|
2018-01-21 01:19:55 +00:00
|
|
|
s.logger.Printf("--------- HTTP: Copied payload: %d bytes", n)
|
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
|
|
|
|
if codedErr != nil && (codedErr == io.EOF || strings.Contains(codedErr.Error(), "closed")) {
|
|
|
|
codedErr = nil
|
|
|
|
}
|
|
|
|
return nil, codedErr
|
2016-07-18 16:48:29 +00:00
|
|
|
}
|