github.com/hernad/nomad@v1.6.112/command/agent/node_pool_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 "strconv" 11 "testing" 12 13 "github.com/google/go-cmp/cmp/cmpopts" 14 "github.com/hernad/nomad/ci" 15 "github.com/hernad/nomad/nomad/mock" 16 "github.com/hernad/nomad/nomad/structs" 17 "github.com/shoenig/test/must" 18 ) 19 20 func TestHTTP_NodePool_List(t *testing.T) { 21 ci.Parallel(t) 22 httpTest(t, nil, func(s *TestAgent) { 23 // Populate state with test data. 24 pool1 := mock.NodePool() 25 pool2 := mock.NodePool() 26 pool3 := mock.NodePool() 27 args := structs.NodePoolUpsertRequest{ 28 NodePools: []*structs.NodePool{pool1, pool2, pool3}, 29 } 30 var resp structs.GenericResponse 31 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 32 must.NoError(t, err) 33 34 // Make HTTP request. 35 req, err := http.NewRequest("GET", "/v1/node/pools", nil) 36 must.NoError(t, err) 37 respW := httptest.NewRecorder() 38 39 obj, err := s.Server.NodePoolsRequest(respW, req) 40 must.NoError(t, err) 41 42 // Expect 5 node pools: 3 created + 2 built-in. 43 must.SliceLen(t, 5, obj.([]*structs.NodePool)) 44 45 // Verify response index. 46 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 47 must.NoError(t, err) 48 must.NonZero(t, gotIndex) 49 }) 50 } 51 52 func TestHTTP_NodePool_Info(t *testing.T) { 53 ci.Parallel(t) 54 httpTest(t, nil, func(s *TestAgent) { 55 // Populate state with test data. 56 pool := mock.NodePool() 57 args := structs.NodePoolUpsertRequest{ 58 NodePools: []*structs.NodePool{pool}, 59 } 60 var resp structs.GenericResponse 61 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 62 must.NoError(t, err) 63 64 t.Run("test pool", func(t *testing.T) { 65 // Make HTTP request for test pool. 66 req, err := http.NewRequest("GET", fmt.Sprintf("/v1/node/pool/%s", pool.Name), nil) 67 must.NoError(t, err) 68 respW := httptest.NewRecorder() 69 70 obj, err := s.Server.NodePoolSpecificRequest(respW, req) 71 must.NoError(t, err) 72 73 // Verify expected pool is returned. 74 must.Eq(t, pool, obj.(*structs.NodePool), must.Cmp(cmpopts.IgnoreFields( 75 structs.NodePool{}, 76 "Hash", 77 "CreateIndex", 78 "ModifyIndex", 79 ))) 80 81 // Verify response index. 82 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 83 must.NoError(t, err) 84 must.NonZero(t, gotIndex) 85 }) 86 87 t.Run("built-in pool", func(t *testing.T) { 88 // Make HTTP request for built-in pool. 89 req, err := http.NewRequest("GET", fmt.Sprintf("/v1/node/pool/%s", structs.NodePoolAll), nil) 90 must.NoError(t, err) 91 respW := httptest.NewRecorder() 92 93 obj, err := s.Server.NodePoolSpecificRequest(respW, req) 94 must.NoError(t, err) 95 96 // Verify expected pool is returned. 97 must.Eq(t, structs.NodePoolAll, obj.(*structs.NodePool).Name) 98 99 // Verify response index. 100 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 101 must.NoError(t, err) 102 must.NonZero(t, gotIndex) 103 }) 104 105 t.Run("invalid pool", func(t *testing.T) { 106 // Make HTTP request for built-in pool. 107 req, err := http.NewRequest("GET", "/v1/node/pool/doesn-exist", nil) 108 must.NoError(t, err) 109 respW := httptest.NewRecorder() 110 111 // Verify error. 112 _, err = s.Server.NodePoolSpecificRequest(respW, req) 113 must.ErrorContains(t, err, "not found") 114 115 // Verify response index. 116 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 117 must.NoError(t, err) 118 must.NonZero(t, gotIndex) 119 }) 120 }) 121 } 122 123 func TestHTTP_NodePool_Create(t *testing.T) { 124 ci.Parallel(t) 125 httpTest(t, nil, func(s *TestAgent) { 126 // Create test node pool. 127 pool := mock.NodePool() 128 buf := encodeReq(pool) 129 req, err := http.NewRequest("PUT", "/v1/node/pools", buf) 130 must.NoError(t, err) 131 132 respW := httptest.NewRecorder() 133 obj, err := s.Server.NodePoolsRequest(respW, req) 134 must.NoError(t, err) 135 must.Nil(t, obj) 136 137 // Verify response index. 138 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 139 must.NoError(t, err) 140 must.NonZero(t, gotIndex) 141 142 // Verify test node pool is in state. 143 got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name) 144 must.NoError(t, err) 145 must.Eq(t, pool, got, must.Cmp(cmpopts.IgnoreFields( 146 structs.NodePool{}, 147 "Hash", 148 "CreateIndex", 149 "ModifyIndex", 150 ))) 151 must.Eq(t, gotIndex, got.CreateIndex) 152 must.Eq(t, gotIndex, got.ModifyIndex) 153 }) 154 } 155 156 func TestHTTP_NodePool_Update(t *testing.T) { 157 ci.Parallel(t) 158 httpTest(t, nil, func(s *TestAgent) { 159 t.Run("success", func(t *testing.T) { 160 // Populate state with test node pool. 161 pool := mock.NodePool() 162 args := structs.NodePoolUpsertRequest{ 163 NodePools: []*structs.NodePool{pool}, 164 } 165 var resp structs.GenericResponse 166 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 167 must.NoError(t, err) 168 169 // Update node pool. 170 updated := pool.Copy() 171 updated.Description = "updated node pool" 172 updated.Meta = map[string]string{ 173 "updated": "true", 174 } 175 176 buf := encodeReq(updated) 177 req, err := http.NewRequest("PUT", fmt.Sprintf("/v1/node/pool/%s", updated.Name), buf) 178 must.NoError(t, err) 179 180 respW := httptest.NewRecorder() 181 obj, err := s.Server.NodePoolsRequest(respW, req) 182 must.NoError(t, err) 183 must.Nil(t, obj) 184 185 // Verify response index. 186 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 187 must.NoError(t, err) 188 must.NonZero(t, gotIndex) 189 190 // Verify node pool was updated. 191 got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name) 192 must.NoError(t, err) 193 must.Eq(t, updated, got, must.Cmp(cmpopts.IgnoreFields( 194 structs.NodePool{}, 195 "Hash", 196 "CreateIndex", 197 "ModifyIndex", 198 ))) 199 must.NotEq(t, gotIndex, got.CreateIndex) 200 must.Eq(t, gotIndex, got.ModifyIndex) 201 }) 202 203 t.Run("no name in path", func(t *testing.T) { 204 // Populate state with test node pool. 205 pool := mock.NodePool() 206 args := structs.NodePoolUpsertRequest{ 207 NodePools: []*structs.NodePool{pool}, 208 } 209 var resp structs.GenericResponse 210 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 211 must.NoError(t, err) 212 213 // Update node pool with no name in path. 214 updated := pool.Copy() 215 updated.Description = "updated node pool" 216 updated.Meta = map[string]string{ 217 "updated": "true", 218 } 219 220 buf := encodeReq(updated) 221 req, err := http.NewRequest("PUT", "/v1/node/pool/", buf) 222 must.NoError(t, err) 223 224 respW := httptest.NewRecorder() 225 obj, err := s.Server.NodePoolsRequest(respW, req) 226 must.NoError(t, err) 227 must.Nil(t, obj) 228 229 // Verify response index. 230 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 231 must.NoError(t, err) 232 must.NonZero(t, gotIndex) 233 234 // Verify node pool was updated. 235 got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name) 236 must.NoError(t, err) 237 must.Eq(t, updated, got, must.Cmp(cmpopts.IgnoreFields( 238 structs.NodePool{}, 239 "Hash", 240 "CreateIndex", 241 "ModifyIndex", 242 ))) 243 }) 244 245 t.Run("wrong name in path", func(t *testing.T) { 246 // Populate state with test node pool. 247 pool := mock.NodePool() 248 args := structs.NodePoolUpsertRequest{ 249 NodePools: []*structs.NodePool{pool}, 250 } 251 var resp structs.GenericResponse 252 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 253 must.NoError(t, err) 254 255 // Update node pool. 256 updated := pool.Copy() 257 updated.Description = "updated node pool" 258 updated.Meta = map[string]string{ 259 "updated": "true", 260 } 261 262 // Make request with the wrong path. 263 buf := encodeReq(updated) 264 req, err := http.NewRequest("PUT", "/v1/node/pool/wrong", buf) 265 must.NoError(t, err) 266 267 respW := httptest.NewRecorder() 268 _, err = s.Server.NodePoolSpecificRequest(respW, req) 269 must.ErrorContains(t, err, "name does not match request path") 270 271 // Verify node pool was NOT updated. 272 got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name) 273 must.NoError(t, err) 274 must.Eq(t, pool, got, must.Cmp(cmpopts.IgnoreFields( 275 structs.NodePool{}, 276 "Hash", 277 "CreateIndex", 278 "ModifyIndex", 279 ))) 280 }) 281 }) 282 } 283 284 func TestHTTP_NodePool_Delete(t *testing.T) { 285 ci.Parallel(t) 286 httpTest(t, nil, func(s *TestAgent) { 287 // Populate state with test node pool. 288 pool := mock.NodePool() 289 args := structs.NodePoolUpsertRequest{ 290 NodePools: []*structs.NodePool{pool}, 291 } 292 var resp structs.GenericResponse 293 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 294 must.NoError(t, err) 295 296 // Delete test node pool. 297 req, err := http.NewRequest("DELETE", fmt.Sprintf("/v1/node/pool/%s", pool.Name), nil) 298 must.NoError(t, err) 299 300 respW := httptest.NewRecorder() 301 obj, err := s.Server.NodePoolSpecificRequest(respW, req) 302 must.NoError(t, err) 303 must.Nil(t, obj) 304 305 // Verify node pool was deleted. 306 got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name) 307 must.NoError(t, err) 308 must.Nil(t, got) 309 }) 310 } 311 312 func TestHTTP_NodePool_NodesList(t *testing.T) { 313 ci.Parallel(t) 314 httpTest(t, 315 func(c *Config) { 316 // Disable client so it doesn't impact tests since we're registering 317 // our own test nodes. 318 c.Client.Enabled = false 319 }, 320 func(s *TestAgent) { 321 // Populate state with test data. 322 pool1 := mock.NodePool() 323 pool2 := mock.NodePool() 324 args := structs.NodePoolUpsertRequest{ 325 NodePools: []*structs.NodePool{pool1, pool2}, 326 } 327 var resp structs.GenericResponse 328 err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp) 329 must.NoError(t, err) 330 331 // Split test nodes between default, pool1, and pool2. 332 nodesByPool := make(map[string][]*structs.Node) 333 for i := 0; i < 10; i++ { 334 node := mock.Node() 335 switch i % 3 { 336 case 0: 337 // Leave node pool value empty so node goes to default. 338 case 1: 339 node.NodePool = pool1.Name 340 case 2: 341 node.NodePool = pool2.Name 342 } 343 nodeRegReq := structs.NodeRegisterRequest{ 344 Node: node, 345 WriteRequest: structs.WriteRequest{ 346 Region: "global", 347 }, 348 } 349 var nodeRegResp structs.NodeUpdateResponse 350 err := s.Agent.RPC("Node.Register", &nodeRegReq, &nodeRegResp) 351 must.NoError(t, err) 352 353 nodesByPool[node.NodePool] = append(nodesByPool[node.NodePool], node) 354 } 355 356 testCases := []struct { 357 name string 358 pool string 359 args string 360 expectedNodes []*structs.Node 361 expectedErr string 362 validateFn func(*testing.T, []*structs.NodeListStub) 363 }{ 364 { 365 name: "nodes in default", 366 pool: structs.NodePoolDefault, 367 expectedNodes: nodesByPool[structs.NodePoolDefault], 368 validateFn: func(t *testing.T, stubs []*structs.NodeListStub) { 369 must.Nil(t, stubs[0].NodeResources) 370 }, 371 }, 372 { 373 name: "nodes in pool1 with resources", 374 pool: pool1.Name, 375 args: "resources=true", 376 expectedNodes: nodesByPool[pool1.Name], 377 validateFn: func(t *testing.T, stubs []*structs.NodeListStub) { 378 must.NotNil(t, stubs[0].NodeResources) 379 }, 380 }, 381 } 382 for _, tc := range testCases { 383 t.Run(tc.name, func(t *testing.T) { 384 // Make HTTP request. 385 path := fmt.Sprintf("/v1/node/pool/%s/nodes?%s", tc.pool, tc.args) 386 req, err := http.NewRequest("GET", path, nil) 387 must.NoError(t, err) 388 respW := httptest.NewRecorder() 389 390 obj, err := s.Server.NodePoolSpecificRequest(respW, req) 391 if tc.expectedErr != "" { 392 must.ErrorContains(t, err, tc.expectedErr) 393 return 394 } 395 must.NoError(t, err) 396 397 // Verify request only has expected nodes. 398 stubs := obj.([]*structs.NodeListStub) 399 must.Len(t, len(tc.expectedNodes), stubs) 400 for _, node := range tc.expectedNodes { 401 must.SliceContainsFunc(t, stubs, node, func(s *structs.NodeListStub, n *structs.Node) bool { 402 return s.ID == n.ID 403 }) 404 } 405 406 // Verify respose. 407 if tc.validateFn != nil { 408 tc.validateFn(t, stubs) 409 } 410 411 // Verify response index. 412 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 413 must.NoError(t, err) 414 must.NonZero(t, gotIndex) 415 }) 416 } 417 }) 418 } 419 420 func TestHTTP_NodePool_JobsList(t *testing.T) { 421 ci.Parallel(t) 422 httpTest(t, nil, func(s *TestAgent) { 423 424 pool1, pool2 := mock.NodePool(), mock.NodePool() 425 npUpReq := structs.NodePoolUpsertRequest{ 426 NodePools: []*structs.NodePool{pool1, pool2}, 427 } 428 var npUpResp structs.GenericResponse 429 err := s.Agent.RPC("NodePool.UpsertNodePools", &npUpReq, &npUpResp) 430 must.NoError(t, err) 431 432 for _, poolName := range []string{pool1.Name, "default", "all"} { 433 for i := 0; i < 2; i++ { 434 job := mock.MinJob() 435 job.NodePool = poolName 436 jobRegReq := structs.JobRegisterRequest{ 437 Job: job, 438 WriteRequest: structs.WriteRequest{ 439 Region: "global", 440 Namespace: structs.DefaultNamespace, 441 }, 442 } 443 var jobRegResp structs.JobRegisterResponse 444 must.NoError(t, s.Agent.RPC("Job.Register", &jobRegReq, &jobRegResp)) 445 } 446 } 447 448 // Make HTTP request to occupied pool 449 req, err := http.NewRequest(http.MethodGet, 450 fmt.Sprintf("/v1/node/pool/%s/jobs", pool1.Name), nil) 451 must.NoError(t, err) 452 respW := httptest.NewRecorder() 453 454 obj, err := s.Server.NodePoolSpecificRequest(respW, req) 455 must.NoError(t, err) 456 must.SliceLen(t, 2, obj.([]*structs.JobListStub)) 457 458 // Verify response index. 459 gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64) 460 must.NoError(t, err) 461 must.NonZero(t, gotIndex) 462 463 // Make HTTP request to empty pool 464 req, err = http.NewRequest(http.MethodGet, 465 fmt.Sprintf("/v1/node/pool/%s/jobs", pool2.Name), nil) 466 must.NoError(t, err) 467 respW = httptest.NewRecorder() 468 469 obj, err = s.Server.NodePoolSpecificRequest(respW, req) 470 must.NoError(t, err) 471 must.SliceLen(t, 0, obj.([]*structs.JobListStub)) 472 473 // Make HTTP request to the "all"" pool 474 req, err = http.NewRequest(http.MethodGet, "/v1/node/pool/all/jobs", nil) 475 must.NoError(t, err) 476 respW = httptest.NewRecorder() 477 478 obj, err = s.Server.NodePoolSpecificRequest(respW, req) 479 must.NoError(t, err) 480 must.SliceLen(t, 2, obj.([]*structs.JobListStub)) 481 482 }) 483 }