@@ -7,15 +7,20 @@ import (
77 "testing"
88 "time"
99
10+ "github.com/sirupsen/logrus"
1011 "github.com/stretchr/testify/assert"
12+ "github.com/stretchr/testify/require"
1113 appsv1 "k8s.io/api/apps/v1"
1214 batchv1 "k8s.io/api/batch/v1"
1315 v1 "k8s.io/api/core/v1"
1416 "k8s.io/apimachinery/pkg/api/meta"
1517 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+ "k8s.io/apimachinery/pkg/fields"
19+ "k8s.io/apimachinery/pkg/labels"
1620 "k8s.io/apimachinery/pkg/runtime"
1721 watch "k8s.io/apimachinery/pkg/watch"
1822 "k8s.io/client-go/kubernetes/fake"
23+ k8stesting "k8s.io/client-go/testing"
1924
2025 argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
2126 fakeargoclientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake"
@@ -48,8 +53,36 @@ func newTestFixtures() testFixtures {
4853}
4954
5055func setupTestClients () kube.Clients {
56+ fakeClient := fake .NewClientset ()
57+
58+ // Add a reactor to handle Pod DeleteCollection with support for filtering on Phase
59+ fakeClient .PrependReactor ("delete-collection" , "pods" , func (action k8stesting.Action ) (handled bool , ret runtime.Object , err error ) {
60+ deleteAction , _ := action .(k8stesting.DeleteCollectionActionImpl )
61+ labelSelector , fieldSelector , _ := k8stesting .ExtractFromListOptions (deleteAction .ListOptions )
62+ gvk := v1 .SchemeGroupVersion .WithKind ("Pod" )
63+ obj , _ := fakeClient .Tracker ().List (deleteAction .GetResource (), gvk , deleteAction .GetNamespace (), deleteAction .ListOptions )
64+ podList , _ := obj .(* v1.PodList )
65+ for _ , pod := range podList .Items {
66+ podFields := fields .Set (map [string ]string {
67+ "status.phase" : string (pod .Status .Phase ),
68+ })
69+ if fieldSelector != nil && ! fieldSelector .Matches (podFields ) {
70+ continue
71+ }
72+ if ! labelSelector .Matches (labels .Set (pod .Labels )) {
73+ continue
74+ }
75+ logrus .Infof ("Deleting pod %s via DeleteCollection" , pod .Name )
76+ err = fakeClient .Tracker ().Delete (deleteAction .GetResource (), deleteAction .GetNamespace (), pod .Name )
77+ if err != nil {
78+ logrus .Errorf ("Error deleting pod %s: %v" , pod .Name , err )
79+ }
80+ }
81+ return true , nil , nil
82+ })
83+
5184 return kube.Clients {
52- KubernetesClient : fake . NewClientset () ,
85+ KubernetesClient : fakeClient ,
5386 ArgoRolloutClient : fakeargoclientset .NewSimpleClientset (),
5487 }
5588}
@@ -412,30 +445,48 @@ func TestPatchResources(t *testing.T) {
412445 }
413446}
414447
415- func TestCreateJobFromCronjob (t * testing.T ) {
448+ func TestRestartRunningCronjobPods (t * testing.T ) {
416449 fixtures := newTestFixtures ()
417450
418451 runtimeObj , err := createTestCronJobWithAnnotations (clients , fixtures .namespace , "1" )
419- assert .NoError (t , err )
420-
452+ require .NoError (t , err )
421453 cronJob := runtimeObj .(* batchv1.CronJob )
422- err = callbacks .CreateJobFromCronjob (clients , fixtures .namespace , cronJob )
423- assert .NoError (t , err )
424454
425- jobList , err := clients .KubernetesClient .BatchV1 ().Jobs (fixtures .namespace ).List (context .TODO (), metav1.ListOptions {})
426- assert .NoError (t , err )
455+ // Create running job and pod
456+ runningJob , err := createTestJobForCronJob (clients , cronJob , "job1" , true )
457+ require .NoError (t , err )
458+ runningPod , err := createTestPodForJob (clients , runningJob , "pod1" , v1 .PodRunning )
459+ require .NoError (t , err )
427460
428- ownerFound := false
429- for _ , job := range jobList .Items {
430- if isControllerOwner ("CronJob" , cronJob .Name , job .OwnerReferences ) {
431- ownerFound = true
432- break
433- }
434- }
435- assert .Truef (t , ownerFound , "Missing CronJob owner reference" )
461+ // Create succeeded job and pod
462+ succeededJob , err := createTestJobForCronJob (clients , cronJob , "job2" , false )
463+ require .NoError (t , err )
464+ succeededPod , err := createTestPodForJob (clients , succeededJob , "pod2" , v1 .PodSucceeded )
465+ require .NoError (t , err )
466+
467+ // Create a pod that is not associated with a Job
468+ otherPod , err := createTestPod (clients , fixtures .namespace , "other" , nil , v1 .PodRunning )
469+ require .NoError (t , err )
470+
471+ // Run the restart
472+ err = callbacks .RestartRunningCronjobPods (clients , fixtures .namespace , cronJob )
473+ require .NoError (t , err )
436474
475+ // Verify running pod was deleted
476+ _ , err = clients .KubernetesClient .CoreV1 ().Pods (fixtures .namespace ).Get (context .TODO (), runningPod .Name , metav1.GetOptions {})
477+ assert .Error (t , err , "Running pod should have been deleted" )
478+
479+ // Verify succeeded pod remains
480+ _ , err = clients .KubernetesClient .CoreV1 ().Pods (fixtures .namespace ).Get (context .TODO (), succeededPod .Name , metav1.GetOptions {})
481+ assert .NoError (t , err , "Succeeded pod should not have been deleted" )
482+
483+ // Verify other pod remains
484+ _ , err = clients .KubernetesClient .CoreV1 ().Pods (fixtures .namespace ).Get (context .TODO (), otherPod .Name , metav1.GetOptions {})
485+ assert .NoError (t , err , "Other pod should not have been deleted" )
486+
487+ // Clean up
437488 err = deleteTestCronJob (clients , fixtures .namespace , cronJob .Name )
438- assert .NoError (t , err )
489+ require .NoError (t , err )
439490}
440491
441492func TestReCreateJobFromJob (t * testing.T ) {
@@ -763,11 +814,56 @@ func deleteTestJob(clients kube.Clients, namespace, name string) error {
763814 return clients .KubernetesClient .BatchV1 ().Jobs (namespace ).Delete (context .TODO (), name , metav1.DeleteOptions {})
764815}
765816
766- func isControllerOwner (kind , name string , ownerRefs []metav1.OwnerReference ) bool {
767- for _ , ownerRef := range ownerRefs {
768- if * ownerRef .Controller && ownerRef .Kind == kind && ownerRef .Name == name {
769- return true
817+ // createTestJobForCronJob creates a minimal representation of a Job for the given CronJob
818+ func createTestJobForCronJob (clients kube.Clients , cronJob * batchv1.CronJob , name string , active bool ) (* batchv1.Job , error ) {
819+ job := & batchv1.Job {
820+ ObjectMeta : metav1.ObjectMeta {
821+ Name : cronJob .Name + "-" + name ,
822+ Namespace : cronJob .Namespace ,
823+ Labels : map [string ]string {
824+ batchv1 .ControllerUidLabel : string (cronJob .UID ),
825+ },
826+ },
827+ }
828+ job , err := clients .KubernetesClient .BatchV1 ().Jobs (cronJob .Namespace ).Create (context .TODO (), job , metav1.CreateOptions {})
829+ if err != nil {
830+ return nil , err
831+ }
832+
833+ // Update the CronJob status to include the new Job if it is active
834+ if active {
835+ cronJob .Status .Active = append (cronJob .Status .Active , v1.ObjectReference {
836+ Name : job .Name ,
837+ Namespace : job .Namespace ,
838+ })
839+ _ , err = clients .KubernetesClient .BatchV1 ().CronJobs (cronJob .Namespace ).Update (context .TODO (), cronJob , metav1.UpdateOptions {})
840+ if err != nil {
841+ return nil , err
770842 }
771843 }
772- return false
844+ return job , err
845+ }
846+
847+ // createTestPod creates a minimal representation of a pod
848+ func createTestPod (clients kube.Clients , namespace , name string , metaLabels map [string ]string , phase v1.PodPhase ) (* v1.Pod , error ) {
849+ pod := & v1.Pod {
850+ ObjectMeta : metav1.ObjectMeta {
851+ UID : patchtypes .UID (name ),
852+ Name : name ,
853+ Namespace : namespace ,
854+ Labels : metaLabels ,
855+ },
856+ Status : v1.PodStatus {
857+ Phase : phase ,
858+ },
859+ }
860+ return clients .KubernetesClient .CoreV1 ().Pods (namespace ).Create (context .TODO (), pod , metav1.CreateOptions {})
861+ }
862+
863+ // createTestPodForJob creates a minimal representation of a pod for the given Job
864+ func createTestPodForJob (clients kube.Clients , job * batchv1.Job , name string , phase v1.PodPhase ) (* v1.Pod , error ) {
865+ metaLabels := map [string ]string {
866+ batchv1 .JobNameLabel : job .Name ,
867+ }
868+ return createTestPod (clients , job .Namespace , job .Name + "-" + name , metaLabels , phase )
773869}
0 commit comments