github.com/hernad/nomad@v1.6.112/command/agent/eval_endpoint_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package agent
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"net/url"
    11  	"testing"
    12  
    13  	"github.com/hernad/nomad/api"
    14  	"github.com/hernad/nomad/ci"
    15  	"github.com/hernad/nomad/helper/uuid"
    16  	"github.com/hernad/nomad/nomad/mock"
    17  	"github.com/hernad/nomad/nomad/structs"
    18  	"github.com/shoenig/test/must"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func TestHTTP_EvalList(t *testing.T) {
    23  	ci.Parallel(t)
    24  	httpTest(t, nil, func(s *TestAgent) {
    25  		// Directly manipulate the state
    26  		state := s.Agent.server.State()
    27  		eval1 := mock.Eval()
    28  		eval2 := mock.Eval()
    29  		err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2})
    30  		require.NoError(t, err)
    31  
    32  		// simple list request
    33  		req, err := http.NewRequest("GET", "/v1/evaluations", nil)
    34  		require.NoError(t, err)
    35  		respW := httptest.NewRecorder()
    36  		obj, err := s.Server.EvalsRequest(respW, req)
    37  		require.NoError(t, err)
    38  
    39  		// check headers and response body
    40  		require.NotEqual(t, "", respW.Result().Header.Get("X-Nomad-Index"), "missing index")
    41  		require.Equal(t, "true", respW.Result().Header.Get("X-Nomad-KnownLeader"), "missing known leader")
    42  		require.NotEqual(t, "", respW.Result().Header.Get("X-Nomad-LastContact"), "missing last contact")
    43  		require.Len(t, obj.([]*structs.Evaluation), 2, "expected 2 evals")
    44  
    45  		// paginated list request
    46  		req, err = http.NewRequest("GET", "/v1/evaluations?per_page=1", nil)
    47  		require.NoError(t, err)
    48  		respW = httptest.NewRecorder()
    49  		obj, err = s.Server.EvalsRequest(respW, req)
    50  		require.NoError(t, err)
    51  
    52  		// check response body
    53  		require.Len(t, obj.([]*structs.Evaluation), 1, "expected 1 eval")
    54  
    55  		// filtered list request
    56  		req, err = http.NewRequest("GET",
    57  			fmt.Sprintf("/v1/evaluations?per_page=10&job=%s", eval2.JobID), nil)
    58  		require.NoError(t, err)
    59  		respW = httptest.NewRecorder()
    60  		obj, err = s.Server.EvalsRequest(respW, req)
    61  		require.NoError(t, err)
    62  
    63  		// check response body
    64  		require.Len(t, obj.([]*structs.Evaluation), 1, "expected 1 eval")
    65  
    66  	})
    67  }
    68  
    69  func TestHTTP_EvalPrefixList(t *testing.T) {
    70  	ci.Parallel(t)
    71  	httpTest(t, nil, func(s *TestAgent) {
    72  		// Directly manipulate the state
    73  		state := s.Agent.server.State()
    74  		eval1 := mock.Eval()
    75  		eval1.ID = "aaabbbbb-e8f7-fd38-c855-ab94ceb89706"
    76  		eval2 := mock.Eval()
    77  		eval2.ID = "aaabbbbb-e8f7-fd38-c855-ab94ceb89706"
    78  		err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2})
    79  		if err != nil {
    80  			t.Fatalf("err: %v", err)
    81  		}
    82  
    83  		// Make the HTTP request
    84  		req, err := http.NewRequest("GET", "/v1/evaluations?prefix=aaab", nil)
    85  		if err != nil {
    86  			t.Fatalf("err: %v", err)
    87  		}
    88  		respW := httptest.NewRecorder()
    89  
    90  		// Make the request
    91  		obj, err := s.Server.EvalsRequest(respW, req)
    92  		if err != nil {
    93  			t.Fatalf("err: %v", err)
    94  		}
    95  
    96  		// Check for the index
    97  		if respW.Result().Header.Get("X-Nomad-Index") == "" {
    98  			t.Fatalf("missing index")
    99  		}
   100  		if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" {
   101  			t.Fatalf("missing known leader")
   102  		}
   103  		if respW.Result().Header.Get("X-Nomad-LastContact") == "" {
   104  			t.Fatalf("missing last contact")
   105  		}
   106  
   107  		// Check the eval
   108  		e := obj.([]*structs.Evaluation)
   109  		if len(e) != 1 {
   110  			t.Fatalf("bad: %#v", e)
   111  		}
   112  
   113  		// Check the identifier
   114  		if e[0].ID != eval2.ID {
   115  			t.Fatalf("expected eval ID: %v, Actual: %v", eval2.ID, e[0].ID)
   116  		}
   117  	})
   118  }
   119  
   120  func TestHTTP_EvalsDelete(t *testing.T) {
   121  	ci.Parallel(t)
   122  
   123  	testCases := []struct {
   124  		testFn func()
   125  		name   string
   126  	}{
   127  		{
   128  			testFn: func() {
   129  				httpTest(t, nil, func(s *TestAgent) {
   130  
   131  					// Create an empty request object which doesn't contain any
   132  					// eval IDs.
   133  					deleteReq := api.EvalDeleteRequest{}
   134  					buf := encodeReq(&deleteReq)
   135  
   136  					// Generate the HTTP request.
   137  					req, err := http.NewRequest(http.MethodDelete, "/v1/evaluations", buf)
   138  					require.NoError(t, err)
   139  					respW := httptest.NewRecorder()
   140  
   141  					// Make the request and check the response.
   142  					obj, err := s.Server.EvalsRequest(respW, req)
   143  					require.Equal(t,
   144  						CodedError(http.StatusBadRequest, "evals must be deleted by either ID or filter"), err)
   145  					require.Nil(t, obj)
   146  				})
   147  			},
   148  			name: "too few eval IDs",
   149  		},
   150  		{
   151  			testFn: func() {
   152  				httpTest(t, nil, func(s *TestAgent) {
   153  
   154  					deleteReq := api.EvalDeleteRequest{EvalIDs: make([]string, 8000)}
   155  
   156  					// Generate a UUID and add it 8000 times to the eval ID
   157  					// request array.
   158  					evalID := uuid.Generate()
   159  
   160  					for i := 0; i < 8000; i++ {
   161  						deleteReq.EvalIDs[i] = evalID
   162  					}
   163  
   164  					buf := encodeReq(&deleteReq)
   165  
   166  					// Generate the HTTP request.
   167  					req, err := http.NewRequest(http.MethodDelete, "/v1/evaluations", buf)
   168  					require.NoError(t, err)
   169  					respW := httptest.NewRecorder()
   170  
   171  					// Make the request and check the response.
   172  					obj, err := s.Server.EvalsRequest(respW, req)
   173  					require.Equal(t,
   174  						CodedError(http.StatusBadRequest,
   175  							"request includes 8000 evaluation IDs, must be 7281 or fewer"), err)
   176  					require.Nil(t, obj)
   177  				})
   178  			},
   179  			name: "too many eval IDs",
   180  		},
   181  		{
   182  			testFn: func() {
   183  				httpTest(t, func(c *Config) {
   184  					c.NomadConfig.DefaultSchedulerConfig.PauseEvalBroker = true
   185  				}, func(s *TestAgent) {
   186  
   187  					// Generate a request with an eval ID that doesn't exist
   188  					// within state.
   189  					deleteReq := api.EvalDeleteRequest{EvalIDs: []string{uuid.Generate()}}
   190  					buf := encodeReq(&deleteReq)
   191  
   192  					// Generate the HTTP request.
   193  					req, err := http.NewRequest(http.MethodDelete, "/v1/evaluations", buf)
   194  					require.NoError(t, err)
   195  					respW := httptest.NewRecorder()
   196  
   197  					// Make the request and check the response.
   198  					obj, err := s.Server.EvalsRequest(respW, req)
   199  					require.Contains(t, err.Error(), "eval not found")
   200  					require.Nil(t, obj)
   201  				})
   202  			},
   203  			name: "eval doesn't exist",
   204  		},
   205  		{
   206  			testFn: func() {
   207  				httpTest(t, func(c *Config) {
   208  					c.NomadConfig.DefaultSchedulerConfig.PauseEvalBroker = true
   209  				}, func(s *TestAgent) {
   210  
   211  					// Upsert an eval into state.
   212  					mockEval := mock.Eval()
   213  
   214  					err := s.Agent.server.State().UpsertEvals(
   215  						structs.MsgTypeTestSetup, 10, []*structs.Evaluation{mockEval})
   216  					require.NoError(t, err)
   217  
   218  					// Generate a request with the ID of the eval previously upserted.
   219  					deleteReq := api.EvalDeleteRequest{EvalIDs: []string{mockEval.ID}}
   220  					buf := encodeReq(&deleteReq)
   221  
   222  					// Generate the HTTP request.
   223  					req, err := http.NewRequest(http.MethodDelete, "/v1/evaluations", buf)
   224  					require.NoError(t, err)
   225  					respW := httptest.NewRecorder()
   226  
   227  					// Make the request and check the response.
   228  					obj, err := s.Server.EvalsRequest(respW, req)
   229  					require.NoError(t, err)
   230  					require.NotNil(t, obj)
   231  					deleteResp := obj.(structs.EvalDeleteResponse)
   232  					require.Equal(t, deleteResp.Count, 1)
   233  
   234  					// Ensure the eval is not found.
   235  					readEval, err := s.Agent.server.State().EvalByID(nil, mockEval.ID)
   236  					require.NoError(t, err)
   237  					require.Nil(t, readEval)
   238  				})
   239  			},
   240  			name: "successfully delete eval",
   241  		},
   242  	}
   243  
   244  	for _, tc := range testCases {
   245  		t.Run(tc.name, func(t *testing.T) {
   246  			tc.testFn()
   247  		})
   248  	}
   249  }
   250  
   251  func TestHTTP_EvalAllocations(t *testing.T) {
   252  	ci.Parallel(t)
   253  	httpTest(t, nil, func(s *TestAgent) {
   254  		// Directly manipulate the state
   255  		state := s.Agent.server.State()
   256  		alloc1 := mock.Alloc()
   257  		alloc2 := mock.Alloc()
   258  		alloc2.EvalID = alloc1.EvalID
   259  		state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID))
   260  		state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))
   261  		err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc1, alloc2})
   262  		if err != nil {
   263  			t.Fatalf("err: %v", err)
   264  		}
   265  
   266  		// Make the HTTP request
   267  		req, err := http.NewRequest("GET",
   268  			"/v1/evaluation/"+alloc1.EvalID+"/allocations", nil)
   269  		if err != nil {
   270  			t.Fatalf("err: %v", err)
   271  		}
   272  		respW := httptest.NewRecorder()
   273  
   274  		// Make the request
   275  		obj, err := s.Server.EvalSpecificRequest(respW, req)
   276  		if err != nil {
   277  			t.Fatalf("err: %v", err)
   278  		}
   279  
   280  		// Check for the index
   281  		if respW.Result().Header.Get("X-Nomad-Index") == "" {
   282  			t.Fatalf("missing index")
   283  		}
   284  		if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" {
   285  			t.Fatalf("missing known leader")
   286  		}
   287  		if respW.Result().Header.Get("X-Nomad-LastContact") == "" {
   288  			t.Fatalf("missing last contact")
   289  		}
   290  
   291  		// Check the output
   292  		allocs := obj.([]*structs.AllocListStub)
   293  		if len(allocs) != 2 {
   294  			t.Fatalf("bad: %#v", allocs)
   295  		}
   296  	})
   297  }
   298  
   299  func TestHTTP_EvalQuery(t *testing.T) {
   300  	ci.Parallel(t)
   301  	httpTest(t, nil, func(s *TestAgent) {
   302  		// Directly manipulate the state
   303  		state := s.Agent.server.State()
   304  		eval := mock.Eval()
   305  		err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval})
   306  		if err != nil {
   307  			t.Fatalf("err: %v", err)
   308  		}
   309  
   310  		// Make the HTTP request
   311  		req, err := http.NewRequest("GET", "/v1/evaluation/"+eval.ID, nil)
   312  		if err != nil {
   313  			t.Fatalf("err: %v", err)
   314  		}
   315  		respW := httptest.NewRecorder()
   316  
   317  		// Make the request
   318  		obj, err := s.Server.EvalSpecificRequest(respW, req)
   319  		if err != nil {
   320  			t.Fatalf("err: %v", err)
   321  		}
   322  
   323  		// Check for the index
   324  		if respW.Result().Header.Get("X-Nomad-Index") == "" {
   325  			t.Fatalf("missing index")
   326  		}
   327  		if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" {
   328  			t.Fatalf("missing known leader")
   329  		}
   330  		if respW.Result().Header.Get("X-Nomad-LastContact") == "" {
   331  			t.Fatalf("missing last contact")
   332  		}
   333  
   334  		// Check the job
   335  		e := obj.(*structs.Evaluation)
   336  		if e.ID != eval.ID {
   337  			t.Fatalf("bad: %#v", e)
   338  		}
   339  	})
   340  }
   341  
   342  func TestHTTP_EvalQueryWithRelated(t *testing.T) {
   343  	ci.Parallel(t)
   344  	httpTest(t, nil, func(s *TestAgent) {
   345  		// Directly manipulate the state
   346  		state := s.Agent.server.State()
   347  		eval1 := mock.Eval()
   348  		eval2 := mock.Eval()
   349  
   350  		// Link related evals
   351  		eval1.NextEval = eval2.ID
   352  		eval2.PreviousEval = eval1.ID
   353  
   354  		err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2})
   355  		require.NoError(t, err)
   356  
   357  		// Make the HTTP request
   358  		req, err := http.NewRequest("GET", fmt.Sprintf("/v1/evaluation/%s?related=true", eval1.ID), nil)
   359  		require.NoError(t, err)
   360  		respW := httptest.NewRecorder()
   361  
   362  		// Make the request
   363  		obj, err := s.Server.EvalSpecificRequest(respW, req)
   364  		require.NoError(t, err)
   365  
   366  		// Check for the index
   367  		require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index"))
   368  		require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-KnownLeader"))
   369  		require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-LastContact"))
   370  
   371  		// Check the eval
   372  		e := obj.(*structs.Evaluation)
   373  		require.Equal(t, eval1.ID, e.ID)
   374  
   375  		// Check for the related evals
   376  		expected := []*structs.EvaluationStub{
   377  			eval2.Stub(),
   378  		}
   379  		require.Equal(t, expected, e.RelatedEvals)
   380  	})
   381  }
   382  
   383  func TestHTTP_EvalCount(t *testing.T) {
   384  	ci.Parallel(t)
   385  	httpTest(t, nil, func(s *TestAgent) {
   386  		// Directly manipulate the state
   387  		state := s.Agent.server.State()
   388  		eval1 := mock.Eval()
   389  		eval2 := mock.Eval()
   390  		err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2})
   391  		must.NoError(t, err)
   392  
   393  		// simple count request
   394  		req, err := http.NewRequest("GET", "/v1/evaluations/count", nil)
   395  		must.NoError(t, err)
   396  		respW := httptest.NewRecorder()
   397  		obj, err := s.Server.EvalsCountRequest(respW, req)
   398  		must.NoError(t, err)
   399  
   400  		// check headers and response body
   401  		must.NotEq(t, "", respW.Result().Header.Get("X-Nomad-Index"),
   402  			must.Sprint("missing index"))
   403  		must.Eq(t, "true", respW.Result().Header.Get("X-Nomad-KnownLeader"),
   404  			must.Sprint("missing known leader"))
   405  		must.NotEq(t, "", respW.Result().Header.Get("X-Nomad-LastContact"),
   406  			must.Sprint("missing last contact"))
   407  
   408  		resp := obj.(*structs.EvalCountResponse)
   409  		must.Eq(t, resp.Count, 2)
   410  
   411  		// filtered count request
   412  		v := url.Values{}
   413  		v.Add("filter", fmt.Sprintf("JobID==\"%s\"", eval2.JobID))
   414  		req, err = http.NewRequest("GET", "/v1/evaluations/count?"+v.Encode(), nil)
   415  		must.NoError(t, err)
   416  		respW = httptest.NewRecorder()
   417  		obj, err = s.Server.EvalsCountRequest(respW, req)
   418  		must.NoError(t, err)
   419  		resp = obj.(*structs.EvalCountResponse)
   420  		must.Eq(t, resp.Count, 1)
   421  
   422  	})
   423  }