/* Copyright 2022 The Karmada Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package native import ( "testing" "time" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/util/helper" ) func TestAggregateDeploymentStatus(t *testing.T) { statusMap := map[string]interface{}{ "replicas": 1, "readyReplicas": 1, "updatedReplicas": 1, "availableReplicas": 1, "unavailableReplicas": 0, } raw, _ := helper.BuildStatusRawExtension(statusMap) aggregatedStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw, Applied: true}, {ClusterName: "member2", Status: raw, Applied: true}, } oldDeploy := &appsv1.Deployment{} newDeploy := &appsv1.Deployment{Status: appsv1.DeploymentStatus{Replicas: 2, ReadyReplicas: 2, UpdatedReplicas: 2, AvailableReplicas: 2, UnavailableReplicas: 0}} oldObj, _ := helper.ToUnstructured(oldDeploy) newObj, _ := helper.ToUnstructured(newDeploy) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update deployment status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, { name: "ignore update deployment status as up to date", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, } for _, tt := range tests { actualObj, _ := aggregateDeploymentStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregateServiceStatus(t *testing.T) { statusMapNotLB := map[string]interface{}{} rawNotLB, _ := helper.BuildStatusRawExtension(statusMapNotLB) aggregatedStatusItemsNotLB := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: rawNotLB, Applied: true}, } statusMapLB := map[string]interface{}{ "loadBalancer": corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "8.8.8.8"}}}, } rawLB, _ := helper.BuildStatusRawExtension(statusMapLB) aggregatedStatusItemsLB := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: rawLB, Applied: true}, } serviceClusterIP := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP}} serviceNodePort := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeNodePort}} serviceExternalName := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeExternalName}} objServiceClusterIP, _ := helper.ToUnstructured(serviceClusterIP) objServiceNodePort, _ := helper.ToUnstructured(serviceNodePort) objServiceExternalName, _ := helper.ToUnstructured(serviceExternalName) oldServiceLoadBalancer := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}} newServiceLoadBalancer := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}, Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "8.8.8.8", Hostname: "member1"}}}}} oldObjServiceLoadBalancer, _ := helper.ToUnstructured(oldServiceLoadBalancer) newObjServiceLoadBalancer, _ := helper.ToUnstructured(newServiceLoadBalancer) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "ignore update service status when type is ClusterIP", curObj: objServiceClusterIP, aggregatedStatusItems: aggregatedStatusItemsNotLB, expectedObj: objServiceClusterIP, }, { name: "ignore update service status when type is NodePort", curObj: objServiceNodePort, aggregatedStatusItems: aggregatedStatusItemsNotLB, expectedObj: objServiceNodePort, }, { name: "ignore update service status when type is ExternalName", curObj: objServiceExternalName, aggregatedStatusItems: aggregatedStatusItemsNotLB, expectedObj: objServiceExternalName, }, { name: "update service status when type is LoadBalancer", curObj: oldObjServiceLoadBalancer, aggregatedStatusItems: aggregatedStatusItemsLB, expectedObj: newObjServiceLoadBalancer, }, { name: "ignore update service status as up to date when type is LoadBalancer", curObj: newObjServiceLoadBalancer, aggregatedStatusItems: aggregatedStatusItemsLB, expectedObj: newObjServiceLoadBalancer, }, } for _, tt := range tests { actualObj, _ := aggregateServiceStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregateIngressStatus(t *testing.T) { statusMap := map[string]interface{}{ "loadBalancer": corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "8.8.8.8"}}}, } raw, _ := helper.BuildStatusRawExtension(statusMap) aggregatedStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw, Applied: true}, } oldIngress := &networkingv1.Ingress{} newIngress := &networkingv1.Ingress{Status: networkingv1.IngressStatus{LoadBalancer: networkingv1.IngressLoadBalancerStatus{Ingress: []networkingv1.IngressLoadBalancerIngress{{IP: "8.8.8.8", Hostname: "member1"}}}}} oldObj, _ := helper.ToUnstructured(oldIngress) newObj, _ := helper.ToUnstructured(newIngress) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update ingress status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, { name: "ignore update ingress status as up to date", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, } for _, tt := range tests { actualObj, _ := aggregateIngressStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregateJobStatus(t *testing.T) { startTime := metav1.Now() completionTime := startTime statusMap := map[string]interface{}{ "active": 0, "succeeded": 1, "failed": 0, "startTime": startTime, "completionTime": completionTime, "conditions": []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: corev1.ConditionTrue}}, } raw, _ := helper.BuildStatusRawExtension(statusMap) aggregatedStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw, Applied: true}, {ClusterName: "member2", Status: raw, Applied: true}, } statusMapWithJobfailed := map[string]interface{}{ "active": 0, "succeeded": 0, "failed": 1, "startTime": startTime, "completionTime": completionTime, "conditions": []batchv1.JobCondition{{Type: batchv1.JobFailed, Status: corev1.ConditionTrue}}, } rawWithJobFailed, _ := helper.BuildStatusRawExtension(statusMapWithJobfailed) aggregatedStatusItemsWithJobFailed := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw, Applied: true}, {ClusterName: "member2", Status: rawWithJobFailed, Applied: true}, } oldJob := &batchv1.Job{} newJob := &batchv1.Job{Status: batchv1.JobStatus{Active: 0, Succeeded: 2, Failed: 0, StartTime: &startTime, CompletionTime: &completionTime, Conditions: []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: corev1.ConditionTrue, Reason: "Completed", Message: "Job completed"}}}} oldObj, _ := helper.ToUnstructured(oldJob) newObj, _ := helper.ToUnstructured(newJob) newJobWithJobFailed := &batchv1.Job{Status: batchv1.JobStatus{Active: 0, Succeeded: 1, Failed: 1, StartTime: &startTime, CompletionTime: &completionTime, Conditions: []batchv1.JobCondition{{Type: batchv1.JobFailed, Status: corev1.ConditionTrue, Reason: "JobFailed", Message: "Job executed failed in member clusters member2"}}}} newObjWithJobFailed, _ := helper.ToUnstructured(newJobWithJobFailed) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update job status without job failed", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, { name: "update job status with job failed", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItemsWithJobFailed, expectedObj: newObjWithJobFailed, }, { name: "ignore update job status as up to date", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, } for _, tt := range tests { actualObj, _ := aggregateJobStatus(tt.curObj, tt.aggregatedStatusItems) // Clean condition time before compare, due to issue: https://github.com/karmada-io/karmada/issues/1767 actualObj = cleanUnstructuredJobConditionTime(actualObj) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregateDaemonSetStatus(t *testing.T) { statusMap := map[string]interface{}{ "currentNumberScheduled": 1, "numberMisscheduled": 0, "desiredNumberScheduled": 1, "numberReady": 1, "updatedNumberScheduled": 1, "numberAvailable": 1, "numberUnavailable": 0, } raw, _ := helper.BuildStatusRawExtension(statusMap) aggregatedStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw, Applied: true}, {ClusterName: "member2", Status: raw, Applied: true}, } oldDaemonSet := &appsv1.DaemonSet{} newDaemonSet := &appsv1.DaemonSet{Status: appsv1.DaemonSetStatus{CurrentNumberScheduled: 2, NumberMisscheduled: 0, DesiredNumberScheduled: 2, NumberReady: 2, UpdatedNumberScheduled: 2, NumberAvailable: 2, NumberUnavailable: 0}} oldObj, _ := helper.ToUnstructured(oldDaemonSet) newObj, _ := helper.ToUnstructured(newDaemonSet) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update daemonSet status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, { name: "ignore update daemonSet status as up to date", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, } for _, tt := range tests { actualObj, _ := aggregateDaemonSetStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregateStatefulSetStatus(t *testing.T) { statusMap := map[string]interface{}{ "replicas": 1, "readyReplicas": 1, "currentReplicas": 1, "updatedReplicas": 1, "availableReplicas": 1, } raw, _ := helper.BuildStatusRawExtension(statusMap) aggregatedStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw, Applied: true}, {ClusterName: "member2", Status: raw, Applied: true}, } oldStatefulSet := &appsv1.StatefulSet{} newStatefulSet := &appsv1.StatefulSet{Status: appsv1.StatefulSetStatus{Replicas: 2, ReadyReplicas: 2, UpdatedReplicas: 2, AvailableReplicas: 2, CurrentReplicas: 2}} oldObj, _ := helper.ToUnstructured(oldStatefulSet) newObj, _ := helper.ToUnstructured(newStatefulSet) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update statefulSet status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, { name: "ignore update statefulSet status as up to date", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems, expectedObj: newObj, }, } for _, tt := range tests { actualObj, _ := aggregateStatefulSetStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func cleanUnstructuredJobConditionTime(object *unstructured.Unstructured) *unstructured.Unstructured { job := &batchv1.Job{} _ = helper.ConvertToTypedObject(object, job) for i := range job.Status.Conditions { cond := &job.Status.Conditions[i] cond.LastProbeTime = metav1.Time{} cond.LastTransitionTime = metav1.Time{} } ret, _ := helper.ToUnstructured(job) return ret } func TestAggregatePodStatus(t *testing.T) { startedTrue := true timeNow := time.Now() containerStatuses1 := []corev1.ContainerStatus{ { ContainerID: "containerd://6cee0afa333472f672341352e0d", Image: "nginx:latest", ImageID: "docker.io/library/import-2022-06-05@sha256:dfb593", Name: "busybox-2", Ready: true, RestartCount: 0, Started: &startedTrue, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, { ContainerID: "containerd://b373fb05ebf57020573cdf4a4518a3b2a8", Image: "nginx:latest", ImageID: "docker.io/library/import-2022-06-05@sha256:a29d07a75", Name: "busybox-2", Ready: true, RestartCount: 0, Started: &startedTrue, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, } newContainerStatuses1 := []corev1.ContainerStatus{ { Ready: true, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, { Ready: true, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, } newInitContainerStatuses1 := []corev1.ContainerStatus{ { Ready: false, State: corev1.ContainerState{ Waiting: &corev1.ContainerStateWaiting{Reason: "PodInitializing"}, }, }, { Ready: false, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, } newInitContainerObj, _ := helper.ToUnstructured(&corev1.Pod{Status: corev1.PodStatus{ InitContainerStatuses: newInitContainerStatuses1, Phase: corev1.PodPending, }}) initContainerStatusMap1 := map[string]interface{}{ "initContainerStatuses": []corev1.ContainerStatus{newInitContainerStatuses1[0], newInitContainerStatuses1[1]}, "phase": corev1.PodPending, } initContainerRaw1, _ := helper.BuildStatusRawExtension(initContainerStatusMap1) aggregatedInitContainerStatusItems1 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: initContainerRaw1, Applied: true}, } curObj, _ := helper.ToUnstructured(&corev1.Pod{}) newObj, _ := helper.ToUnstructured(&corev1.Pod{Status: corev1.PodStatus{ ContainerStatuses: newContainerStatuses1, Phase: corev1.PodRunning, }}) statusMap1 := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{containerStatuses1[0]}, "phase": corev1.PodRunning, } raw1, _ := helper.BuildStatusRawExtension(statusMap1) statusMap2 := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{containerStatuses1[1]}, "phase": corev1.PodRunning, } raw2, _ := helper.BuildStatusRawExtension(statusMap2) aggregatedStatusItems1 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw1, Applied: true}, {ClusterName: "member2", Status: raw2, Applied: true}, } containerStatuses2 := []corev1.ContainerStatus{ { ContainerID: "containerd://6cee0afa333472f672341352e0d", Image: "nginx:latest", ImageID: "docker.io/library/import-2022-06-05@sha256:dfb593", Name: "busybox-2", Ready: true, RestartCount: 0, Started: &startedTrue, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, { ContainerID: "containerd://b373fb05ebf57020573cdf4a4518a3b2a8", Image: "nginx:latest", ImageID: "docker.io/library/import-2022-06-05@sha256:a29d07a75", Name: "busybox-2", Ready: false, RestartCount: 0, Started: &startedTrue, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, } statusMap3 := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{containerStatuses2[0]}, "phase": corev1.PodRunning, } raw3, _ := helper.BuildStatusRawExtension(statusMap3) statusMap4 := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{containerStatuses2[1]}, "phase": corev1.PodPending, } raw4, _ := helper.BuildStatusRawExtension(statusMap4) aggregatedStatusItems2 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: raw3, Applied: true}, {ClusterName: "member2", Status: raw4, Applied: true}, } newContainerStatuses2 := []corev1.ContainerStatus{ { Ready: true, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, { Ready: false, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Time{ Time: timeNow, }, }, }, }, } newPodFailed := &corev1.Pod{Status: corev1.PodStatus{ ContainerStatuses: newContainerStatuses2, Phase: corev1.PodPending, }} newObjFailed, _ := helper.ToUnstructured(newPodFailed) containerStatusesRunning := []corev1.ContainerStatus{ { Ready: true, State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.Time{Time: time.Now()}}}, }, } newContainerStatusesFail := []corev1.ContainerStatus{ { Ready: false, State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 125}}, }, } newContainerStatusesSucceeded := []corev1.ContainerStatus{ { Ready: false, State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}}, }, } statusMapRunning := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{containerStatusesRunning[0]}, "phase": corev1.PodRunning, } statusMapFailed := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{newContainerStatusesFail[0]}, "phase": corev1.PodFailed, } statusMapSucceeded := map[string]interface{}{ "containerStatuses": []corev1.ContainerStatus{newContainerStatusesSucceeded[0]}, "phase": corev1.PodSucceeded, } rawRunning, _ := helper.BuildStatusRawExtension(statusMapRunning) // test failed rawFail, _ := helper.BuildStatusRawExtension(statusMapFailed) aggregatedStatusItemsFail := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: rawRunning, Applied: true}, {ClusterName: "member2", Status: rawFail, Applied: true}, } aggregatedStatusItemsPending := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: rawRunning, Applied: true}, {ClusterName: "member2", Status: nil, Applied: true}, } failObj, _ := helper.ToUnstructured(&corev1.Pod{Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{containerStatusesRunning[0], newContainerStatusesFail[0]}, Phase: corev1.PodFailed, }}) // test succeeded rawSucceeded, _ := helper.BuildStatusRawExtension(statusMapSucceeded) aggregatedStatusItemsSucceeded := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: rawRunning, Applied: true}, {ClusterName: "member2", Status: rawSucceeded, Applied: true}, } succeededObj, _ := helper.ToUnstructured(&corev1.Pod{Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{containerStatusesRunning[0], newContainerStatusesSucceeded[0]}, Phase: corev1.PodRunning, }}) pendingObj, _ := helper.ToUnstructured(&corev1.Pod{Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{containerStatusesRunning[0]}, Phase: corev1.PodPending, }}) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update initContainer status", curObj: curObj, aggregatedStatusItems: aggregatedInitContainerStatusItems1, expectedObj: newInitContainerObj, }, { name: "update pod status", curObj: curObj, aggregatedStatusItems: aggregatedStatusItems1, expectedObj: newObj, }, { name: "ignore update pod status as up to date", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems1, expectedObj: newObj, }, { name: "update pod status as one Pod failed", curObj: newObj, aggregatedStatusItems: aggregatedStatusItems2, expectedObj: newObjFailed, }, // More details please refer to: https://github.com/karmada-io/karmada/issues/2137 { name: "failed pod status", curObj: curObj, aggregatedStatusItems: aggregatedStatusItemsFail, // Running + Failed => Failed expectedObj: failObj, }, { name: "succeeded pod status", curObj: curObj, aggregatedStatusItems: aggregatedStatusItemsSucceeded, // Running + Succeeded => Running expectedObj: succeededObj, }, { name: "pending pod status", curObj: curObj, aggregatedStatusItems: aggregatedStatusItemsPending, // Running + nil => Pending expectedObj: pendingObj, }, } for _, tt := range tests { actualObj, _ := aggregatePodStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregatePVCStatus(t *testing.T) { statusBoundMap := map[string]interface{}{ "phase": corev1.ClaimBound, } // Bound status boundRaw1, _ := helper.BuildStatusRawExtension(statusBoundMap) boundRaw2, _ := helper.BuildStatusRawExtension(statusBoundMap) aggregatedStatusItems1 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: boundRaw1, Applied: true}, {ClusterName: "member2", Status: boundRaw2, Applied: true}, } // Lost status statusLostMap := map[string]interface{}{ "phase": corev1.ClaimLost, } lostRaw1, _ := helper.BuildStatusRawExtension(statusBoundMap) lostRaw2, _ := helper.BuildStatusRawExtension(statusLostMap) aggregatedStatusItems2 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: lostRaw1, Applied: true}, {ClusterName: "member2", Status: lostRaw2, Applied: true}, } // Pending status statusPendingMap := map[string]interface{}{ "phase": corev1.ClaimPending, } pendingRaw1, _ := helper.BuildStatusRawExtension(statusBoundMap) pendingRaw2, _ := helper.BuildStatusRawExtension(statusPendingMap) aggregatedStatusItems3 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: pendingRaw1, Applied: true}, {ClusterName: "member2", Status: pendingRaw2, Applied: true}, } // test aggregatePersistentVolumeClaimStatus function oldPVC := &corev1.PersistentVolumeClaim{} oldObj, _ := helper.ToUnstructured(oldPVC) boundNewPVC := &corev1.PersistentVolumeClaim{Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimBound}} newBoundPVCObj, _ := helper.ToUnstructured(boundNewPVC) lostNewPVC := &corev1.PersistentVolumeClaim{Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimLost}} newLostPVCObj, _ := helper.ToUnstructured(lostNewPVC) pendingNewPVC := &corev1.PersistentVolumeClaim{Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimPending}} newPendingPVCObj, _ := helper.ToUnstructured(pendingNewPVC) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update pvc status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems1, expectedObj: newBoundPVCObj, }, { name: "update pvc status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems2, expectedObj: newLostPVCObj, }, { name: "update pvc status", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems3, expectedObj: newPendingPVCObj, }, { name: "ignore update pvc status as up to date", curObj: newBoundPVCObj, aggregatedStatusItems: aggregatedStatusItems1, expectedObj: newBoundPVCObj, }, } for _, tt := range tests { actualObj, _ := aggregatePersistentVolumeClaimStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) } } func TestAggregatePVStatus(t *testing.T) { statusAvailableMap := map[string]interface{}{ "phase": corev1.VolumeAvailable, } // Available status availableRaw1, _ := helper.BuildStatusRawExtension(statusAvailableMap) availableRaw2, _ := helper.BuildStatusRawExtension(statusAvailableMap) aggregatedStatusItems1 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: availableRaw1, Applied: true}, {ClusterName: "member2", Status: availableRaw2, Applied: true}, } statusReleasedMap := map[string]interface{}{ "phase": corev1.VolumeReleased, } // Release status releasedRaw1, _ := helper.BuildStatusRawExtension(statusReleasedMap) releasedRaw2, _ := helper.BuildStatusRawExtension(statusReleasedMap) aggregatedStatusItems2 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: releasedRaw1, Applied: true}, {ClusterName: "member2", Status: releasedRaw2, Applied: true}, } statusBoundMap := map[string]interface{}{ "phase": corev1.VolumeBound, } // Bound status boundRaw1, _ := helper.BuildStatusRawExtension(statusBoundMap) boundRaw2, _ := helper.BuildStatusRawExtension(statusReleasedMap) aggregatedStatusItems3 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: boundRaw1, Applied: true}, {ClusterName: "member2", Status: boundRaw2, Applied: true}, } // Failed status statusFailedMap := map[string]interface{}{ "phase": corev1.VolumeFailed, } failedRaw1, _ := helper.BuildStatusRawExtension(releasedRaw1) failedRaw2, _ := helper.BuildStatusRawExtension(statusFailedMap) aggregatedStatusItems4 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: failedRaw1, Applied: true}, {ClusterName: "member2", Status: failedRaw2, Applied: true}, } // Pending status statusPendingMap := map[string]interface{}{ "phase": corev1.VolumePending, } pendingRaw1, _ := helper.BuildStatusRawExtension(statusAvailableMap) pendingRaw2, _ := helper.BuildStatusRawExtension(statusPendingMap) aggregatedStatusItems5 := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: pendingRaw1, Applied: true}, {ClusterName: "member2", Status: pendingRaw2, Applied: true}, } // test aggregatePersistentVolumeStatus function oldPVC := &corev1.PersistentVolume{} oldObj, _ := helper.ToUnstructured(oldPVC) availableNewPv := &corev1.PersistentVolume{Status: corev1.PersistentVolumeStatus{Phase: corev1.VolumeAvailable}} newAvailablePvObj, _ := helper.ToUnstructured(availableNewPv) boundNewPV := &corev1.PersistentVolume{Status: corev1.PersistentVolumeStatus{Phase: corev1.VolumeBound}} newBoundPVObj, _ := helper.ToUnstructured(boundNewPV) releaseNewPV := &corev1.PersistentVolume{Status: corev1.PersistentVolumeStatus{Phase: corev1.VolumeReleased}} newReleasePVObj, _ := helper.ToUnstructured(releaseNewPV) failedNewPV := &corev1.PersistentVolume{Status: corev1.PersistentVolumeStatus{Phase: corev1.VolumeFailed}} newFailedPVObj, _ := helper.ToUnstructured(failedNewPV) pendingNewPV := &corev1.PersistentVolume{Status: corev1.PersistentVolumeStatus{Phase: corev1.VolumePending}} newPendingPVObj, _ := helper.ToUnstructured(pendingNewPV) tests := []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update pvc status1", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems1, expectedObj: newAvailablePvObj, }, { name: "update pvc status2", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems2, expectedObj: newReleasePVObj, }, { name: "update pvc status3", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems3, expectedObj: newBoundPVObj, }, { name: "update pvc status4", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems4, expectedObj: newFailedPVObj, }, { name: "update pvc status5", curObj: oldObj, aggregatedStatusItems: aggregatedStatusItems5, expectedObj: newPendingPVObj, }, { name: "ignore update pvc status as up to date", curObj: newAvailablePvObj, aggregatedStatusItems: aggregatedStatusItems1, expectedObj: newAvailablePvObj, }, } for _, tt := range tests { actualObj, _ := aggregatePersistentVolumeStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj, tt.name) } } func TestAggregatedPodDisruptionBudgetStatus(t *testing.T) { currPdbObj, _ := helper.ToUnstructured(&policyv1.PodDisruptionBudget{ Status: policyv1.PodDisruptionBudgetStatus{ CurrentHealthy: 1, DesiredHealthy: 1, DisruptionsAllowed: 1, ExpectedPods: 1, }, }) expectedPdbObj, _ := helper.ToUnstructured(&policyv1.PodDisruptionBudget{ Status: policyv1.PodDisruptionBudgetStatus{ CurrentHealthy: 2, DesiredHealthy: 2, DisruptionsAllowed: 2, ExpectedPods: 2, }, }) healthyStatusRaw, _ := helper.BuildStatusRawExtension(map[string]interface{}{ "currentHealthy": 1, "desiredHealthy": 1, "disruptionsAllowed": 1, "expectedPods": 1, }) evictionTime := metav1.Now() unHealthyStatusRaw, _ := helper.BuildStatusRawExtension(map[string]interface{}{ "currentHealthy": 0, "desiredHealthy": 1, "disruptionsAllowed": 0, "expectedPods": 1, "disruptedPods": map[string]metav1.Time{ "pod-1234": evictionTime, }, }) expectedUnhealthyPdbObj, _ := helper.ToUnstructured(&policyv1.PodDisruptionBudget{ Status: policyv1.PodDisruptionBudgetStatus{ CurrentHealthy: 0, DesiredHealthy: 2, DisruptionsAllowed: 0, ExpectedPods: 2, DisruptedPods: map[string]metav1.Time{ "member1/pod-1234": evictionTime, "member2/pod-1234": evictionTime, }, }, }) aggregateStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: healthyStatusRaw, Applied: true}, {ClusterName: "member2", Status: healthyStatusRaw, Applied: true}, } unhealthyAggregateStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: unHealthyStatusRaw, Applied: true}, {ClusterName: "member2", Status: unHealthyStatusRaw, Applied: true}, } for _, tt := range []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update pdb status", curObj: currPdbObj, aggregatedStatusItems: aggregateStatusItems, expectedObj: expectedPdbObj, }, { name: "update pdb status with disrupted pods", curObj: currPdbObj, aggregatedStatusItems: unhealthyAggregateStatusItems, expectedObj: expectedUnhealthyPdbObj, }, { name: "ignore update pdb status as up to date", curObj: expectedPdbObj, aggregatedStatusItems: aggregateStatusItems, expectedObj: expectedPdbObj, }, } { t.Run(tt.name, func(t *testing.T) { actualObj, _ := aggregatePodDisruptionBudgetStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) }) } } func Test_aggregateCronJobStatus(t *testing.T) { currCronJobObj, _ := helper.ToUnstructured(&batchv1.CronJob{ Status: batchv1.CronJobStatus{ Active: []corev1.ObjectReference{}, LastScheduleTime: nil, LastSuccessfulTime: nil, }, }) cronjobStatusRaw1, _ := helper.BuildStatusRawExtension(map[string]interface{}{ "active": []corev1.ObjectReference{ { APIVersion: "batch/v1", Kind: "Job", Name: "foo", Namespace: "default", ResourceVersion: "1", UID: "1d5db04f-f2e8-4807-b6d4-7b78f402250d", }, }, "lastScheduleTime": "2023-02-08T07:16:00Z", "lastSuccessfulTime": nil, }) cronjobStatusRaw2, _ := helper.BuildStatusRawExtension(map[string]interface{}{ "active": []corev1.ObjectReference{ { APIVersion: "batch/v1", Kind: "Job", Name: "foo", Namespace: "default", ResourceVersion: "1", UID: "1d5db04f-f2e8-4807-b6d4-7b78f402250d", }, }, "lastScheduleTime": "2023-02-08T07:17:00Z", "lastSuccessfulTime": "2023-02-08T07:17:00Z", }) parse, _ := time.Parse("2006-01-02 15:04:05", "2023-02-08 07:17:00") successfulTime := metav1.NewTime(parse) expectedCronJobObj, _ := helper.ToUnstructured(&batchv1.CronJob{ Status: batchv1.CronJobStatus{ Active: []corev1.ObjectReference{ { APIVersion: "batch/v1", Kind: "Job", Name: "foo", Namespace: "default", ResourceVersion: "1", UID: "1d5db04f-f2e8-4807-b6d4-7b78f402250d", }, { APIVersion: "batch/v1", Kind: "Job", Name: "foo", Namespace: "default", ResourceVersion: "1", UID: "1d5db04f-f2e8-4807-b6d4-7b78f402250d", }, }, LastScheduleTime: &successfulTime, LastSuccessfulTime: &successfulTime, }, }) aggregateStatusItems := []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: cronjobStatusRaw1, Applied: true}, {ClusterName: "member2", Status: cronjobStatusRaw2, Applied: true}, } for _, tt := range []struct { name string curObj *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem expectedObj *unstructured.Unstructured }{ { name: "update cronjob status", curObj: currCronJobObj, expectedObj: expectedCronJobObj, aggregatedStatusItems: aggregateStatusItems, }, } { t.Run(tt.name, func(t *testing.T) { actualObj, _ := aggregateCronJobStatus(tt.curObj, tt.aggregatedStatusItems) assert.Equal(t, tt.expectedObj, actualObj) }) } } func Test_aggregateHorizontalPodAutoscalerStatus(t *testing.T) { curHPA, _ := helper.ToUnstructured(&autoscalingv2.HorizontalPodAutoscaler{ Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 0, DesiredReplicas: 0, }, }) aggregatedStatusItem1, _ := helper.BuildStatusRawExtension(map[string]interface{}{ "currentReplicas": 2, "desiredReplicas": 2, }) aggregatedStatusItem2, _ := helper.BuildStatusRawExtension(map[string]interface{}{ "currentReplicas": 4, "desiredReplicas": 4, }) expectHPA, _ := helper.ToUnstructured(&autoscalingv2.HorizontalPodAutoscaler{ Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 6, DesiredReplicas: 6, }, }) type args struct { object *unstructured.Unstructured aggregatedStatusItems []workv1alpha2.AggregatedStatusItem } tests := []struct { name string args args want *unstructured.Unstructured wantErr bool }{ { name: "update hpa status", args: args{ object: curHPA, aggregatedStatusItems: []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: aggregatedStatusItem1, Applied: true}, {ClusterName: "member2", Status: aggregatedStatusItem2, Applied: true}, {ClusterName: "member3", Status: nil, Applied: true}, }, }, want: expectHPA, wantErr: false, }, { name: "hpa status update to dates", args: args{ object: expectHPA, aggregatedStatusItems: []workv1alpha2.AggregatedStatusItem{ {ClusterName: "member1", Status: aggregatedStatusItem1, Applied: true}, {ClusterName: "member2", Status: aggregatedStatusItem2, Applied: true}, {ClusterName: "member3", Status: nil, Applied: true}, }, }, want: expectHPA, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := aggregateHorizontalPodAutoscalerStatus(tt.args.object, tt.args.aggregatedStatusItems) if (err != nil) != tt.wantErr { t.Errorf("Test_aggregateHorizontalPodAutoscalerStatus() err = %v, wantErr %v", err, tt.wantErr) } assert.Equalf(t, tt.want, got, "aggregateHorizontalPodAutoscalerStatus(%v, %v)", tt.args.object, tt.args.aggregatedStatusItems) }) } }