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 }