|
4 | 4 | package linuxrouting |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "context" |
7 | 8 | "net" |
8 | 9 | "net/netip" |
9 | 10 | "testing" |
| 11 | + "time" |
10 | 12 |
|
11 | 13 | "github.com/cilium/hive/hivetest" |
12 | 14 | "github.com/stretchr/testify/require" |
13 | 15 | "github.com/vishvananda/netlink" |
| 16 | + "k8s.io/apimachinery/pkg/util/wait" |
14 | 17 |
|
15 | 18 | "github.com/cilium/cilium/pkg/datapath/linux/linux_defaults" |
16 | 19 | "github.com/cilium/cilium/pkg/datapath/linux/route" |
@@ -331,6 +334,125 @@ func getFakes(t *testing.T, ipamMode string, masquerade bool, withZeroCIDR bool) |
331 | 334 | return netip.MustParseAddr("192.168.2.123"), *fakeRoutingInfo |
332 | 335 | } |
333 | 336 |
|
| 337 | +// withTestBackoff swaps in a faster backoff for tests. Mutates a package |
| 338 | +// global, so callers must not use t.Parallel(). |
| 339 | +func withTestBackoff(t *testing.T, bo wait.Backoff) { |
| 340 | + t.Helper() |
| 341 | + orig := WaitForENIInterfaceBackoff |
| 342 | + WaitForENIInterfaceBackoff = bo |
| 343 | + t.Cleanup(func() { WaitForENIInterfaceBackoff = orig }) |
| 344 | +} |
| 345 | + |
| 346 | +func TestPrivilegedWaitForENIInterfaceAlreadyPresent(t *testing.T) { |
| 347 | + setupLinuxRoutingSuite(t) |
| 348 | + withTestBackoff(t, wait.Backoff{Duration: 10 * time.Millisecond, Factor: 2, Steps: 3}) |
| 349 | + |
| 350 | + ns := netns.NewNetNS(t) |
| 351 | + require.NoError(t, ns.Do(func() error { |
| 352 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:66") |
| 353 | + require.NoError(t, err) |
| 354 | + |
| 355 | + cleanup := createDummyDevice(t, macAddr) |
| 356 | + defer cleanup() |
| 357 | + |
| 358 | + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
| 359 | + defer cancel() |
| 360 | + |
| 361 | + require.NoError(t, WaitForENIInterface(ctx, macAddr)) |
| 362 | + return nil |
| 363 | + })) |
| 364 | +} |
| 365 | + |
| 366 | +func TestPrivilegedWaitForENIInterfaceAppearsLate(t *testing.T) { |
| 367 | + setupLinuxRoutingSuite(t) |
| 368 | + withTestBackoff(t, wait.Backoff{Duration: 20 * time.Millisecond, Factor: 1.5, Jitter: 0.1, Steps: 10}) |
| 369 | + |
| 370 | + // ns.Do pins the netns to its own goroutine's OS thread; a bare |
| 371 | + // `go func(){}()` inside it would run in the host netns instead. |
| 372 | + ns := netns.NewNetNS(t) |
| 373 | + |
| 374 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:77") |
| 375 | + require.NoError(t, err) |
| 376 | + |
| 377 | + require.NoError(t, ns.Do(func() error { |
| 378 | + require.False(t, linkExistsWithMAC(t, macAddr), "interface must not exist yet") |
| 379 | + return nil |
| 380 | + })) |
| 381 | + |
| 382 | + // require/FailNow must run on the test goroutine, so failures are propagated back over a channel instead. |
| 383 | + type deviceResult struct { |
| 384 | + cleanup func() |
| 385 | + err error |
| 386 | + } |
| 387 | + resultCh := make(chan deviceResult, 1) |
| 388 | + go func() { |
| 389 | + time.Sleep(60 * time.Millisecond) |
| 390 | + var res deviceResult |
| 391 | + res.err = ns.Do(func() error { |
| 392 | + dummy := &netlink.Dummy{ |
| 393 | + LinkAttrs: netlink.LinkAttrs{ |
| 394 | + Name: "linuxrout-test", |
| 395 | + HardwareAddr: net.HardwareAddr(macAddr), |
| 396 | + }, |
| 397 | + } |
| 398 | + if err := netlink.LinkAdd(dummy); err != nil { |
| 399 | + return err |
| 400 | + } |
| 401 | + // Wrapped in ns.Do because cleanup runs after the host netns has been restored. |
| 402 | + res.cleanup = func() { _ = ns.Do(func() error { return netlink.LinkDel(dummy) }) } |
| 403 | + return nil |
| 404 | + }) |
| 405 | + resultCh <- res |
| 406 | + }() |
| 407 | + |
| 408 | + var waitErr error |
| 409 | + require.NoError(t, ns.Do(func() error { |
| 410 | + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| 411 | + defer cancel() |
| 412 | + waitErr = WaitForENIInterface(ctx, macAddr) |
| 413 | + return nil |
| 414 | + })) |
| 415 | + |
| 416 | + res := <-resultCh |
| 417 | + require.NoError(t, res.err, "failed to create dummy device asynchronously") |
| 418 | + require.NotNil(t, res.cleanup) |
| 419 | + defer res.cleanup() |
| 420 | + |
| 421 | + require.NoError(t, waitErr, "WaitForENIInterface should succeed once the interface appears") |
| 422 | +} |
| 423 | + |
| 424 | +func TestPrivilegedWaitForENIInterfaceTimeout(t *testing.T) { |
| 425 | + setupLinuxRoutingSuite(t) |
| 426 | + withTestBackoff(t, wait.Backoff{Duration: 10 * time.Millisecond, Factor: 1.5, Steps: 3}) |
| 427 | + |
| 428 | + ns := netns.NewNetNS(t) |
| 429 | + require.NoError(t, ns.Do(func() error { |
| 430 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:88") |
| 431 | + require.NoError(t, err) |
| 432 | + |
| 433 | + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| 434 | + defer cancel() |
| 435 | + |
| 436 | + err = WaitForENIInterface(ctx, macAddr) |
| 437 | + require.Error(t, err, "interface never appears, so WaitForENIInterface should give up and return an error") |
| 438 | + return nil |
| 439 | + })) |
| 440 | +} |
| 441 | + |
| 442 | +// Backoff is deliberately enormous so the test hangs (rather than passing spuriously) if cancellation is not honored. |
| 443 | +func TestWaitForENIInterfaceContextCancelled(t *testing.T) { |
| 444 | + withTestBackoff(t, wait.Backoff{Duration: time.Hour, Factor: 1, Steps: 100}) |
| 445 | + |
| 446 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:99") |
| 447 | + require.NoError(t, err) |
| 448 | + |
| 449 | + ctx, cancel := context.WithCancel(context.Background()) |
| 450 | + cancel() |
| 451 | + |
| 452 | + err = WaitForENIInterface(ctx, macAddr) |
| 453 | + require.Error(t, err, "an already-cancelled context should cause WaitForENIInterface to return without exhausting the backoff") |
| 454 | +} |
| 455 | + |
334 | 456 | func linkExistsWithMAC(t *testing.T, macAddr mac.MAC) bool { |
335 | 457 | links, err := safenetlink.LinkList() |
336 | 458 | require.NoError(t, err) |
|
0 commit comments