From ad4294c29f3218b1dd0bca9732386e7e713be9c4 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 29 Oct 2026 20:40:01 -0500 Subject: [PATCH 1/2] fix: ignore NetworkBinding annotation churn in federator --- .../workloaddeployment_federator.go | 1 + .../workloaddeployment_network_binding.go | 22 ++++++++++++ ...workloaddeployment_network_binding_test.go | 34 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/internal/controller/workloaddeployment_federator.go b/internal/controller/workloaddeployment_federator.go index 2331c70d..02fb8df7 100644 --- a/internal/controller/workloaddeployment_federator.go +++ b/internal/controller/workloaddeployment_federator.go @@ -942,6 +942,7 @@ func (r *WorkloadDeploymentFederator) SetupWithManager(mgr mcmanager.Manager) er r.FederationCluster, &networkingv1alpha.NetworkBinding{}, preserveOnBinding, + networkBindingChanged(), )) } diff --git a/internal/controller/workloaddeployment_network_binding.go b/internal/controller/workloaddeployment_network_binding.go index 6232ec09..e80d8e24 100644 --- a/internal/controller/workloaddeployment_network_binding.go +++ b/internal/controller/workloaddeployment_network_binding.go @@ -15,7 +15,9 @@ import ( "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" @@ -333,3 +335,23 @@ func applyNetworkBindingRefusal( refusal.ObservedGeneration = observedGeneration apimeta.SetStatusCondition(&status.Conditions, *refusal) } + +// networkBindingChanged ignores annotation bookkeeping while preserving +// reconciliation inputs, ownership, and deletion lifecycle changes. +func networkBindingChanged() predicate.TypedPredicate[*networkingv1alpha.NetworkBinding] { + return predicate.TypedFuncs[*networkingv1alpha.NetworkBinding]{ + UpdateFunc: func(e event.TypedUpdateEvent[*networkingv1alpha.NetworkBinding]) bool { + a, b := e.ObjectOld, e.ObjectNew + if a == nil || b == nil { + return true + } + return a.UID != b.UID || + !equality.Semantic.DeepEqual(a.Spec, b.Spec) || + !equality.Semantic.DeepEqual(a.Status, b.Status) || + !equality.Semantic.DeepEqual(a.Labels, b.Labels) || + !equality.Semantic.DeepEqual(a.OwnerReferences, b.OwnerReferences) || + !equality.Semantic.DeepEqual(a.Finalizers, b.Finalizers) || + !equality.Semantic.DeepEqual(a.DeletionTimestamp, b.DeletionTimestamp) + }, + } +} diff --git a/internal/controller/workloaddeployment_network_binding_test.go b/internal/controller/workloaddeployment_network_binding_test.go index 85eb3983..e2f5a6fa 100644 --- a/internal/controller/workloaddeployment_network_binding_test.go +++ b/internal/controller/workloaddeployment_network_binding_test.go @@ -4,6 +4,7 @@ package controller import ( "context" + "sigs.k8s.io/controller-runtime/pkg/event" "testing" "github.com/stretchr/testify/assert" @@ -566,3 +567,36 @@ func TestNetworkBindingStateDoesNotGateInstances(t *testing.T) { assert.True(t, networkInterfaceClaimSatisfied(claim), "binding state is not an input to instance gating") } + +func TestNetworkBindingChanged(t *testing.T) { + base := &networkingv1alpha.NetworkBinding{} + cases := []struct { + name string + change func(*networkingv1alpha.NetworkBinding) + want bool + }{ + {"annotation", func(b *networkingv1alpha.NetworkBinding) { + b.Annotations = map[string]string{"networking.datumapis.com/presence-observed": "new"} + b.ResourceVersion = "2" + }, false}, + {"status", func(b *networkingv1alpha.NetworkBinding) { + b.Status.Conditions = []metav1.Condition{{Type: "Ready", Status: metav1.ConditionFalse}} + }, true}, + {"spec", func(b *networkingv1alpha.NetworkBinding) { b.Spec.Network.Name = "changed" }, true}, + {"owner", func(b *networkingv1alpha.NetworkBinding) { + b.OwnerReferences = []metav1.OwnerReference{{Name: "changed"}} + }, true}, + {"deletion", func(b *networkingv1alpha.NetworkBinding) { now := metav1.Now(); b.DeletionTimestamp = &now }, true}, + {"labels", func(b *networkingv1alpha.NetworkBinding) { b.Labels = map[string]string{"network": "changed"} }, true}, + } + p := networkBindingChanged() + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + b := base.DeepCopy() + tc.change(b) + require.Equal(t, tc.want, p.Update(event.TypedUpdateEvent[*networkingv1alpha.NetworkBinding]{ObjectOld: base, ObjectNew: b})) + }) + } + require.True(t, p.Create(event.TypedCreateEvent[*networkingv1alpha.NetworkBinding]{Object: base})) + require.True(t, p.Delete(event.TypedDeleteEvent[*networkingv1alpha.NetworkBinding]{Object: base})) +} From eef8d81454faef9a91ee03ce926b413fe5a4ff37 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 29 Oct 2026 20:43:45 -0500 Subject: [PATCH 2/2] test: fix federator regression test lint findings --- .../workloaddeployment_network_binding_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/controller/workloaddeployment_network_binding_test.go b/internal/controller/workloaddeployment_network_binding_test.go index e2f5a6fa..a0ef34b3 100644 --- a/internal/controller/workloaddeployment_network_binding_test.go +++ b/internal/controller/workloaddeployment_network_binding_test.go @@ -4,9 +4,10 @@ package controller import ( "context" - "sigs.k8s.io/controller-runtime/pkg/event" "testing" + "sigs.k8s.io/controller-runtime/pkg/event" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -569,6 +570,7 @@ func TestNetworkBindingStateDoesNotGateInstances(t *testing.T) { } func TestNetworkBindingChanged(t *testing.T) { + const changedName = "changed" base := &networkingv1alpha.NetworkBinding{} cases := []struct { name string @@ -580,14 +582,14 @@ func TestNetworkBindingChanged(t *testing.T) { b.ResourceVersion = "2" }, false}, {"status", func(b *networkingv1alpha.NetworkBinding) { - b.Status.Conditions = []metav1.Condition{{Type: "Ready", Status: metav1.ConditionFalse}} + b.Status.Conditions = []metav1.Condition{{Type: networkingv1alpha.NetworkBindingReady, Status: metav1.ConditionFalse}} }, true}, - {"spec", func(b *networkingv1alpha.NetworkBinding) { b.Spec.Network.Name = "changed" }, true}, + {"spec", func(b *networkingv1alpha.NetworkBinding) { b.Spec.Network.Name = changedName }, true}, {"owner", func(b *networkingv1alpha.NetworkBinding) { - b.OwnerReferences = []metav1.OwnerReference{{Name: "changed"}} + b.OwnerReferences = []metav1.OwnerReference{{Name: changedName}} }, true}, {"deletion", func(b *networkingv1alpha.NetworkBinding) { now := metav1.Now(); b.DeletionTimestamp = &now }, true}, - {"labels", func(b *networkingv1alpha.NetworkBinding) { b.Labels = map[string]string{"network": "changed"} }, true}, + {"labels", func(b *networkingv1alpha.NetworkBinding) { b.Labels = map[string]string{"network": changedName} }, true}, } p := networkBindingChanged() for _, tc := range cases {