Skip to content

Commit 41dc6ba

Browse files
committed
api: add follow param to file stream endpoint
The `/v1/client/fs/stream endpoint` supports tailing a file by writing chunks out as they come in. But not all browsers support streams (ex IE11) so we need to be able to tail a file without streaming. The fs stream and logs endpoint use the same implementation for filesystem streaming under the hood, but the fs stream always passes the `follow` parameter set to true. This adds the same toggle to the fs stream endpoint that we have for logs. It defaults to true for backwards compatibility.
1 parent 3fef983 commit 41dc6ba

File tree

4 files changed

+65
-8
lines changed

4 files changed

+65
-8
lines changed

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
IMPROVEMENTS:
44
* agent: allow the job GC interval to be configured [[GH-5978](https://github.com/hashicorp/nomad/issues/5978)]
5+
* api: add follow parameter to file streaming endpoint to support older browsers [[GH-6049](https://github.com/hashicorp/nomad/issues/6049)]
56

67
## 0.9.5 (Unreleased)
78

@@ -46,7 +47,7 @@ BUG FIXES:
4647
* driver: Fixed an issue preventing external driver plugins from launching executor process [[GH-5726](https://github.com/hashicorp/nomad/issues/5726)]
4748
* driver/docker: Fixed a bug mounting relative paths on Windows [[GH-5811](https://github.com/hashicorp/nomad/issues/5811)]
4849
* driver/exec: Upgraded libcontainer dependency to avoid zombie `runc:[1:CHILD]]` processes [[GH-5851](https://github.com/hashicorp/nomad/issues/5851)]
49-
* metrics: Added metrics for raft and state store indexes. [[GH-5841](https://github.com/hashicorp/nomad/issues/5841)]
50+
* metrics: Added metrics for raft and state store indexes. [[GH-5841](https://github.com/hashicorp/nomad/issues/5841)]
5051
* metrics: Upgrade prometheus client to avoid label conflicts [[GH-5850](https://github.com/hashicorp/nomad/issues/5850)]
5152
* ui: Fixed ability to click sort arrow to change sort direction [[GH-5833](https://github.com/hashicorp/nomad/pull/5833)]
5253

@@ -1629,4 +1630,3 @@ BUG FIXES:
16291630
## 0.1.0 (September 28, 2015)
16301631

16311632
* Initial release
1632-

command/agent/fs_endpoint.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,13 @@ func (s *HTTPServer) FileCatRequest(resp http.ResponseWriter, req *http.Request)
194194
// Stream streams the content of a file blocking on EOF.
195195
// The parameters are:
196196
// * path: path to file to stream.
197+
// * follow: A boolean of whether to follow the file, defaults to true.
197198
// * offset: The offset to start streaming data at, defaults to zero.
198199
// * origin: Either "start" or "end" and defines from where the offset is
199200
// applied. Defaults to "start".
200201
func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
201202
var allocID, path string
203+
var err error
202204

203205
q := req.URL.Query()
204206

@@ -210,10 +212,16 @@ func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interf
210212
return nil, fileNameNotPresentErr
211213
}
212214

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+
213222
var offset int64
214223
offsetString := q.Get("offset")
215224
if offsetString != "" {
216-
var err error
217225
if offset, err = strconv.ParseInt(offsetString, 10, 64); err != nil {
218226
return nil, fmt.Errorf("error parsing offset: %v", err)
219227
}
@@ -234,7 +242,7 @@ func (s *HTTPServer) Stream(resp http.ResponseWriter, req *http.Request) (interf
234242
Path: path,
235243
Origin: origin,
236244
Offset: offset,
237-
Follow: true,
245+
Follow: follow,
238246
}
239247
s.parse(resp, req, &fsReq.QueryOptions.Region, &fsReq.QueryOptions)
240248

@@ -265,13 +273,13 @@ func (s *HTTPServer) Logs(resp http.ResponseWriter, req *http.Request) (interfac
265273

266274
if followStr := q.Get("follow"); followStr != "" {
267275
if follow, err = strconv.ParseBool(followStr); err != nil {
268-
return nil, fmt.Errorf("Failed to parse follow field to boolean: %v", err)
276+
return nil, fmt.Errorf("failed to parse follow field to boolean: %v", err)
269277
}
270278
}
271279

272280
if plainStr := q.Get("plain"); plainStr != "" {
273281
if plain, err = strconv.ParseBool(plainStr); err != nil {
274-
return nil, fmt.Errorf("Failed to parse plain field to boolean: %v", err)
282+
return nil, fmt.Errorf("failed to parse plain field to boolean: %v", err)
275283
}
276284
}
277285

command/agent/fs_endpoint_test.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,54 @@ func TestHTTP_FS_Cat(t *testing.T) {
341341
})
342342
}
343343

344-
func TestHTTP_FS_Stream(t *testing.T) {
344+
func TestHTTP_FS_Stream_NoFollow(t *testing.T) {
345+
t.Parallel()
346+
require := require.New(t)
347+
httpTest(t, nil, func(s *TestAgent) {
348+
a := mockFSAlloc(s.client.NodeID(), nil)
349+
addAllocToClient(s, a, terminalClientAlloc)
350+
351+
offset := 4
352+
expectation := base64.StdEncoding.EncodeToString(
353+
[]byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]))
354+
path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end&follow=false",
355+
a.ID, offset)
356+
357+
p, _ := io.Pipe()
358+
req, err := http.NewRequest("GET", path, p)
359+
require.Nil(err)
360+
respW := testutil.NewResponseRecorder()
361+
doneCh := make(chan struct{})
362+
go func() {
363+
_, err = s.Server.Stream(respW, req)
364+
require.Nil(err)
365+
close(doneCh)
366+
}()
367+
368+
out := ""
369+
testutil.WaitForResult(func() (bool, error) {
370+
output, err := ioutil.ReadAll(respW)
371+
if err != nil {
372+
return false, err
373+
}
374+
375+
out += string(output)
376+
return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation)
377+
}, func(err error) {
378+
t.Fatal(err)
379+
})
380+
381+
select {
382+
case <-doneCh:
383+
case <-time.After(1 * time.Second):
384+
t.Fatal("should close but did not")
385+
}
386+
387+
p.Close()
388+
})
389+
}
390+
391+
func TestHTTP_FS_Stream_Follow(t *testing.T) {
345392
t.Parallel()
346393
require := require.New(t)
347394
httpTest(t, nil, func(s *TestAgent) {

website/source/api/client.html.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: |-
99

1010
# Client HTTP API
1111

12-
The `/client` endpoints are used to interact with the Nomad clients.
12+
The `/client` endpoints are used to interact with the Nomad clients.
1313

1414
Since Nomad 0.8.0, both a client and server can handle client endpoints. This is
1515
particularly useful for when a direct connection to a client is not possible due
@@ -363,6 +363,8 @@ The table below shows this endpoint's support for
363363
- `path` `(string: "/")` - Specifies the path of the file to read, relative to
364364
the root of the allocation directory.
365365

366+
- `follow` `(bool: true)`- Specifies whether to tail the file.
367+
366368
- `offset` `(int: <required>)` - Specifies the byte offset from where content
367369
will be read.
368370

0 commit comments

Comments
 (0)