@@ -22,10 +22,13 @@ import (
2222 "github.com/google/go-cmp/cmp/cmpopts"
2323 "github.com/stretchr/testify/require"
2424 corev1 "k8s.io/api/core/v1"
25+ policyv1 "k8s.io/api/policy/v1"
26+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
2527 "k8s.io/apimachinery/pkg/api/resource"
2628 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2729 "k8s.io/apimachinery/pkg/runtime"
2830 "k8s.io/apimachinery/pkg/types"
31+ "k8s.io/apimachinery/pkg/util/intstr"
2932 "k8s.io/utils/ptr"
3033 sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1"
3134 ctrl "sigs.k8s.io/controller-runtime"
@@ -378,6 +381,86 @@ func TestReconcile(t *testing.T) {
378381 },
379382 },
380383 },
384+ {
385+ name : "sandbox with high resilience creates PDB and adds pod annotation" ,
386+ sandboxSpec : sandboxv1alpha1.SandboxSpec {
387+ Resilience : sandboxv1alpha1 .ResilienceLevelHigh ,
388+ PodTemplate : sandboxv1alpha1.PodTemplate {
389+ Spec : corev1.PodSpec {
390+ Containers : []corev1.Container {{Name : "test-container" }},
391+ },
392+ },
393+ },
394+ // Verify Sandbox status
395+ wantStatus : sandboxv1alpha1.SandboxStatus {
396+ Service : sandboxName ,
397+ ServiceFQDN : "sandbox-name.sandbox-ns.svc.cluster.local" ,
398+ Conditions : []metav1.Condition {
399+ {
400+ Type : "Ready" ,
401+ Status : "False" ,
402+ ObservedGeneration : 1 ,
403+ Reason : "DependenciesNotReady" ,
404+ Message : "Pod exists with phase: ; Service Exists" ,
405+ },
406+ },
407+ },
408+ wantObjs : []client.Object {
409+ // Verify Pod has the new annotation
410+ & corev1.Pod {
411+ ObjectMeta : metav1.ObjectMeta {
412+ Name : sandboxName ,
413+ Namespace : sandboxNs ,
414+ ResourceVersion : "1" ,
415+ Labels : map [string ]string {
416+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
417+ },
418+ Annotations : map [string ]string {
419+ "cluster-autoscaler.kubernetes.io/safe-to-evict" : "false" ,
420+ },
421+ OwnerReferences : []metav1.OwnerReference {sandboxControllerRef (sandboxName )},
422+ },
423+ Spec : corev1.PodSpec {
424+ Containers : []corev1.Container {{Name : "test-container" }},
425+ },
426+ },
427+ // Verify Service
428+ & corev1.Service {
429+ ObjectMeta : metav1.ObjectMeta {
430+ Name : sandboxName ,
431+ Namespace : sandboxNs ,
432+ ResourceVersion : "1" ,
433+ Labels : map [string ]string {
434+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
435+ },
436+ OwnerReferences : []metav1.OwnerReference {sandboxControllerRef (sandboxName )},
437+ },
438+ Spec : corev1.ServiceSpec {
439+ Selector : map [string ]string {
440+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
441+ },
442+ ClusterIP : "None" ,
443+ },
444+ },
445+ // Verify the new PDB
446+ & policyv1.PodDisruptionBudget {
447+ ObjectMeta : metav1.ObjectMeta {
448+ Name : sandboxName ,
449+ Namespace : sandboxNs ,
450+ ResourceVersion : "1" ,
451+ OwnerReferences : []metav1.OwnerReference {sandboxControllerRef (sandboxName )},
452+ },
453+ Spec : policyv1.PodDisruptionBudgetSpec {
454+ MinAvailable : ptr .To (intstr .FromInt (1 )),
455+ Selector : & metav1.LabelSelector {
456+ MatchLabels : map [string ]string {
457+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
458+ },
459+ },
460+ },
461+ },
462+ },
463+ },
381464 }
382465
383466 for _ , tc := range testCases {
@@ -534,3 +617,54 @@ func TestReconcilePod(t *testing.T) {
534617 })
535618 }
536619}
620+
621+ // This test simulates updating a Sandbox and ensures the controller correctly deletes the now-unneeded PDB
622+ func TestReconcile_ResilienceCleanup (t * testing.T ) {
623+ sandboxName := "sandbox-name"
624+ sandboxNs := "sandbox-ns"
625+
626+ // Initial Sandbox with High Resilience
627+ sb := & sandboxv1alpha1.Sandbox {
628+ ObjectMeta : metav1.ObjectMeta {
629+ Name : sandboxName ,
630+ Namespace : sandboxNs ,
631+ Generation : 1 ,
632+ },
633+ Spec : sandboxv1alpha1.SandboxSpec {
634+ Resilience : sandboxv1alpha1 .ResilienceLevelHigh ,
635+ PodTemplate : sandboxv1alpha1.PodTemplate {
636+ Spec : corev1.PodSpec {
637+ Containers : []corev1.Container {{Name : "test-container" }},
638+ },
639+ },
640+ },
641+ }
642+
643+ r := SandboxReconciler {
644+ Client : newFakeClient (sb ),
645+ Scheme : Scheme ,
646+ }
647+ req := ctrl.Request {NamespacedName : types.NamespacedName {Name : sandboxName , Namespace : sandboxNs }}
648+
649+ _ , err := r .Reconcile (t .Context (), req )
650+ require .NoError (t , err )
651+
652+ // Verify PDB was created
653+ pdb := & policyv1.PodDisruptionBudget {}
654+ require .NoError (t , r .Get (t .Context (), req .NamespacedName , pdb ), "PDB should exist after first reconcile" )
655+
656+ // Update Sandbox to remove resilience
657+ liveSandbox := & sandboxv1alpha1.Sandbox {}
658+ require .NoError (t , r .Get (t .Context (), req .NamespacedName , liveSandbox ))
659+ liveSandbox .Spec .Resilience = "" // Remove resilience
660+ liveSandbox .Generation = 2
661+ require .NoError (t , r .Update (t .Context (), liveSandbox ))
662+
663+ // Re-run reconcile
664+ _ , err = r .Reconcile (t .Context (), req )
665+ require .NoError (t , err )
666+
667+ // Verify PDB was deleted
668+ err = r .Get (t .Context (), req .NamespacedName , pdb )
669+ require .True (t , k8serrors .IsNotFound (err ), "PDB should be deleted after resilience is removed" )
670+ }
0 commit comments