github.com/hernad/nomad@v1.6.112/nomad/state/state_store_node_pools_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package state 5 6 import ( 7 "fmt" 8 "testing" 9 10 memdb "github.com/hashicorp/go-memdb" 11 "github.com/hernad/nomad/ci" 12 "github.com/hernad/nomad/nomad/mock" 13 "github.com/hernad/nomad/nomad/structs" 14 "github.com/shoenig/test/must" 15 ) 16 17 func TestStateStore_NodePools(t *testing.T) { 18 ci.Parallel(t) 19 20 // Create test node pools. 21 state := testStateStore(t) 22 pools := make([]*structs.NodePool, 10) 23 for i := 0; i < 10; i++ { 24 pools[i] = mock.NodePool() 25 } 26 must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools)) 27 28 // Create a watchset to test that getters don't cause it to fire. 29 ws := memdb.NewWatchSet() 30 iter, err := state.NodePools(ws, SortDefault) 31 must.NoError(t, err) 32 33 // Verify all pools are returned. 34 foundBuiltIn := map[string]bool{ 35 structs.NodePoolAll: false, 36 structs.NodePoolDefault: false, 37 } 38 got := make([]*structs.NodePool, 0, 10) 39 40 for raw := iter.Next(); raw != nil; raw = iter.Next() { 41 pool := raw.(*structs.NodePool) 42 43 if pool.IsBuiltIn() { 44 must.False(t, foundBuiltIn[pool.Name]) 45 foundBuiltIn[pool.Name] = true 46 continue 47 } 48 49 got = append(got, pool) 50 } 51 52 must.SliceContainsAll(t, got, pools) 53 must.False(t, watchFired(ws)) 54 for k, v := range foundBuiltIn { 55 must.True(t, v, must.Sprintf("built-in pool %q not found", k)) 56 } 57 } 58 59 func TestStateStore_NodePools_Ordering(t *testing.T) { 60 ci.Parallel(t) 61 62 // Create test node pools with stable sortable names. 63 state := testStateStore(t) 64 pools := make([]*structs.NodePool, 10) 65 for i := 0; i < 5; i++ { 66 pool := mock.NodePool() 67 pool.Name = fmt.Sprintf("%02d", i+1) 68 pools[i] = pool 69 } 70 must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools)) 71 72 testCases := []struct { 73 name string 74 order SortOption 75 expected []string 76 }{ 77 { 78 name: "default order", 79 order: SortDefault, 80 expected: []string{"01", "02", "03", "04", "05"}, 81 }, 82 { 83 name: "reverse order", 84 order: SortReverse, 85 expected: []string{"05", "04", "03", "02", "01"}, 86 }, 87 } 88 89 for _, tc := range testCases { 90 t.Run(tc.name, func(t *testing.T) { 91 ws := memdb.NewWatchSet() 92 iter, err := state.NodePools(ws, tc.order) 93 must.NoError(t, err) 94 95 var got []string 96 for raw := iter.Next(); raw != nil; raw = iter.Next() { 97 pool := raw.(*structs.NodePool) 98 if pool.IsBuiltIn() { 99 continue 100 } 101 102 got = append(got, pool.Name) 103 } 104 105 must.Eq(t, got, tc.expected) 106 }) 107 } 108 } 109 110 func TestStateStore_NodePool_ByName(t *testing.T) { 111 ci.Parallel(t) 112 113 // Create test node pools. 114 state := testStateStore(t) 115 pools := make([]*structs.NodePool, 10) 116 for i := 0; i < 10; i++ { 117 pools[i] = mock.NodePool() 118 } 119 must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools)) 120 121 testCases := []struct { 122 name string 123 pool string 124 expected *structs.NodePool 125 }{ 126 { 127 name: "find a pool", 128 pool: pools[3].Name, 129 expected: pools[3], 130 }, 131 { 132 name: "find built-in pool all", 133 pool: structs.NodePoolAll, 134 expected: &structs.NodePool{ 135 Name: structs.NodePoolAll, 136 Description: structs.NodePoolAllDescription, 137 CreateIndex: 1, 138 ModifyIndex: 1, 139 }, 140 }, 141 { 142 name: "find built-in pool default", 143 pool: structs.NodePoolDefault, 144 expected: &structs.NodePool{ 145 Name: structs.NodePoolDefault, 146 Description: structs.NodePoolDefaultDescription, 147 CreateIndex: 1, 148 ModifyIndex: 1, 149 }, 150 }, 151 { 152 name: "pool not found", 153 pool: "no-pool", 154 expected: nil, 155 }, 156 { 157 name: "must be exact match", 158 pool: pools[2].Name[:4], 159 expected: nil, 160 }, 161 { 162 name: "empty search", 163 pool: "", 164 expected: nil, 165 }, 166 } 167 168 for _, tc := range testCases { 169 t.Run(tc.name, func(t *testing.T) { 170 ws := memdb.NewWatchSet() 171 got, err := state.NodePoolByName(ws, tc.pool) 172 173 must.NoError(t, err) 174 must.Eq(t, tc.expected, got) 175 must.False(t, watchFired(ws)) 176 }) 177 } 178 } 179 180 func TestStateStore_NodePool_ByNamePrefix(t *testing.T) { 181 ci.Parallel(t) 182 183 // Create test node pools. 184 state := testStateStore(t) 185 existingPools := []*structs.NodePool{ 186 {Name: "prod-1"}, 187 {Name: "prod-2"}, 188 {Name: "prod-3"}, 189 {Name: "dev-1"}, 190 {Name: "dev-2"}, 191 {Name: "qa"}, 192 } 193 err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, existingPools) 194 must.NoError(t, err) 195 196 testCases := []struct { 197 name string 198 prefix string 199 expected []string 200 order SortOption 201 }{ 202 { 203 name: "multiple prefix match", 204 prefix: "prod", 205 order: SortDefault, 206 expected: []string{"prod-1", "prod-2", "prod-3"}, 207 }, 208 { 209 name: "single prefix match", 210 prefix: "qa", 211 order: SortDefault, 212 expected: []string{"qa"}, 213 }, 214 { 215 name: "no match", 216 prefix: "nope", 217 order: SortDefault, 218 expected: []string{}, 219 }, 220 { 221 name: "empty prefix", 222 prefix: "", 223 order: SortDefault, 224 expected: []string{ 225 "all", 226 "default", 227 "prod-1", 228 "prod-2", 229 "prod-3", 230 "dev-1", 231 "dev-2", 232 "qa", 233 }, 234 }, 235 { 236 name: "reverse order", 237 prefix: "prod", 238 order: SortReverse, 239 expected: []string{"prod-3", "prod-2", "prod-1"}, 240 }, 241 } 242 243 for _, tc := range testCases { 244 t.Run(tc.name, func(t *testing.T) { 245 ws := memdb.NewWatchSet() 246 iter, err := state.NodePoolsByNamePrefix(ws, tc.prefix, tc.order) 247 must.NoError(t, err) 248 249 got := []string{} 250 for raw := iter.Next(); raw != nil; raw = iter.Next() { 251 got = append(got, raw.(*structs.NodePool).Name) 252 } 253 must.SliceContainsAll(t, tc.expected, got) 254 }) 255 } 256 } 257 258 func TestStateStore_NodePool_Upsert(t *testing.T) { 259 ci.Parallel(t) 260 261 existingPools := make([]*structs.NodePool, 10) 262 for i := 0; i < 10; i++ { 263 existingPools[i] = mock.NodePool() 264 } 265 266 testCases := []struct { 267 name string 268 input []*structs.NodePool 269 expectedErr string 270 }{ 271 { 272 name: "add single pool", 273 input: []*structs.NodePool{ 274 mock.NodePool(), 275 }, 276 }, 277 { 278 name: "add multiple pools", 279 input: []*structs.NodePool{ 280 mock.NodePool(), 281 mock.NodePool(), 282 mock.NodePool(), 283 }, 284 }, 285 { 286 name: "update existing pools", 287 input: []*structs.NodePool{ 288 { 289 Name: existingPools[0].Name, 290 Description: "updated", 291 Meta: map[string]string{ 292 "updated": "true", 293 }, 294 SchedulerConfiguration: &structs.NodePoolSchedulerConfiguration{ 295 SchedulerAlgorithm: structs.SchedulerAlgorithmBinpack, 296 }, 297 }, 298 { 299 Name: existingPools[1].Name, 300 Description: "use global scheduler config", 301 }, 302 }, 303 }, 304 { 305 name: "update with nil", 306 input: []*structs.NodePool{ 307 nil, 308 }, 309 }, 310 { 311 name: "empty name", 312 input: []*structs.NodePool{ 313 { 314 Name: "", 315 }, 316 }, 317 expectedErr: "missing primary index", 318 }, 319 { 320 name: "update bulit-in pool all", 321 input: []*structs.NodePool{ 322 { 323 Name: structs.NodePoolAll, 324 Description: "changed", 325 }, 326 }, 327 expectedErr: "not allowed", 328 }, 329 { 330 name: "update built-in pool default", 331 input: []*structs.NodePool{ 332 { 333 Name: structs.NodePoolDefault, 334 Description: "changed", 335 }, 336 }, 337 expectedErr: "not allowed", 338 }, 339 } 340 341 for _, tc := range testCases { 342 t.Run(tc.name, func(t *testing.T) { 343 // Create test pools. 344 state := testStateStore(t) 345 must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, existingPools)) 346 347 // Update pools from test case. 348 err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1001, tc.input) 349 350 if tc.expectedErr != "" { 351 must.ErrorContains(t, err, tc.expectedErr) 352 } else { 353 must.NoError(t, err) 354 355 ws := memdb.NewWatchSet() 356 for _, pool := range tc.input { 357 if pool == nil { 358 continue 359 } 360 361 got, err := state.NodePoolByName(ws, pool.Name) 362 must.NoError(t, err) 363 must.Eq(t, pool, got) 364 } 365 } 366 }) 367 } 368 } 369 370 func TestStateStore_NodePool_Delete(t *testing.T) { 371 ci.Parallel(t) 372 373 pools := make([]*structs.NodePool, 10) 374 for i := 0; i < 10; i++ { 375 pools[i] = mock.NodePool() 376 } 377 378 testCases := []struct { 379 name string 380 del []string 381 expectedErr string 382 }{ 383 { 384 name: "delete one", 385 del: []string{pools[0].Name}, 386 }, 387 { 388 name: "delete multiple", 389 del: []string{pools[0].Name, pools[3].Name}, 390 }, 391 { 392 name: "delete non-existing", 393 del: []string{"nope"}, 394 expectedErr: "not found", 395 }, 396 { 397 name: "delete is atomic", 398 del: []string{pools[0].Name, "nope"}, 399 expectedErr: "not found", 400 }, 401 { 402 name: "delete built-in pool all", 403 del: []string{structs.NodePoolAll}, 404 expectedErr: "not allowed", 405 }, 406 { 407 name: "delete built-in pool default", 408 del: []string{structs.NodePoolDefault}, 409 expectedErr: "not allowed", 410 }, 411 } 412 413 for _, tc := range testCases { 414 t.Run(tc.name, func(t *testing.T) { 415 state := testStateStore(t) 416 must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools)) 417 418 err := state.DeleteNodePools(structs.MsgTypeTestSetup, 1001, tc.del) 419 if tc.expectedErr != "" { 420 must.ErrorContains(t, err, tc.expectedErr) 421 422 // Make sure delete is atomic and nothing is removed if an 423 // error happens. 424 for _, p := range pools { 425 got, err := state.NodePoolByName(nil, p.Name) 426 must.NoError(t, err) 427 must.Eq(t, p, got) 428 } 429 } else { 430 must.NoError(t, err) 431 432 // Check that the node pools is deleted. 433 for _, p := range tc.del { 434 got, err := state.NodePoolByName(nil, p) 435 must.NoError(t, err) 436 must.Nil(t, got) 437 } 438 } 439 }) 440 } 441 } 442 443 func TestStateStore_NodePool_Restore(t *testing.T) { 444 ci.Parallel(t) 445 446 state := testStateStore(t) 447 pool := mock.NodePool() 448 449 restore, err := state.Restore() 450 must.NoError(t, err) 451 452 err = restore.NodePoolRestore(pool) 453 must.NoError(t, err) 454 455 restore.Commit() 456 457 ws := memdb.NewWatchSet() 458 out, err := state.NodePoolByName(ws, pool.Name) 459 must.NoError(t, err) 460 must.Eq(t, out, pool) 461 }