|
15 | 15 | package controllers |
16 | 16 |
|
17 | 17 | import ( |
| 18 | + "encoding/json" |
18 | 19 | "regexp" |
| 20 | + "slices" |
19 | 21 | "strings" |
20 | 22 | "testing" |
21 | 23 |
|
@@ -497,6 +499,181 @@ func TestAteomSecurityContextByClass(t *testing.T) { |
497 | 499 | } |
498 | 500 | } |
499 | 501 |
|
| 502 | +func TestDeploymentApplyConfiguration(t *testing.T) { |
| 503 | + for _, class := range []atev1alpha1.SandboxClass{atev1alpha1.SandboxClassGvisor, atev1alpha1.SandboxClassMicroVM} { |
| 504 | + defaults := slices.Clone(ateomGvisorCapabilities) |
| 505 | + if class == atev1alpha1.SandboxClassMicroVM { |
| 506 | + defaults = slices.Clone(ateomMicroVMCapabilities) |
| 507 | + } |
| 508 | + for _, tt := range []struct { |
| 509 | + name string |
| 510 | + drop []corev1.Capability |
| 511 | + wantEmptyAdd bool |
| 512 | + }{ |
| 513 | + {name: "defaults"}, |
| 514 | + {name: "partial drop", drop: []corev1.Capability{"SYS_PTRACE"}}, |
| 515 | + {name: "drop all", drop: []corev1.Capability{"ALL"}, wantEmptyAdd: true}, |
| 516 | + {name: "drop individually", drop: defaults, wantEmptyAdd: true}, |
| 517 | + } { |
| 518 | + t.Run(string(class)+"/"+tt.name, func(t *testing.T) { |
| 519 | + wp := testWorkerPoolApplyConfig(&atev1alpha1.WorkerPoolPodTemplate{ |
| 520 | + NodeSelector: map[string]string{"workload": "substrate"}, |
| 521 | + SecurityContext: &atev1alpha1.WorkerPoolSecurityContext{ |
| 522 | + DropCapabilities: tt.drop, |
| 523 | + }, |
| 524 | + }) |
| 525 | + wp.Spec.SandboxClasses = []atev1alpha1.WorkerPoolSandboxClass{{Name: class}} |
| 526 | + dep := buildDeploymentApplyConfig(wp, ateomOTelSettings{}, installdefaults.SystemNamespace, installdefaults.AteletServiceAccount, installdefaults.RouterServiceAccount) |
| 527 | + dep.Spec.Template.Spec.WithContainers( |
| 528 | + corev1ac.Container().WithName("metrics").WithImage("metrics:v1"). |
| 529 | + WithSecurityContext(corev1ac.SecurityContext(). |
| 530 | + WithCapabilities(corev1ac.Capabilities().WithAdd("NET_BIND_SERVICE"))), |
| 531 | + corev1ac.Container().WithName("helper").WithImage("helper:v1"), |
| 532 | + ) |
| 533 | + before, err := json.Marshal(dep) |
| 534 | + if err != nil { |
| 535 | + t.Fatal(err) |
| 536 | + } |
| 537 | + var want appsv1ac.DeploymentApplyConfiguration |
| 538 | + if err := json.Unmarshal(before, &want); err != nil { |
| 539 | + t.Fatal(err) |
| 540 | + } |
| 541 | + if tt.wantEmptyAdd { |
| 542 | + want.Spec.Template.Spec.Containers[0].SecurityContext.Capabilities.Add = []corev1.Capability{} |
| 543 | + } |
| 544 | + applied, err := deploymentApplyConfiguration(dep) |
| 545 | + if err != nil { |
| 546 | + t.Fatal(err) |
| 547 | + } |
| 548 | + if !tt.wantEmptyAdd { |
| 549 | + if typed, ok := applied.(*appsv1ac.DeploymentApplyConfiguration); !ok || typed != dep { |
| 550 | + t.Fatalf("non-empty capabilities must keep the original typed apply configuration, got %T", applied) |
| 551 | + } |
| 552 | + } |
| 553 | + payload, err := json.Marshal(applied) |
| 554 | + if err != nil { |
| 555 | + t.Fatal(err) |
| 556 | + } |
| 557 | + var got appsv1ac.DeploymentApplyConfiguration |
| 558 | + if err := json.Unmarshal(payload, &got); err != nil { |
| 559 | + t.Fatal(err) |
| 560 | + } |
| 561 | + // An explicit [] decodes as a non-nil empty slice. An omitted |
| 562 | + // add field decodes as nil, so cmp detects the SSA regression. |
| 563 | + if diff := cmp.Diff(&want, &got); diff != "" { |
| 564 | + t.Errorf("serialized Deployment (-want +got):\n%s", diff) |
| 565 | + } |
| 566 | + after, err := json.Marshal(dep) |
| 567 | + if err != nil { |
| 568 | + t.Fatal(err) |
| 569 | + } |
| 570 | + if diff := cmp.Diff(string(before), string(after)); diff != "" { |
| 571 | + t.Errorf("input apply configuration was mutated (-before +after):\n%s", diff) |
| 572 | + } |
| 573 | + }) |
| 574 | + } |
| 575 | + } |
| 576 | +} |
| 577 | + |
| 578 | +func TestDeploymentApplyConfigurationRequiresPodSpec(t *testing.T) { |
| 579 | + for _, tt := range []struct { |
| 580 | + name string |
| 581 | + dep *appsv1ac.DeploymentApplyConfiguration |
| 582 | + }{ |
| 583 | + {name: "nil deployment"}, |
| 584 | + {name: "missing spec", dep: appsv1ac.Deployment("worker", "default")}, |
| 585 | + {name: "missing template", dep: appsv1ac.Deployment("worker", "default").WithSpec(appsv1ac.DeploymentSpec())}, |
| 586 | + {name: "missing pod spec", dep: appsv1ac.Deployment("worker", "default").WithSpec(appsv1ac.DeploymentSpec().WithTemplate(corev1ac.PodTemplateSpec()))}, |
| 587 | + } { |
| 588 | + t.Run(tt.name, func(t *testing.T) { |
| 589 | + if _, err := deploymentApplyConfiguration(tt.dep); err == nil || !strings.Contains(err.Error(), "must include a pod spec") { |
| 590 | + t.Fatalf("got %v, want missing pod spec error", err) |
| 591 | + } |
| 592 | + }) |
| 593 | + } |
| 594 | +} |
| 595 | + |
| 596 | +func TestWorkerSecurityContext(t *testing.T) { |
| 597 | + gvisorDefaults := slices.Clone(ateomGvisorCapabilities) |
| 598 | + microVMDefaults := slices.Clone(ateomMicroVMCapabilities) |
| 599 | + tests := []struct { |
| 600 | + name string |
| 601 | + class atev1alpha1.SandboxClass |
| 602 | + settings *atev1alpha1.WorkerPoolSecurityContext |
| 603 | + wantCaps []corev1.Capability |
| 604 | + }{ |
| 605 | + {"omitted settings", atev1alpha1.SandboxClassGvisor, nil, gvisorDefaults}, |
| 606 | + {"empty settings", atev1alpha1.SandboxClassGvisor, &atev1alpha1.WorkerPoolSecurityContext{}, gvisorDefaults}, |
| 607 | + {"empty microvm settings", atev1alpha1.SandboxClassMicroVM, &atev1alpha1.WorkerPoolSecurityContext{}, microVMDefaults}, |
| 608 | + {"drop gvisor capabilities", atev1alpha1.SandboxClassGvisor, &atev1alpha1.WorkerPoolSecurityContext{DropCapabilities: []corev1.Capability{"SYS_PTRACE", "MKNOD"}}, []corev1.Capability{"NET_ADMIN", "SYS_ADMIN", "SYS_CHROOT", "SETUID", "SETGID", "SETPCAP", "DAC_OVERRIDE", "FOWNER", "CHOWN", "NET_RAW", "SETFCAP"}}, |
| 609 | + {"drop microvm capabilities", atev1alpha1.SandboxClassMicroVM, &atev1alpha1.WorkerPoolSecurityContext{DropCapabilities: []corev1.Capability{"FSETID", "DAC_READ_SEARCH"}}, gvisorDefaults}, |
| 610 | + {"capability absent from class", atev1alpha1.SandboxClassGvisor, &atev1alpha1.WorkerPoolSecurityContext{DropCapabilities: []corev1.Capability{"FSETID"}}, gvisorDefaults}, |
| 611 | + {"drop all", atev1alpha1.SandboxClassGvisor, &atev1alpha1.WorkerPoolSecurityContext{DropCapabilities: []corev1.Capability{"ALL"}}, nil}, |
| 612 | + } |
| 613 | + for _, tt := range tests { |
| 614 | + t.Run(tt.name, func(t *testing.T) { |
| 615 | + wp := testWorkerPoolApplyConfig(&atev1alpha1.WorkerPoolPodTemplate{SecurityContext: tt.settings}) |
| 616 | + wp.Spec.SandboxClasses = []atev1alpha1.WorkerPoolSandboxClass{{Name: tt.class}} |
| 617 | + sc := buildDeploymentApplyConfig(wp, ateomOTelSettings{}, installdefaults.SystemNamespace, installdefaults.AteletServiceAccount, installdefaults.RouterServiceAccount).Spec.Template.Spec.Containers[0].SecurityContext |
| 618 | + if sc == nil || sc.Capabilities == nil { |
| 619 | + t.Fatal("worker security context and capabilities must be set") |
| 620 | + } |
| 621 | + if diff := cmp.Diff(tt.wantCaps, sc.Capabilities.Add); diff != "" { |
| 622 | + t.Fatalf("capabilities (-want +got):\n%s", diff) |
| 623 | + } |
| 624 | + if !ptr.Equal(sc.Privileged, ptr.To(false)) || !ptr.Equal(sc.RunAsUser, ptr.To(int64(0))) || !ptr.Equal(sc.RunAsGroup, ptr.To(int64(0))) || !slices.Equal(sc.Capabilities.Drop, []corev1.Capability{"ALL"}) { |
| 625 | + t.Fatal("worker identity, privileged mode or capability default changed") |
| 626 | + } |
| 627 | + if diff := cmp.Diff(corev1ac.SeccompProfile().WithType(corev1.SeccompProfileTypeUnconfined), sc.SeccompProfile); diff != "" { |
| 628 | + t.Errorf("default seccomp profile (-want +got):\n%s", diff) |
| 629 | + } |
| 630 | + if sc.AllowPrivilegeEscalation != nil { |
| 631 | + t.Errorf("AllowPrivilegeEscalation = %v, want nil", *sc.AllowPrivilegeEscalation) |
| 632 | + } |
| 633 | + // Compare against copies taken before any overrides, not the globals |
| 634 | + // that a slice-aliasing regression could have mutated. |
| 635 | + for _, defaults := range []struct { |
| 636 | + class atev1alpha1.SandboxClass |
| 637 | + want []corev1.Capability |
| 638 | + got []corev1.Capability |
| 639 | + }{ |
| 640 | + {atev1alpha1.SandboxClassGvisor, gvisorDefaults, ateomGvisorCapabilities}, |
| 641 | + {atev1alpha1.SandboxClassMicroVM, microVMDefaults, ateomMicroVMCapabilities}, |
| 642 | + } { |
| 643 | + if diff := cmp.Diff(defaults.want, defaults.got); diff != "" { |
| 644 | + t.Errorf("%s global defaults mutated (-want +got):\n%s", defaults.class, diff) |
| 645 | + } |
| 646 | + otherPool := testWorkerPoolApplyConfig(nil) |
| 647 | + otherPool.Spec.SandboxClasses = []atev1alpha1.WorkerPoolSandboxClass{{Name: defaults.class}} |
| 648 | + otherSC := buildDeploymentApplyConfig(otherPool, ateomOTelSettings{}, installdefaults.SystemNamespace, installdefaults.AteletServiceAccount, installdefaults.RouterServiceAccount).Spec.Template.Spec.Containers[0].SecurityContext |
| 649 | + if diff := cmp.Diff(defaults.want, otherSC.Capabilities.Add); diff != "" { |
| 650 | + t.Errorf("%s subsequent pool capabilities (-want +got):\n%s", defaults.class, diff) |
| 651 | + } |
| 652 | + } |
| 653 | + }) |
| 654 | + } |
| 655 | + for _, profile := range []*corev1.SeccompProfile{ |
| 656 | + {Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: ptr.To("substrate-worker.json")}, |
| 657 | + {Type: corev1.SeccompProfileTypeRuntimeDefault}, |
| 658 | + {Type: corev1.SeccompProfileTypeUnconfined}, |
| 659 | + } { |
| 660 | + t.Run(string(profile.Type), func(t *testing.T) { |
| 661 | + wp := testWorkerPoolApplyConfig(&atev1alpha1.WorkerPoolPodTemplate{SecurityContext: &atev1alpha1.WorkerPoolSecurityContext{SeccompProfile: profile, AllowPrivilegeEscalation: ptr.To(false)}}) |
| 662 | + sc := buildDeploymentApplyConfig(wp, ateomOTelSettings{}, installdefaults.SystemNamespace, installdefaults.AteletServiceAccount, installdefaults.RouterServiceAccount).Spec.Template.Spec.Containers[0].SecurityContext |
| 663 | + want := corev1ac.SeccompProfile().WithType(profile.Type) |
| 664 | + if profile.LocalhostProfile != nil { |
| 665 | + want.WithLocalhostProfile(*profile.LocalhostProfile) |
| 666 | + } |
| 667 | + if diff := cmp.Diff(want, sc.SeccompProfile); diff != "" { |
| 668 | + t.Errorf("seccomp profile (-want +got):\n%s", diff) |
| 669 | + } |
| 670 | + if sc.AllowPrivilegeEscalation == nil || *sc.AllowPrivilegeEscalation { |
| 671 | + t.Fatal("privilege escalation was not disabled") |
| 672 | + } |
| 673 | + }) |
| 674 | + } |
| 675 | +} |
| 676 | + |
500 | 677 | // TestTerminationGracePeriodSeconds asserts the pod's grace period is hardcoded to 3600s. |
501 | 678 | func TestTerminationGracePeriodSeconds(t *testing.T) { |
502 | 679 | wp := testWorkerPoolApplyConfig(nil) |
|
0 commit comments