open-nomad/command/agent/fs_endpoint.go

815 lines
19 KiB
Go
Raw Normal View History

package agent
import (
"bytes"
"fmt"
2016-01-14 21:35:42 +00:00
"io"
2016-07-18 16:48:29 +00:00
"math"
"net/http"
2016-07-18 16:48:29 +00:00
"os"
"path/filepath"
2016-01-13 06:06:42 +00:00
"strconv"
"strings"
"sync"
2016-07-06 00:08:58 +00:00
"time"
"gopkg.in/tomb.v1"
2016-07-06 03:48:25 +00:00
"github.com/docker/docker/pkg/ioutils"
2016-07-07 15:15:22 +00:00
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hpcloud/tail/watch"
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")
)
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
// 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-06 00:08:58 +00:00
deleteEvent = "file deleted"
truncateEvent = "file truncated"
2016-01-13 19:49:39 +00:00
)
func (s *HTTPServer) FsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.agent.client == nil {
return nil, clientNotRunning
}
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/"):
return s.FileReadAtRequest(resp, req)
2016-03-28 18:06:22 +00:00
case strings.HasPrefix(path, "cat/"):
return s.FileCatRequest(resp, req)
2016-07-06 00:08:58 +00:00
case strings.HasPrefix(path, "stream/"):
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
2016-07-07 15:15:22 +00:00
// StreamFrame is used to frame data of a file when streaming
2016-07-06 00:08:58 +00:00
type StreamFrame struct {
2016-07-07 15:15:22 +00:00
// Offset is the offset the data was read from
2016-07-10 22:56:13 +00:00
Offset int64 `json:",omitempty"`
2016-07-07 15:15:22 +00:00
2016-07-10 22:56:13 +00:00
// Data is the read data
Data []byte `json:",omitempty"`
2016-07-07 15:15:22 +00:00
// File is the file that the data was read from
2016-07-10 22:56:13 +00:00
File string `json:",omitempty"`
2016-07-07 15:15:22 +00:00
2016-07-18 14:24:46 +00:00
// FileEvent is the last file event that occurred that could cause the
2016-07-07 15:15:22 +00:00
// streams position to change or end
2016-07-10 22:56:13 +00:00
FileEvent string `json:",omitempty"`
2016-07-06 00:08:58 +00:00
}
2016-07-10 17:55:52 +00:00
// IsHeartbeat returns if the frame is a heartbeat frame
func (s *StreamFrame) IsHeartbeat() bool {
2016-07-10 22:56:13 +00:00
return s.Offset == 0 && len(s.Data) == 0 && s.File == "" && s.FileEvent == ""
2016-07-10 17:55:52 +00:00
}
// StreamFramer is used to buffer and send frames as well as heartbeat.
type StreamFramer struct {
2016-07-12 16:45:05 +00:00
out io.WriteCloser
enc *codec.Encoder
frameSize int
heartbeat *time.Ticker
flusher *time.Ticker
shutdownCh chan struct{}
exitCh chan struct{}
outbound chan *StreamFrame
// The mutex protects everything below
l sync.Mutex
// The current working frame
f *StreamFrame
data *bytes.Buffer
2016-07-18 14:24:46 +00:00
// Captures whether the framer is running and any error that occurred to
// cause it to stop.
running bool
2016-07-19 17:04:57 +00:00
Err error
}
// NewStreamFramer creates a new stream framer that will output StreamFrames to
// the passed output.
2016-07-10 17:55:52 +00:00
func NewStreamFramer(out io.WriteCloser, heartbeatRate, batchWindow time.Duration, frameSize int) *StreamFramer {
// Create a JSON encoder
enc := codec.NewEncoder(out, jsonHandle)
// Create the heartbeat and flush ticker
2016-07-10 17:55:52 +00:00
heartbeat := time.NewTicker(heartbeatRate)
flusher := time.NewTicker(batchWindow)
return &StreamFramer{
2016-07-12 16:45:05 +00:00
out: out,
enc: enc,
frameSize: frameSize,
heartbeat: heartbeat,
flusher: flusher,
outbound: make(chan *StreamFrame),
data: bytes.NewBuffer(make([]byte, 0, 2*frameSize)),
shutdownCh: make(chan struct{}),
exitCh: make(chan struct{}),
}
}
// Destroy is used to cleanup the StreamFramer and flush any pending frames
func (s *StreamFramer) Destroy() {
s.l.Lock()
2016-07-12 16:45:05 +00:00
close(s.shutdownCh)
s.heartbeat.Stop()
s.flusher.Stop()
s.l.Unlock()
// Ensure things were flushed
2016-07-19 17:04:57 +00:00
if s.running {
<-s.exitCh
}
2016-07-19 01:41:21 +00:00
s.out.Close()
}
// Run starts a long lived goroutine that handles sending data as well as
// heartbeating
func (s *StreamFramer) Run() {
s.l.Lock()
2016-07-13 19:23:33 +00:00
defer s.l.Unlock()
2016-07-10 22:56:13 +00:00
if s.running {
return
}
s.running = true
go s.run()
}
// ExitCh returns a channel that will be closed when the run loop terminates.
func (s *StreamFramer) ExitCh() <-chan struct{} {
return s.exitCh
}
// run is the internal run method. It exits if Destroy is called or an error
// occurs, in which case the exit channel is closed.
func (s *StreamFramer) run() {
// Store any error and mark it as not running
var err error
defer func() {
s.l.Lock()
2016-07-19 17:04:57 +00:00
s.Err = err
close(s.exitCh)
2016-07-12 23:01:33 +00:00
close(s.outbound)
2016-07-19 17:04:57 +00:00
s.running = false
s.l.Unlock()
}()
// Start a heartbeat/flusher go-routine. This is done seprately to avoid blocking
// the outbound channel.
go func() {
for {
select {
2016-07-19 17:04:57 +00:00
case <-s.exitCh:
return
2016-07-12 16:45:05 +00:00
case <-s.shutdownCh:
return
case <-s.flusher.C:
// Skip if there is nothing to flush
s.l.Lock()
if s.f == nil {
s.l.Unlock()
continue
}
// Read the data for the frame, and send it
s.f.Data = s.readData()
2016-07-19 17:04:57 +00:00
s.outbound <- s.f
s.f = nil
s.l.Unlock()
case <-s.heartbeat.C:
// Send a heartbeat frame
2016-07-19 17:04:57 +00:00
select {
case s.outbound <- &StreamFrame{}:
default:
}
}
}
}()
OUTER:
for {
select {
2016-07-12 16:45:05 +00:00
case <-s.shutdownCh:
break OUTER
case o := <-s.outbound:
2016-07-19 01:41:21 +00:00
// Send the frame
if err = s.enc.Encode(o); err != nil {
return
}
}
}
// Flush any existing frames
s.l.Lock()
select {
case o := <-s.outbound:
// Send the frame and then clear the current working frame
if err = s.enc.Encode(o); err != nil {
2016-07-19 17:04:57 +00:00
s.l.Unlock()
return
}
default:
}
if s.f != nil {
s.f.Data = s.readData()
s.enc.Encode(s.f)
}
2016-07-19 17:04:57 +00:00
s.l.Unlock()
}
2016-07-10 22:56:13 +00:00
// readData is a helper which reads the buffered data returning up to the frame
// size of data. Must be called with the lock held. The returned value is
// invalid on the next read or write into the StreamFramer buffer
func (s *StreamFramer) readData() []byte {
// Compute the amount to read from the buffer
size := s.data.Len()
2016-07-10 17:55:52 +00:00
if size > s.frameSize {
size = s.frameSize
}
2016-07-10 22:56:13 +00:00
if size == 0 {
return nil
}
2016-07-19 17:04:57 +00:00
d := s.data.Next(size)
b := make([]byte, size)
copy(b, d)
return b
}
// Send creates and sends a StreamFrame based on the passed parameters. An error
// is returned if the run routine hasn't run or encountered an error. Send is
// asyncronous and does not block for the data to be transferred.
func (s *StreamFramer) Send(file, fileEvent string, data []byte, offset int64) error {
s.l.Lock()
defer s.l.Unlock()
// If we are not running, return the error that caused us to not run or
// indicated that it was never started.
if !s.running {
2016-07-19 17:04:57 +00:00
if s.Err != nil {
return s.Err
}
return fmt.Errorf("StreamFramer not running")
}
// Check if not mergeable
if s.f != nil && (s.f.File != file || s.f.FileEvent != fileEvent) {
// Flush the old frame
2016-07-19 17:04:57 +00:00
f := *s.f
f.Data = s.readData()
select {
case <-s.exitCh:
return nil
case s.outbound <- &f:
s.f = nil
}
}
// Store the new data as the current frame.
if s.f == nil {
s.f = &StreamFrame{
Offset: offset,
File: file,
FileEvent: fileEvent,
}
}
// Write the data to the buffer
s.data.Write(data)
// Handle the delete case in which there is no data
if s.data.Len() == 0 && s.f.FileEvent != "" {
2016-07-19 17:04:57 +00:00
select {
case <-s.exitCh:
return nil
case s.outbound <- &StreamFrame{
Offset: s.f.Offset,
File: s.f.File,
FileEvent: s.f.FileEvent,
2016-07-19 17:04:57 +00:00
}:
}
}
// Flush till we are under the max frame size
2016-07-10 17:55:52 +00:00
for s.data.Len() >= s.frameSize {
// Create a new frame to send it
2016-07-19 17:04:57 +00:00
d := s.readData()
select {
case <-s.exitCh:
return nil
case s.outbound <- &StreamFrame{
Offset: s.f.Offset,
File: s.f.File,
FileEvent: s.f.FileEvent,
2016-07-19 17:04:57 +00:00
Data: d,
}:
}
}
if s.data.Len() == 0 {
s.f = nil
}
return nil
}
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
framer := NewStreamFramer(output, streamHeartbeatRate, streamBatchWindow, streamFrameSize)
framer.Run()
defer framer.Destroy()
return nil, s.stream(offset, path, fs, framer, nil)
2016-07-07 15:15:22 +00:00
}
2016-07-18 16:48:29 +00:00
func (s *HTTPServer) stream(offset int64, path string,
fs allocdir.AllocDirFS, framer *StreamFramer,
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
if n != 0 {
if err := framer.Send(path, lastEvent, data[:n], offset); err != nil {
return 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:
return 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():
2016-07-07 15:15:22 +00:00
return nil
2016-07-18 16:48:29 +00:00
case err := <-eofCancelCh:
return err
2016-07-06 00:08:58 +00:00
}
}
}
2016-07-07 15:15:22 +00:00
return nil
2016-07-06 00:08:58 +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.
// * 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
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
}
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
}
fs, err := s.agent.client.GetAllocFS(allocID)
if err != nil {
return nil, err
}
// Create an output that gets flushed on every write
output := ioutils.NewWriteFlusher(resp)
return nil, s.logs(offset, origin, task, logType, fs, output)
}
func (s *HTTPServer) logs(offset int64, origin, task, logType string, fs allocdir.AllocDirFS, output io.WriteCloser) error {
// Create the framer
framer := NewStreamFramer(output, streamHeartbeatRate, streamBatchWindow, streamFrameSize)
framer.Run()
defer framer.Destroy()
// Path to the logs
logPath := filepath.Join(allocdir.SharedAllocName, allocdir.LogDirName)
// nextIdx is the next index to read logs from
var nextIdx int64
switch origin {
case "start":
nextIdx = 0
case "end":
nextIdx = math.MaxInt64
offset *= -1
default:
return invalidOrigin
}
// Create a tomb to cancel watch events
t := tomb.Tomb{}
defer func() {
t.Kill(nil)
t.Done()
}()
for {
// Logic for picking next file is:
// 1) List log files
// 2) Pick log file closest to desired index
// 3) Open log file at correct offset
// 3a) No error, read contents
// 3b) If file doesn't exist, goto 1 as it may have been rotated out
entries, err := fs.List(logPath)
if err != nil {
return fmt.Errorf("failed to list entries: %v", err)
}
logEntry, idx, err := findClosest(entries, nextIdx, task, logType)
if err != nil {
return err
}
// Apply the offset we should open at. Handling the negative case is
// only for the first time.
openOffset := offset
if openOffset < 0 {
openOffset = logEntry.Size + openOffset
if openOffset < 0 {
openOffset = 0
}
}
p := filepath.Join(logPath, logEntry.Name)
nextPath := filepath.Join(logPath, fmt.Sprintf("%s.%s.%d", task, logType, idx+1))
nextExists := fs.BlockUntilExists(nextPath, &t)
err = s.stream(openOffset, p, fs, framer, nextExists)
// Check if there was an error where the file does not exist. That means
// it got rotated out from under us.
if err != nil {
if os.IsNotExist(err) {
continue
}
return err
}
//Since we successfully streamed, update the overall offset/idx.
offset = int64(0)
2016-07-19 17:04:57 +00:00
nextIdx = idx + 1
2016-07-18 16:48:29 +00:00
}
return nil
}
func findClosest(entries []*allocdir.AllocFileInfo, desiredIdx int64,
task, logType string) (*allocdir.AllocFileInfo, int64, error) {
if len(entries) == 0 {
return nil, 0, fmt.Errorf("no file entries found")
}
prefix := fmt.Sprintf("%s.%s.", task, logType)
var closest *allocdir.AllocFileInfo
2016-07-19 17:04:57 +00:00
closestIdx := int64(math.MaxInt64)
2016-07-18 16:48:29 +00:00
closestDist := int64(math.MaxInt64)
for _, entry := range entries {
if entry.IsDir {
continue
}
idxStr := strings.TrimPrefix(entry.Name, prefix)
// If nothing was trimmed, then it is not a match
if idxStr == entry.Name {
continue
}
// Convert to an int
idx, err := strconv.Atoi(idxStr)
if err != nil {
return nil, 0, fmt.Errorf("failed to convert %q to a log index: %v", idxStr, err)
}
// Determine distance to desired
d := desiredIdx - int64(idx)
if d < 0 {
d *= -1
}
2016-07-19 17:04:57 +00:00
if d <= closestDist && (int64(idx) < closestIdx || int64(idx) == desiredIdx) {
2016-07-18 16:48:29 +00:00
closestDist = d
closest = entry
closestIdx = int64(idx)
}
}
if closest == nil {
return nil, 0, fmt.Errorf("log entry for task %q and log type %q not found", task, logType)
}
return closest, closestIdx, nil
}