diff --git a/apis/placement/v1alpha1/eviction_types.go b/apis/placement/v1alpha1/eviction_types.go index d91769d55..1525223a4 100644 --- a/apis/placement/v1alpha1/eviction_types.go +++ b/apis/placement/v1alpha1/eviction_types.go @@ -6,6 +6,7 @@ Licensed under the MIT license. package v1alpha1 import ( + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -118,6 +119,18 @@ type ClusterResourcePlacementEvictionList struct { Items []ClusterResourcePlacementEviction `json:"items"` } +// SetConditions set the given conditions on the ClusterResourcePlacementEviction. +func (e *ClusterResourcePlacementEviction) SetConditions(conditions ...metav1.Condition) { + for _, c := range conditions { + meta.SetStatusCondition(&e.Status.Conditions, c) + } +} + +// GetCondition returns the condition of the given ClusterResourcePlacementEviction. +func (e *ClusterResourcePlacementEviction) GetCondition(conditionType string) *metav1.Condition { + return meta.FindStatusCondition(e.Status.Conditions, conditionType) +} + func init() { SchemeBuilder.Register( &ClusterResourcePlacementEviction{}, diff --git a/pkg/controllers/clusterresourceplacementeviction/controller.go b/pkg/controllers/clusterresourceplacementeviction/controller.go new file mode 100644 index 000000000..9c6d36216 --- /dev/null +++ b/pkg/controllers/clusterresourceplacementeviction/controller.go @@ -0,0 +1,243 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package clusterresourceplacementeviction + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/klog/v2" + runtime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrl "sigs.k8s.io/controller-runtime/pkg/controller" + + placementv1alpha1 "go.goms.io/fleet/apis/placement/v1alpha1" + placementv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" + "go.goms.io/fleet/pkg/utils/condition" + "go.goms.io/fleet/pkg/utils/controller" +) + +const ( + reasonClusterResourcePlacementEvictionValid = "ClusterResourcePlacementEvictionValid" + reasonClusterResourcePlacementEvictionInvalid = "ClusterResourcePlacementEvictionInvalid" + reasonClusterResourcePlacementEvictionExecuted = "ClusterResourcePlacementEvictionExecuted" + reasonClusterResourcePlacementEvictionNotExecuted = "ClusterResourcePlacementEvictionNotExecuted" + + evictionInvalidMissingCRP = "Failed to find cluster resource placement targeted by eviction" + evictionInvalidMissingCRB = "Failed to find cluster resource binding for cluster targeted by eviction" + evictionValid = "Eviction is valid" + evictionAllowedNoPDB = "Eviction Allowed, no ClusterResourcePlacementDisruptionBudget specified" + + evictionAllowedPDBSpecified = "Eviction is allowed by specified ClusterResourcePlacementDisruptionBudget, disruptionsAllowed: %d, availableBindings: %d, desiredBindings: %d, totalBindings: %d" + evictionBlockedPDBSpecified = "Eviction is blocked by specified ClusterResourcePlacementDisruptionBudget, disruptionsAllowed: %d, availableBindings: %d, desiredBindings: %d, totalBindings: %d" +) + +// Reconciler reconciles a ClusterResourcePlacementEviction object. +type Reconciler struct { + client.Client +} + +// Reconcile triggers a single eviction reconcile round. +func (r *Reconciler) Reconcile(ctx context.Context, req runtime.Request) (runtime.Result, error) { + startTime := time.Now() + evictionName := req.NamespacedName.Name + klog.V(2).InfoS("ClusterResourcePlacementEviction reconciliation starts", "clusterResourcePlacementEviction", evictionName) + defer func() { + latency := time.Since(startTime).Milliseconds() + klog.V(2).InfoS("ClusterResourcePlacementEviction reconciliation ends", "clusterResourcePlacementEviction", evictionName, "latency", latency) + }() + + var eviction placementv1alpha1.ClusterResourcePlacementEviction + if err := r.Client.Get(ctx, req.NamespacedName, &eviction); err != nil { + klog.ErrorS(err, "Failed to get cluster resource placement eviction", "clusterResourcePlacementEviction", evictionName) + return runtime.Result{}, client.IgnoreNotFound(err) + } + + validCondition := eviction.GetCondition(string(placementv1alpha1.PlacementEvictionConditionTypeValid)) + if condition.IsConditionStatusFalse(validCondition, eviction.GetGeneration()) { + klog.V(2).InfoS("Invalid eviction, no need to reconcile", "clusterResourcePlacementEviction", evictionName) + return runtime.Result{}, nil + } + + executedCondition := eviction.GetCondition(string(placementv1alpha1.PlacementEvictionConditionTypeExecuted)) + if executedCondition != nil { + klog.V(2).InfoS("Eviction has executed condition specified, no need to reconcile", "clusterResourcePlacementEviction", evictionName) + return runtime.Result{}, nil + } + + isCRPPresent := true + var crp placementv1beta1.ClusterResourcePlacement + if err := r.Client.Get(ctx, types.NamespacedName{Name: eviction.Spec.PlacementName}, &crp); err != nil { + if !errors.IsNotFound(err) { + return runtime.Result{}, err + } + isCRPPresent = false + } + if !isCRPPresent { + klog.V(2).InfoS(evictionInvalidMissingCRP, "clusterResourcePlacementEviction", evictionName, "clusterResourcePlacement", eviction.Spec.PlacementName) + markEvictionInvalid(&eviction, evictionInvalidMissingCRP) + return runtime.Result{}, r.updateEvictionStatus(ctx, &eviction) + } + + var crbList placementv1beta1.ClusterResourceBindingList + if err := r.Client.List(ctx, &crbList, client.MatchingLabels{placementv1beta1.CRPTrackingLabel: crp.Name}); err != nil { + return runtime.Result{}, err + } + + var evictionTargetBinding *placementv1beta1.ClusterResourceBinding + for i := range crbList.Items { + if crbList.Items[i].Spec.TargetCluster == eviction.Spec.ClusterName { + evictionTargetBinding = &crbList.Items[i] + } + } + if evictionTargetBinding == nil { + klog.V(2).InfoS(evictionInvalidMissingCRB, "clusterResourcePlacementEviction", evictionName, "clusterName", eviction.Spec.ClusterName) + markEvictionInvalid(&eviction, evictionInvalidMissingCRB) + return runtime.Result{}, r.updateEvictionStatus(ctx, &eviction) + } + + markEvictionValid(&eviction) + isDBPresent := true + var db placementv1alpha1.ClusterResourcePlacementDisruptionBudget + if err := r.Client.Get(ctx, types.NamespacedName{Name: crp.Name}, &db); err != nil { + if !errors.IsNotFound(err) { + return runtime.Result{}, err + } + isDBPresent = false + } + + if !isDBPresent { + if err := r.deleteClusterResourceBinding(ctx, evictionTargetBinding); err != nil { + return runtime.Result{}, err + } + markEvictionExecuted(&eviction, evictionAllowedNoPDB) + return runtime.Result{}, r.updateEvictionStatus(ctx, &eviction) + } + + var desiredBindings int + switch crp.Spec.Policy.PlacementType { + case placementv1beta1.PickAllPlacementType: + desiredBindings = len(crbList.Items) + case placementv1beta1.PickNPlacementType: + desiredBindings = int(*crp.Spec.Policy.NumberOfClusters) + case placementv1beta1.PickFixedPlacementType: + desiredBindings = len(crp.Spec.Policy.ClusterNames) + } + + totalBindings := len(crbList.Items) + allowed, disruptionsAllowed, availableBindings := isEvictionAllowed(desiredBindings, crbList.Items, db) + if allowed { + if err := r.deleteClusterResourceBinding(ctx, evictionTargetBinding); err != nil { + return runtime.Result{}, err + } + markEvictionExecuted(&eviction, fmt.Sprintf(evictionAllowedPDBSpecified, disruptionsAllowed, availableBindings, desiredBindings, totalBindings)) + } else { + markEvictionNotExecuted(&eviction, fmt.Sprintf(evictionBlockedPDBSpecified, disruptionsAllowed, availableBindings, desiredBindings, totalBindings)) + } + + return runtime.Result{}, r.updateEvictionStatus(ctx, &eviction) +} + +func (r *Reconciler) updateEvictionStatus(ctx context.Context, eviction *placementv1alpha1.ClusterResourcePlacementEviction) error { + evictionRef := klog.KObj(eviction) + if err := r.Client.Status().Update(ctx, eviction); err != nil { + klog.ErrorS(err, "Failed to update eviction status", "clusterResourcePlacementEviction", evictionRef) + return controller.NewUpdateIgnoreConflictError(err) + } + klog.V(2).InfoS("Updated the status of a eviction", "clusterResourcePlacementEviction", evictionRef) + return nil +} + +func (r *Reconciler) deleteClusterResourceBinding(ctx context.Context, binding *placementv1beta1.ClusterResourceBinding) error { + bindingRef := klog.KObj(binding) + if err := r.Client.Delete(ctx, binding); err != nil { + klog.ErrorS(err, "Failed to delete cluster resource binding", "clusterResourceBinding", bindingRef) + return controller.NewDeleteIgnoreNotFoundError(err) + } + klog.V(2).InfoS("Issued delete on cluster resource binding, eviction succeeded", "clusterResourceBinding", bindingRef) + return nil +} + +func isEvictionAllowed(desiredBindings int, bindings []placementv1beta1.ClusterResourceBinding, db placementv1alpha1.ClusterResourcePlacementDisruptionBudget) (bool, int, int) { + availableBindings := 0 + for i := range bindings { + availableCondition := bindings[i].GetCondition(string(placementv1beta1.ResourceBindingAvailable)) + if condition.IsConditionStatusTrue(availableCondition, bindings[i].GetGeneration()) { + availableBindings++ + } + } + var disruptionsAllowed int + if db.Spec.MaxUnavailable != nil { + maxUnavailable, _ := intstr.GetScaledValueFromIntOrPercent(db.Spec.MaxUnavailable, desiredBindings, true) + unavailableBindings := len(bindings) - availableBindings + disruptionsAllowed = maxUnavailable - unavailableBindings + } + if db.Spec.MinAvailable != nil { + minAvailable, _ := intstr.GetScaledValueFromIntOrPercent(db.Spec.MinAvailable, desiredBindings, true) + disruptionsAllowed = availableBindings - minAvailable + } + if disruptionsAllowed < 0 { + disruptionsAllowed = 0 + } + return disruptionsAllowed > 0, disruptionsAllowed, availableBindings +} + +func markEvictionValid(eviction *placementv1alpha1.ClusterResourcePlacementEviction) { + cond := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeValid), + Status: metav1.ConditionTrue, + ObservedGeneration: eviction.Generation, + Reason: reasonClusterResourcePlacementEvictionValid, + Message: evictionValid, + } + eviction.SetConditions(cond) +} + +func markEvictionInvalid(eviction *placementv1alpha1.ClusterResourcePlacementEviction, message string) { + cond := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeValid), + Status: metav1.ConditionFalse, + ObservedGeneration: eviction.Generation, + Reason: reasonClusterResourcePlacementEvictionInvalid, + Message: message, + } + eviction.SetConditions(cond) +} + +func markEvictionExecuted(eviction *placementv1alpha1.ClusterResourcePlacementEviction, message string) { + cond := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeExecuted), + Status: metav1.ConditionTrue, + ObservedGeneration: eviction.Generation, + Reason: reasonClusterResourcePlacementEvictionExecuted, + Message: message, + } + eviction.SetConditions(cond) +} + +func markEvictionNotExecuted(eviction *placementv1alpha1.ClusterResourcePlacementEviction, message string) { + cond := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeExecuted), + Status: metav1.ConditionFalse, + ObservedGeneration: eviction.Generation, + Reason: reasonClusterResourcePlacementEvictionNotExecuted, + Message: message, + } + eviction.SetConditions(cond) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr runtime.Manager) error { + return runtime.NewControllerManagedBy(mgr). + WithOptions(ctrl.Options{MaxConcurrentReconciles: 1}). // set the max number of concurrent reconciles + For(&placementv1alpha1.ClusterResourcePlacementEviction{}). + Complete(r) +} diff --git a/pkg/controllers/clusterresourceplacementeviction/controller_intergration_test.go b/pkg/controllers/clusterresourceplacementeviction/controller_intergration_test.go new file mode 100644 index 000000000..aeef39186 --- /dev/null +++ b/pkg/controllers/clusterresourceplacementeviction/controller_intergration_test.go @@ -0,0 +1,714 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package clusterresourceplacementeviction + +import ( + "fmt" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + placementv1alpha1 "go.goms.io/fleet/apis/placement/v1alpha1" + placementv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" +) + +const ( + crbNameTemplate = "crb-%d" + crpNameTemplate = "crp-%d" + evictionNameTemplate = "eviction-%d" +) + +var ( + lessFuncCondition = func(a, b metav1.Condition) bool { + return a.Type < b.Type + } + + evictionStatusCmpOptions = cmp.Options{ + cmpopts.SortSlices(lessFuncCondition), + cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"), + cmpopts.EquateEmpty(), + } +) + +const ( + eventuallyDuration = time.Minute * 2 + eventuallyInterval = time.Millisecond * 250 + consistentlyDuration = time.Second * 15 + consistentlyInterval = time.Millisecond * 500 +) + +var _ = Describe("Test ClusterResourcePlacementEviction Controller", Ordered, func() { + Context("Invalid Eviction - ClusterResourcePlacement not found", func() { + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: false, msg: evictionInvalidMissingCRP}, nil) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Clean up resources", func() { + // Delete eviction. + ensureEvictionRemoved(evictionName) + }) + }) + + Context("Invalid Eviction - ClusterResourceBinding not found", func() { + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + It("Create ClusterResourcePlacement", func() { + crp := buildTestCRP(crpName) + Expect(k8sClient.Create(ctx, &crp)).Should(Succeed()) + // ensure CRP exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crp.Name}, &crp) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: false, msg: evictionInvalidMissingCRB}, nil) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Clean up resources", func() { + ensureEvictionRemoved(evictionName) + ensureCRPRemoved(crpName) + }) + }) + + Context("Eviction Allowed - ClusterResourcePlacementDisruptionBudget is not present", func() { + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + crbName := fmt.Sprintf(crbNameTemplate, GinkgoParallelProcess()) + + It("Create ClusterResourcePlacement", func() { + // Create CRP. + crp := buildTestCRP(crpName) + Expect(k8sClient.Create(ctx, &crp)).Should(Succeed()) + // ensure CRP exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crp.Name}, &crp) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourceBinding", func() { + // Create CRB. + crb := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: crbName, + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: crpName}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateScheduled, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster", + }, + } + Expect(k8sClient.Create(ctx, &crb)).Should(Succeed()) + // ensure CRB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crb.Name}, &crb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: true, msg: evictionValid}, &isExecutedEviction{bool: true, msg: evictionAllowedNoPDB}) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Ensure eviction was successful", func() { + var crb placementv1beta1.ClusterResourceBinding + // Ensure CRB was deleted. + Eventually(func() bool { + return k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, eventuallyDuration, eventuallyInterval).Should(BeTrue()) + Consistently(func() bool { + return k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, consistentlyDuration, consistentlyInterval).Should(BeTrue()) + }) + + It("Clean up resources", func() { + ensureEvictionRemoved(evictionName) + ensureCRPRemoved(crpName) + }) + }) + + Context("Eviction Blocked - ClusterResourcePlacementDisruptionBudget's maxUnavailable blocks eviction", func() { + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + crbName := fmt.Sprintf(crbNameTemplate, GinkgoParallelProcess()) + + It("Create ClusterResourcePlacement", func() { + // Create CRP. + crp := buildTestCRP(crpName) + Expect(k8sClient.Create(ctx, &crp)).Should(Succeed()) + // ensure CRP exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crp.Name}, &crp) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourceBinding", func() { + // Create CRB. + crb := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: crbName, + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: crpName}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateScheduled, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster", + }, + } + Expect(k8sClient.Create(ctx, &crb)).Should(Succeed()) + // ensure CRB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crb.Name}, &crb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementDisruptionBudget", func() { + crpdb := placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: crpName, + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + } + Expect(k8sClient.Create(ctx, &crpdb)).Should(Succeed()) + // ensure CRPDB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crpdb.Name}, &crpdb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: true, msg: evictionValid}, &isExecutedEviction{bool: false, msg: fmt.Sprintf(evictionBlockedPDBSpecified, 0, 0, 1, 1)}) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Ensure eviction was not successful", func() { + var crb placementv1beta1.ClusterResourceBinding + // check to see CRB was not deleted. + Consistently(func() bool { + return !k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, consistentlyDuration, consistentlyInterval).Should(BeTrue()) + }) + + It("Clean up resources", func() { + ensureEvictionRemoved(evictionName) + ensureCRPDBRemoved(crpName) + ensureCRBRemoved(crbName) + ensureCRPRemoved(crpName) + }) + }) + + Context("Eviction Allowed - ClusterResourcePlacementDisruptionBudget's maxUnavailable allows eviction", func() { + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + crbName := fmt.Sprintf(crbNameTemplate, GinkgoParallelProcess()) + + It("Create ClusterResourcePlacement", func() { + // Create CRP. + crp := buildTestCRP(crpName) + Expect(k8sClient.Create(ctx, &crp)).Should(Succeed()) + // ensure CRP exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crp.Name}, &crp) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourceBinding and update status with available condition", func() { + // Create CRB. + crb := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: crbName, + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: crpName}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateBound, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster", + }, + } + Expect(k8sClient.Create(ctx, &crb)).Should(Succeed()) + // ensure CRB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crb.Name}, &crb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + + // Update CRB status to have available condition. + // Ideally binding would contain more condition before available, but for the sake testing we only specify available condition. + availableCondition := metav1.Condition{ + Type: string(placementv1beta1.ResourceBindingAvailable), + Status: metav1.ConditionTrue, + Reason: "available", + ObservedGeneration: crb.GetGeneration(), + } + crb.SetConditions(availableCondition) + Expect(k8sClient.Status().Update(ctx, &crb)).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementDisruptionBudget", func() { + crpdb := placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: crpName, + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + } + Expect(k8sClient.Create(ctx, &crpdb)).Should(Succeed()) + // ensure CRPDB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crpdb.Name}, &crpdb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: true, msg: evictionValid}, &isExecutedEviction{bool: true, msg: fmt.Sprintf(evictionAllowedPDBSpecified, 1, 1, 1, 1)}) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Ensure eviction was successful", func() { + var crb placementv1beta1.ClusterResourceBinding + // Ensure CRB was deleted. + Eventually(func() bool { + return k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, eventuallyDuration, eventuallyInterval).Should(BeTrue()) + Consistently(func() bool { + return k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, consistentlyDuration, consistentlyInterval).Should(BeTrue()) + }) + + It("Clean up resources", func() { + ensureEvictionRemoved(evictionName) + ensureCRPDBRemoved(crpName) + ensureCRPRemoved(crpName) + }) + }) + + Context("Eviction Blocked - ClusterResourcePlacementDisruptionBudget's minAvailable blocks eviction", func() { + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + crbName := fmt.Sprintf(crbNameTemplate, GinkgoParallelProcess()) + + It("Create ClusterResourcePlacement", func() { + // Create CRP. + crp := buildTestCRP(crpName) + Expect(k8sClient.Create(ctx, &crp)).Should(Succeed()) + // ensure CRP exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crp.Name}, &crp) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourceBinding", func() { + // Create CRB. + crb := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: crbName, + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: crpName}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateScheduled, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster", + }, + } + Expect(k8sClient.Create(ctx, &crb)).Should(Succeed()) + // ensure CRB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crb.Name}, &crb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementDisruptionBudget", func() { + crpdb := placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: crpName, + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + } + Expect(k8sClient.Create(ctx, &crpdb)).Should(Succeed()) + // ensure CRPDB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crpdb.Name}, &crpdb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: true, msg: evictionValid}, &isExecutedEviction{bool: false, msg: fmt.Sprintf(evictionBlockedPDBSpecified, 0, 0, 1, 1)}) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Ensure eviction was not successful", func() { + var crb placementv1beta1.ClusterResourceBinding + // check to see CRB was not deleted. + Consistently(func() bool { + return !k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, consistentlyDuration, consistentlyInterval).Should(BeTrue()) + }) + + It("Clean up resources", func() { + ensureEvictionRemoved(evictionName) + ensureCRPDBRemoved(crpName) + ensureCRBRemoved(crbName) + ensureCRPRemoved(crpName) + }) + }) + + Context("Eviction Allowed - ClusterResourcePlacementDisruptionBudget's minUnavailable allows eviction", func() { + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + crpName := fmt.Sprintf(crpNameTemplate, GinkgoParallelProcess()) + crbName := fmt.Sprintf(crbNameTemplate, GinkgoParallelProcess()) + anotherCRBName := fmt.Sprintf("another-crb-%d", GinkgoParallelProcess()) + + It("Create ClusterResourcePlacement", func() { + // Create CRP. + crp := buildTestCRP(crpName) + Expect(k8sClient.Create(ctx, &crp)).Should(Succeed()) + // ensure CRP exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crp.Name}, &crp) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create two ClusterResourceBindings with available condition specified", func() { + // Create CRB. + crb := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: crbName, + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: crpName}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateBound, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster", + }, + } + Expect(k8sClient.Create(ctx, &crb)).Should(Succeed()) + // ensure CRB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crb.Name}, &crb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + + // Create another CRB. + anotherCRB := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: anotherCRBName, + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: crpName}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateScheduled, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "another-test-cluster", + }, + } + Expect(k8sClient.Create(ctx, &anotherCRB)).Should(Succeed()) + // ensure CRB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crb.Name}, &crb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + + // Update CRB status to have available condition. + // Ideally binding would contain more condition before available, but for the sake testing we only specify available condition. + availableCondition := metav1.Condition{ + Type: string(placementv1beta1.ResourceBindingAvailable), + Status: metav1.ConditionTrue, + Reason: "available", + ObservedGeneration: crb.GetGeneration(), + } + crb.SetConditions(availableCondition) + Expect(k8sClient.Status().Update(ctx, &crb)).Should(Succeed()) + availableCondition.ObservedGeneration = anotherCRB.GetGeneration() + anotherCRB.SetConditions(availableCondition) + Expect(k8sClient.Status().Update(ctx, &anotherCRB)).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementDisruptionBudget", func() { + crpdb := placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: crpName, + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + } + Expect(k8sClient.Create(ctx, &crpdb)).Should(Succeed()) + // ensure CRPDB exists. + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: crpdb.Name}, &crpdb) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Create ClusterResourcePlacementEviction", func() { + eviction := buildTestEviction(evictionName, crpName, "test-cluster") + Expect(k8sClient.Create(ctx, &eviction)).Should(Succeed()) + }) + + It("Check eviction status", func() { + evictionStatusUpdatedActual := evictionStatusUpdatedActual(&isValidEviction{bool: true, msg: evictionValid}, &isExecutedEviction{bool: true, msg: fmt.Sprintf(evictionAllowedPDBSpecified, 1, 2, 2, 2)}) + Eventually(evictionStatusUpdatedActual, eventuallyDuration, eventuallyInterval).Should(Succeed()) + }) + + It("Ensure eviction was successful for one ClusterResourceBinding", func() { + var crb placementv1beta1.ClusterResourceBinding + Eventually(func() bool { + return k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, eventuallyDuration, eventuallyInterval).Should(BeTrue()) + Consistently(func() bool { + return k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbName}, &crb)) + }, consistentlyDuration, consistentlyInterval).Should(BeTrue()) + }) + + It("Ensure other ClusterResourceBinding was not delete", func() { + var anotherCRB placementv1beta1.ClusterResourceBinding + // Ensure another CRB was not deleted. + Consistently(func() bool { + return !k8serrors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: anotherCRBName}, &anotherCRB)) + }, consistentlyDuration, consistentlyInterval).Should(BeTrue()) + }) + + It("Clean up resources", func() { + // Clean up resources. + ensureEvictionRemoved(evictionName) + ensureCRPDBRemoved(crpName) + ensureCRBRemoved(anotherCRBName) + ensureCRPRemoved(crpName) + }) + }) +}) + +func evictionStatusUpdatedActual(isValid *isValidEviction, isExecuted *isExecutedEviction) func() error { + evictionName := fmt.Sprintf(evictionNameTemplate, GinkgoParallelProcess()) + return func() error { + var eviction placementv1alpha1.ClusterResourcePlacementEviction + if err := k8sClient.Get(ctx, types.NamespacedName{Name: evictionName}, &eviction); err != nil { + return err + } + var conditions []metav1.Condition + if isValid != nil { + if isValid.bool { + validCondition := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeValid), + Status: metav1.ConditionTrue, + ObservedGeneration: eviction.GetGeneration(), + Reason: reasonClusterResourcePlacementEvictionValid, + Message: isValid.msg, + } + conditions = append(conditions, validCondition) + } else { + invalidCondition := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeValid), + Status: metav1.ConditionFalse, + ObservedGeneration: eviction.GetGeneration(), + Reason: reasonClusterResourcePlacementEvictionInvalid, + Message: isValid.msg, + } + conditions = append(conditions, invalidCondition) + } + } + if isExecuted != nil { + if isExecuted.bool { + executedCondition := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeExecuted), + Status: metav1.ConditionTrue, + ObservedGeneration: eviction.GetGeneration(), + Reason: reasonClusterResourcePlacementEvictionExecuted, + Message: isExecuted.msg, + } + conditions = append(conditions, executedCondition) + } else { + notExecutedCondition := metav1.Condition{ + Type: string(placementv1alpha1.PlacementEvictionConditionTypeExecuted), + Status: metav1.ConditionFalse, + ObservedGeneration: eviction.GetGeneration(), + Reason: reasonClusterResourcePlacementEvictionNotExecuted, + Message: isExecuted.msg, + } + conditions = append(conditions, notExecutedCondition) + } + } + wantStatus := placementv1alpha1.PlacementEvictionStatus{ + Conditions: conditions, + } + if diff := cmp.Diff(eviction.Status, wantStatus, evictionStatusCmpOptions...); diff != "" { + return fmt.Errorf("CRP status diff (-got, +want): %s", diff) + } + return nil + } +} + +func buildTestCRP(crpName string) placementv1beta1.ClusterResourcePlacement { + return placementv1beta1.ClusterResourcePlacement{ + ObjectMeta: metav1.ObjectMeta{ + Name: crpName, + }, + Spec: placementv1beta1.ClusterResourcePlacementSpec{ + Policy: &placementv1beta1.PlacementPolicy{ + PlacementType: placementv1beta1.PickAllPlacementType, + }, + ResourceSelectors: []placementv1beta1.ClusterResourceSelector{ + { + Group: "", + Kind: "Namespace", + Version: "v1", + Name: "test-ns", + }, + }, + }, + } +} + +func buildTestEviction(evictionName, placementName, clusterName string) placementv1alpha1.ClusterResourcePlacementEviction { + return placementv1alpha1.ClusterResourcePlacementEviction{ + ObjectMeta: metav1.ObjectMeta{ + Name: evictionName, + }, + Spec: placementv1alpha1.PlacementEvictionSpec{ + PlacementName: placementName, + ClusterName: clusterName, + }, + } +} + +func ensureEvictionRemoved(name string) { + // Delete eviction. + eviction := placementv1alpha1.ClusterResourcePlacementEviction{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + Expect(k8sClient.Delete(ctx, &eviction)).Should(Succeed()) + // Ensure eviction doesn't exist. + Eventually(func() error { + if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, &eviction); !k8serrors.IsNotFound(err) { + return fmt.Errorf("eviction still exists or an unexpected error occurred: %w", err) + } + return nil + }, eventuallyDuration, eventuallyInterval) +} + +func ensureCRPRemoved(name string) { + // Delete CRP. + crp := placementv1beta1.ClusterResourcePlacement{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + Expect(k8sClient.Delete(ctx, &crp)).Should(Succeed()) + // Ensure CRP doesn't exist. + Eventually(func() error { + if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, &crp); !k8serrors.IsNotFound(err) { + return fmt.Errorf("CRP still exists or an unexpected error occurred: %w", err) + } + return nil + }, eventuallyDuration, eventuallyInterval) +} + +func ensureCRPDBRemoved(name string) { + // Delete CRPDB. + crpdb := placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + Expect(k8sClient.Delete(ctx, &crpdb)).Should(Succeed()) + // Ensure CRPDB doesn't exist. + Eventually(func() error { + if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, &crpdb); !k8serrors.IsNotFound(err) { + return fmt.Errorf("CRPDB still exists or an unexpected error occurred: %w", err) + } + return nil + }, eventuallyDuration, eventuallyInterval) +} + +func ensureCRBRemoved(name string) { + // Delete CRB. + crb := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + Expect(k8sClient.Delete(ctx, &crb)).Should(Succeed()) + // Ensure CRB doesn't exist. + Eventually(func() error { + if err := k8sClient.Get(ctx, types.NamespacedName{Name: name}, &crb); !k8serrors.IsNotFound(err) { + return fmt.Errorf("CRB still exists or an unexpected error occurred: %w", err) + } + return nil + }, eventuallyDuration, eventuallyInterval) +} + +type isValidEviction struct { + bool + msg string +} + +type isExecutedEviction struct { + bool + msg string +} diff --git a/pkg/controllers/clusterresourceplacementeviction/controller_test.go b/pkg/controllers/clusterresourceplacementeviction/controller_test.go new file mode 100644 index 000000000..a20c3c18f --- /dev/null +++ b/pkg/controllers/clusterresourceplacementeviction/controller_test.go @@ -0,0 +1,783 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package clusterresourceplacementeviction + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + placementv1alpha1 "go.goms.io/fleet/apis/placement/v1alpha1" + placementv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" +) + +func TestGetOrCreateClusterSchedulingPolicySnapshot(t *testing.T) { + availableCondition := metav1.Condition{ + Type: string(placementv1beta1.ResourceBindingAvailable), + Status: metav1.ConditionTrue, + Reason: "available", + ObservedGeneration: 0, + } + scheduledUnavailableBinding := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scheduled-binding", + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: "test-crp"}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateScheduled, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster-1", + }, + } + boundAvailableBinding := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bound-available-binding", + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: "test-crp"}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateBound, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster-2", + }, + Status: placementv1beta1.ResourceBindingStatus{ + Conditions: []metav1.Condition{availableCondition}, + }, + } + anotherBoundAvailableBinding := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "another-bound-available-binding", + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: "test-crp"}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateBound, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster-3", + }, + Status: placementv1beta1.ResourceBindingStatus{ + Conditions: []metav1.Condition{availableCondition}, + }, + } + boundUnavailableBinding := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bound-unavailable-binding", + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: "test-crp"}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateBound, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster-4", + }, + } + unScheduledAvailableBinding := placementv1beta1.ClusterResourceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unscheduled-available-binding", + Labels: map[string]string{placementv1beta1.CRPTrackingLabel: "test-crp"}, + }, + Spec: placementv1beta1.ResourceBindingSpec{ + State: placementv1beta1.BindingStateBound, + ResourceSnapshotName: "test-resource-snapshot", + SchedulingPolicySnapshotName: "test-scheduling-policy-snapshot", + TargetCluster: "test-cluster-5", + }, + Status: placementv1beta1.ResourceBindingStatus{ + Conditions: []metav1.Condition{availableCondition}, + }, + } + tests := []struct { + name string + targetNumber int + bindings []placementv1beta1.ClusterResourceBinding + disruptionBudget placementv1alpha1.ClusterResourcePlacementDisruptionBudget + wantAllowed bool + wantDisruptionsAllowed int + wantAvailableBindings int + }{ + { + name: "MaxUnavailable specified as Integer zero, one available binding - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 0, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 1, + }, + { + name: "MaxUnavailable specified as Integer zero, one unavailable bindings - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 0, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MaxUnavailable specified as Integer one, one unavailable binding - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MaxUnavailable specified as Integer one, one available binding - allow eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 1, + }, + { + name: "MaxUnavailable specified as Integer one, one available, one unavailable binding - block eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, boundUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 1, + }, + { + name: "MaxUnavailable specified as Integer one, two available binding - allow eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as Integer greater than one - block eviction", + targetNumber: 4, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, anotherBoundAvailableBinding, boundUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 2, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as Integer greater than one - allow eviction", + targetNumber: 3, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 2, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as Integer large number greater than target number - allows eviction", + targetNumber: 4, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, boundUnavailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 10, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 8, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as percentage zero - block eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "0%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as percentage greater than zero, rounds up to 1 - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "10%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MaxUnavailable specified as percentage greater than zero, rounds up to 1 - allow eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "10%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 1, + }, + { + name: "MaxUnavailable specified as percentage greater than zero, rounds up to greater than 1 - block eviction", + targetNumber: 4, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, boundUnavailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ // equates to 2. + Type: intstr.String, + StrVal: "40%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as percentage greater than zero, rounds up to greater than 1 - allow eviction", + targetNumber: 3, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ // equates to 2. + Type: intstr.String, + StrVal: "50%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 2, + }, + { + name: "MaxUnavailable specified as percentage hundred, target number greater than bindings - allow eviction", + targetNumber: 10, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, boundUnavailableBinding, anotherBoundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ // equates to 10. + Type: intstr.String, + StrVal: "100%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 8, + wantAvailableBindings: 3, + }, + { + name: "MaxUnavailable specified as percentage hundred, target number equal to bindings - block eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ // equates to 2. + Type: intstr.String, + StrVal: "100%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MaxUnavailable specified as percentage hundred, target number equal to bindings - allow eviction", + targetNumber: 4, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, boundUnavailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ // equates to 4. + Type: intstr.String, + StrVal: "100%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 2, + wantAvailableBindings: 2, + }, + { + name: "MinAvailable specified as Integer zero, unavailable binding - block eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 0, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MinAvailable specified as Integer zero, available binding - allow eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 0, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 1, + }, + { + name: "MinAvailable specified as Integer one, unavailable binding - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MinAvailable specified as Integer one, available binding - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 1, + }, + { + name: "MinAvailable specified as Integer one, one available, one unavailable binding - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, boundUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 1, + }, + { + name: "MinAvailable specified as Integer one, two available bindings - allow eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 2, + }, + { + name: "MinAvailable specified as Integer greater than one - block eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 2, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 2, + }, + { + name: "MinAvailable specified as Integer greater than one - allow eviction", + targetNumber: 4, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, anotherBoundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 2, + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 3, + }, + { + name: "MinAvailable specified as Integer large number greater than target number - blocks eviction", + targetNumber: 5, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, anotherBoundAvailableBinding, boundUnavailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 10, + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 3, + }, + { + name: "MinAvailable specified as percentage zero, all bindings are unavailable - block eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "0%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MinAvailable specified as percentage zero, all bindings are available - allow eviction", + targetNumber: 3, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, anotherBoundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "0%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 3, + wantAvailableBindings: 3, + }, + { + name: "MinAvailable specified as percentage rounds upto one - block eviction", + targetNumber: 1, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "10%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 0, + }, + { + name: "MinAvailable specified as percentage rounds upto one - allow eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "10%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 2, + }, + { + name: "MinAvailable specified as percentage greater than zero, rounds up to greater than 1 - block eviction", + targetNumber: 3, + bindings: []placementv1beta1.ClusterResourceBinding{scheduledUnavailableBinding, boundAvailableBinding, anotherBoundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ // equates to 2. + Type: intstr.String, + StrVal: "40%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 2, + }, + { + name: "MinAvailable specified as percentage greater than zero, rounds up to greater than 1 - allow eviction", + targetNumber: 3, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, anotherBoundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ // equates to 2. + Type: intstr.String, + StrVal: "40%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 3, + }, + { + name: "MinAvailable specified as percentage hundred, bindings less than target number - block eviction", + targetNumber: 10, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, anotherBoundAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ // equates to 10. + Type: intstr.String, + StrVal: "100%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 2, + }, + { + name: "MinAvailable specified as percentage hundred, bindings equal to target number - block eviction", + targetNumber: 3, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, anotherBoundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ // equates to 3. + Type: intstr.String, + StrVal: "100%", + }, + }, + }, + wantAllowed: false, + wantDisruptionsAllowed: 0, + wantAvailableBindings: 3, + }, + { + name: "MinAvailable specified as percentage hundred, bindings greater than target number - allow eviction", + targetNumber: 2, + bindings: []placementv1beta1.ClusterResourceBinding{boundAvailableBinding, anotherBoundAvailableBinding, unScheduledAvailableBinding}, + disruptionBudget: placementv1alpha1.ClusterResourcePlacementDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-disruption-budget", + }, + Spec: placementv1alpha1.PlacementDisruptionBudgetSpec{ + MinAvailable: &intstr.IntOrString{ // equates to 2. + Type: intstr.String, + StrVal: "100%", + }, + }, + }, + wantAllowed: true, + wantDisruptionsAllowed: 1, + wantAvailableBindings: 3, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotAllowed, gotDisruptionsAllowed, gotAvailableBindings := isEvictionAllowed(tc.targetNumber, tc.bindings, tc.disruptionBudget) + if gotAllowed != tc.wantAllowed { + t.Errorf("isEvictionAllowed test `%s` failed gotAllowed: %v, wantAllowedAllowed: %v", tc.name, gotAllowed, tc.wantAllowed) + } + if gotDisruptionsAllowed != tc.wantDisruptionsAllowed { + t.Errorf("isEvictionAllowed test `%s` failed gotDisruptionsAllowed: %v, wantDisruptionsAllowed: %v", tc.name, gotDisruptionsAllowed, tc.wantDisruptionsAllowed) + } + if gotAvailableBindings != tc.wantAvailableBindings { + t.Errorf("isEvictionAllowed test `%s` failed gotAvailableBindings: %v, wantAvailableBindings: %v", tc.name, gotAvailableBindings, tc.wantAvailableBindings) + } + }) + } +} diff --git a/pkg/controllers/clusterresourceplacementeviction/suite_test.go b/pkg/controllers/clusterresourceplacementeviction/suite_test.go new file mode 100644 index 000000000..5ed603eae --- /dev/null +++ b/pkg/controllers/clusterresourceplacementeviction/suite_test.go @@ -0,0 +1,105 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package clusterresourceplacementeviction + +import ( + "context" + "flag" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + "k8s.io/klog/v2/textlogger" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + placementv1alpha1 "go.goms.io/fleet/apis/placement/v1alpha1" + placementv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" +) + +var ( + cfg *rest.Config + mgr manager.Manager + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "ClusterResourcePlacementEviction Controller Suite") +} + +var _ = BeforeSuite(func() { + klog.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("../../../", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).Should(Succeed()) + Expect(cfg).NotTo(BeNil()) + + err = placementv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = placementv1beta1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + By("construct the k8s client") + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).Should(Succeed()) + Expect(k8sClient).NotTo(BeNil()) + + By("starting the controller manager") + klog.InitFlags(flag.CommandLine) + flag.Parse() + + mgr, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + Logger: textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(4))), + }) + Expect(err).Should(Succeed()) + + err = (&Reconciler{ + Client: k8sClient, + }).SetupWithManager(mgr) + Expect(err).Should(Succeed()) + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).Should(Succeed(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + defer klog.Flush() + + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).Should(Succeed()) +}) diff --git a/test/apis/placement/v1alpha1/api_validation_integration_test.go b/test/apis/placement/v1alpha1/api_validation_integration_test.go index 7a5f17d10..e437827d5 100644 --- a/test/apis/placement/v1alpha1/api_validation_integration_test.go +++ b/test/apis/placement/v1alpha1/api_validation_integration_test.go @@ -1,3 +1,8 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + package v1alpha1 import ( diff --git a/test/apis/placement/v1alpha1/suite_test.go b/test/apis/placement/v1alpha1/suite_test.go index 45af00112..6b81b00d8 100644 --- a/test/apis/placement/v1alpha1/suite_test.go +++ b/test/apis/placement/v1alpha1/suite_test.go @@ -1,3 +1,8 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + package v1alpha1 import (