@@ -22,11 +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"
2526 k8serrors "k8s.io/apimachinery/pkg/api/errors"
2627 "k8s.io/apimachinery/pkg/api/resource"
2728 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829 "k8s.io/apimachinery/pkg/runtime"
2930 "k8s.io/apimachinery/pkg/types"
31+ "k8s.io/apimachinery/pkg/util/intstr"
3032 "k8s.io/utils/ptr"
3133 sandboxv1alpha1 "sigs.k8s.io/agent-sandbox/api/v1alpha1"
3234 ctrl "sigs.k8s.io/controller-runtime"
@@ -405,6 +407,88 @@ func TestReconcile(t *testing.T) {
405407 },
406408 },
407409 },
410+ {
411+ name : "sandbox with high resilience creates PDB and adds pod annotation" ,
412+ sandboxSpec : sandboxv1alpha1.SandboxSpec {
413+ Resilience : sandboxv1alpha1 .ResilienceLevelHigh ,
414+ PodTemplate : sandboxv1alpha1.PodTemplate {
415+ Spec : corev1.PodSpec {
416+ Containers : []corev1.Container {{Name : "test-container" }},
417+ },
418+ },
419+ },
420+ // Verify Sandbox status
421+ wantStatus : sandboxv1alpha1.SandboxStatus {
422+ Service : sandboxName ,
423+ ServiceFQDN : "sandbox-name.sandbox-ns.svc.cluster.local" ,
424+ Replicas : 1 ,
425+ LabelSelector : "agents.x-k8s.io/sandbox-name-hash=ab179450" ,
426+ Conditions : []metav1.Condition {
427+ {
428+ Type : "Ready" ,
429+ Status : "False" ,
430+ ObservedGeneration : 1 ,
431+ Reason : "DependenciesNotReady" ,
432+ Message : "Pod exists with phase: ; Service Exists" ,
433+ },
434+ },
435+ },
436+ wantObjs : []client.Object {
437+ // Verify Pod has the new annotation
438+ & corev1.Pod {
439+ ObjectMeta : metav1.ObjectMeta {
440+ Name : sandboxName ,
441+ Namespace : sandboxNs ,
442+ ResourceVersion : "1" ,
443+ Labels : map [string ]string {
444+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
445+ },
446+ Annotations : map [string ]string {
447+ "cluster-autoscaler.kubernetes.io/safe-to-evict" : "false" ,
448+ },
449+ OwnerReferences : []metav1.OwnerReference {sandboxControllerRef (sandboxName )},
450+ },
451+ Spec : corev1.PodSpec {
452+ Containers : []corev1.Container {{Name : "test-container" }},
453+ },
454+ },
455+ // Verify Service
456+ & corev1.Service {
457+ ObjectMeta : metav1.ObjectMeta {
458+ Name : sandboxName ,
459+ Namespace : sandboxNs ,
460+ ResourceVersion : "1" ,
461+ Labels : map [string ]string {
462+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
463+ },
464+ OwnerReferences : []metav1.OwnerReference {sandboxControllerRef (sandboxName )},
465+ },
466+ Spec : corev1.ServiceSpec {
467+ Selector : map [string ]string {
468+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
469+ },
470+ ClusterIP : "None" ,
471+ },
472+ },
473+ // Verify the new PDB
474+ & policyv1.PodDisruptionBudget {
475+ ObjectMeta : metav1.ObjectMeta {
476+ Name : sandboxName ,
477+ Namespace : sandboxNs ,
478+ ResourceVersion : "1" ,
479+ OwnerReferences : []metav1.OwnerReference {sandboxControllerRef (sandboxName )},
480+ },
481+ Spec : policyv1.PodDisruptionBudgetSpec {
482+ MinAvailable : ptr .To (intstr .FromInt (1 )),
483+ Selector : & metav1.LabelSelector {
484+ MatchLabels : map [string ]string {
485+ "agents.x-k8s.io/sandbox-name-hash" : "ab179450" ,
486+ },
487+ },
488+ },
489+ },
490+ },
491+ },
408492 }
409493
410494 for _ , tc := range testCases {
@@ -595,15 +679,68 @@ func TestReconcilePod(t *testing.T) {
595679 require .Equal (t , tc .wantPod , pod )
596680 // Validate the Pod from the "cluster" (fake client)
597681 if tc .wantPod != nil {
682+ // If we expect a pod, verify it exists and matches.
598683 livePod := & corev1.Pod {}
599- err = r .Get (t .Context (), types.NamespacedName {Name : pod . Name , Namespace : pod .Namespace }, livePod )
684+ err = r .Get (t .Context (), types.NamespacedName {Name : tc . wantPod . Name , Namespace : tc . wantPod .Namespace }, livePod )
600685 require .NoError (t , err )
601686 require .Equal (t , tc .wantPod , livePod )
602687 } else {
688+ // If we don't expect a pod (it was deleted), verify it's gone.
603689 livePod := & corev1.Pod {}
604- err = r .Get (t .Context (), types.NamespacedName {Name : sandboxName , Namespace : sandboxNs }, livePod )
690+ err = r .Get (t .Context (), types.NamespacedName {Name : tc . sandbox . Name , Namespace : tc . sandbox . Namespace }, livePod )
605691 require .True (t , k8serrors .IsNotFound (err ))
606692 }
607693 })
608694 }
609695}
696+
697+ // This test simulates updating a Sandbox and ensures the controller correctly deletes the now-unneeded PDB
698+ func TestReconcile_ResilienceCleanup (t * testing.T ) {
699+ sandboxName := "sandbox-name"
700+ sandboxNs := "sandbox-ns"
701+
702+ // Initial Sandbox with High Resilience
703+ sb := & sandboxv1alpha1.Sandbox {
704+ ObjectMeta : metav1.ObjectMeta {
705+ Name : sandboxName ,
706+ Namespace : sandboxNs ,
707+ Generation : 1 ,
708+ },
709+ Spec : sandboxv1alpha1.SandboxSpec {
710+ Resilience : sandboxv1alpha1 .ResilienceLevelHigh ,
711+ PodTemplate : sandboxv1alpha1.PodTemplate {
712+ Spec : corev1.PodSpec {
713+ Containers : []corev1.Container {{Name : "test-container" }},
714+ },
715+ },
716+ },
717+ }
718+
719+ r := SandboxReconciler {
720+ Client : newFakeClient (sb ),
721+ Scheme : Scheme ,
722+ }
723+ req := ctrl.Request {NamespacedName : types.NamespacedName {Name : sandboxName , Namespace : sandboxNs }}
724+
725+ _ , err := r .Reconcile (t .Context (), req )
726+ require .NoError (t , err )
727+
728+ // Verify PDB was created
729+ pdb := & policyv1.PodDisruptionBudget {}
730+ require .NoError (t , r .Get (t .Context (), req .NamespacedName , pdb ), "PDB should exist after first reconcile" )
731+
732+ // Update Sandbox to remove resilience
733+ liveSandbox := & sandboxv1alpha1.Sandbox {}
734+ require .NoError (t , r .Get (t .Context (), req .NamespacedName , liveSandbox ))
735+ liveSandbox .Spec .Resilience = "" // Remove resilience
736+ liveSandbox .Generation = 2
737+ require .NoError (t , r .Update (t .Context (), liveSandbox ))
738+
739+ // Re-run reconcile
740+ _ , err = r .Reconcile (t .Context (), req )
741+ require .NoError (t , err )
742+
743+ // Verify PDB was deleted
744+ err = r .Get (t .Context (), req .NamespacedName , pdb )
745+ require .True (t , k8serrors .IsNotFound (err ), "PDB should be deleted after resilience is removed" )
746+ }
0 commit comments