From 2d30c7fe8fe169799680e5afb5080e20db332e16 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Mon, 27 Jan 2025 11:42:43 +0800 Subject: [PATCH] Added new watcher logic --- .../v1beta1/zz_generated.deepcopy.go | 2 +- .../clusterresourcebindingwatcher/watcher.go | 15 +- .../watcher_integration_test.go | 351 +++++++++++ pkg/utils/common.go | 47 ++ pkg/utils/common_test.go | 569 ++++++++++++++++++ 5 files changed, 980 insertions(+), 4 deletions(-) diff --git a/apis/placement/v1beta1/zz_generated.deepcopy.go b/apis/placement/v1beta1/zz_generated.deepcopy.go index 4ab8c4a7d..315846360 100644 --- a/apis/placement/v1beta1/zz_generated.deepcopy.go +++ b/apis/placement/v1beta1/zz_generated.deepcopy.go @@ -10,7 +10,7 @@ Licensed under the MIT license. package v1beta1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ) diff --git a/pkg/controllers/clusterresourcebindingwatcher/watcher.go b/pkg/controllers/clusterresourcebindingwatcher/watcher.go index 0b4f2c90e..870123d3b 100644 --- a/pkg/controllers/clusterresourcebindingwatcher/watcher.go +++ b/pkg/controllers/clusterresourcebindingwatcher/watcher.go @@ -115,14 +115,23 @@ func isBindingStatusUpdated(oldBinding, newBinding *fleetv1beta1.ClusterResource oldCond := oldBinding.GetCondition(string(i.ResourceBindingConditionType())) newCond := newBinding.GetCondition(string(i.ResourceBindingConditionType())) if !condition.EqualCondition(oldCond, newCond) { - klog.V(2).InfoS("The binding condition has changed, need to update the corresponding CRP", "oldBinding", klog.KObj(oldBinding), "newBinding", klog.KObj(newBinding), "type", i.ResourceBindingConditionType()) + klog.V(2).InfoS("The binding status conditions have changed, need to refresh the CRP status", "binding", klog.KObj(oldBinding), "type", i.ResourceBindingConditionType()) return true } } if !utils.IsFailedResourcePlacementsEqual(oldBinding.Status.FailedPlacements, newBinding.Status.FailedPlacements) { - klog.V(2).InfoS("The binding failed placement has changed, need to update the corresponding CRP", "oldBinding", klog.KObj(oldBinding), "newBinding", klog.KObj(newBinding)) + klog.V(2).InfoS("Failed placements reported on the binding status has changed, need to refresh the CRP status", "binding", klog.KObj(oldBinding)) return true } - klog.V(5).InfoS("The binding status has not changed, no need to update the corresponding CRP", "oldBinding", klog.KObj(oldBinding), "newBinding", klog.KObj(newBinding)) + if !utils.IsDriftedResourcePlacementsEqual(oldBinding.Status.DriftedPlacements, newBinding.Status.DriftedPlacements) { + klog.V(2).InfoS("Drifted placements reported on the binding status has changed, need to refresh the CRP status", "binding", klog.KObj(oldBinding)) + return true + } + if !utils.IsDiffedResourcePlacementsEqual(oldBinding.Status.DiffedPlacements, newBinding.Status.DiffedPlacements) { + klog.V(2).InfoS("Diffed placements reported on the binding status has changed, need to refresh the CRP status", "binding", klog.KObj(oldBinding)) + return true + } + + klog.V(5).InfoS("The binding status has not changed, no need to refresh the CRP status", "binding", klog.KObj(oldBinding)) return false } diff --git a/pkg/controllers/clusterresourcebindingwatcher/watcher_integration_test.go b/pkg/controllers/clusterresourcebindingwatcher/watcher_integration_test.go index c470d724f..f87a2b15a 100644 --- a/pkg/controllers/clusterresourcebindingwatcher/watcher_integration_test.go +++ b/pkg/controllers/clusterresourcebindingwatcher/watcher_integration_test.go @@ -12,6 +12,7 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" fleetv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" ) @@ -258,6 +259,356 @@ var _ = Describe("Test ClusterResourceBinding Watcher - update status", Serial, fakePlacementController.ResetQueue() }) }) + + Context("Should enqueue the clusterResourcePlacement name for reconciling, when the drifted placement list has changed", Serial, Ordered, func() { + startTime := metav1.Now() + + It("Should enqueue the clusterResourcePlacement name for reconciling, when there are new drifted placements", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DriftedPlacements = []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Service", + Name: "svc-name", + Namespace: "svc-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: 1, + FirstDriftedObservedTime: startTime, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "bar", + ValueInMember: "baz", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + Name: "config-name", + Namespace: "config-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: 1, + FirstDriftedObservedTime: startTime, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "qux", + ValueInMember: "quux", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + + It("Should not enqueue the clusterResourcePlacement name for reconciling, when there are only order changes in the drifted placement list", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DriftedPlacements = []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + Name: "config-name", + Namespace: "config-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: 1, + FirstDriftedObservedTime: startTime, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "qux", + ValueInMember: "quux", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Service", + Name: "svc-name", + Namespace: "svc-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: 1, + FirstDriftedObservedTime: startTime, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "bar", + ValueInMember: "baz", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + consistentlyCheckPlacementControllerQueueIsEmpty() + }) + + It("Should enqueue the clusterResourcePlacement name for reconciling, when there are one less drifted placement", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DriftedPlacements = []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Service", + Name: "svc-name", + Namespace: "svc-namespace", + }, + ObservationTime: metav1.Now(), + TargetClusterObservedGeneration: 1, + FirstDriftedObservedTime: metav1.Now(), + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "bar", + ValueInMember: "baz", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + + It("Should enqueue the clusterResourcePlacement name for reconciling, when the drift details change", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DriftedPlacements = []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Service", + Name: "svc-name", + Namespace: "svc-namespace", + }, + ObservationTime: metav1.Now(), + TargetClusterObservedGeneration: 1, + FirstDriftedObservedTime: metav1.Now(), + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "qux", + ValueInMember: "quux", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + + It("Should enqueue the clusterResourcePlacement name for reconciling, when there are no more drifted placements", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DriftedPlacements = []fleetv1beta1.DriftedResourcePlacement{} + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + }) + + Context("Should enqueue the clusterResourcePlacement name for reconciling, when the diffed placement list has changed", Serial, Ordered, func() { + startTime := metav1.Now() + + It("Should enqueue the clusterResourcePlacement name for reconciling, when there are new diffed placements", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DiffedPlacements = []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Service", + Name: "svc-name", + Namespace: "svc-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: ptr.To(int64(1)), + FirstDiffedObservedTime: startTime, + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "bar", + ValueInMember: "baz", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + Name: "config-name", + Namespace: "config-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: ptr.To(int64(1)), + FirstDiffedObservedTime: startTime, + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "qux", + ValueInMember: "quux", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + + It("Should not enqueue the clusterResourcePlacement name for reconciling, when there are only order changes in the drifted placement list", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DiffedPlacements = []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + Name: "config-name", + Namespace: "config-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: ptr.To(int64(1)), + FirstDiffedObservedTime: startTime, + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "qux", + ValueInMember: "quux", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Service", + Name: "svc-name", + Namespace: "svc-namespace", + }, + ObservationTime: startTime, + TargetClusterObservedGeneration: ptr.To(int64(1)), + FirstDiffedObservedTime: startTime, + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "bar", + ValueInMember: "baz", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + consistentlyCheckPlacementControllerQueueIsEmpty() + }) + + It("Should enqueue the clusterResourcePlacement name for reconciling, when there are one less diffed placement", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DiffedPlacements = []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + Name: "config-name", + Namespace: "config-namespace", + }, + ObservationTime: metav1.Now(), + TargetClusterObservedGeneration: ptr.To(int64(1)), + FirstDiffedObservedTime: metav1.Now(), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/foo", + ValueInHub: "qux", + ValueInMember: "quux", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + + It("Should enqueue the clusterResourcePlacement name for reconciling, when the diff details change", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DiffedPlacements = []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "ConfigMap", + Name: "config-name", + Namespace: "config-namespace", + }, + ObservationTime: metav1.Now(), + FirstDiffedObservedTime: metav1.Now(), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/", + ValueInHub: "(the whole object)", + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + + It("Should enqueue the clusterResourcePlacement name for reconciling, when there are no more drifted placements", func() { + crb := &fleetv1beta1.ClusterResourceBinding{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: testCRBName}, crb)).Should(Succeed(), "failed to get cluster resource binding") + crb.Status.DiffedPlacements = []fleetv1beta1.DiffedResourcePlacement{} + Expect(k8sClient.Status().Update(ctx, crb)).Should(Succeed(), "failed to update cluster resource binding status") + + By("Checking placement controller queue") + eventuallyCheckPlacementControllerQueue(crb.GetLabels()[fleetv1beta1.CRPTrackingLabel]) + fakePlacementController.ResetQueue() + }) + }) }) func clusterResourceBindingForTest() *fleetv1beta1.ClusterResourceBinding { diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 923f35703..52c39273e 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -597,6 +597,30 @@ var LessFuncDriftedResourcePlacements = func(a, b placementv1beta1.DriftedResour return aStr < bStr } +// IsDriftedResourcePlacementsEqual returns true if the two set of drifted resource placements are equal. +func IsDriftedResourcePlacementsEqual(oldDriftedResourcePlacements, newDriftedResourcePlacements []placementv1beta1.DriftedResourcePlacement) bool { + if len(oldDriftedResourcePlacements) != len(newDriftedResourcePlacements) { + return false + } + sort.Slice(oldDriftedResourcePlacements, func(i, j int) bool { + return LessFuncDriftedResourcePlacements(oldDriftedResourcePlacements[i], oldDriftedResourcePlacements[j]) + }) + sort.Slice(newDriftedResourcePlacements, func(i, j int) bool { + return LessFuncDriftedResourcePlacements(newDriftedResourcePlacements[i], newDriftedResourcePlacements[j]) + }) + for i := range oldDriftedResourcePlacements { + oldDriftedResourcePlacement := oldDriftedResourcePlacements[i] + newDriftedResourcePlacement := newDriftedResourcePlacements[i] + + // Note that here Fleet will not attempt to sort the ObservedDrifts slice as it yields no + // performance benefits; ObservedDrifts changes are always paired with ObservationTime changes. + if !equality.Semantic.DeepEqual(oldDriftedResourcePlacement, newDriftedResourcePlacement) { + return false + } + } + return true +} + // LessFuncDiffedResourcePlacements is a less function for sorting drifted resource placements var LessFuncDiffedResourcePlacements = func(a, b placementv1beta1.DiffedResourcePlacement) bool { var aStr, bStr string @@ -614,6 +638,29 @@ var LessFuncDiffedResourcePlacements = func(a, b placementv1beta1.DiffedResource return aStr < bStr } +// IsDiffedResourcePlacementsEqual returns true if the two sets of diffed resource placements are equal. +func IsDiffedResourcePlacementsEqual(oldDiffedResourcePlacements, newDiffedResourcePlacements []placementv1beta1.DiffedResourcePlacement) bool { + if len(oldDiffedResourcePlacements) != len(newDiffedResourcePlacements) { + return false + } + sort.Slice(oldDiffedResourcePlacements, func(i, j int) bool { + return LessFuncDiffedResourcePlacements(oldDiffedResourcePlacements[i], oldDiffedResourcePlacements[j]) + }) + sort.Slice(newDiffedResourcePlacements, func(i, j int) bool { + return LessFuncDiffedResourcePlacements(newDiffedResourcePlacements[i], newDiffedResourcePlacements[j]) + }) + for i := range oldDiffedResourcePlacements { + oldDiffedResourcePlacement := oldDiffedResourcePlacements[i] + newDiffedResourcePlacement := newDiffedResourcePlacements[i] + // Note that here Fleet will not attempt to sort the ObservedDiffs slice as it yields no + // performance benefits; ObservedDiffs changes are always paired with ObservationTime changes. + if !equality.Semantic.DeepEqual(oldDiffedResourcePlacement, newDiffedResourcePlacement) { + return false + } + } + return true +} + // IsFleetAnnotationPresent returns true if a key with fleet prefix is present in the annotations map. func IsFleetAnnotationPresent(annotations map[string]string) bool { for k := range annotations { diff --git a/pkg/utils/common_test.go b/pkg/utils/common_test.go index 8419f8215..c4e9c1360 100644 --- a/pkg/utils/common_test.go +++ b/pkg/utils/common_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" fleetv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" ) @@ -620,3 +621,571 @@ func TestIsFailedResourcePlacementsEqual(t *testing.T) { }) } } + +// TestIsDriftedResourcePlacementsEqual tests the IsDriftedResourcePlacementsEqual function. +func TestIsDriftedResourcePlacementsEqual(t *testing.T) { + now := metav1.Now() + + testCases := []struct { + name string + oldDRP []fleetv1beta1.DriftedResourcePlacement + newDRP []fleetv1beta1.DriftedResourcePlacement + want bool + }{ + { + name: "two empty slices", + oldDRP: []fleetv1beta1.DriftedResourcePlacement{}, + newDRP: []fleetv1beta1.DriftedResourcePlacement{}, + want: true, + }, + { + name: "old is nil, new is empty", + oldDRP: nil, + newDRP: []fleetv1beta1.DriftedResourcePlacement{}, + want: true, + }, + { + name: "old is empty, new is nil", + oldDRP: []fleetv1beta1.DriftedResourcePlacement{}, + newDRP: nil, + want: true, + }, + { + name: "different length", + oldDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + }, + { + name: "same list, different order", + oldDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + }, + want: true, + }, + { + name: "same list, same order", + oldDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + want: true, + }, + { + name: "drift info updated", + oldDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DriftedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: metav1.Now(), + FirstDriftedObservedTime: now, + TargetClusterObservedGeneration: 1, + ObservedDrifts: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "2", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := IsDriftedResourcePlacementsEqual(tc.oldDRP, tc.newDRP) + if got != tc.want { + t.Errorf("IsDriftedResourcePlacementsEqual() = %v, want %v", got, tc.want) + } + }) + } +} + +// TestIsDiffedResourcePlacementEqual tests the IsDiffedResourcePlacementEqual function. +func TestIsDiffedResourcePlacementEqual(t *testing.T) { + now := metav1.Now() + + testCases := []struct { + name string + oldDRP []fleetv1beta1.DiffedResourcePlacement + newDRP []fleetv1beta1.DiffedResourcePlacement + want bool + }{ + { + name: "two empty slices", + oldDRP: []fleetv1beta1.DiffedResourcePlacement{}, + newDRP: []fleetv1beta1.DiffedResourcePlacement{}, + want: true, + }, + { + name: "old is nil, new is empty", + oldDRP: nil, + newDRP: []fleetv1beta1.DiffedResourcePlacement{}, + want: true, + }, + { + name: "old is empty, new is nil", + oldDRP: []fleetv1beta1.DiffedResourcePlacement{}, + newDRP: nil, + want: true, + }, + { + name: "different length", + oldDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + }, + { + name: "same list, different order", + oldDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + }, + want: true, + }, + { + name: "same list, same order", + oldDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "default", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/spec/replicas", + ValueInHub: "1", + ValueInMember: "2", + }, + }, + }, + }, + want: true, + }, + { + name: "diff info updated", + oldDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(1)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + newDRP: []fleetv1beta1.DiffedResourcePlacement{ + { + ResourceIdentifier: fleetv1beta1.ResourceIdentifier{ + Group: "", + Version: "v1", + Kind: "Namespace", + Name: "work", + }, + ObservationTime: now, + FirstDiffedObservedTime: now, + TargetClusterObservedGeneration: ptr.To(int64(2)), + ObservedDiffs: []fleetv1beta1.PatchDetail{ + { + Path: "/metadata/labels/custom", + ValueInMember: "1", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := IsDiffedResourcePlacementsEqual(tc.oldDRP, tc.newDRP) + if got != tc.want { + t.Errorf("IsDiffedResourcePlacementsEqual() = %v, want %v", got, tc.want) + } + }) + } +}