|
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,145 @@ 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 WaitForENIInterfaceBackoff for a faster one so tests |
| 338 | +// need not wait out the production-sized backoff. It mutates a package-level |
| 339 | +// global, so callers must not use t.Parallel(). |
| 340 | +func withTestBackoff(t *testing.T, bo wait.Backoff) { |
| 341 | + t.Helper() |
| 342 | + orig := WaitForENIInterfaceBackoff |
| 343 | + WaitForENIInterfaceBackoff = bo |
| 344 | + t.Cleanup(func() { WaitForENIInterfaceBackoff = orig }) |
| 345 | +} |
| 346 | + |
| 347 | +// TestPrivilegedWaitForENIInterfaceAlreadyPresent: returns immediately when |
| 348 | +// the interface already exists. |
| 349 | +func TestPrivilegedWaitForENIInterfaceAlreadyPresent(t *testing.T) { |
| 350 | + setupLinuxRoutingSuite(t) |
| 351 | + withTestBackoff(t, wait.Backoff{Duration: 10 * time.Millisecond, Factor: 2, Steps: 3}) |
| 352 | + |
| 353 | + ns := netns.NewNetNS(t) |
| 354 | + require.NoError(t, ns.Do(func() error { |
| 355 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:66") |
| 356 | + require.NoError(t, err) |
| 357 | + |
| 358 | + cleanup := createDummyDevice(t, macAddr) |
| 359 | + defer cleanup() |
| 360 | + |
| 361 | + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
| 362 | + defer cancel() |
| 363 | + |
| 364 | + require.NoError(t, WaitForENIInterface(ctx, macAddr)) |
| 365 | + return nil |
| 366 | + })) |
| 367 | +} |
| 368 | + |
| 369 | +// TestPrivilegedWaitForENIInterfaceAppearsLate: the interface is absent when |
| 370 | +// WaitForENIInterface is first called but shows up shortly after, so the |
| 371 | +// helper must poll until it appears rather than fail immediately. |
| 372 | +func TestPrivilegedWaitForENIInterfaceAppearsLate(t *testing.T) { |
| 373 | + setupLinuxRoutingSuite(t) |
| 374 | + withTestBackoff(t, wait.Backoff{Duration: 20 * time.Millisecond, Factor: 1.5, Jitter: 0.1, Steps: 10}) |
| 375 | + |
| 376 | + // Each goroutine enters the namespace via its own ns.Do call: ns.Do pins |
| 377 | + // the netns switch to the OS thread backing the goroutine it creates, so a |
| 378 | + // bare `go func(){...}()` inside an ns.Do closure would run in the host |
| 379 | + // netns, not ns. |
| 380 | + ns := netns.NewNetNS(t) |
| 381 | + |
| 382 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:77") |
| 383 | + require.NoError(t, err) |
| 384 | + |
| 385 | + require.NoError(t, ns.Do(func() error { |
| 386 | + require.False(t, linkExistsWithMAC(t, macAddr), "interface must not exist yet") |
| 387 | + return nil |
| 388 | + })) |
| 389 | + |
| 390 | + // Propagate the goroutine's failure back over a channel rather than |
| 391 | + // calling require/t.FailNow from it: those must run on the test's own |
| 392 | + // goroutine, so the assertions happen on the main goroutine below. |
| 393 | + type deviceResult struct { |
| 394 | + cleanup func() |
| 395 | + err error |
| 396 | + } |
| 397 | + resultCh := make(chan deviceResult, 1) |
| 398 | + go func() { |
| 399 | + // Simulate the ENI's netlink interface showing up asynchronously, |
| 400 | + // after WaitForENIInterface has already started polling. |
| 401 | + time.Sleep(60 * time.Millisecond) |
| 402 | + var res deviceResult |
| 403 | + res.err = ns.Do(func() error { |
| 404 | + dummy := &netlink.Dummy{ |
| 405 | + LinkAttrs: netlink.LinkAttrs{ |
| 406 | + Name: "linuxrout-test", |
| 407 | + HardwareAddr: net.HardwareAddr(macAddr), |
| 408 | + }, |
| 409 | + } |
| 410 | + if err := netlink.LinkAdd(dummy); err != nil { |
| 411 | + return err |
| 412 | + } |
| 413 | + // Delete from inside the namespace: res.cleanup runs on the main |
| 414 | + // goroutine after the host netns has been restored, so a direct |
| 415 | + // LinkDel would target the host netns instead. |
| 416 | + res.cleanup = func() { _ = ns.Do(func() error { return netlink.LinkDel(dummy) }) } |
| 417 | + return nil |
| 418 | + }) |
| 419 | + resultCh <- res |
| 420 | + }() |
| 421 | + |
| 422 | + var waitErr error |
| 423 | + require.NoError(t, ns.Do(func() error { |
| 424 | + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| 425 | + defer cancel() |
| 426 | + waitErr = WaitForENIInterface(ctx, macAddr) |
| 427 | + return nil |
| 428 | + })) |
| 429 | + |
| 430 | + res := <-resultCh |
| 431 | + require.NoError(t, res.err, "failed to create dummy device asynchronously") |
| 432 | + require.NotNil(t, res.cleanup) |
| 433 | + defer res.cleanup() |
| 434 | + |
| 435 | + require.NoError(t, waitErr, "WaitForENIInterface should succeed once the interface appears") |
| 436 | +} |
| 437 | + |
| 438 | +// TestPrivilegedWaitForENIInterfaceTimeout verifies that WaitForENIInterface |
| 439 | +// gives up and returns an error if the interface never appears within the |
| 440 | +// configured backoff. |
| 441 | +func TestPrivilegedWaitForENIInterfaceTimeout(t *testing.T) { |
| 442 | + setupLinuxRoutingSuite(t) |
| 443 | + withTestBackoff(t, wait.Backoff{Duration: 10 * time.Millisecond, Factor: 1.5, Steps: 3}) |
| 444 | + |
| 445 | + ns := netns.NewNetNS(t) |
| 446 | + require.NoError(t, ns.Do(func() error { |
| 447 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:88") |
| 448 | + require.NoError(t, err) |
| 449 | + |
| 450 | + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| 451 | + defer cancel() |
| 452 | + |
| 453 | + err = WaitForENIInterface(ctx, macAddr) |
| 454 | + require.Error(t, err, "interface never appears, so WaitForENIInterface should give up and return an error") |
| 455 | + return nil |
| 456 | + })) |
| 457 | +} |
| 458 | + |
| 459 | +// TestWaitForENIInterfaceContextCancelled: a cancelled context returns |
| 460 | +// promptly instead of exhausting the backoff. The backoff is deliberately |
| 461 | +// enormous, so the test hangs rather than passing spuriously if cancellation |
| 462 | +// is not honoured. |
| 463 | +func TestWaitForENIInterfaceContextCancelled(t *testing.T) { |
| 464 | + withTestBackoff(t, wait.Backoff{Duration: time.Hour, Factor: 1, Steps: 100}) |
| 465 | + |
| 466 | + macAddr, err := mac.ParseMAC("00:11:22:33:44:99") |
| 467 | + require.NoError(t, err) |
| 468 | + |
| 469 | + ctx, cancel := context.WithCancel(context.Background()) |
| 470 | + cancel() |
| 471 | + |
| 472 | + err = WaitForENIInterface(ctx, macAddr) |
| 473 | + require.Error(t, err, "an already-cancelled context should cause WaitForENIInterface to return without exhausting the backoff") |
| 474 | +} |
| 475 | + |
334 | 476 | func linkExistsWithMAC(t *testing.T, macAddr mac.MAC) bool { |
335 | 477 | links, err := safenetlink.LinkList() |
336 | 478 | require.NoError(t, err) |
|
0 commit comments