github.com/cilium/cilium@v1.16.2/pkg/l2announcer/l2announcer_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package l2announcer 5 6 import ( 7 "context" 8 "net/netip" 9 "slices" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/cilium/hive/cell" 15 "github.com/cilium/hive/hivetest" 16 "github.com/cilium/hive/job" 17 "github.com/cilium/statedb" 18 "github.com/sirupsen/logrus" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "go.uber.org/goleak" 22 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/apimachinery/pkg/runtime" 24 "k8s.io/client-go/kubernetes/fake" 25 "k8s.io/client-go/tools/cache" 26 "k8s.io/utils/ptr" 27 28 "github.com/cilium/cilium/daemon/k8s" 29 "github.com/cilium/cilium/pkg/datapath/tables" 30 "github.com/cilium/cilium/pkg/hive" 31 v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 32 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" 33 "github.com/cilium/cilium/pkg/k8s/client" 34 "github.com/cilium/cilium/pkg/k8s/resource" 35 slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" 36 slim_meta_v1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 37 "github.com/cilium/cilium/pkg/lock" 38 "github.com/cilium/cilium/pkg/option" 39 ) 40 41 type fixture struct { 42 announcer *L2Announcer 43 proxyNeighborTable statedb.Table[*tables.L2AnnounceEntry] 44 stateDB *statedb.DB 45 fakeSvcStore *fakeStore[*slim_corev1.Service] 46 fakePolicyStore *fakeStore[*v2alpha1.CiliumL2AnnouncementPolicy] 47 } 48 49 func newFixture(t testing.TB) *fixture { 50 var ( 51 tbl statedb.RWTable[*tables.L2AnnounceEntry] 52 db *statedb.DB 53 jr job.Registry 54 jg job.Group 55 h cell.Health 56 ) 57 58 hive.New( 59 cell.Provide(tables.NewL2AnnounceTable), 60 cell.Module("test", "test", cell.Invoke(func(d *statedb.DB, t statedb.RWTable[*tables.L2AnnounceEntry], h_ cell.Health, j job.Registry, jg_ job.Group) { 61 d.RegisterTable(t) 62 db = d 63 tbl = t 64 jr = j 65 jg = jg_ 66 h = h_ 67 })), 68 ).Populate(hivetest.Logger(t)) 69 70 fakeSvcStore := &fakeStore[*slim_corev1.Service]{} 71 fakePolicyStore := &fakeStore[*v2alpha1.CiliumL2AnnouncementPolicy]{} 72 73 params := l2AnnouncerParams{ 74 Logger: logrus.New(), 75 Lifecycle: &cell.DefaultLifecycle{}, 76 Health: h, 77 DaemonConfig: &option.DaemonConfig{ 78 ConfigPatchMutex: new(lock.RWMutex), 79 K8sNamespace: "kube_system", 80 EnableL2Announcements: true, 81 L2AnnouncerLeaseDuration: 15 * time.Second, 82 L2AnnouncerRenewDeadline: 5 * time.Second, 83 L2AnnouncerRetryPeriod: 2 * time.Second, 84 }, 85 Clientset: &client.FakeClientset{ 86 KubernetesFakeClientset: fake.NewSimpleClientset(), 87 }, 88 L2AnnounceTable: tbl, 89 StateDB: db, 90 JobGroup: jg, 91 } 92 93 // Setting stores normally happens in .run which we bypass for testing purposes 94 announcer := NewL2Announcer(params) 95 announcer.policyStore = fakePolicyStore 96 announcer.svcStore = fakeSvcStore 97 announcer.params.JobGroup = jr.NewGroup(h) 98 announcer.scopedGroup = announcer.params.JobGroup.Scoped("leader-election") 99 announcer.params.JobGroup.Start(context.Background()) 100 101 return &fixture{ 102 announcer: announcer, 103 proxyNeighborTable: tbl, 104 stateDB: db, 105 fakeSvcStore: fakeSvcStore, 106 fakePolicyStore: fakePolicyStore, 107 } 108 } 109 110 var _ resource.Store[runtime.Object] = (*fakeStore[runtime.Object])(nil) 111 112 type fakeStore[T runtime.Object] struct { 113 slice []T 114 } 115 116 func (fs *fakeStore[T]) List() []T { 117 return fs.slice 118 } 119 func (fs *fakeStore[T]) IterKeys() resource.KeyIter { return nil } 120 func (fs *fakeStore[T]) Get(obj T) (item T, exists bool, err error) { 121 var def T 122 return def, false, nil 123 } 124 func (fs *fakeStore[T]) GetByKey(key resource.Key) (item T, exists bool, err error) { 125 var def T 126 return def, false, nil 127 } 128 func (fs *fakeStore[T]) IndexKeys(indexName, indexedValue string) ([]string, error) { 129 return nil, nil 130 } 131 func (fs *fakeStore[T]) ByIndex(indexName, indexedValue string) ([]T, error) { 132 return nil, nil 133 } 134 func (fs *fakeStore[T]) CacheStore() cache.Store { return nil } 135 func (fs *fakeStore[T]) Release() {} 136 137 var _ resource.Resource[runtime.Object] = (*fakeResource[runtime.Object])(nil) 138 139 type fakeResource[T runtime.Object] struct { 140 store resource.Store[T] 141 } 142 143 func (fr *fakeResource[T]) Observe(ctx context.Context, next func(event resource.Event[T]), complete func(error)) { 144 145 } 146 147 func (fr *fakeResource[T]) Events(ctx context.Context, opts ...resource.EventsOpt) <-chan resource.Event[T] { 148 return make(<-chan resource.Event[T]) 149 } 150 151 func (fr *fakeResource[T]) Store(context.Context) (resource.Store[T], error) { 152 if fr.store != nil { 153 return fr.store, nil 154 } 155 156 return &fakeStore[T]{}, nil 157 } 158 159 func blueNode() *v2.CiliumNode { 160 return &v2.CiliumNode{ 161 ObjectMeta: meta_v1.ObjectMeta{ 162 Name: "blue-node", 163 Labels: map[string]string{ 164 "color": "blue", 165 }, 166 }, 167 } 168 } 169 170 func bluePolicy() *v2alpha1.CiliumL2AnnouncementPolicy { 171 return &v2alpha1.CiliumL2AnnouncementPolicy{ 172 ObjectMeta: meta_v1.ObjectMeta{ 173 Name: "blue-policy", 174 }, 175 Spec: v2alpha1.CiliumL2AnnouncementPolicySpec{ 176 NodeSelector: &slim_meta_v1.LabelSelector{ 177 MatchLabels: map[string]string{ 178 "color": "blue", 179 }, 180 }, 181 ServiceSelector: &slim_meta_v1.LabelSelector{ 182 MatchLabels: map[string]string{ 183 "color": "blue", 184 }, 185 }, 186 ExternalIPs: true, 187 Interfaces: []string{ 188 "eno01", 189 }, 190 }, 191 } 192 } 193 194 func blueService() *slim_corev1.Service { 195 return &slim_corev1.Service{ 196 ObjectMeta: slim_meta_v1.ObjectMeta{ 197 Namespace: "default", 198 Name: "blue-service", 199 Labels: map[string]string{ 200 "color": "blue", 201 }, 202 }, 203 Spec: slim_corev1.ServiceSpec{ 204 ExternalIPs: []string{"192.168.2.1"}, 205 }, 206 } 207 } 208 209 // Test the happy path, make sure that we create proxy neighbor entries 210 func TestHappyPath(t *testing.T) { 211 fix := newFixture(t) 212 213 fix.announcer.devices = []string{"eno01"} 214 err := fix.announcer.processDevicesChanged(context.Background()) 215 assert.NoError(t, err) 216 217 localNode := blueNode() 218 err = fix.announcer.upsertLocalNode(context.Background(), localNode) 219 assert.NoError(t, err) 220 assert.Equal(t, localNode, fix.announcer.localNode) 221 222 policy := bluePolicy() 223 fix.fakePolicyStore.slice = append(fix.fakePolicyStore.slice, policy) 224 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 225 Kind: resource.Upsert, 226 Key: resource.NewKey(policy), 227 Object: policy, 228 Done: func(err error) {}, 229 }) 230 assert.NoError(t, err) 231 assert.Contains(t, fix.announcer.selectedPolicies, resource.NewKey(policy)) 232 233 svc := blueService() 234 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 235 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 236 Kind: resource.Upsert, 237 Key: resource.NewKey(svc), 238 Object: svc, 239 Done: func(err error) {}, 240 }) 241 assert.NoError(t, err) 242 243 svcKey := serviceKey(blueService()) 244 if !assert.Contains(t, fix.announcer.selectedServices, svcKey) { 245 return 246 } 247 248 rtx := fix.stateDB.ReadTxn() 249 iter := fix.proxyNeighborTable.All(rtx) 250 entries := statedb.Collect(iter) 251 assert.Len(t, entries, 0) 252 253 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 254 typ: leaderElectionLeading, 255 selectedService: fix.announcer.selectedServices[svcKey], 256 }) 257 assert.NoError(t, err) 258 259 rtx = fix.stateDB.ReadTxn() 260 iter = fix.proxyNeighborTable.All(rtx) 261 entries = statedb.Collect(iter) 262 assert.Len(t, entries, 1) 263 assert.Equal(t, entries[0], &tables.L2AnnounceEntry{ 264 L2AnnounceKey: tables.L2AnnounceKey{ 265 IP: netip.MustParseAddr(svc.Spec.ExternalIPs[0]), 266 NetworkInterface: policy.Spec.Interfaces[0], 267 }, 268 Origins: []resource.Key{svcKey}, 269 }) 270 271 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 272 fix.announcer.params.JobGroup.Stop(ctx) 273 cancel() 274 } 275 276 // Test the happy path, but in every permutation of events. It should not matter in which order objects are processed 277 // we should always end on the same result. 278 func TestHappyPathPermutations(t *testing.T) { 279 addDevices := func(fix *fixture, tt *testing.T) { 280 fix.announcer.devices = []string{"eno01"} 281 err := fix.announcer.processDevicesChanged(context.Background()) 282 assert.NoError(t, err) 283 } 284 addPolicy := func(fix *fixture, tt *testing.T) { 285 policy := bluePolicy() 286 fix.fakePolicyStore.slice = append(fix.fakePolicyStore.slice, policy) 287 err := fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 288 Kind: resource.Upsert, 289 Key: resource.NewKey(policy), 290 Object: policy, 291 Done: func(err error) {}, 292 }) 293 assert.NoError(tt, err) 294 } 295 addService := func(fix *fixture, tt *testing.T) { 296 svc := blueService() 297 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 298 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 299 Kind: resource.Upsert, 300 Key: resource.NewKey(svc), 301 Object: svc, 302 Done: func(err error) {}, 303 }) 304 assert.NoError(tt, err) 305 } 306 307 type fn struct { 308 name string 309 fn func(fix *fixture, tt *testing.T) 310 } 311 funcs := []fn{ 312 {name: "policy", fn: addPolicy}, 313 {name: "svc", fn: addService}, 314 {name: "dev", fn: addDevices}, 315 } 316 run := func(fns []fn) { 317 var names []string 318 for _, fn := range fns { 319 names = append(names, fn.name) 320 } 321 t.Run(strings.Join(names, "_"), func(tt *testing.T) { 322 fix := newFixture(tt) 323 defer func() { 324 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 325 fix.announcer.params.JobGroup.Stop(ctx) 326 cancel() 327 }() 328 329 err := fix.announcer.upsertLocalNode(context.Background(), blueNode()) 330 assert.NoError(tt, err) 331 332 for _, fn := range fns { 333 fn.fn(fix, tt) 334 } 335 336 rtx := fix.stateDB.ReadTxn() 337 iter := fix.proxyNeighborTable.All(rtx) 338 entries := statedb.Collect(iter) 339 assert.Len(tt, entries, 0) 340 341 if assert.Contains(tt, fix.announcer.selectedServices, serviceKey(blueService())) { 342 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 343 typ: leaderElectionLeading, 344 selectedService: fix.announcer.selectedServices[serviceKey(blueService())], 345 }) 346 assert.NoError(tt, err) 347 } 348 349 rtx = fix.stateDB.ReadTxn() 350 iter = fix.proxyNeighborTable.All(rtx) 351 entries = statedb.Collect(iter) 352 if assert.Len(tt, entries, 1) { 353 assert.Equal(tt, entries[0], &tables.L2AnnounceEntry{ 354 L2AnnounceKey: tables.L2AnnounceKey{ 355 IP: netip.MustParseAddr(blueService().Spec.ExternalIPs[0]), 356 NetworkInterface: bluePolicy().Spec.Interfaces[0], 357 }, 358 Origins: []resource.Key{serviceKey(blueService())}, 359 }) 360 } 361 }) 362 } 363 364 // Heap's algorithm to run every permutation 365 // https://en.wikipedia.org/wiki/Heap%27s_algorithm#Details_of_the_algorithm 366 var generate func(k int, fns []fn) 367 generate = func(k int, fns []fn) { 368 if k == 1 { 369 run(fns) 370 } else { 371 generate(k-1, fns) 372 373 for i := 0; i < k-1; i++ { 374 if k%2 == 0 { 375 fns[i], fns[k-1] = fns[k-1], fns[i] 376 } else { 377 fns[0], fns[k-1] = fns[k-1], fns[0] 378 } 379 380 generate(k-1, fns) 381 } 382 } 383 } 384 generate(len(funcs), funcs) 385 } 386 387 // Test that when two policies select the same service, and one goes away, the service still stays selected 388 func TestPolicyRedundancy(t *testing.T) { 389 fix := newFixture(t) 390 391 fix.announcer.devices = []string{"eno01"} 392 err := fix.announcer.processDevicesChanged(context.Background()) 393 assert.NoError(t, err) 394 395 // Add local node 396 localNode := blueNode() 397 err = fix.announcer.upsertLocalNode(context.Background(), localNode) 398 assert.NoError(t, err) 399 assert.Equal(t, localNode, fix.announcer.localNode) 400 401 // Add first policy 402 policy := bluePolicy() 403 fix.fakePolicyStore.slice = append(fix.fakePolicyStore.slice, policy) 404 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 405 Kind: resource.Upsert, 406 Key: resource.NewKey(policy), 407 Object: policy, 408 Done: func(err error) {}, 409 }) 410 assert.NoError(t, err) 411 412 // Add second policy 413 policy2 := bluePolicy() 414 policy2.Name = "second-blue-policy" 415 fix.fakePolicyStore.slice = append(fix.fakePolicyStore.slice, policy2) 416 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 417 Kind: resource.Upsert, 418 Key: resource.NewKey(policy2), 419 Object: policy2, 420 Done: func(err error) {}, 421 }) 422 assert.NoError(t, err) 423 424 // Add service policy 425 svc := blueService() 426 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 427 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 428 Kind: resource.Upsert, 429 Key: resource.NewKey(svc), 430 Object: svc, 431 Done: func(err error) {}, 432 }) 433 assert.NoError(t, err) 434 435 // Assert service is selected 436 svcKey := serviceKey(blueService()) 437 if !assert.Contains(t, fix.announcer.selectedServices, svcKey) { 438 return 439 } 440 441 // Assert both policies selected service 442 assert.Contains(t, fix.announcer.selectedServices[svcKey].byPolicies, policyKey(policy)) 443 assert.Contains(t, fix.announcer.selectedServices[svcKey].byPolicies, policyKey(policy2)) 444 445 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 446 typ: leaderElectionLeading, 447 selectedService: fix.announcer.selectedServices[svcKey], 448 }) 449 assert.NoError(t, err) 450 451 // Assert selected service turned into Proxy Neighbor Entry 452 rtx := fix.stateDB.ReadTxn() 453 iter := fix.proxyNeighborTable.All(rtx) 454 entries := statedb.Collect(iter) 455 assert.Len(t, entries, 1) 456 assert.Equal(t, entries[0], &tables.L2AnnounceEntry{ 457 L2AnnounceKey: tables.L2AnnounceKey{ 458 IP: netip.MustParseAddr(svc.Spec.ExternalIPs[0]), 459 NetworkInterface: policy.Spec.Interfaces[0], 460 }, 461 Origins: []resource.Key{svcKey}, 462 }) 463 464 // Delete second policy 465 idx := slices.Index(fix.fakePolicyStore.slice, policy2) 466 fix.fakePolicyStore.slice = slices.Delete(fix.fakePolicyStore.slice, idx, idx+1) 467 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 468 Kind: resource.Delete, 469 Key: resource.NewKey(policy2), 470 Object: policy2, 471 Done: func(err error) {}, 472 }) 473 assert.NoError(t, err) 474 475 // Assert only one policy selected 476 assert.Equal(t, []resource.Key{ 477 policyKey(policy), 478 }, fix.announcer.selectedServices[svcKey].byPolicies) 479 480 // Assert Proxy Neighbor Entry still exists 481 rtx = fix.stateDB.ReadTxn() 482 iter = fix.proxyNeighborTable.All(rtx) 483 entries = statedb.Collect(iter) 484 assert.Len(t, entries, 1) 485 assert.Equal(t, entries[0], &tables.L2AnnounceEntry{ 486 L2AnnounceKey: tables.L2AnnounceKey{ 487 IP: netip.MustParseAddr(svc.Spec.ExternalIPs[0]), 488 NetworkInterface: policy.Spec.Interfaces[0], 489 }, 490 Origins: []resource.Key{svcKey}, 491 }) 492 493 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 494 fix.announcer.params.JobGroup.Stop(ctx) 495 cancel() 496 } 497 498 func baseUpdateSetup(t *testing.T) *fixture { 499 fix := newFixture(t) 500 501 fix.announcer.devices = []string{"eno01"} 502 err := fix.announcer.processDevicesChanged(context.Background()) 503 require.NoError(t, err) 504 require.Len(t, fix.announcer.devices, 1) 505 require.Contains(t, fix.announcer.devices, "eno01") 506 507 localNode := blueNode() 508 err = fix.announcer.upsertLocalNode(context.Background(), localNode) 509 require.NoError(t, err) 510 require.Equal(t, localNode, fix.announcer.localNode) 511 512 policy := bluePolicy() 513 fix.fakePolicyStore.slice = append(fix.fakePolicyStore.slice, policy) 514 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 515 Kind: resource.Upsert, 516 Key: resource.NewKey(policy), 517 Object: policy, 518 Done: func(err error) {}, 519 }) 520 require.NoError(t, err) 521 522 require.Len(t, fix.announcer.selectedPolicies, 1) 523 require.Len(t, fix.announcer.selectedServices, 0) 524 525 svc := blueService() 526 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 527 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 528 Kind: resource.Upsert, 529 Key: resource.NewKey(svc), 530 Object: svc, 531 Done: func(err error) {}, 532 }) 533 require.NoError(t, err) 534 535 require.Len(t, fix.announcer.selectedPolicies, 1) 536 require.Len(t, fix.announcer.selectedServices, 1) 537 538 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 539 typ: leaderElectionLeading, 540 selectedService: fix.announcer.selectedServices[serviceKey(svc)], 541 }) 542 require.NoError(t, err) 543 544 rtx := fix.stateDB.ReadTxn() 545 iter := fix.proxyNeighborTable.All(rtx) 546 entries := statedb.Collect(iter) 547 548 require.Len(t, entries, 1) 549 550 return fix 551 } 552 553 // Update the host labels so the currently policy does not match anymore. Assert that policies are no longer selected 554 // services are no longer selected and proxy neighbor entries are removed. 555 func TestUpdateHostLabels_NoMatch(t *testing.T) { 556 fix := baseUpdateSetup(t) 557 558 node := blueNode() 559 node.Labels["color"] = "cyan" 560 561 err := fix.announcer.processLocalNodeEvent(context.Background(), resource.Event[*v2.CiliumNode]{ 562 Kind: resource.Upsert, 563 Key: resource.NewKey(node), 564 Object: node, 565 Done: func(err error) {}, 566 }) 567 assert.NoError(t, err) 568 569 assert.Len(t, fix.announcer.selectedPolicies, 0) 570 assert.Len(t, fix.announcer.selectedServices, 0) 571 572 // Assert Proxy Neighbor Entry is deleted 573 rtx := fix.stateDB.ReadTxn() 574 iter := fix.proxyNeighborTable.All(rtx) 575 entries := statedb.Collect(iter) 576 assert.Len(t, entries, 0) 577 578 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 579 fix.announcer.params.JobGroup.Stop(ctx) 580 cancel() 581 } 582 583 // When policies and services exist that currently don't match, assert that these are added properly when the labels 584 // on the local node change. 585 func TestUpdateHostLabels_AdditionalMatch(t *testing.T) { 586 fix := baseUpdateSetup(t) 587 588 // Check that active policies and selected services is 1 589 assert.Len(t, fix.announcer.selectedPolicies, 1) 590 assert.Len(t, fix.announcer.selectedServices, 1) 591 592 // Add a non matching policy 593 policy := bluePolicy() 594 policy.Name = "cyan-policy" 595 policy.Spec.NodeSelector.MatchLabels = map[string]string{ 596 "hue": "cyan", 597 } 598 policy.Spec.ServiceSelector.MatchLabels = map[string]string{ 599 "hue": "cyan", 600 } 601 fix.fakePolicyStore.slice = append(fix.fakePolicyStore.slice, policy) 602 err := fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 603 Kind: resource.Upsert, 604 Key: resource.NewKey(policy), 605 Object: policy, 606 Done: func(err error) {}, 607 }) 608 assert.NoError(t, err) 609 610 // Add a non matching service 611 svc := blueService() 612 svc.Name = "cyan-service" 613 svc.Labels = map[string]string{ 614 "hue": "cyan", 615 } 616 svc.Spec.ExternalIPs = []string{"192.168.2.2"} 617 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 618 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 619 Kind: resource.Upsert, 620 Key: resource.NewKey(svc), 621 Object: svc, 622 Done: func(err error) {}, 623 }) 624 assert.NoError(t, err) 625 626 // Check that active policies and selected services is still 1 627 assert.Len(t, fix.announcer.selectedPolicies, 1) 628 assert.Len(t, fix.announcer.selectedServices, 1) 629 630 // Check that proxy neighbor entries are still 1 631 rtx := fix.stateDB.ReadTxn() 632 iter := fix.proxyNeighborTable.All(rtx) 633 entries := statedb.Collect(iter) 634 assert.Len(t, entries, 1) 635 636 node := blueNode() 637 node.Labels = map[string]string{ 638 "color": "blue", 639 "hue": "cyan", 640 } 641 642 err = fix.announcer.processLocalNodeEvent(context.Background(), resource.Event[*v2.CiliumNode]{ 643 Kind: resource.Upsert, 644 Key: resource.NewKey(node), 645 Object: node, 646 Done: func(err error) {}, 647 }) 648 assert.NoError(t, err) 649 650 // Check that active policies and selected services are now 2 651 assert.Len(t, fix.announcer.selectedPolicies, 2) 652 assert.Len(t, fix.announcer.selectedServices, 2) 653 654 // Become leader for service 655 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 656 typ: leaderElectionLeading, 657 selectedService: fix.announcer.selectedServices[serviceKey(svc)], 658 }) 659 assert.NoError(t, err) 660 661 // Check that proxy neighbor entries are now 2 662 rtx = fix.stateDB.ReadTxn() 663 iter = fix.proxyNeighborTable.All(rtx) 664 entries = statedb.Collect(iter) 665 assert.Len(t, entries, 2) 666 667 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 668 fix.announcer.params.JobGroup.Stop(ctx) 669 cancel() 670 } 671 672 // Test that when a policy update causes a service to no longer match, that the service is removed 673 func TestUpdatePolicy_NoMatch(t *testing.T) { 674 fix := baseUpdateSetup(t) 675 676 policy := bluePolicy() 677 policy.Spec.ServiceSelector.MatchLabels["color"] = "red" 678 fix.fakePolicyStore.slice[0] = policy 679 err := fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 680 Kind: resource.Upsert, 681 Key: resource.NewKey(policy), 682 Object: policy, 683 Done: func(err error) {}, 684 }) 685 assert.NoError(t, err) 686 687 assert.Len(t, fix.announcer.selectedPolicies, 1) 688 assert.Len(t, fix.announcer.selectedServices, 0) 689 690 // Assert Proxy Neighbor Entry is deleted 691 rtx := fix.stateDB.ReadTxn() 692 iter := fix.proxyNeighborTable.All(rtx) 693 entries := statedb.Collect(iter) 694 assert.Len(t, entries, 0) 695 696 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 697 fix.announcer.params.JobGroup.Stop(ctx) 698 cancel() 699 } 700 701 // Test that when a policy is updated to match an addition service, that it is added and reflected in the proxy 702 // neighbor table. 703 func TestUpdatePolicy_AdditionalMatch(t *testing.T) { 704 fix := baseUpdateSetup(t) 705 706 // Add a non matching service 707 svc := blueService() 708 svc.Name = "cyan-service" 709 svc.Labels = map[string]string{ 710 "color": "cyan", 711 } 712 svc.Spec.ExternalIPs = []string{"192.168.2.2"} 713 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 714 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 715 Kind: resource.Upsert, 716 Key: resource.NewKey(svc), 717 Object: svc, 718 Done: func(err error) {}, 719 }) 720 assert.NoError(t, err) 721 722 policy := bluePolicy() 723 policy.Spec.ServiceSelector.MatchLabels = nil 724 policy.Spec.ServiceSelector.MatchExpressions = []slim_meta_v1.LabelSelectorRequirement{ 725 {Key: "color", Operator: slim_meta_v1.LabelSelectorOpIn, Values: []string{"blue", "cyan"}}, 726 } 727 fix.fakePolicyStore.slice[0] = policy 728 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 729 Kind: resource.Upsert, 730 Key: resource.NewKey(policy), 731 Object: policy, 732 Done: func(err error) {}, 733 }) 734 assert.NoError(t, err) 735 736 assert.Len(t, fix.announcer.selectedPolicies, 1) 737 assert.Len(t, fix.announcer.selectedServices, 2) 738 739 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 740 typ: leaderElectionLeading, 741 selectedService: fix.announcer.selectedServices[serviceKey(svc)], 742 }) 743 assert.NoError(t, err) 744 745 // Assert that entries for both are added 746 rtx := fix.stateDB.ReadTxn() 747 iter := fix.proxyNeighborTable.All(rtx) 748 entries := statedb.Collect(iter) 749 assert.Len(t, entries, 2) 750 751 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 752 fix.announcer.params.JobGroup.Stop(ctx) 753 cancel() 754 } 755 756 // Test service selection under various conditions 757 func TestPolicySelection(t *testing.T) { 758 fix := baseUpdateSetup(t) 759 760 // Setting external and LB IP to true should select a service from the baseUpdateSetup 761 policy := bluePolicy() 762 policy.Spec.ExternalIPs = true 763 policy.Spec.LoadBalancerIPs = true 764 fix.fakePolicyStore.slice[0] = policy 765 err := fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 766 Kind: resource.Upsert, 767 Key: resource.NewKey(policy), 768 Object: policy, 769 Done: func(err error) {}, 770 }) 771 assert.NoError(t, err) 772 773 assert.Len(t, fix.announcer.selectedPolicies, 1) 774 assert.Len(t, fix.announcer.selectedServices, 1) 775 776 // A service with no externalIP and no LB IP should never be selected 777 svc := blueService() 778 svc.Spec.ExternalIPs = nil 779 svc.Status.LoadBalancer.Ingress = nil 780 fix.fakeSvcStore.slice[0] = svc 781 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 782 Kind: resource.Upsert, 783 Key: resource.NewKey(svc), 784 Object: svc, 785 Done: func(err error) {}, 786 }) 787 assert.NoError(t, err) 788 789 assert.Len(t, fix.announcer.selectedPolicies, 1) 790 assert.Len(t, fix.announcer.selectedServices, 0) 791 792 // Setting external and LB IP to false should not select any services anymore 793 policy.Spec.ExternalIPs = false 794 policy.Spec.LoadBalancerIPs = false 795 fix.fakePolicyStore.slice[0] = policy 796 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 797 Kind: resource.Upsert, 798 Key: resource.NewKey(policy), 799 Object: policy, 800 Done: func(err error) {}, 801 }) 802 assert.NoError(t, err) 803 804 assert.Len(t, fix.announcer.selectedPolicies, 1) 805 assert.Len(t, fix.announcer.selectedServices, 0) 806 807 // Updating an existing non-selected service should not select it 808 svc.Spec = slim_corev1.ServiceSpec{ 809 ExternalIPs: []string{"192.168.2.2"}, 810 } 811 fix.fakeSvcStore.slice[0] = svc 812 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 813 Kind: resource.Upsert, 814 Key: resource.NewKey(svc), 815 Object: svc, 816 Done: func(err error) {}, 817 }) 818 assert.NoError(t, err) 819 820 assert.Len(t, fix.announcer.selectedPolicies, 1) 821 assert.Len(t, fix.announcer.selectedServices, 0) 822 823 // Adding an LB IP to an existing non-selected service should not select it 824 svc.Status.LoadBalancer.Ingress = []slim_corev1.LoadBalancerIngress{ 825 {IP: "192.168.2.7"}, 826 } 827 fix.fakeSvcStore.slice[0] = svc 828 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 829 Kind: resource.Upsert, 830 Key: resource.NewKey(svc), 831 Object: svc, 832 Done: func(err error) {}, 833 }) 834 assert.NoError(t, err) 835 836 assert.Len(t, fix.announcer.selectedPolicies, 1) 837 assert.Len(t, fix.announcer.selectedServices, 0) 838 839 // Altering the policy to select services with LB IPs should only have an entry for LB IPs 840 policy.Spec.ExternalIPs = false 841 policy.Spec.LoadBalancerIPs = true 842 fix.fakePolicyStore.slice[0] = policy 843 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 844 Kind: resource.Upsert, 845 Key: resource.NewKey(policy), 846 Object: policy, 847 Done: func(err error) {}, 848 }) 849 assert.NoError(t, err) 850 assert.Len(t, fix.announcer.selectedPolicies, 1) 851 assert.Len(t, fix.announcer.selectedServices, 1) 852 853 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 854 typ: leaderElectionLeading, 855 selectedService: fix.announcer.selectedServices[serviceKey(svc)], 856 }) 857 assert.NoError(t, err) 858 859 rtx := fix.stateDB.ReadTxn() 860 iter := fix.proxyNeighborTable.All(rtx) 861 entries := statedb.Collect(iter) 862 assert.Len(t, entries, 1) 863 assert.Contains(t, entries, &tables.L2AnnounceEntry{ 864 L2AnnounceKey: tables.L2AnnounceKey{ 865 IP: netip.MustParseAddr("192.168.2.7"), 866 NetworkInterface: bluePolicy().Spec.Interfaces[0], 867 }, 868 Origins: []resource.Key{resource.NewKey(svc)}, 869 }) 870 871 // A service with an LB hostname but not an LB IP should not be selected 872 svc.Status.LoadBalancer.Ingress = []slim_corev1.LoadBalancerIngress{ 873 {Hostname: "example.com"}, 874 } 875 fix.fakeSvcStore.slice[0] = svc 876 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 877 Kind: resource.Upsert, 878 Key: resource.NewKey(svc), 879 Object: svc, 880 Done: func(err error) {}, 881 }) 882 assert.NoError(t, err) 883 884 assert.Len(t, fix.announcer.selectedPolicies, 1) 885 assert.Len(t, fix.announcer.selectedServices, 0) 886 887 } 888 889 // Test that when the selected IP types in the policy changes, that proxy neighbor table is updated properly. 890 func TestUpdatePolicy_ChangeIPType(t *testing.T) { 891 fix := baseUpdateSetup(t) 892 893 // Service has no LB IP so it should not be selected 894 policy := bluePolicy() 895 policy.Spec.ExternalIPs = false 896 policy.Spec.LoadBalancerIPs = true 897 fix.fakePolicyStore.slice[0] = policy 898 err := fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 899 Kind: resource.Upsert, 900 Key: resource.NewKey(policy), 901 Object: policy, 902 Done: func(err error) {}, 903 }) 904 assert.NoError(t, err) 905 906 assert.Len(t, fix.announcer.selectedPolicies, 1) 907 assert.Len(t, fix.announcer.selectedServices, 0) 908 909 rtx := fix.stateDB.ReadTxn() 910 iter := fix.proxyNeighborTable.All(rtx) 911 entries := statedb.Collect(iter) 912 assert.Len(t, entries, 0) 913 914 // Adding an LB IP should select the service and create an entry 915 svc := blueService() 916 svc.Spec.ExternalIPs = nil 917 svc.Status.LoadBalancer.Ingress = []slim_corev1.LoadBalancerIngress{ 918 {IP: "192.168.2.3"}, 919 } 920 fix.fakeSvcStore.slice[0] = svc 921 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 922 Kind: resource.Upsert, 923 Key: resource.NewKey(svc), 924 Object: svc, 925 Done: func(err error) {}, 926 }) 927 assert.NoError(t, err) 928 929 assert.Len(t, fix.announcer.selectedPolicies, 1) 930 assert.Len(t, fix.announcer.selectedServices, 1) 931 932 err = fix.announcer.processLeaderEvent(leaderElectionEvent{ 933 typ: leaderElectionLeading, 934 selectedService: fix.announcer.selectedServices[serviceKey(svc)], 935 }) 936 assert.NoError(t, err) 937 938 rtx = fix.stateDB.ReadTxn() 939 iter = fix.proxyNeighborTable.All(rtx) 940 entries = statedb.Collect(iter) 941 assert.Len(t, entries, 1) 942 assert.Contains(t, entries, &tables.L2AnnounceEntry{ 943 L2AnnounceKey: tables.L2AnnounceKey{ 944 IP: netip.MustParseAddr("192.168.2.3"), 945 NetworkInterface: bluePolicy().Spec.Interfaces[0], 946 }, 947 Origins: []resource.Key{resource.NewKey(svc)}, 948 }) 949 950 // Setting an empty LB IP should unselect the service 951 svc.Status.LoadBalancer.Ingress = []slim_corev1.LoadBalancerIngress{ 952 {IP: ""}, 953 } 954 fix.fakeSvcStore.slice[0] = svc 955 err = fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 956 Kind: resource.Upsert, 957 Key: resource.NewKey(svc), 958 Object: svc, 959 Done: func(err error) {}, 960 }) 961 assert.NoError(t, err) 962 963 assert.Len(t, fix.announcer.selectedPolicies, 1) 964 assert.Len(t, fix.announcer.selectedServices, 0) 965 966 rtx = fix.stateDB.ReadTxn() 967 iter = fix.proxyNeighborTable.All(rtx) 968 entries = statedb.Collect(iter) 969 assert.Len(t, entries, 0) 970 971 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 972 fix.announcer.params.JobGroup.Stop(ctx) 973 cancel() 974 } 975 976 // Test that when the interfaces in a policy change, that the proxy neighbor entries are updated. 977 func TestUpdatePolicy_ChangeInterfaces(t *testing.T) { 978 fix := baseUpdateSetup(t) 979 980 fix.announcer.devices = []string{"eno01", "eth0"} 981 err := fix.announcer.processDevicesChanged(context.Background()) 982 assert.NoError(t, err) 983 984 policy := bluePolicy() 985 policy.Spec.Interfaces = []string{"eth0"} 986 fix.fakePolicyStore.slice[0] = policy 987 err = fix.announcer.processPolicyEvent(context.Background(), resource.Event[*v2alpha1.CiliumL2AnnouncementPolicy]{ 988 Kind: resource.Upsert, 989 Key: resource.NewKey(policy), 990 Object: policy, 991 Done: func(err error) {}, 992 }) 993 assert.NoError(t, err) 994 995 assert.Len(t, fix.announcer.selectedPolicies, 1) 996 assert.Len(t, fix.announcer.selectedServices, 1) 997 998 // Check that the old entry is deleted and the new entry added 999 rtx := fix.stateDB.ReadTxn() 1000 iter := fix.proxyNeighborTable.All(rtx) 1001 entries := statedb.Collect(iter) 1002 assert.Len(t, entries, 1) 1003 assert.Contains(t, entries, &tables.L2AnnounceEntry{ 1004 L2AnnounceKey: tables.L2AnnounceKey{ 1005 IP: netip.MustParseAddr(blueService().Spec.ExternalIPs[0]), 1006 NetworkInterface: "eth0", 1007 }, 1008 Origins: []resource.Key{resource.NewKey(blueService())}, 1009 }) 1010 1011 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1012 fix.announcer.params.JobGroup.Stop(ctx) 1013 cancel() 1014 } 1015 1016 // Test that when a service deletes an IP the proxy neighbor table is updated accordingly 1017 func TestUpdateService_DelIP(t *testing.T) { 1018 fix := baseUpdateSetup(t) 1019 1020 svc := blueService() 1021 svc.Spec.ExternalIPs = []string{} 1022 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 1023 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 1024 Kind: resource.Upsert, 1025 Key: resource.NewKey(svc), 1026 Object: svc, 1027 Done: func(err error) {}, 1028 }) 1029 assert.NoError(t, err) 1030 1031 // Check that the entry for the IP was deleted 1032 rtx := fix.stateDB.ReadTxn() 1033 iter := fix.proxyNeighborTable.All(rtx) 1034 entries := statedb.Collect(iter) 1035 assert.Len(t, entries, 0) 1036 1037 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1038 fix.announcer.params.JobGroup.Stop(ctx) 1039 cancel() 1040 } 1041 1042 // Test that when a service adds and IP, the proxy neighbor table is updated accordingly. 1043 func TestUpdateService_AddIP(t *testing.T) { 1044 fix := baseUpdateSetup(t) 1045 1046 svc := blueService() 1047 svc.Spec.ExternalIPs = []string{"192.168.2.1", "192.168.2.2"} 1048 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 1049 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 1050 Kind: resource.Upsert, 1051 Key: resource.NewKey(svc), 1052 Object: svc, 1053 Done: func(err error) {}, 1054 }) 1055 assert.NoError(t, err) 1056 1057 // Check that the interface on the proxy neighbor entry changed 1058 rtx := fix.stateDB.ReadTxn() 1059 iter := fix.proxyNeighborTable.All(rtx) 1060 entries := statedb.Collect(iter) 1061 assert.Len(t, entries, 2) 1062 1063 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1064 fix.announcer.params.JobGroup.Stop(ctx) 1065 cancel() 1066 } 1067 1068 // Test that a service is removed if it no longer matches any policies 1069 func TestUpdateService_NoMatch(t *testing.T) { 1070 fix := baseUpdateSetup(t) 1071 1072 svc := blueService() 1073 svc.Labels["color"] = "red" 1074 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 1075 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 1076 Kind: resource.Upsert, 1077 Key: resource.NewKey(svc), 1078 Object: svc, 1079 Done: func(err error) {}, 1080 }) 1081 assert.NoError(t, err) 1082 1083 // Check that the entry got deleted 1084 rtx := fix.stateDB.ReadTxn() 1085 iter := fix.proxyNeighborTable.All(rtx) 1086 entries := statedb.Collect(iter) 1087 assert.Len(t, entries, 0) 1088 1089 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1090 fix.announcer.params.JobGroup.Stop(ctx) 1091 cancel() 1092 } 1093 1094 // Test that when a service load balancer class is set to a supported value, 1095 // it matches policies. 1096 func TestUpdateService_LoadBalancerClassMatch(t *testing.T) { 1097 fix := baseUpdateSetup(t) 1098 1099 svc := blueService() 1100 svc.Spec.LoadBalancerClass = ptr.To[string](v2alpha1.L2AnnounceLoadBalancerClass) 1101 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 1102 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 1103 Kind: resource.Upsert, 1104 Key: resource.NewKey(svc), 1105 Object: svc, 1106 Done: func(err error) {}, 1107 }) 1108 assert.NoError(t, err) 1109 1110 // Check that the entry got deleted 1111 rtx := fix.stateDB.ReadTxn() 1112 iter := fix.proxyNeighborTable.All(rtx) 1113 entries := statedb.Collect(iter) 1114 assert.Len(t, entries, 1) 1115 1116 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1117 fix.announcer.params.JobGroup.Stop(ctx) 1118 cancel() 1119 } 1120 1121 // Test that when a service load balancer class is set to an unsupported value, 1122 // it no longer matches any policies. 1123 func TestUpdateService_LoadBalancerClassNotMatch(t *testing.T) { 1124 fix := baseUpdateSetup(t) 1125 1126 svc := blueService() 1127 svc.Spec.LoadBalancerClass = ptr.To[string]("unsupported.io/lb-class") 1128 fix.fakeSvcStore.slice = append(fix.fakeSvcStore.slice, svc) 1129 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 1130 Kind: resource.Upsert, 1131 Key: resource.NewKey(svc), 1132 Object: svc, 1133 Done: func(err error) {}, 1134 }) 1135 assert.NoError(t, err) 1136 1137 // Check that the entry got deleted 1138 rtx := fix.stateDB.ReadTxn() 1139 iter := fix.proxyNeighborTable.All(rtx) 1140 entries := statedb.Collect(iter) 1141 assert.Len(t, entries, 0) 1142 1143 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1144 fix.announcer.params.JobGroup.Stop(ctx) 1145 cancel() 1146 } 1147 1148 // Test that deleting a service removes its entries 1149 func TestDelService(t *testing.T) { 1150 fix := baseUpdateSetup(t) 1151 1152 svc := blueService() 1153 fix.fakeSvcStore.slice = nil 1154 err := fix.announcer.processSvcEvent(resource.Event[*slim_corev1.Service]{ 1155 Kind: resource.Delete, 1156 Key: resource.NewKey(svc), 1157 Object: svc, 1158 Done: func(err error) {}, 1159 }) 1160 assert.NoError(t, err) 1161 1162 // Check that the entry got deleted 1163 rtx := fix.stateDB.ReadTxn() 1164 iter := fix.proxyNeighborTable.All(rtx) 1165 entries := statedb.Collect(iter) 1166 assert.Len(t, entries, 0) 1167 1168 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1169 fix.announcer.params.JobGroup.Stop(ctx) 1170 cancel() 1171 } 1172 1173 // This tests affirms that the L2 announcer behaves as expected during it lifecycle, shutting down cleanly 1174 func TestL2AnnouncerLifecycle(t *testing.T) { 1175 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 1176 1177 startCtx, cancel := context.WithTimeout(context.Background(), time.Minute) 1178 defer cancel() 1179 1180 h := hive.New( 1181 Cell, 1182 cell.Provide(tables.NewL2AnnounceTable), 1183 cell.Invoke(statedb.RegisterTable[*tables.L2AnnounceEntry]), 1184 cell.Provide(tables.NewDeviceTable, statedb.RWTable[*tables.Device].ToTable), 1185 cell.Invoke(statedb.RegisterTable[*tables.Device]), 1186 cell.Provide(func() *option.DaemonConfig { 1187 return &option.DaemonConfig{ 1188 ConfigPatchMutex: new(lock.RWMutex), 1189 EnableL2Announcements: true, 1190 } 1191 }), 1192 client.FakeClientCell, 1193 k8s.ResourcesCell, 1194 cell.Invoke(func(_ *L2Announcer) {}), 1195 ) 1196 tlog := hivetest.Logger(t) 1197 err := h.Start(tlog, startCtx) 1198 if assert.NoError(t, err) { 1199 // Give everything some time to start 1200 time.Sleep(3 * time.Second) 1201 1202 stopCtx, cancel := context.WithTimeout(context.Background(), time.Minute) 1203 defer cancel() 1204 1205 err = h.Stop(tlog, stopCtx) 1206 assert.NoError(t, err) 1207 } 1208 }