github.com/janma/nomad@v0.11.3/command/agent/fs_endpoint_test.go (about)

     1  package agent
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	cstructs "github.com/hashicorp/nomad/client/structs"
    14  	"github.com/hashicorp/nomad/helper/uuid"
    15  	"github.com/hashicorp/nomad/nomad/mock"
    16  	"github.com/hashicorp/nomad/nomad/structs"
    17  	"github.com/hashicorp/nomad/testutil"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  const (
    22  	defaultLoggerMockDriverStdout = "Hello from the other side"
    23  	xssLoggerMockDriverStdout     = "<script>alert(document.domain);</script>"
    24  )
    25  
    26  var (
    27  	defaultLoggerMockDriver = map[string]interface{}{
    28  		"run_for":       "2s",
    29  		"stdout_string": defaultLoggerMockDriverStdout,
    30  	}
    31  	xssLoggerMockDriver = map[string]interface{}{
    32  		"run_for":       "2s",
    33  		"stdout_string": xssLoggerMockDriverStdout,
    34  	}
    35  )
    36  
    37  type clientAllocWaiter int
    38  
    39  const (
    40  	noWaitClientAlloc clientAllocWaiter = iota
    41  	runningClientAlloc
    42  	terminalClientAlloc
    43  )
    44  
    45  func addAllocToClient(agent *TestAgent, alloc *structs.Allocation, wait clientAllocWaiter) {
    46  	require := require.New(agent.T)
    47  
    48  	// Wait for the client to connect
    49  	testutil.WaitForResult(func() (bool, error) {
    50  		node, err := agent.server.State().NodeByID(nil, agent.client.NodeID())
    51  		if err != nil {
    52  			return false, err
    53  		}
    54  		if node == nil {
    55  			return false, fmt.Errorf("unknown node")
    56  		}
    57  
    58  		return node.Status == structs.NodeStatusReady, fmt.Errorf("bad node status")
    59  	}, func(err error) {
    60  		agent.T.Fatal(err)
    61  	})
    62  
    63  	// Upsert the allocation
    64  	state := agent.server.State()
    65  	require.Nil(state.UpsertJob(999, alloc.Job))
    66  	require.Nil(state.UpsertAllocs(1003, []*structs.Allocation{alloc}))
    67  
    68  	if wait == noWaitClientAlloc {
    69  		return
    70  	}
    71  
    72  	// Wait for the client to run the allocation
    73  	testutil.WaitForResult(func() (bool, error) {
    74  		alloc, err := state.AllocByID(nil, alloc.ID)
    75  		if err != nil {
    76  			return false, err
    77  		}
    78  		if alloc == nil {
    79  			return false, fmt.Errorf("unknown alloc")
    80  		}
    81  
    82  		expectation := alloc.ClientStatus == structs.AllocClientStatusComplete ||
    83  			alloc.ClientStatus == structs.AllocClientStatusFailed
    84  		if wait == runningClientAlloc {
    85  			expectation = expectation || alloc.ClientStatus == structs.AllocClientStatusRunning
    86  		}
    87  
    88  		if !expectation {
    89  			return false, fmt.Errorf("alloc client status: %v", alloc.ClientStatus)
    90  		}
    91  
    92  		return true, nil
    93  	}, func(err error) {
    94  		agent.T.Fatal(err)
    95  	})
    96  }
    97  
    98  // mockFSAlloc returns a suitable mock alloc for testing the fs system. If
    99  // config isn't provided, the defaultLoggerMockDriver config is used.
   100  func mockFSAlloc(nodeID string, config map[string]interface{}) *structs.Allocation {
   101  	a := mock.Alloc()
   102  	a.NodeID = nodeID
   103  	a.Job.Type = structs.JobTypeBatch
   104  	a.Job.TaskGroups[0].Count = 1
   105  	a.Job.TaskGroups[0].Tasks[0].Driver = "mock_driver"
   106  
   107  	if config != nil {
   108  		a.Job.TaskGroups[0].Tasks[0].Config = config
   109  	} else {
   110  		a.Job.TaskGroups[0].Tasks[0].Config = defaultLoggerMockDriver
   111  	}
   112  
   113  	return a
   114  }
   115  
   116  func TestHTTP_FS_List_MissingParams(t *testing.T) {
   117  	t.Parallel()
   118  	require := require.New(t)
   119  	httpTest(t, nil, func(s *TestAgent) {
   120  		req, err := http.NewRequest("GET", "/v1/client/fs/ls/", nil)
   121  		require.Nil(err)
   122  		respW := httptest.NewRecorder()
   123  		_, err = s.Server.DirectoryListRequest(respW, req)
   124  		require.EqualError(err, allocIDNotPresentErr.Error())
   125  	})
   126  }
   127  
   128  func TestHTTP_FS_Stat_MissingParams(t *testing.T) {
   129  	t.Parallel()
   130  	require := require.New(t)
   131  	httpTest(t, nil, func(s *TestAgent) {
   132  		req, err := http.NewRequest("GET", "/v1/client/fs/stat/", nil)
   133  		require.Nil(err)
   134  		respW := httptest.NewRecorder()
   135  
   136  		_, err = s.Server.FileStatRequest(respW, req)
   137  		require.EqualError(err, allocIDNotPresentErr.Error())
   138  
   139  		req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil)
   140  		require.Nil(err)
   141  		respW = httptest.NewRecorder()
   142  
   143  		_, err = s.Server.FileStatRequest(respW, req)
   144  		require.EqualError(err, fileNameNotPresentErr.Error())
   145  	})
   146  }
   147  
   148  func TestHTTP_FS_ReadAt_MissingParams(t *testing.T) {
   149  	t.Parallel()
   150  	require := require.New(t)
   151  	httpTest(t, nil, func(s *TestAgent) {
   152  		req, err := http.NewRequest("GET", "/v1/client/fs/readat/", nil)
   153  		require.NoError(err)
   154  
   155  		_, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req)
   156  		require.Error(err)
   157  
   158  		req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo", nil)
   159  		require.NoError(err)
   160  
   161  		_, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req)
   162  		require.Error(err)
   163  
   164  		req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo?path=/path/to/file", nil)
   165  		require.NoError(err)
   166  
   167  		_, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req)
   168  		require.Error(err)
   169  	})
   170  }
   171  
   172  func TestHTTP_FS_Cat_MissingParams(t *testing.T) {
   173  	t.Parallel()
   174  	require := require.New(t)
   175  	httpTest(t, nil, func(s *TestAgent) {
   176  		req, err := http.NewRequest("GET", "/v1/client/fs/cat/", nil)
   177  		require.Nil(err)
   178  		respW := httptest.NewRecorder()
   179  
   180  		_, err = s.Server.FileCatRequest(respW, req)
   181  		require.EqualError(err, allocIDNotPresentErr.Error())
   182  
   183  		req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil)
   184  		require.Nil(err)
   185  		respW = httptest.NewRecorder()
   186  
   187  		_, err = s.Server.FileCatRequest(respW, req)
   188  		require.EqualError(err, fileNameNotPresentErr.Error())
   189  	})
   190  }
   191  
   192  func TestHTTP_FS_Stream_MissingParams(t *testing.T) {
   193  	t.Parallel()
   194  	require := require.New(t)
   195  	httpTest(t, nil, func(s *TestAgent) {
   196  		req, err := http.NewRequest("GET", "/v1/client/fs/stream/", nil)
   197  		require.NoError(err)
   198  		respW := httptest.NewRecorder()
   199  
   200  		_, err = s.Server.Stream(respW, req)
   201  		require.EqualError(err, allocIDNotPresentErr.Error())
   202  
   203  		req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo", nil)
   204  		require.NoError(err)
   205  		respW = httptest.NewRecorder()
   206  
   207  		_, err = s.Server.Stream(respW, req)
   208  		require.EqualError(err, fileNameNotPresentErr.Error())
   209  
   210  		req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo?path=/path/to/file", nil)
   211  		require.NoError(err)
   212  		respW = httptest.NewRecorder()
   213  
   214  		_, err = s.Server.Stream(respW, req)
   215  		require.Error(err)
   216  		require.Contains(err.Error(), "alloc lookup failed")
   217  	})
   218  }
   219  
   220  // TestHTTP_FS_Logs_MissingParams asserts proper error codes and messages are
   221  // returned for incorrect parameters (eg missing tasks).
   222  func TestHTTP_FS_Logs_MissingParams(t *testing.T) {
   223  	t.Parallel()
   224  	require := require.New(t)
   225  	httpTest(t, nil, func(s *TestAgent) {
   226  		// AllocID Not Present
   227  		req, err := http.NewRequest("GET", "/v1/client/fs/logs/", nil)
   228  		require.NoError(err)
   229  		respW := httptest.NewRecorder()
   230  
   231  		s.Server.mux.ServeHTTP(respW, req)
   232  		require.Equal(respW.Body.String(), allocIDNotPresentErr.Error())
   233  		require.Equal(400, respW.Code)
   234  
   235  		// Task Not Present
   236  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo", nil)
   237  		require.NoError(err)
   238  		respW = httptest.NewRecorder()
   239  
   240  		s.Server.mux.ServeHTTP(respW, req)
   241  		require.Equal(respW.Body.String(), taskNotPresentErr.Error())
   242  		require.Equal(400, respW.Code)
   243  
   244  		// Log Type Not Present
   245  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo", nil)
   246  		require.NoError(err)
   247  		respW = httptest.NewRecorder()
   248  
   249  		s.Server.mux.ServeHTTP(respW, req)
   250  		require.Equal(respW.Body.String(), logTypeNotPresentErr.Error())
   251  		require.Equal(400, respW.Code)
   252  
   253  		// case where all parameters are set but alloc isn't found
   254  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo&type=stdout", nil)
   255  		require.NoError(err)
   256  		respW = httptest.NewRecorder()
   257  
   258  		s.Server.mux.ServeHTTP(respW, req)
   259  		require.Equal(500, respW.Code)
   260  		require.Contains(respW.Body.String(), "alloc lookup failed")
   261  	})
   262  }
   263  
   264  func TestHTTP_FS_List(t *testing.T) {
   265  	t.Parallel()
   266  	require := require.New(t)
   267  	httpTest(t, nil, func(s *TestAgent) {
   268  		a := mockFSAlloc(s.client.NodeID(), nil)
   269  		addAllocToClient(s, a, terminalClientAlloc)
   270  
   271  		req, err := http.NewRequest("GET", "/v1/client/fs/ls/"+a.ID, nil)
   272  		require.Nil(err)
   273  		respW := httptest.NewRecorder()
   274  		raw, err := s.Server.DirectoryListRequest(respW, req)
   275  		require.Nil(err)
   276  
   277  		files, ok := raw.([]*cstructs.AllocFileInfo)
   278  		require.True(ok)
   279  		require.NotEmpty(files)
   280  		require.True(files[0].IsDir)
   281  	})
   282  }
   283  
   284  func TestHTTP_FS_Stat(t *testing.T) {
   285  	t.Parallel()
   286  	require := require.New(t)
   287  	httpTest(t, nil, func(s *TestAgent) {
   288  		a := mockFSAlloc(s.client.NodeID(), nil)
   289  		addAllocToClient(s, a, terminalClientAlloc)
   290  
   291  		path := fmt.Sprintf("/v1/client/fs/stat/%s?path=alloc/", a.ID)
   292  		req, err := http.NewRequest("GET", path, nil)
   293  		require.Nil(err)
   294  		respW := httptest.NewRecorder()
   295  		raw, err := s.Server.FileStatRequest(respW, req)
   296  		require.Nil(err)
   297  
   298  		info, ok := raw.(*cstructs.AllocFileInfo)
   299  		require.True(ok)
   300  		require.NotNil(info)
   301  		require.True(info.IsDir)
   302  	})
   303  }
   304  
   305  func TestHTTP_FS_ReadAt(t *testing.T) {
   306  	t.Parallel()
   307  	require := require.New(t)
   308  	httpTest(t, nil, func(s *TestAgent) {
   309  		a := mockFSAlloc(s.client.NodeID(), nil)
   310  		addAllocToClient(s, a, terminalClientAlloc)
   311  
   312  		offset := 1
   313  		limit := 3
   314  		expectation := defaultLoggerMockDriverStdout[offset : offset+limit]
   315  		path := fmt.Sprintf("/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=%d&limit=%d",
   316  			a.ID, offset, limit)
   317  
   318  		req, err := http.NewRequest("GET", path, nil)
   319  		require.Nil(err)
   320  		respW := httptest.NewRecorder()
   321  		_, err = s.Server.FileReadAtRequest(respW, req)
   322  		require.Nil(err)
   323  
   324  		output, err := ioutil.ReadAll(respW.Result().Body)
   325  		require.Nil(err)
   326  		require.EqualValues(expectation, output)
   327  	})
   328  }
   329  
   330  // TestHTTP_FS_ReadAt_XSS asserts that the readat API is safe from XSS.
   331  func TestHTTP_FS_ReadAt_XSS(t *testing.T) {
   332  	t.Parallel()
   333  	httpTest(t, nil, func(s *TestAgent) {
   334  		a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver)
   335  		addAllocToClient(s, a, terminalClientAlloc)
   336  
   337  		path := fmt.Sprintf("%s/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=0&limit=%d",
   338  			s.HTTPAddr(), a.ID, len(xssLoggerMockDriverStdout))
   339  		resp, err := http.DefaultClient.Get(path)
   340  		require.NoError(t, err)
   341  		defer resp.Body.Close()
   342  
   343  		buf, err := ioutil.ReadAll(resp.Body)
   344  		require.NoError(t, err)
   345  		require.Equal(t, xssLoggerMockDriverStdout, string(buf))
   346  
   347  		require.Equal(t, []string{"text/plain"}, resp.Header.Values("Content-Type"))
   348  		require.Equal(t, []string{"nosniff"}, resp.Header.Values("X-Content-Type-Options"))
   349  		require.Equal(t, []string{"1; mode=block"}, resp.Header.Values("X-XSS-Protection"))
   350  		require.Equal(t, []string{"default-src 'none'; style-src 'unsafe-inline'; sandbox"},
   351  			resp.Header.Values("Content-Security-Policy"))
   352  	})
   353  }
   354  
   355  func TestHTTP_FS_Cat(t *testing.T) {
   356  	t.Parallel()
   357  	require := require.New(t)
   358  	httpTest(t, nil, func(s *TestAgent) {
   359  		a := mockFSAlloc(s.client.NodeID(), nil)
   360  		addAllocToClient(s, a, terminalClientAlloc)
   361  
   362  		path := fmt.Sprintf("/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", a.ID)
   363  
   364  		req, err := http.NewRequest("GET", path, nil)
   365  		require.Nil(err)
   366  		respW := httptest.NewRecorder()
   367  		_, err = s.Server.FileCatRequest(respW, req)
   368  		require.Nil(err)
   369  
   370  		output, err := ioutil.ReadAll(respW.Result().Body)
   371  		require.Nil(err)
   372  		require.EqualValues(defaultLoggerMockDriverStdout, output)
   373  	})
   374  }
   375  
   376  // TestHTTP_FS_Cat_XSS asserts that the cat API is safe from XSS.
   377  func TestHTTP_FS_Cat_XSS(t *testing.T) {
   378  	t.Parallel()
   379  	httpTest(t, nil, func(s *TestAgent) {
   380  		a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver)
   381  		addAllocToClient(s, a, terminalClientAlloc)
   382  
   383  		path := fmt.Sprintf("%s/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", s.HTTPAddr(), a.ID)
   384  		resp, err := http.DefaultClient.Get(path)
   385  		require.NoError(t, err)
   386  		defer resp.Body.Close()
   387  
   388  		buf, err := ioutil.ReadAll(resp.Body)
   389  		require.NoError(t, err)
   390  		require.Equal(t, xssLoggerMockDriverStdout, string(buf))
   391  
   392  		require.Equal(t, []string{"text/plain"}, resp.Header.Values("Content-Type"))
   393  		require.Equal(t, []string{"nosniff"}, resp.Header.Values("X-Content-Type-Options"))
   394  		require.Equal(t, []string{"1; mode=block"}, resp.Header.Values("X-XSS-Protection"))
   395  		require.Equal(t, []string{"default-src 'none'; style-src 'unsafe-inline'; sandbox"},
   396  			resp.Header.Values("Content-Security-Policy"))
   397  	})
   398  }
   399  
   400  func TestHTTP_FS_Stream_NoFollow(t *testing.T) {
   401  	t.Parallel()
   402  	require := require.New(t)
   403  	httpTest(t, nil, func(s *TestAgent) {
   404  		a := mockFSAlloc(s.client.NodeID(), nil)
   405  		addAllocToClient(s, a, terminalClientAlloc)
   406  
   407  		offset := 4
   408  		expectation := base64.StdEncoding.EncodeToString(
   409  			[]byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]))
   410  		path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end&follow=false",
   411  			a.ID, offset)
   412  
   413  		req, err := http.NewRequest("GET", path, nil)
   414  		require.Nil(err)
   415  		respW := testutil.NewResponseRecorder()
   416  		doneCh := make(chan struct{})
   417  		go func() {
   418  			_, err = s.Server.Stream(respW, req)
   419  			require.Nil(err)
   420  			close(doneCh)
   421  		}()
   422  
   423  		out := ""
   424  		testutil.WaitForResult(func() (bool, error) {
   425  			output, err := ioutil.ReadAll(respW)
   426  			if err != nil {
   427  				return false, err
   428  			}
   429  
   430  			out += string(output)
   431  			return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation)
   432  		}, func(err error) {
   433  			t.Fatal(err)
   434  		})
   435  
   436  		select {
   437  		case <-doneCh:
   438  		case <-time.After(1 * time.Second):
   439  			t.Fatal("should close but did not")
   440  		}
   441  	})
   442  }
   443  
   444  // TestHTTP_FS_Stream_NoFollow_XSS asserts that the stream API is safe from XSS.
   445  func TestHTTP_FS_Stream_NoFollow_XSS(t *testing.T) {
   446  	t.Parallel()
   447  	httpTest(t, nil, func(s *TestAgent) {
   448  		a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver)
   449  		addAllocToClient(s, a, terminalClientAlloc)
   450  
   451  		path := fmt.Sprintf("%s/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&follow=false",
   452  			s.HTTPAddr(), a.ID)
   453  		resp, err := http.DefaultClient.Get(path)
   454  		require.NoError(t, err)
   455  		defer resp.Body.Close()
   456  
   457  		buf, err := ioutil.ReadAll(resp.Body)
   458  		require.NoError(t, err)
   459  		expected := `{"Data":"PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pOzwvc2NyaXB0Pg==","File":"alloc/logs/web.stdout.0","Offset":40}`
   460  		require.Equal(t, expected, string(buf))
   461  	})
   462  }
   463  
   464  func TestHTTP_FS_Stream_Follow(t *testing.T) {
   465  	t.Parallel()
   466  	require := require.New(t)
   467  	httpTest(t, nil, func(s *TestAgent) {
   468  		a := mockFSAlloc(s.client.NodeID(), nil)
   469  		addAllocToClient(s, a, terminalClientAlloc)
   470  
   471  		offset := 4
   472  		expectation := base64.StdEncoding.EncodeToString(
   473  			[]byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]))
   474  		path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end",
   475  			a.ID, offset)
   476  
   477  		req, err := http.NewRequest("GET", path, nil)
   478  		require.Nil(err)
   479  		respW := httptest.NewRecorder()
   480  		doneCh := make(chan struct{})
   481  		go func() {
   482  			_, err = s.Server.Stream(respW, req)
   483  			require.Nil(err)
   484  			close(doneCh)
   485  		}()
   486  
   487  		out := ""
   488  		testutil.WaitForResult(func() (bool, error) {
   489  			output, err := ioutil.ReadAll(respW.Body)
   490  			if err != nil {
   491  				return false, err
   492  			}
   493  
   494  			out += string(output)
   495  			return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation)
   496  		}, func(err error) {
   497  			t.Fatal(err)
   498  		})
   499  
   500  		select {
   501  		case <-doneCh:
   502  			t.Fatal("shouldn't close")
   503  		case <-time.After(1 * time.Second):
   504  		}
   505  	})
   506  }
   507  
   508  func TestHTTP_FS_Logs(t *testing.T) {
   509  	t.Parallel()
   510  	require := require.New(t)
   511  	httpTest(t, nil, func(s *TestAgent) {
   512  		a := mockFSAlloc(s.client.NodeID(), nil)
   513  		addAllocToClient(s, a, terminalClientAlloc)
   514  
   515  		offset := 4
   516  		expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]
   517  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true",
   518  			a.ID, offset)
   519  
   520  		req, err := http.NewRequest("GET", path, nil)
   521  		require.Nil(err)
   522  		respW := testutil.NewResponseRecorder()
   523  		go func() {
   524  			_, err = s.Server.Logs(respW, req)
   525  			require.Nil(err)
   526  		}()
   527  
   528  		out := ""
   529  		testutil.WaitForResult(func() (bool, error) {
   530  			output, err := ioutil.ReadAll(respW)
   531  			if err != nil {
   532  				return false, err
   533  			}
   534  
   535  			out += string(output)
   536  			return out == expectation, fmt.Errorf("%q != %q", out, expectation)
   537  		}, func(err error) {
   538  			t.Fatal(err)
   539  		})
   540  	})
   541  }
   542  
   543  // TestHTTP_FS_Logs_XSS asserts that the logs endpoint always returns
   544  // text/plain or application/json content regardless of whether the logs are
   545  // HTML+Javascript or not.
   546  func TestHTTP_FS_Logs_XSS(t *testing.T) {
   547  	t.Parallel()
   548  	httpTest(t, nil, func(s *TestAgent) {
   549  		a := mockFSAlloc(s.client.NodeID(), xssLoggerMockDriver)
   550  		addAllocToClient(s, a, terminalClientAlloc)
   551  
   552  		// Must make a "real" request to ensure Go's default content
   553  		// type detection does not detect text/html
   554  		path := fmt.Sprintf("%s/v1/client/fs/logs/%s?type=stdout&task=web&plain=true", s.HTTPAddr(), a.ID)
   555  		resp, err := http.DefaultClient.Get(path)
   556  		require.NoError(t, err)
   557  		defer resp.Body.Close()
   558  
   559  		buf, err := ioutil.ReadAll(resp.Body)
   560  		require.NoError(t, err)
   561  		require.Equal(t, xssLoggerMockDriverStdout, string(buf))
   562  
   563  		require.Equal(t, []string{"text/plain"}, resp.Header.Values("Content-Type"))
   564  	})
   565  }
   566  
   567  func TestHTTP_FS_Logs_Follow(t *testing.T) {
   568  	t.Parallel()
   569  	require := require.New(t)
   570  	httpTest(t, nil, func(s *TestAgent) {
   571  		a := mockFSAlloc(s.client.NodeID(), nil)
   572  		addAllocToClient(s, a, terminalClientAlloc)
   573  
   574  		offset := 4
   575  		expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]
   576  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true&follow=true",
   577  			a.ID, offset)
   578  
   579  		req, err := http.NewRequest("GET", path, nil)
   580  		require.Nil(err)
   581  		respW := testutil.NewResponseRecorder()
   582  		errCh := make(chan error, 1)
   583  		go func() {
   584  			_, err := s.Server.Logs(respW, req)
   585  			errCh <- err
   586  		}()
   587  
   588  		out := ""
   589  		testutil.WaitForResult(func() (bool, error) {
   590  			output, err := ioutil.ReadAll(respW)
   591  			if err != nil {
   592  				return false, err
   593  			}
   594  
   595  			out += string(output)
   596  			return out == expectation, fmt.Errorf("%q != %q", out, expectation)
   597  		}, func(err error) {
   598  			t.Fatal(err)
   599  		})
   600  
   601  		select {
   602  		case err := <-errCh:
   603  			t.Fatalf("shouldn't exit: %v", err)
   604  		case <-time.After(1 * time.Second):
   605  		}
   606  	})
   607  }
   608  
   609  func TestHTTP_FS_Logs_PropagatesErrors(t *testing.T) {
   610  	t.Parallel()
   611  	httpTest(t, nil, func(s *TestAgent) {
   612  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=0&origin=end&plain=true",
   613  			uuid.Generate())
   614  
   615  		req, err := http.NewRequest("GET", path, nil)
   616  		require.NoError(t, err)
   617  		respW := testutil.NewResponseRecorder()
   618  
   619  		_, err = s.Server.Logs(respW, req)
   620  		require.Error(t, err)
   621  
   622  		_, ok := err.(HTTPCodedError)
   623  		require.Truef(t, ok, "expected a coded error but found: %#+v", err)
   624  	})
   625  }