github.com/karlem/nomad@v0.10.2-rc1/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  )
    24  
    25  var (
    26  	defaultLoggerMockDriver = map[string]interface{}{
    27  		"run_for":       "2s",
    28  		"stdout_string": defaultLoggerMockDriverStdout,
    29  	}
    30  )
    31  
    32  type clientAllocWaiter int
    33  
    34  const (
    35  	noWaitClientAlloc clientAllocWaiter = iota
    36  	runningClientAlloc
    37  	terminalClientAlloc
    38  )
    39  
    40  func addAllocToClient(agent *TestAgent, alloc *structs.Allocation, wait clientAllocWaiter) {
    41  	require := require.New(agent.T)
    42  
    43  	// Wait for the client to connect
    44  	testutil.WaitForResult(func() (bool, error) {
    45  		node, err := agent.server.State().NodeByID(nil, agent.client.NodeID())
    46  		if err != nil {
    47  			return false, err
    48  		}
    49  		if node == nil {
    50  			return false, fmt.Errorf("unknown node")
    51  		}
    52  
    53  		return node.Status == structs.NodeStatusReady, fmt.Errorf("bad node status")
    54  	}, func(err error) {
    55  		agent.T.Fatal(err)
    56  	})
    57  
    58  	// Upsert the allocation
    59  	state := agent.server.State()
    60  	require.Nil(state.UpsertJob(999, alloc.Job))
    61  	require.Nil(state.UpsertAllocs(1003, []*structs.Allocation{alloc}))
    62  
    63  	if wait == noWaitClientAlloc {
    64  		return
    65  	}
    66  
    67  	// Wait for the client to run the allocation
    68  	testutil.WaitForResult(func() (bool, error) {
    69  		alloc, err := state.AllocByID(nil, alloc.ID)
    70  		if err != nil {
    71  			return false, err
    72  		}
    73  		if alloc == nil {
    74  			return false, fmt.Errorf("unknown alloc")
    75  		}
    76  
    77  		expectation := alloc.ClientStatus == structs.AllocClientStatusComplete ||
    78  			alloc.ClientStatus == structs.AllocClientStatusFailed
    79  		if wait == runningClientAlloc {
    80  			expectation = expectation || alloc.ClientStatus == structs.AllocClientStatusRunning
    81  		}
    82  
    83  		if !expectation {
    84  			return false, fmt.Errorf("alloc client status: %v", alloc.ClientStatus)
    85  		}
    86  
    87  		return true, nil
    88  	}, func(err error) {
    89  		agent.T.Fatal(err)
    90  	})
    91  }
    92  
    93  // mockFSAlloc returns a suitable mock alloc for testing the fs system. If
    94  // config isn't provided, the defaultLoggerMockDriver config is used.
    95  func mockFSAlloc(nodeID string, config map[string]interface{}) *structs.Allocation {
    96  	a := mock.Alloc()
    97  	a.NodeID = nodeID
    98  	a.Job.Type = structs.JobTypeBatch
    99  	a.Job.TaskGroups[0].Count = 1
   100  	a.Job.TaskGroups[0].Tasks[0].Driver = "mock_driver"
   101  
   102  	if config != nil {
   103  		a.Job.TaskGroups[0].Tasks[0].Config = config
   104  	} else {
   105  		a.Job.TaskGroups[0].Tasks[0].Config = defaultLoggerMockDriver
   106  	}
   107  
   108  	return a
   109  }
   110  
   111  func TestHTTP_FS_List_MissingParams(t *testing.T) {
   112  	t.Parallel()
   113  	require := require.New(t)
   114  	httpTest(t, nil, func(s *TestAgent) {
   115  		req, err := http.NewRequest("GET", "/v1/client/fs/ls/", nil)
   116  		require.Nil(err)
   117  		respW := httptest.NewRecorder()
   118  		_, err = s.Server.DirectoryListRequest(respW, req)
   119  		require.EqualError(err, allocIDNotPresentErr.Error())
   120  	})
   121  }
   122  
   123  func TestHTTP_FS_Stat_MissingParams(t *testing.T) {
   124  	t.Parallel()
   125  	require := require.New(t)
   126  	httpTest(t, nil, func(s *TestAgent) {
   127  		req, err := http.NewRequest("GET", "/v1/client/fs/stat/", nil)
   128  		require.Nil(err)
   129  		respW := httptest.NewRecorder()
   130  
   131  		_, err = s.Server.FileStatRequest(respW, req)
   132  		require.EqualError(err, allocIDNotPresentErr.Error())
   133  
   134  		req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil)
   135  		require.Nil(err)
   136  		respW = httptest.NewRecorder()
   137  
   138  		_, err = s.Server.FileStatRequest(respW, req)
   139  		require.EqualError(err, fileNameNotPresentErr.Error())
   140  	})
   141  }
   142  
   143  func TestHTTP_FS_ReadAt_MissingParams(t *testing.T) {
   144  	t.Parallel()
   145  	require := require.New(t)
   146  	httpTest(t, nil, func(s *TestAgent) {
   147  		req, err := http.NewRequest("GET", "/v1/client/fs/readat/", nil)
   148  		require.NoError(err)
   149  
   150  		_, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req)
   151  		require.Error(err)
   152  
   153  		req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo", nil)
   154  		require.NoError(err)
   155  
   156  		_, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req)
   157  		require.Error(err)
   158  
   159  		req, err = http.NewRequest("GET", "/v1/client/fs/readat/foo?path=/path/to/file", nil)
   160  		require.NoError(err)
   161  
   162  		_, err = s.Server.FileReadAtRequest(httptest.NewRecorder(), req)
   163  		require.Error(err)
   164  	})
   165  }
   166  
   167  func TestHTTP_FS_Cat_MissingParams(t *testing.T) {
   168  	t.Parallel()
   169  	require := require.New(t)
   170  	httpTest(t, nil, func(s *TestAgent) {
   171  		req, err := http.NewRequest("GET", "/v1/client/fs/cat/", nil)
   172  		require.Nil(err)
   173  		respW := httptest.NewRecorder()
   174  
   175  		_, err = s.Server.FileCatRequest(respW, req)
   176  		require.EqualError(err, allocIDNotPresentErr.Error())
   177  
   178  		req, err = http.NewRequest("GET", "/v1/client/fs/stat/foo", nil)
   179  		require.Nil(err)
   180  		respW = httptest.NewRecorder()
   181  
   182  		_, err = s.Server.FileCatRequest(respW, req)
   183  		require.EqualError(err, fileNameNotPresentErr.Error())
   184  	})
   185  }
   186  
   187  func TestHTTP_FS_Stream_MissingParams(t *testing.T) {
   188  	t.Parallel()
   189  	require := require.New(t)
   190  	httpTest(t, nil, func(s *TestAgent) {
   191  		req, err := http.NewRequest("GET", "/v1/client/fs/stream/", nil)
   192  		require.NoError(err)
   193  		respW := httptest.NewRecorder()
   194  
   195  		_, err = s.Server.Stream(respW, req)
   196  		require.EqualError(err, allocIDNotPresentErr.Error())
   197  
   198  		req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo", nil)
   199  		require.NoError(err)
   200  		respW = httptest.NewRecorder()
   201  
   202  		_, err = s.Server.Stream(respW, req)
   203  		require.EqualError(err, fileNameNotPresentErr.Error())
   204  
   205  		req, err = http.NewRequest("GET", "/v1/client/fs/stream/foo?path=/path/to/file", nil)
   206  		require.NoError(err)
   207  		respW = httptest.NewRecorder()
   208  
   209  		_, err = s.Server.Stream(respW, req)
   210  		require.Error(err)
   211  		require.Contains(err.Error(), "alloc lookup failed")
   212  	})
   213  }
   214  
   215  // TestHTTP_FS_Logs_MissingParams asserts proper error codes and messages are
   216  // returned for incorrect parameters (eg missing tasks).
   217  func TestHTTP_FS_Logs_MissingParams(t *testing.T) {
   218  	t.Parallel()
   219  	require := require.New(t)
   220  	httpTest(t, nil, func(s *TestAgent) {
   221  		// AllocID Not Present
   222  		req, err := http.NewRequest("GET", "/v1/client/fs/logs/", nil)
   223  		require.NoError(err)
   224  		respW := httptest.NewRecorder()
   225  
   226  		s.Server.mux.ServeHTTP(respW, req)
   227  		require.Equal(respW.Body.String(), allocIDNotPresentErr.Error())
   228  		require.Equal(400, respW.Code)
   229  
   230  		// Task Not Present
   231  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo", nil)
   232  		require.NoError(err)
   233  		respW = httptest.NewRecorder()
   234  
   235  		s.Server.mux.ServeHTTP(respW, req)
   236  		require.Equal(respW.Body.String(), taskNotPresentErr.Error())
   237  		require.Equal(400, respW.Code)
   238  
   239  		// Log Type Not Present
   240  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo", nil)
   241  		require.NoError(err)
   242  		respW = httptest.NewRecorder()
   243  
   244  		s.Server.mux.ServeHTTP(respW, req)
   245  		require.Equal(respW.Body.String(), logTypeNotPresentErr.Error())
   246  		require.Equal(400, respW.Code)
   247  
   248  		// case where all parameters are set but alloc isn't found
   249  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo&type=stdout", nil)
   250  		require.NoError(err)
   251  		respW = httptest.NewRecorder()
   252  
   253  		s.Server.mux.ServeHTTP(respW, req)
   254  		require.Equal(500, respW.Code)
   255  		require.Contains(respW.Body.String(), "alloc lookup failed")
   256  	})
   257  }
   258  
   259  func TestHTTP_FS_List(t *testing.T) {
   260  	t.Parallel()
   261  	require := require.New(t)
   262  	httpTest(t, nil, func(s *TestAgent) {
   263  		a := mockFSAlloc(s.client.NodeID(), nil)
   264  		addAllocToClient(s, a, terminalClientAlloc)
   265  
   266  		req, err := http.NewRequest("GET", "/v1/client/fs/ls/"+a.ID, nil)
   267  		require.Nil(err)
   268  		respW := httptest.NewRecorder()
   269  		raw, err := s.Server.DirectoryListRequest(respW, req)
   270  		require.Nil(err)
   271  
   272  		files, ok := raw.([]*cstructs.AllocFileInfo)
   273  		require.True(ok)
   274  		require.NotEmpty(files)
   275  		require.True(files[0].IsDir)
   276  	})
   277  }
   278  
   279  func TestHTTP_FS_Stat(t *testing.T) {
   280  	t.Parallel()
   281  	require := require.New(t)
   282  	httpTest(t, nil, func(s *TestAgent) {
   283  		a := mockFSAlloc(s.client.NodeID(), nil)
   284  		addAllocToClient(s, a, terminalClientAlloc)
   285  
   286  		path := fmt.Sprintf("/v1/client/fs/stat/%s?path=alloc/", a.ID)
   287  		req, err := http.NewRequest("GET", path, nil)
   288  		require.Nil(err)
   289  		respW := httptest.NewRecorder()
   290  		raw, err := s.Server.FileStatRequest(respW, req)
   291  		require.Nil(err)
   292  
   293  		info, ok := raw.(*cstructs.AllocFileInfo)
   294  		require.True(ok)
   295  		require.NotNil(info)
   296  		require.True(info.IsDir)
   297  	})
   298  }
   299  
   300  func TestHTTP_FS_ReadAt(t *testing.T) {
   301  	t.Parallel()
   302  	require := require.New(t)
   303  	httpTest(t, nil, func(s *TestAgent) {
   304  		a := mockFSAlloc(s.client.NodeID(), nil)
   305  		addAllocToClient(s, a, terminalClientAlloc)
   306  
   307  		offset := 1
   308  		limit := 3
   309  		expectation := defaultLoggerMockDriverStdout[offset : offset+limit]
   310  		path := fmt.Sprintf("/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=%d&limit=%d",
   311  			a.ID, offset, limit)
   312  
   313  		req, err := http.NewRequest("GET", path, nil)
   314  		require.Nil(err)
   315  		respW := httptest.NewRecorder()
   316  		_, err = s.Server.FileReadAtRequest(respW, req)
   317  		require.Nil(err)
   318  
   319  		output, err := ioutil.ReadAll(respW.Result().Body)
   320  		require.Nil(err)
   321  		require.EqualValues(expectation, output)
   322  	})
   323  }
   324  
   325  func TestHTTP_FS_Cat(t *testing.T) {
   326  	t.Parallel()
   327  	require := require.New(t)
   328  	httpTest(t, nil, func(s *TestAgent) {
   329  		a := mockFSAlloc(s.client.NodeID(), nil)
   330  		addAllocToClient(s, a, terminalClientAlloc)
   331  
   332  		path := fmt.Sprintf("/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", a.ID)
   333  
   334  		req, err := http.NewRequest("GET", path, nil)
   335  		require.Nil(err)
   336  		respW := httptest.NewRecorder()
   337  		_, err = s.Server.FileCatRequest(respW, req)
   338  		require.Nil(err)
   339  
   340  		output, err := ioutil.ReadAll(respW.Result().Body)
   341  		require.Nil(err)
   342  		require.EqualValues(defaultLoggerMockDriverStdout, output)
   343  	})
   344  }
   345  
   346  func TestHTTP_FS_Stream_NoFollow(t *testing.T) {
   347  	t.Parallel()
   348  	require := require.New(t)
   349  	httpTest(t, nil, func(s *TestAgent) {
   350  		a := mockFSAlloc(s.client.NodeID(), nil)
   351  		addAllocToClient(s, a, terminalClientAlloc)
   352  
   353  		offset := 4
   354  		expectation := base64.StdEncoding.EncodeToString(
   355  			[]byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]))
   356  		path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end&follow=false",
   357  			a.ID, offset)
   358  
   359  		req, err := http.NewRequest("GET", path, nil)
   360  		require.Nil(err)
   361  		respW := testutil.NewResponseRecorder()
   362  		doneCh := make(chan struct{})
   363  		go func() {
   364  			_, err = s.Server.Stream(respW, req)
   365  			require.Nil(err)
   366  			close(doneCh)
   367  		}()
   368  
   369  		out := ""
   370  		testutil.WaitForResult(func() (bool, error) {
   371  			output, err := ioutil.ReadAll(respW)
   372  			if err != nil {
   373  				return false, err
   374  			}
   375  
   376  			out += string(output)
   377  			return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation)
   378  		}, func(err error) {
   379  			t.Fatal(err)
   380  		})
   381  
   382  		select {
   383  		case <-doneCh:
   384  		case <-time.After(1 * time.Second):
   385  			t.Fatal("should close but did not")
   386  		}
   387  	})
   388  }
   389  
   390  func TestHTTP_FS_Stream_Follow(t *testing.T) {
   391  	t.Parallel()
   392  	require := require.New(t)
   393  	httpTest(t, nil, func(s *TestAgent) {
   394  		a := mockFSAlloc(s.client.NodeID(), nil)
   395  		addAllocToClient(s, a, terminalClientAlloc)
   396  
   397  		offset := 4
   398  		expectation := base64.StdEncoding.EncodeToString(
   399  			[]byte(defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]))
   400  		path := fmt.Sprintf("/v1/client/fs/stream/%s?path=alloc/logs/web.stdout.0&offset=%d&origin=end",
   401  			a.ID, offset)
   402  
   403  		req, err := http.NewRequest("GET", path, nil)
   404  		require.Nil(err)
   405  		respW := httptest.NewRecorder()
   406  		doneCh := make(chan struct{})
   407  		go func() {
   408  			_, err = s.Server.Stream(respW, req)
   409  			require.Nil(err)
   410  			close(doneCh)
   411  		}()
   412  
   413  		out := ""
   414  		testutil.WaitForResult(func() (bool, error) {
   415  			output, err := ioutil.ReadAll(respW.Body)
   416  			if err != nil {
   417  				return false, err
   418  			}
   419  
   420  			out += string(output)
   421  			return strings.Contains(out, expectation), fmt.Errorf("%q doesn't contain %q", out, expectation)
   422  		}, func(err error) {
   423  			t.Fatal(err)
   424  		})
   425  
   426  		select {
   427  		case <-doneCh:
   428  			t.Fatal("shouldn't close")
   429  		case <-time.After(1 * time.Second):
   430  		}
   431  	})
   432  }
   433  
   434  func TestHTTP_FS_Logs(t *testing.T) {
   435  	t.Parallel()
   436  	require := require.New(t)
   437  	httpTest(t, nil, func(s *TestAgent) {
   438  		a := mockFSAlloc(s.client.NodeID(), nil)
   439  		addAllocToClient(s, a, terminalClientAlloc)
   440  
   441  		offset := 4
   442  		expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]
   443  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true",
   444  			a.ID, offset)
   445  
   446  		req, err := http.NewRequest("GET", path, nil)
   447  		require.Nil(err)
   448  		respW := testutil.NewResponseRecorder()
   449  		go func() {
   450  			_, err = s.Server.Logs(respW, req)
   451  			require.Nil(err)
   452  		}()
   453  
   454  		out := ""
   455  		testutil.WaitForResult(func() (bool, error) {
   456  			output, err := ioutil.ReadAll(respW)
   457  			if err != nil {
   458  				return false, err
   459  			}
   460  
   461  			out += string(output)
   462  			return out == expectation, fmt.Errorf("%q != %q", out, expectation)
   463  		}, func(err error) {
   464  			t.Fatal(err)
   465  		})
   466  	})
   467  }
   468  
   469  func TestHTTP_FS_Logs_Follow(t *testing.T) {
   470  	t.Parallel()
   471  	require := require.New(t)
   472  	httpTest(t, nil, func(s *TestAgent) {
   473  		a := mockFSAlloc(s.client.NodeID(), nil)
   474  		addAllocToClient(s, a, terminalClientAlloc)
   475  
   476  		offset := 4
   477  		expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]
   478  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true&follow=true",
   479  			a.ID, offset)
   480  
   481  		req, err := http.NewRequest("GET", path, nil)
   482  		require.Nil(err)
   483  		respW := testutil.NewResponseRecorder()
   484  		errCh := make(chan error)
   485  		go func() {
   486  			_, err := s.Server.Logs(respW, req)
   487  			errCh <- err
   488  		}()
   489  
   490  		out := ""
   491  		testutil.WaitForResult(func() (bool, error) {
   492  			output, err := ioutil.ReadAll(respW)
   493  			if err != nil {
   494  				return false, err
   495  			}
   496  
   497  			out += string(output)
   498  			return out == expectation, fmt.Errorf("%q != %q", out, expectation)
   499  		}, func(err error) {
   500  			t.Fatal(err)
   501  		})
   502  
   503  		select {
   504  		case err := <-errCh:
   505  			t.Fatalf("shouldn't exit: %v", err)
   506  		case <-time.After(1 * time.Second):
   507  		}
   508  	})
   509  }
   510  
   511  func TestHTTP_FS_Logs_PropagatesErrors(t *testing.T) {
   512  	t.Parallel()
   513  	httpTest(t, nil, func(s *TestAgent) {
   514  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=0&origin=end&plain=true",
   515  			uuid.Generate())
   516  
   517  		req, err := http.NewRequest("GET", path, nil)
   518  		require.NoError(t, err)
   519  		respW := testutil.NewResponseRecorder()
   520  
   521  		_, err = s.Server.Logs(respW, req)
   522  		require.Error(t, err)
   523  
   524  		_, ok := err.(HTTPCodedError)
   525  		require.Truef(t, ok, "expected a coded error but found: %#+v", err)
   526  	})
   527  }