github.com/cilium/cilium@v1.16.2/pkg/ciliumenvoyconfig/cec_reconciler_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package ciliumenvoyconfig 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "maps" 11 "slices" 12 "strings" 13 "testing" 14 15 "github.com/sirupsen/logrus" 16 "github.com/stretchr/testify/assert" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 k8sRuntime "k8s.io/apimachinery/pkg/runtime" 19 20 ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 21 "github.com/cilium/cilium/pkg/k8s/resource" 22 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 23 "github.com/cilium/cilium/pkg/node" 24 "github.com/cilium/cilium/pkg/node/types" 25 ) 26 27 type configTestCase struct { 28 name string 29 configs map[resource.Key]*config 30 currentNodeLabels map[string]string 31 kind resource.EventKind 32 configSpecOpt func(spec *ciliumv2.CiliumEnvoyConfigSpec) 33 shouldFailFor []string 34 configKey resource.Key 35 expectedError bool 36 expectedAdded []string 37 expectedUpdated []string 38 expectedDeleted []string 39 } 40 41 var configTestCases = []configTestCase{ 42 // Additions 43 { 44 name: "Upsert event: new / no nodeselector / empty list of node labels / match", 45 configs: map[resource.Key]*config{}, 46 currentNodeLabels: map[string]string{}, 47 configSpecOpt: withoutNodeSelector(), 48 kind: resource.Upsert, 49 expectedError: false, 50 expectedAdded: []string{"test/test"}, 51 expectedUpdated: []string{}, 52 expectedDeleted: []string{}, 53 }, 54 { 55 name: "Upsert event: new / no nodeselector / populated list of node labels / match", 56 configs: map[resource.Key]*config{}, 57 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 58 configSpecOpt: withoutNodeSelector(), 59 kind: resource.Upsert, 60 expectedError: false, 61 expectedAdded: []string{"test/test"}, 62 expectedUpdated: []string{}, 63 expectedDeleted: []string{}, 64 }, 65 { 66 name: "Upsert event: new / existing nodeselector / populated list of node labels / match", 67 configs: map[resource.Key]*config{}, 68 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 69 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "infra"}), 70 kind: resource.Upsert, 71 expectedError: false, 72 expectedAdded: []string{"test/test"}, 73 expectedUpdated: []string{}, 74 expectedDeleted: []string{}, 75 }, 76 { 77 name: "Upsert event: new / existing nodeselector / populated list of node labels / no match", 78 configs: map[resource.Key]*config{}, 79 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 80 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "app"}), 81 kind: resource.Upsert, 82 expectedError: false, 83 expectedAdded: []string{}, 84 expectedUpdated: []string{}, 85 expectedDeleted: []string{}, 86 }, 87 { 88 name: "Upsert event: new / existing nodeselector / empty list of node labels / no match", 89 configs: map[resource.Key]*config{}, 90 currentNodeLabels: map[string]string{}, 91 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "app"}), 92 kind: resource.Upsert, 93 expectedError: false, 94 expectedAdded: []string{}, 95 expectedUpdated: []string{}, 96 expectedDeleted: []string{}, 97 }, 98 99 // Updates 100 { 101 name: "Upsert event: update / no nodeselector / empty list of node labels / match / previously match", 102 configs: map[resource.Key]*config{ 103 {Namespace: "test", Name: "test"}: testConfig("test", "test", nil, true), 104 }, 105 currentNodeLabels: map[string]string{}, 106 configSpecOpt: withoutNodeSelector(), 107 kind: resource.Upsert, 108 expectedError: false, 109 expectedAdded: []string{}, 110 expectedUpdated: []string{"test/test"}, 111 expectedDeleted: []string{}, 112 }, 113 { 114 name: "Upsert event: update / no nodeselector / empty list of node labels / match / previously no match", 115 configs: map[resource.Key]*config{ 116 {Namespace: "test", Name: "test"}: testConfig("test", "test", nil, false), 117 }, 118 currentNodeLabels: map[string]string{}, 119 configSpecOpt: withoutNodeSelector(), 120 kind: resource.Upsert, 121 expectedError: false, 122 expectedAdded: []string{"test/test"}, 123 expectedUpdated: []string{}, 124 expectedDeleted: []string{}, 125 }, 126 { 127 name: "Upsert event: new / no nodeselector / populated list of node labels / match / previously match", 128 configs: map[resource.Key]*config{ 129 {Namespace: "test", Name: "test"}: testConfig("test", "test", nil, true), 130 }, 131 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 132 configSpecOpt: withoutNodeSelector(), 133 kind: resource.Upsert, 134 expectedError: false, 135 expectedAdded: []string{}, 136 expectedUpdated: []string{"test/test"}, 137 expectedDeleted: []string{}, 138 }, 139 { 140 name: "Upsert event: new / no nodeselector / populated list of node labels / match / previously no match", 141 configs: map[resource.Key]*config{ 142 {Namespace: "test", Name: "test"}: testConfig("test", "test", nil, false), 143 }, 144 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 145 configSpecOpt: withoutNodeSelector(), 146 kind: resource.Upsert, 147 expectedError: false, 148 expectedAdded: []string{"test/test"}, 149 expectedUpdated: []string{}, 150 expectedDeleted: []string{}, 151 }, 152 { 153 name: "Upsert event: new / existing nodeselector / populated list of node labels / match / previously match", 154 configs: map[resource.Key]*config{ 155 {Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, true), 156 }, 157 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 158 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "infra"}), 159 kind: resource.Upsert, 160 expectedError: false, 161 expectedAdded: []string{}, 162 expectedUpdated: []string{"test/test"}, 163 expectedDeleted: []string{}, 164 }, 165 { 166 name: "Upsert event: new / existing nodeselector / populated list of node labels / match / previously no match", 167 configs: map[resource.Key]*config{ 168 {Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, false), 169 }, 170 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 171 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "infra"}), 172 kind: resource.Upsert, 173 expectedError: false, 174 expectedAdded: []string{"test/test"}, 175 expectedUpdated: []string{}, 176 expectedDeleted: []string{}, 177 }, 178 { 179 name: "Upsert event: new / existing nodeselector / populated list of node labels / no match / previously match", 180 configs: map[resource.Key]*config{ 181 {Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, true), 182 }, 183 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 184 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "app"}), 185 kind: resource.Upsert, 186 expectedError: false, 187 expectedAdded: []string{}, 188 expectedUpdated: []string{}, 189 expectedDeleted: []string{"test/test"}, 190 }, 191 { 192 name: "Upsert event: new / existing nodeselector / populated list of node labels / no match / previously no match", 193 configs: map[resource.Key]*config{ 194 {Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "app"}, false), 195 }, 196 currentNodeLabels: map[string]string{"role": "infra", "name": "node1"}, 197 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "app"}), 198 kind: resource.Upsert, 199 expectedError: false, 200 expectedAdded: []string{}, 201 expectedUpdated: []string{}, 202 expectedDeleted: []string{}, 203 }, 204 { 205 name: "Upsert event: new / existing nodeselector / empty list of node labels / no match / previously match", 206 configs: map[resource.Key]*config{ 207 {Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, true), 208 }, 209 currentNodeLabels: map[string]string{}, 210 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "app"}), 211 kind: resource.Upsert, 212 expectedError: false, 213 expectedAdded: []string{}, 214 expectedUpdated: []string{}, 215 expectedDeleted: []string{"test/test"}, 216 }, 217 { 218 name: "Upsert event: new / existing nodeselector / empty list of node labels / no match / previously no match", 219 configs: map[resource.Key]*config{ 220 {Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "app"}, false), 221 }, 222 currentNodeLabels: map[string]string{}, 223 configSpecOpt: withNodeLabelSelector(map[string]string{"role": "app"}), 224 kind: resource.Upsert, 225 expectedError: false, 226 expectedAdded: []string{}, 227 expectedUpdated: []string{}, 228 expectedDeleted: []string{}, 229 }, 230 231 // Deletions 232 { 233 name: "Delete event: existing / previously matched", 234 configs: map[resource.Key]*config{ 235 {Namespace: "test", Name: "test"}: testConfig("test", "test", nil, true), 236 }, 237 currentNodeLabels: map[string]string{}, 238 configKey: resource.Key{Namespace: "test", Name: "test"}, 239 kind: resource.Delete, 240 expectedError: false, 241 expectedAdded: []string{}, 242 expectedUpdated: []string{}, 243 expectedDeleted: []string{"test/test"}, 244 }, 245 { 246 name: "Delete event: existing / previously not matched", 247 configs: map[resource.Key]*config{ 248 {Namespace: "test", Name: "test"}: testConfig("test", "test", nil, false), 249 }, 250 currentNodeLabels: map[string]string{}, 251 configKey: resource.Key{Namespace: "test", Name: "test"}, 252 kind: resource.Delete, 253 expectedError: false, 254 expectedAdded: []string{}, 255 expectedUpdated: []string{}, 256 expectedDeleted: []string{}, 257 }, 258 { 259 name: "Delete event: not existing", 260 configs: map[resource.Key]*config{}, 261 currentNodeLabels: map[string]string{}, 262 configKey: resource.Key{Namespace: "test", Name: "test"}, 263 kind: resource.Delete, 264 expectedError: false, 265 expectedAdded: []string{}, 266 expectedUpdated: []string{}, 267 expectedDeleted: []string{}, 268 }, 269 270 // Synced 271 { 272 name: "Sync events shouldn't be handled", 273 configs: map[resource.Key]*config{}, 274 currentNodeLabels: map[string]string{}, 275 configSpecOpt: withoutNodeSelector(), 276 kind: resource.Sync, 277 expectedError: false, 278 expectedAdded: []string{}, 279 expectedUpdated: []string{}, 280 expectedDeleted: []string{}, 281 }, 282 } 283 284 func TestHandleCECEvent(t *testing.T) { 285 executeForConfigType(t, 286 configTestCases, 287 testCEC, 288 func(reconciler *ciliumEnvoyConfigReconciler) func(context.Context, resource.Event[*ciliumv2.CiliumEnvoyConfig]) error { 289 return reconciler.handleCECEvent 290 }, 291 ) 292 } 293 294 func TestHandleCCECEvent(t *testing.T) { 295 executeForConfigType(t, 296 configTestCases, 297 testCCEC, 298 func(reconciler *ciliumEnvoyConfigReconciler) func(context.Context, resource.Event[*ciliumv2.CiliumClusterwideEnvoyConfig]) error { 299 return reconciler.handleCCECEvent 300 }, 301 ) 302 } 303 304 // executeForConfigType executes the given test casese for the CEC and CCEC 305 func executeForConfigType[T k8sRuntime.Object](t *testing.T, 306 tests []configTestCase, 307 createConfigFunc func(opts ...cecOpts) T, 308 handleEventFunc func(*ciliumEnvoyConfigReconciler) func(context.Context, resource.Event[T]) error, 309 ) { 310 for _, tc := range tests { 311 t.Run(tc.name, func(t *testing.T) { 312 logger := logrus.New() 313 logger.SetOutput(io.Discard) 314 315 manager := &fakeCECManager{ 316 shouldFailFor: tc.shouldFailFor, 317 } 318 319 reconciler := newCiliumEnvoyConfigReconciler(reconcilerParams{Logger: logger, Manager: manager}) 320 321 // init current state 322 configs := map[resource.Key]*config{} 323 maps.Copy(configs, tc.configs) 324 reconciler.configs = configs 325 326 currentNodeLabels := map[string]string{} 327 maps.Copy(currentNodeLabels, tc.currentNodeLabels) 328 reconciler.localNodeLabels = currentNodeLabels 329 330 doneCalled := false 331 var doneError error 332 333 doneFunc := func(err error) { 334 doneCalled = true 335 doneError = err 336 } 337 338 event := resource.Event[T]{} 339 event.Kind = tc.kind 340 if len(tc.configKey.Name) == 0 && len(tc.configKey.Namespace) == 0 && tc.configSpecOpt != nil { 341 event.Object = createConfigFunc(tc.configSpecOpt) 342 event.Key = resource.NewKey(event.Object) 343 } else { 344 event.Key = tc.configKey 345 } 346 event.Done = doneFunc 347 348 err := handleEventFunc(reconciler)(context.Background(), event) 349 assert.Equal(t, tc.expectedError, err != nil) 350 351 assert.True(t, doneCalled, "Done must be called on the event in all cases") 352 assert.Equal(t, tc.expectedError, doneError != nil, "Expected done error should match") 353 354 assert.ElementsMatch(t, tc.expectedAdded, manager.addedConfigNames, "Expected added configs should match") 355 assert.ElementsMatch(t, tc.expectedUpdated, manager.updatedConfigNames, "Expected updated configs should match") 356 assert.ElementsMatch(t, tc.expectedDeleted, manager.deletedConfigNames, "Expected deleted configs should match") 357 358 // Assert that the stored state whether a config selects the local Node or not has been updated 359 for _, n := range append(manager.addedConfigNames, manager.updatedConfigNames...) { 360 split := strings.Split(n, "/") 361 ns, name := split[0], split[1] 362 assert.True(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode) 363 } 364 for _, n := range manager.deletedConfigNames { 365 split := strings.Split(n, "/") 366 ns, name := split[0], split[1] 367 if event.Kind == resource.Delete { 368 assert.NotContains(t, reconciler.configs, resource.Key{Namespace: ns, Name: name}, 369 "Deleted configs due to deletion event should be deleted from local cache") 370 } else { 371 assert.False(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode, 372 "Deleted configs due to update should be kept in the local cache - but marked as not selecting local node") 373 } 374 } 375 }) 376 } 377 } 378 379 type cecOpts func(spec *ciliumv2.CiliumEnvoyConfigSpec) 380 381 func withoutNodeSelector() func(spec *ciliumv2.CiliumEnvoyConfigSpec) { 382 return func(spec *ciliumv2.CiliumEnvoyConfigSpec) { 383 spec.NodeSelector = nil 384 } 385 } 386 387 func withNodeLabelSelector(labels map[string]string) func(spec *ciliumv2.CiliumEnvoyConfigSpec) { 388 return func(spec *ciliumv2.CiliumEnvoyConfigSpec) { 389 spec.NodeSelector = &slim_metav1.LabelSelector{ 390 MatchLabels: labels, 391 } 392 } 393 } 394 395 func testCEC(opts ...cecOpts) *ciliumv2.CiliumEnvoyConfig { 396 cec := &ciliumv2.CiliumEnvoyConfig{ 397 ObjectMeta: metav1.ObjectMeta{ 398 Namespace: "test", 399 Name: "test", 400 }, 401 Spec: ciliumv2.CiliumEnvoyConfigSpec{}, 402 } 403 404 for _, opt := range opts { 405 opt(&cec.Spec) 406 } 407 408 return cec 409 } 410 411 func testCCEC(opts ...cecOpts) *ciliumv2.CiliumClusterwideEnvoyConfig { 412 ccec := &ciliumv2.CiliumClusterwideEnvoyConfig{ 413 ObjectMeta: metav1.ObjectMeta{ 414 Namespace: "test", 415 Name: "test", 416 }, 417 Spec: ciliumv2.CiliumEnvoyConfigSpec{}, 418 } 419 420 for _, opt := range opts { 421 opt(&ccec.Spec) 422 } 423 424 return ccec 425 } 426 427 func TestReconcileExistingConfigs(t *testing.T) { 428 tests := []struct { 429 name string 430 configs map[resource.Key]*config 431 currentNodeLabels map[string]string 432 failFor []string 433 expectedError bool 434 expectedErrorMessages []string 435 expectedAdded []string 436 expectedDeleted []string 437 }{ 438 { 439 name: "No changes if no configs are present", 440 configs: map[resource.Key]*config{}, 441 currentNodeLabels: map[string]string{}, 442 expectedError: false, 443 expectedAdded: []string{}, 444 expectedDeleted: []string{}, 445 }, 446 { 447 name: "No changes if there are no changes in configs selecting nodes or not", 448 configs: map[resource.Key]*config{ 449 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", nil, true), 450 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true), 451 }, 452 currentNodeLabels: map[string]string{ 453 "role": "worker", 454 }, 455 expectedError: false, 456 expectedAdded: []string{}, 457 expectedDeleted: []string{}, 458 }, 459 { 460 name: "Delete configs that no longer select the local node", 461 configs: map[resource.Key]*config{ 462 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra"}, true), 463 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true), 464 }, 465 currentNodeLabels: map[string]string{ 466 "role": "worker", 467 }, 468 expectedError: false, 469 expectedAdded: []string{}, 470 expectedDeleted: []string{"ns1/config1"}, 471 }, 472 { 473 name: "Add configs that start to select the local node", 474 configs: map[resource.Key]*config{ 475 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra"}, false), 476 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true), 477 }, 478 currentNodeLabels: map[string]string{ 479 "role": "infra", 480 }, 481 expectedError: false, 482 expectedAdded: []string{"ns1/config1"}, 483 expectedDeleted: []string{}, 484 }, 485 { 486 name: "Multiple changes", 487 configs: map[resource.Key]*config{ 488 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra", "node": "node1"}, false), 489 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", map[string]string{"role": "infra", "node": "node1"}, false), 490 {Namespace: "ns1", Name: "config3"}: testConfig("ns1", "config3", map[string]string{"role": "infra", "node": "node1", "environment": "test"}, false), 491 {Namespace: "ns1", Name: "config4"}: testConfig("ns1", "config4", nil, true), 492 {Namespace: "ns1", Name: "config5"}: testConfig("ns1", "config5", map[string]string{"role": "worker", "node": "node1"}, true), 493 {Namespace: "ns1", Name: "config6"}: testConfig("ns1", "config6", map[string]string{"role": "worker", "node": "node1"}, true), 494 {Namespace: "ns1", Name: "config7"}: testConfig("ns1", "config7", map[string]string{"role": "worker", "node": "node1", "environment": "test"}, false), 495 }, 496 currentNodeLabels: map[string]string{ 497 "node": "node1", 498 "role": "infra", 499 }, 500 expectedError: false, 501 expectedAdded: []string{"ns1/config1", "ns1/config2"}, 502 expectedDeleted: []string{"ns1/config5", "ns1/config6"}, 503 }, 504 { 505 name: "Failures during updating individual configs should't abort", 506 configs: map[resource.Key]*config{ 507 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra", "node": "node1"}, false), 508 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", map[string]string{"role": "infra", "node": "node1"}, false), 509 {Namespace: "ns1", Name: "config3"}: testConfig("ns1", "config3", map[string]string{"role": "infra", "node": "node1", "environment": "test"}, false), 510 {Namespace: "ns1", Name: "config4"}: testConfig("ns1", "config4", nil, true), 511 {Namespace: "ns1", Name: "config5"}: testConfig("ns1", "config5", map[string]string{"role": "worker", "node": "node1"}, true), 512 {Namespace: "ns1", Name: "config6"}: testConfig("ns1", "config6", map[string]string{"role": "worker", "node": "node1"}, true), 513 {Namespace: "ns1", Name: "config7"}: testConfig("ns1", "config7", map[string]string{"role": "worker", "node": "node1", "environment": "test"}, false), 514 }, 515 currentNodeLabels: map[string]string{ 516 "node": "node1", 517 "role": "infra", 518 }, 519 failFor: []string{"ns1/config2", "ns1/config5"}, 520 expectedError: true, 521 expectedErrorMessages: []string{ 522 "failed to reconcile existing config (ns1/config2): failed to add config ns1/config2", 523 "failed to reconcile existing config (ns1/config5): failed to delete config ns1/config5", 524 }, 525 expectedAdded: []string{"ns1/config1"}, 526 expectedDeleted: []string{"ns1/config6"}, 527 }, 528 } 529 530 for _, tc := range tests { 531 t.Run(tc.name, func(t *testing.T) { 532 logger := logrus.New() 533 logger.SetOutput(io.Discard) 534 535 manager := &fakeCECManager{ 536 shouldFailFor: tc.failFor, 537 } 538 539 reconciler := newCiliumEnvoyConfigReconciler(reconcilerParams{Logger: logger, Manager: manager}) 540 541 // init current state 542 reconciler.configs = make(map[resource.Key]*config, len(tc.configs)) 543 for k, v := range tc.configs { 544 reconciler.configs[k] = &config{ 545 meta: v.meta, 546 spec: v.spec.DeepCopy(), 547 selectsLocalNode: v.selectsLocalNode, 548 } 549 } 550 reconciler.localNodeLabels = tc.currentNodeLabels 551 552 err := reconciler.reconcileExistingConfigs(context.Background()) 553 assert.Equal(t, tc.expectedError, err != nil) 554 if tc.expectedError { 555 for _, expectedErrorMessage := range tc.expectedErrorMessages { 556 assert.ErrorContains(t, err, expectedErrorMessage) 557 } 558 } 559 560 assert.ElementsMatch(t, tc.expectedAdded, manager.addedConfigNames) 561 assert.ElementsMatch(t, tc.expectedDeleted, manager.deletedConfigNames) 562 563 assert.Empty(t, manager.updatedConfigNames, "Should never update an existing config") 564 565 // Assert that the stored state whether a config selects the local Node or not has been updated 566 for _, n := range manager.addedConfigNames { 567 split := strings.Split(n, "/") 568 ns, name := split[0], split[1] 569 assert.True(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode) 570 } 571 572 for _, n := range manager.deletedConfigNames { 573 split := strings.Split(n, "/") 574 ns, name := split[0], split[1] 575 assert.False(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode) 576 } 577 578 // Check that state didn't change for configs that failed to reconcile 579 for _, n := range tc.failFor { 580 split := strings.Split(n, "/") 581 ns, name := split[0], split[1] 582 assert.Equal(t, 583 tc.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode, 584 reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode, 585 "Configs shouldn't change their selection state if their reconciliation failed", 586 ) 587 } 588 }) 589 } 590 } 591 592 func TestHandleLocalNodeLabels(t *testing.T) { 593 tests := []struct { 594 name string 595 configs map[resource.Key]*config 596 currentNodeLabels map[string]string 597 newNodeLabels map[string]string 598 failFor []string 599 expectedDeleted []string 600 }{ 601 { 602 name: "No changes if no configs are present", 603 configs: map[resource.Key]*config{}, 604 currentNodeLabels: map[string]string{}, 605 newNodeLabels: map[string]string{}, 606 expectedDeleted: []string{}, 607 }, 608 { 609 name: "No changes if node labels don't change", 610 configs: map[resource.Key]*config{ 611 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", nil, true), 612 }, 613 currentNodeLabels: map[string]string{ 614 "role": "infra", 615 }, 616 newNodeLabels: map[string]string{ 617 "role": "infra", 618 }, 619 expectedDeleted: []string{}, 620 }, 621 { 622 name: "No changes if there are no changes in configs selecting nodes or not", 623 configs: map[resource.Key]*config{ 624 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", nil, true), 625 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true), 626 }, 627 currentNodeLabels: map[string]string{ 628 "role": "infra", 629 }, 630 newNodeLabels: map[string]string{ 631 "role": "worker", 632 }, 633 expectedDeleted: []string{}, 634 }, 635 { 636 name: "Updated node labels triggers a best-effort reconciliation of existing configs", 637 configs: map[resource.Key]*config{ 638 {Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra"}, true), 639 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true), 640 }, 641 currentNodeLabels: map[string]string{ 642 "role": "infra", 643 }, 644 newNodeLabels: map[string]string{ 645 "role": "worker", 646 }, 647 expectedDeleted: []string{"ns1/config1"}, 648 }, 649 { 650 name: "Failures during updating individual configs should't result in any error - as it's only best effort", 651 configs: map[resource.Key]*config{ 652 {Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", map[string]string{"role": "infra", "node": "node1"}, false), 653 }, 654 currentNodeLabels: map[string]string{ 655 "node": "node1", 656 "role": "worker", 657 }, 658 newNodeLabels: map[string]string{ 659 "node": "node1", 660 "role": "infra", 661 }, 662 failFor: []string{"ns1/config2"}, 663 expectedDeleted: []string{}, 664 }, 665 } 666 667 for _, tc := range tests { 668 t.Run(tc.name, func(t *testing.T) { 669 logger := logrus.New() 670 logger.SetOutput(io.Discard) 671 672 manager := &fakeCECManager{ 673 shouldFailFor: tc.failFor, 674 } 675 676 reconciler := newCiliumEnvoyConfigReconciler(reconcilerParams{Logger: logger, Manager: manager}) 677 678 // init current state 679 reconciler.configs = make(map[resource.Key]*config, len(tc.configs)) 680 for k, v := range tc.configs { 681 reconciler.configs[k] = &config{ 682 meta: v.meta, 683 spec: v.spec.DeepCopy(), 684 selectsLocalNode: v.selectsLocalNode, 685 } 686 } 687 reconciler.localNodeLabels = tc.currentNodeLabels 688 689 node := node.LocalNode{Node: types.Node{Name: "test", Labels: tc.newNodeLabels}} 690 691 err := reconciler.handleLocalNodeEvent(context.Background(), node) 692 assert.NoError(t, err) 693 694 assert.Equal(t, tc.newNodeLabels, reconciler.localNodeLabels) 695 696 assert.ElementsMatch(t, tc.expectedDeleted, manager.deletedConfigNames) 697 698 assert.Empty(t, manager.addedConfigNames) 699 assert.Empty(t, manager.updatedConfigNames, "Should never update an existing config") 700 701 // Assert that the stored state whether a config selects the local Node or not has been updated 702 for _, n := range manager.addedConfigNames { 703 split := strings.Split(n, "/") 704 ns, name := split[0], split[1] 705 assert.True(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode) 706 } 707 708 for _, n := range manager.deletedConfigNames { 709 split := strings.Split(n, "/") 710 ns, name := split[0], split[1] 711 assert.False(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode) 712 } 713 714 // Check that state didn't change for configs that failed to reconcile 715 for _, n := range tc.failFor { 716 split := strings.Split(n, "/") 717 ns, name := split[0], split[1] 718 assert.Equal(t, 719 tc.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode, 720 reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode, 721 "Configs shouldn't change their selection state if their reconciliation failed", 722 ) 723 } 724 }) 725 } 726 } 727 728 func testConfig(namespace string, name string, nodeSelectorLabels map[string]string, selectsLocalNode bool) *config { 729 cfg := &config{ 730 meta: metav1.ObjectMeta{ 731 Namespace: namespace, 732 Name: name, 733 }, 734 spec: &ciliumv2.CiliumEnvoyConfigSpec{}, 735 selectsLocalNode: selectsLocalNode, 736 } 737 738 if nodeSelectorLabels != nil { 739 cfg.spec.NodeSelector = &slim_metav1.LabelSelector{ 740 MatchLabels: nodeSelectorLabels, 741 } 742 } 743 744 return cfg 745 } 746 747 type fakeCECManager struct { 748 addedConfigNames []string 749 deletedConfigNames []string 750 updatedConfigNames []string 751 shouldFailFor []string 752 } 753 754 var _ ciliumEnvoyConfigManager = &fakeCECManager{} 755 756 func (r *fakeCECManager) addCiliumEnvoyConfig(cecObjectMeta metav1.ObjectMeta, cecSpec *ciliumv2.CiliumEnvoyConfigSpec) error { 757 namespacedName := fmt.Sprintf("%s/%s", cecObjectMeta.Namespace, cecObjectMeta.Name) 758 759 if slices.Contains(r.shouldFailFor, namespacedName) { 760 return fmt.Errorf("failed to add config %s", namespacedName) 761 } 762 763 r.addedConfigNames = append(r.addedConfigNames, namespacedName) 764 765 return nil 766 } 767 768 func (r *fakeCECManager) deleteCiliumEnvoyConfig(cecObjectMeta metav1.ObjectMeta, cecSpec *ciliumv2.CiliumEnvoyConfigSpec) error { 769 namespacedName := fmt.Sprintf("%s/%s", cecObjectMeta.Namespace, cecObjectMeta.Name) 770 771 if slices.Contains(r.shouldFailFor, namespacedName) { 772 return fmt.Errorf("failed to delete config %s", namespacedName) 773 } 774 775 r.deletedConfigNames = append(r.deletedConfigNames, namespacedName) 776 777 return nil 778 } 779 780 func (r *fakeCECManager) updateCiliumEnvoyConfig(oldCECObjectMeta metav1.ObjectMeta, oldCECSpec *ciliumv2.CiliumEnvoyConfigSpec, newCECObjectMeta metav1.ObjectMeta, newCECSpec *ciliumv2.CiliumEnvoyConfigSpec) error { 781 namespacedName := fmt.Sprintf("%s/%s", newCECObjectMeta.Namespace, newCECObjectMeta.Name) 782 783 if slices.Contains(r.shouldFailFor, namespacedName) { 784 return fmt.Errorf("failed to update config %s", namespacedName) 785 } 786 787 r.updatedConfigNames = append(r.updatedConfigNames, namespacedName) 788 789 return nil 790 } 791 792 func (r *fakeCECManager) syncCiliumEnvoyConfigService(name string, namespace string, cecSpec *ciliumv2.CiliumEnvoyConfigSpec) error { 793 return nil 794 }