diff --git a/go.mod b/go.mod index 2199552f2..55baae704 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( k8s.io/apimachinery v0.30.3 k8s.io/client-go v0.30.3 k8s.io/component-base v0.30.2 + k8s.io/component-helpers v0.28.3 k8s.io/klog/v2 v2.130.1 k8s.io/metrics v0.25.2 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 diff --git a/pkg/controllers/workapplier/availability_tracker.go b/pkg/controllers/workapplier/availability_tracker.go index 0c8fbfef3..b95715c14 100644 --- a/pkg/controllers/workapplier/availability_tracker.go +++ b/pkg/controllers/workapplier/availability_tracker.go @@ -11,11 +11,13 @@ import ( appv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" apiextensionshelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/component-helpers/apps/poddisruptionbudget" "k8s.io/klog/v2" "go.goms.io/fleet/pkg/utils" @@ -79,6 +81,8 @@ func trackInMemberClusterObjAvailabilityByGVR( return trackServiceAvailability(inMemberClusterObj) case utils.CustomResourceDefinitionGVR: return trackCRDAvailability(inMemberClusterObj) + case utils.PodDisruptionBudgetGVR: + return trackPDBAvailability(inMemberClusterObj) default: if isDataResource(*gvr) { klog.V(2).InfoS("The object from the member cluster is a data object, consider it to be immediately available", @@ -218,6 +222,21 @@ func trackCRDAvailability(inMemberClusterObj *unstructured.Unstructured) (Manife return ManifestProcessingAvailabilityResultTypeNotYetAvailable, nil } +// trackPDBAvailability tracks the availability of a pod disruption budget in the member cluster +func trackPDBAvailability(curObj *unstructured.Unstructured) (ManifestProcessingAvailabilityResultType, error) { + var pdb policyv1.PodDisruptionBudget + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(curObj.Object, &pdb); err != nil { + return ManifestProcessingAvailabilityResultTypeFailed, controller.NewUnexpectedBehaviorError(err) + } + // Check if conditions are up-to-date + if poddisruptionbudget.ConditionsAreUpToDate(&pdb) { + klog.V(2).InfoS("PodDisruptionBudget is available", "pdb", klog.KObj(curObj)) + return ManifestProcessingAvailabilityResultTypeAvailable, nil + } + klog.V(2).InfoS("Still need to wait for PodDisruptionBudget to be available", "pdb", klog.KObj(curObj)) + return ManifestProcessingAvailabilityResultTypeNotYetAvailable, nil +} + // isDataResource checks if the resource is a data resource; such resources are // available immediately after creation. func isDataResource(gvr schema.GroupVersionResource) bool { diff --git a/pkg/controllers/workapplier/availability_tracker_test.go b/pkg/controllers/workapplier/availability_tracker_test.go index 3c3511932..5e13b2603 100644 --- a/pkg/controllers/workapplier/availability_tracker_test.go +++ b/pkg/controllers/workapplier/availability_tracker_test.go @@ -14,10 +14,12 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/klog/v2" "k8s.io/utils/ptr" @@ -136,6 +138,22 @@ var ( }, }, } + minAvailable = intstr.FromInt32(1) + + pdbTemplate = &policyv1.PodDisruptionBudget{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "policy/v1", + Kind: "PodDisruptionBudget", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb", + Namespace: nsName, + Generation: 2, + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: &minAvailable, + }, + } ) // TestTrackDeploymentAvailability tests the trackDeploymentAvailability function. @@ -667,6 +685,93 @@ func TestTrackCRDAvailability(t *testing.T) { } } +// TestTrackPDBAvailability tests the trackPDBAvailability function. +func TestTrackPDBAvailability(t *testing.T) { + availablePDB := pdbTemplate.DeepCopy() + availablePDB.Status = policyv1.PodDisruptionBudgetStatus{ + DisruptionsAllowed: 1, + CurrentHealthy: 2, + ObservedGeneration: 2, + DesiredHealthy: 2, + ExpectedPods: 1, + Conditions: []metav1.Condition{ + { + Type: policyv1.DisruptionAllowedCondition, + Status: metav1.ConditionTrue, + Reason: policyv1.SufficientPodsReason, + ObservedGeneration: 2, + }, + }, + } + unavailablePDBInsufficientPods := pdbTemplate.DeepCopy() + unavailablePDBInsufficientPods.Status = policyv1.PodDisruptionBudgetStatus{ + DisruptionsAllowed: 0, + CurrentHealthy: 1, + ObservedGeneration: 2, + DesiredHealthy: 2, + ExpectedPods: 1, + Conditions: []metav1.Condition{ + { + Type: policyv1.DisruptionAllowedCondition, + Status: metav1.ConditionTrue, + Reason: policyv1.SufficientPodsReason, + ObservedGeneration: 2, + }, + }, + } + + unavailablePDBStaleCondition := pdbTemplate.DeepCopy() + unavailablePDBStaleCondition.Status = policyv1.PodDisruptionBudgetStatus{ + DisruptionsAllowed: 1, + CurrentHealthy: 2, + ObservedGeneration: 1, + DesiredHealthy: 2, + ExpectedPods: 1, + Conditions: []metav1.Condition{ + { + Type: policyv1.DisruptionAllowedCondition, + Status: metav1.ConditionTrue, + Reason: policyv1.SufficientPodsReason, + ObservedGeneration: 1, + }, + }, + } + + testCases := []struct { + name string + pdb *policyv1.PodDisruptionBudget + wantManifestProcessingAvailabilityResultType ManifestProcessingAvailabilityResultType + }{ + { + name: "available PDB", + pdb: availablePDB, + wantManifestProcessingAvailabilityResultType: ManifestProcessingAvailabilityResultTypeAvailable, + }, + { + name: "unavailable PDB (insufficient pods)", + pdb: unavailablePDBInsufficientPods, + wantManifestProcessingAvailabilityResultType: ManifestProcessingAvailabilityResultTypeNotYetAvailable, + }, + { + name: "unavailable PDB (stale condition)", + pdb: unavailablePDBStaleCondition, + wantManifestProcessingAvailabilityResultType: ManifestProcessingAvailabilityResultTypeNotYetAvailable, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotResTyp, err := trackPDBAvailability(toUnstructured(t, tc.pdb)) + if err != nil { + t.Fatalf("trackPDBAvailability() = %v, want no error", err) + } + if gotResTyp != tc.wantManifestProcessingAvailabilityResultType { + t.Errorf("manifestProcessingAvailabilityResultType = %v, want %v", gotResTyp, tc.wantManifestProcessingAvailabilityResultType) + } + }) + } +} + // TestTrackInMemberClusterObjAvailabilityByGVR tests the trackInMemberClusterObjAvailabilityByGVR function. func TestTrackInMemberClusterObjAvailabilityByGVR(t *testing.T) { availableDeploy := deploy.DeepCopy() diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 85bb5ca1f..923f35703 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -17,6 +17,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" + policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -263,6 +264,12 @@ var ( Kind: "Pod", } + PodDisruptionBudgetGVR = schema.GroupVersionResource{ + Group: policyv1.GroupName, + Version: policyv1.SchemeGroupVersion.Version, + Resource: "poddisruptionbudgets", + } + RoleMetaGVK = metav1.GroupVersionKind{ Group: rbacv1.SchemeGroupVersion.Group, Version: rbacv1.SchemeGroupVersion.Version,