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..a0ef34b3 100644 --- a/internal/controller/workloaddeployment_network_binding_test.go +++ b/internal/controller/workloaddeployment_network_binding_test.go @@ -6,6 +6,8 @@ import ( "context" "testing" + "sigs.k8s.io/controller-runtime/pkg/event" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -566,3 +568,37 @@ func TestNetworkBindingStateDoesNotGateInstances(t *testing.T) { assert.True(t, networkInterfaceClaimSatisfied(claim), "binding state is not an input to instance gating") } + +func TestNetworkBindingChanged(t *testing.T) { + const changedName = "changed" + 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: networkingv1alpha.NetworkBindingReady, Status: metav1.ConditionFalse}} + }, true}, + {"spec", func(b *networkingv1alpha.NetworkBinding) { b.Spec.Network.Name = changedName }, true}, + {"owner", func(b *networkingv1alpha.NetworkBinding) { + 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": changedName} }, 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})) +}