github.com/bigcommerce/nomad@v0.9.3-bc/command/agent/fs_endpoint_test.go (about)

     1  package agent
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	cstructs "github.com/hashicorp/nomad/client/structs"
    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.Nil(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.Nil(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.Nil(err)
   207  		respW = httptest.NewRecorder()
   208  
   209  		_, err = s.Server.Stream(respW, req)
   210  		require.Nil(err)
   211  	})
   212  }
   213  
   214  // TestHTTP_FS_Logs_MissingParams asserts proper error codes and messages are
   215  // returned for incorrect parameters (eg missing tasks).
   216  func TestHTTP_FS_Logs_MissingParams(t *testing.T) {
   217  	t.Parallel()
   218  	require := require.New(t)
   219  	httpTest(t, nil, func(s *TestAgent) {
   220  		// AllocID Not Present
   221  		req, err := http.NewRequest("GET", "/v1/client/fs/logs/", nil)
   222  		require.Nil(err)
   223  		respW := httptest.NewRecorder()
   224  
   225  		s.Server.mux.ServeHTTP(respW, req)
   226  		require.Equal(respW.Body.String(), allocIDNotPresentErr.Error())
   227  		require.Equal(500, respW.Code) // 500 for backward compat
   228  
   229  		// Task Not Present
   230  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo", nil)
   231  		require.Nil(err)
   232  		respW = httptest.NewRecorder()
   233  
   234  		s.Server.mux.ServeHTTP(respW, req)
   235  		require.Equal(respW.Body.String(), taskNotPresentErr.Error())
   236  		require.Equal(500, respW.Code) // 500 for backward compat
   237  
   238  		// Log Type Not Present
   239  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo", nil)
   240  		require.Nil(err)
   241  		respW = httptest.NewRecorder()
   242  
   243  		s.Server.mux.ServeHTTP(respW, req)
   244  		require.Equal(respW.Body.String(), logTypeNotPresentErr.Error())
   245  		require.Equal(500, respW.Code) // 500 for backward compat
   246  
   247  		// Ok
   248  		req, err = http.NewRequest("GET", "/v1/client/fs/logs/foo?task=foo&type=stdout", nil)
   249  		require.Nil(err)
   250  		respW = httptest.NewRecorder()
   251  
   252  		s.Server.mux.ServeHTTP(respW, req)
   253  		require.Equal(200, respW.Code)
   254  	})
   255  }
   256  
   257  func TestHTTP_FS_List(t *testing.T) {
   258  	t.Parallel()
   259  	require := require.New(t)
   260  	httpTest(t, nil, func(s *TestAgent) {
   261  		a := mockFSAlloc(s.client.NodeID(), nil)
   262  		addAllocToClient(s, a, terminalClientAlloc)
   263  
   264  		req, err := http.NewRequest("GET", "/v1/client/fs/ls/"+a.ID, nil)
   265  		require.Nil(err)
   266  		respW := httptest.NewRecorder()
   267  		raw, err := s.Server.DirectoryListRequest(respW, req)
   268  		require.Nil(err)
   269  
   270  		files, ok := raw.([]*cstructs.AllocFileInfo)
   271  		require.True(ok)
   272  		require.NotEmpty(files)
   273  		require.True(files[0].IsDir)
   274  	})
   275  }
   276  
   277  func TestHTTP_FS_Stat(t *testing.T) {
   278  	t.Parallel()
   279  	require := require.New(t)
   280  	httpTest(t, nil, func(s *TestAgent) {
   281  		a := mockFSAlloc(s.client.NodeID(), nil)
   282  		addAllocToClient(s, a, terminalClientAlloc)
   283  
   284  		path := fmt.Sprintf("/v1/client/fs/stat/%s?path=alloc/", a.ID)
   285  		req, err := http.NewRequest("GET", path, nil)
   286  		require.Nil(err)
   287  		respW := httptest.NewRecorder()
   288  		raw, err := s.Server.FileStatRequest(respW, req)
   289  		require.Nil(err)
   290  
   291  		info, ok := raw.(*cstructs.AllocFileInfo)
   292  		require.True(ok)
   293  		require.NotNil(info)
   294  		require.True(info.IsDir)
   295  	})
   296  }
   297  
   298  func TestHTTP_FS_ReadAt(t *testing.T) {
   299  	t.Parallel()
   300  	require := require.New(t)
   301  	httpTest(t, nil, func(s *TestAgent) {
   302  		a := mockFSAlloc(s.client.NodeID(), nil)
   303  		addAllocToClient(s, a, terminalClientAlloc)
   304  
   305  		offset := 1
   306  		limit := 3
   307  		expectation := defaultLoggerMockDriverStdout[offset : offset+limit]
   308  		path := fmt.Sprintf("/v1/client/fs/readat/%s?path=alloc/logs/web.stdout.0&offset=%d&limit=%d",
   309  			a.ID, offset, limit)
   310  
   311  		req, err := http.NewRequest("GET", path, nil)
   312  		require.Nil(err)
   313  		respW := httptest.NewRecorder()
   314  		_, err = s.Server.FileReadAtRequest(respW, req)
   315  		require.Nil(err)
   316  
   317  		output, err := ioutil.ReadAll(respW.Result().Body)
   318  		require.Nil(err)
   319  		require.EqualValues(expectation, output)
   320  	})
   321  }
   322  
   323  func TestHTTP_FS_Cat(t *testing.T) {
   324  	t.Parallel()
   325  	require := require.New(t)
   326  	httpTest(t, nil, func(s *TestAgent) {
   327  		a := mockFSAlloc(s.client.NodeID(), nil)
   328  		addAllocToClient(s, a, terminalClientAlloc)
   329  
   330  		path := fmt.Sprintf("/v1/client/fs/cat/%s?path=alloc/logs/web.stdout.0", a.ID)
   331  
   332  		req, err := http.NewRequest("GET", path, nil)
   333  		require.Nil(err)
   334  		respW := httptest.NewRecorder()
   335  		_, err = s.Server.FileCatRequest(respW, req)
   336  		require.Nil(err)
   337  
   338  		output, err := ioutil.ReadAll(respW.Result().Body)
   339  		require.Nil(err)
   340  		require.EqualValues(defaultLoggerMockDriverStdout, output)
   341  	})
   342  }
   343  
   344  func TestHTTP_FS_Stream(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",
   355  			a.ID, offset)
   356  
   357  		p, _ := io.Pipe()
   358  
   359  		req, err := http.NewRequest("GET", path, p)
   360  		require.Nil(err)
   361  		respW := httptest.NewRecorder()
   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.Body)
   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  			t.Fatal("shouldn't close")
   385  		case <-time.After(1 * time.Second):
   386  		}
   387  
   388  		p.Close()
   389  	})
   390  }
   391  
   392  func TestHTTP_FS_Logs(t *testing.T) {
   393  	t.Parallel()
   394  	require := require.New(t)
   395  	httpTest(t, nil, func(s *TestAgent) {
   396  		a := mockFSAlloc(s.client.NodeID(), nil)
   397  		addAllocToClient(s, a, terminalClientAlloc)
   398  
   399  		offset := 4
   400  		expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]
   401  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true",
   402  			a.ID, offset)
   403  
   404  		p, _ := io.Pipe()
   405  		req, err := http.NewRequest("GET", path, p)
   406  		require.Nil(err)
   407  		respW := testutil.NewResponseRecorder()
   408  		go func() {
   409  			_, err = s.Server.Logs(respW, req)
   410  			require.Nil(err)
   411  		}()
   412  
   413  		out := ""
   414  		testutil.WaitForResult(func() (bool, error) {
   415  			output, err := ioutil.ReadAll(respW)
   416  			if err != nil {
   417  				return false, err
   418  			}
   419  
   420  			out += string(output)
   421  			return out == expectation, fmt.Errorf("%q != %q", out, expectation)
   422  		}, func(err error) {
   423  			t.Fatal(err)
   424  		})
   425  
   426  		p.Close()
   427  	})
   428  }
   429  
   430  func TestHTTP_FS_Logs_Follow(t *testing.T) {
   431  	t.Parallel()
   432  	require := require.New(t)
   433  	httpTest(t, nil, func(s *TestAgent) {
   434  		a := mockFSAlloc(s.client.NodeID(), nil)
   435  		addAllocToClient(s, a, terminalClientAlloc)
   436  
   437  		offset := 4
   438  		expectation := defaultLoggerMockDriverStdout[len(defaultLoggerMockDriverStdout)-offset:]
   439  		path := fmt.Sprintf("/v1/client/fs/logs/%s?type=stdout&task=web&offset=%d&origin=end&plain=true&follow=true",
   440  			a.ID, offset)
   441  
   442  		p, _ := io.Pipe()
   443  		req, err := http.NewRequest("GET", path, p)
   444  		require.Nil(err)
   445  		respW := testutil.NewResponseRecorder()
   446  		errCh := make(chan error)
   447  		go func() {
   448  			_, err := s.Server.Logs(respW, req)
   449  			errCh <- err
   450  		}()
   451  
   452  		out := ""
   453  		testutil.WaitForResult(func() (bool, error) {
   454  			output, err := ioutil.ReadAll(respW)
   455  			if err != nil {
   456  				return false, err
   457  			}
   458  
   459  			out += string(output)
   460  			return out == expectation, fmt.Errorf("%q != %q", out, expectation)
   461  		}, func(err error) {
   462  			t.Fatal(err)
   463  		})
   464  
   465  		select {
   466  		case err := <-errCh:
   467  			t.Fatalf("shouldn't exit: %v", err)
   468  		case <-time.After(1 * time.Second):
   469  		}
   470  
   471  		p.Close()
   472  	})
   473  }