Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion charts/dynamic-prefix-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ securityContext:
resources:
limits:
cpu: 500m
memory: 128Mi
memory: 256Mi
requests:
cpu: 10m
memory: 64Mi
Expand Down
8 changes: 2 additions & 6 deletions internal/controller/bgpsync_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ package controller

import (
"context"
"encoding/json"
"fmt"
"reflect"

"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -187,9 +186,6 @@ func (r *BGPSyncReconciler) reconcileAdvertisement(
log.Info("Created CiliumBGPAdvertisement", "name", advName, "subnet", subnet.Name)
} else {
// Check if the spec or labels actually changed before updating
existingSpec, _ := json.Marshal(adv.Object["spec"])
newSpecJSON, _ := json.Marshal(advSpec)

labels := adv.GetLabels()
if labels == nil {
labels = make(map[string]string)
Expand All @@ -198,7 +194,7 @@ func (r *BGPSyncReconciler) reconcileAdvertisement(
labels[LabelDynamicPrefixName] != dp.Name ||
labels[LabelSubnetName] != subnet.Name

if !reflect.DeepEqual(existingSpec, newSpecJSON) || labelsChanged {
if !equality.Semantic.DeepEqual(adv.Object["spec"], advSpec) || labelsChanged {
adv.Object["spec"] = advSpec
labels[LabelManagedBy] = LabelManagedByValue
labels[LabelDynamicPrefixName] = dp.Name
Expand Down
16 changes: 16 additions & 0 deletions internal/controller/bgpsync_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ var _ = Describe("BGPSync Controller", func() {
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
Expect(communities).To(ContainElement(community))

advResourceVersion := adv.GetResourceVersion()
var dp dynamicprefixiov1alpha1.DynamicPrefix
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dpName}, &dp)).To(Succeed())
dpResourceVersion := dp.ResourceVersion

// A steady-state reconcile must not rewrite the advertisement or DynamicPrefix status.
_, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: dpName},
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, types.NamespacedName{Name: advName}, adv)).To(Succeed())
Expect(adv.GetResourceVersion()).To(Equal(advResourceVersion))
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dpName}, &dp)).To(Succeed())
Expect(dp.ResourceVersion).To(Equal(dpResourceVersion))
})

It("should update DynamicPrefix status with advertisement name and condition", func() {
Expand Down
23 changes: 19 additions & 4 deletions internal/controller/dynamicprefix_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"sync"
"time"

"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -106,13 +107,15 @@ func (r *DynamicPrefixReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{RequeueAfter: time.Second}, nil
}

originalStatus := dp.Status.DeepCopy()

// Get or create the receiver for this DynamicPrefix
receiver, err := r.getOrCreateReceiver(ctx, &dp)
if err != nil {
log.Error(err, "Failed to create receiver")
r.setCondition(&dp, dynamicprefixiov1alpha1.ConditionTypePrefixAcquired, metav1.ConditionFalse,
"ReceiverCreationFailed", err.Error())
if statusErr := r.Status().Update(ctx, &dp); statusErr != nil {
if statusErr := r.updateStatusIfChanged(ctx, &dp, originalStatus); statusErr != nil {
log.Error(statusErr, "Failed to update status")
}
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
Expand All @@ -124,7 +127,7 @@ func (r *DynamicPrefixReconciler) Reconcile(ctx context.Context, req ctrl.Reques
log.Info("No prefix acquired yet")
r.setCondition(&dp, dynamicprefixiov1alpha1.ConditionTypePrefixAcquired, metav1.ConditionFalse,
"WaitingForPrefix", "Waiting to receive prefix from upstream")
if err := r.Status().Update(ctx, &dp); err != nil {
if err := r.updateStatusIfChanged(ctx, &dp, originalStatus); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
Expand Down Expand Up @@ -175,8 +178,8 @@ func (r *DynamicPrefixReconciler) Reconcile(ctx context.Context, req ctrl.Reques
r.setCondition(&dp, dynamicprefixiov1alpha1.ConditionTypePrefixAcquired, metav1.ConditionTrue,
"PrefixAcquired", fmt.Sprintf("Prefix %s acquired via %s", currentPrefix.Network, receiver.Source()))

// Update status
if err := r.Status().Update(ctx, &dp); err != nil {
// Update status only when it actually changed to avoid self-triggered reconcile loops.
if err := r.updateStatusIfChanged(ctx, &dp, originalStatus); err != nil {
return ctrl.Result{}, err
}

Expand All @@ -185,6 +188,18 @@ func (r *DynamicPrefixReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{RequeueAfter: requeueAfter}, nil
}

// updateStatusIfChanged writes the DynamicPrefix status only when a semantic change occurred.
func (r *DynamicPrefixReconciler) updateStatusIfChanged(
ctx context.Context,
dp *dynamicprefixiov1alpha1.DynamicPrefix,
originalStatus *dynamicprefixiov1alpha1.DynamicPrefixStatus,
) error {
if equality.Semantic.DeepEqual(originalStatus, &dp.Status) {
return nil
}
return r.Status().Update(ctx, dp)
}

// getOrCreateReceiver returns an existing receiver or creates a new one
func (r *DynamicPrefixReconciler) getOrCreateReceiver(ctx context.Context, dp *dynamicprefixiov1alpha1.DynamicPrefix) (prefix.Receiver, error) {
r.receiversMu.RLock()
Expand Down
10 changes: 10 additions & 0 deletions internal/controller/dynamicprefix_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ var _ = Describe("DynamicPrefix Controller", func() {
Expect(updatedDP.Status.Subnets[1].Name).To(Equal("services"))
Expect(updatedDP.Status.Subnets[1].CIDR).To(Equal("2001:db8:0:1::/64"))

stableResourceVersion := updatedDP.ResourceVersion

// A steady-state reconcile must not write status again.
_, err = reconciler.Reconcile(ctx, req)
Expect(err).NotTo(HaveOccurred())

var stableDP dynamicprefixiov1alpha1.DynamicPrefix
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: dpName}, &stableDP)).Should(Succeed())
Expect(stableDP.ResourceVersion).To(Equal(stableResourceVersion))

// Cleanup
Expect(k8sClient.Delete(ctx, dp)).Should(Succeed())
})
Expand Down
11 changes: 3 additions & 8 deletions internal/controller/poolsync_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ package controller

import (
"context"
"encoding/json"
"fmt"
"net/netip"
"reflect"
"time"

"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -477,9 +476,7 @@ func (r *PoolSyncReconciler) updateLoadBalancerIPPool(ctx context.Context, pool

// Check if blocks actually changed before updating to avoid feedback loops
currentBlocks, _, _ := unstructured.NestedSlice(pool.Object, "spec", "blocks")
existingJSON, _ := json.Marshal(currentBlocks)
newJSON, _ := json.Marshal(blocks)
if reflect.DeepEqual(existingJSON, newJSON) {
if equality.Semantic.DeepEqual(currentBlocks, blocks) {
log.V(2).Info("Pool blocks unchanged, skipping update", "pool", pool.GetName())
return nil
}
Expand Down Expand Up @@ -526,9 +523,7 @@ func (r *PoolSyncReconciler) updateCIDRGroup(ctx context.Context, pool *unstruct

// Check if CIDRs actually changed before updating to avoid feedback loops
currentCIDRs, _, _ := unstructured.NestedSlice(pool.Object, "spec", "externalCIDRs")
existingJSON, _ := json.Marshal(currentCIDRs)
newJSON, _ := json.Marshal(externalCIDRs)
if reflect.DeepEqual(existingJSON, newJSON) {
if equality.Semantic.DeepEqual(currentCIDRs, externalCIDRs) {
log.V(2).Info("CIDRGroup unchanged, skipping update", "cidrGroup", pool.GetName())
return nil
}
Expand Down
24 changes: 24 additions & 0 deletions internal/controller/poolsync_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ var _ = Describe("PoolSync Controller", func() {
// Check last-sync annotation
annotations := pool.GetAnnotations()
Expect(annotations).To(HaveKey(AnnotationLastSync))
lastSync := annotations[AnnotationLastSync]
resourceVersion := pool.GetResourceVersion()

// A steady-state reconcile must not write the pool again.
_, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: poolName},
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, types.NamespacedName{Name: poolName}, pool)).To(Succeed())
Expect(pool.GetResourceVersion()).To(Equal(resourceVersion))
Expect(pool.GetAnnotations()[AnnotationLastSync]).To(Equal(lastSync))
})
})

Expand Down Expand Up @@ -235,6 +247,18 @@ var _ = Describe("PoolSync Controller", func() {
// Check last-sync annotation
annotations := group.GetAnnotations()
Expect(annotations).To(HaveKey(AnnotationLastSync))
lastSync := annotations[AnnotationLastSync]
resourceVersion := group.GetResourceVersion()

// A steady-state reconcile must not write the CIDRGroup again.
_, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: groupName},
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, types.NamespacedName{Name: groupName}, group)).To(Succeed())
Expect(group.GetResourceVersion()).To(Equal(resourceVersion))
Expect(group.GetAnnotations()[AnnotationLastSync]).To(Equal(lastSync))
})
})

Expand Down
5 changes: 2 additions & 3 deletions internal/controller/servicesync_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,9 @@ func (r *ServiceSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request)
updated = true
}

// Update last-sync annotation
newAnnotations[AnnotationLastSync] = time.Now().UTC().Format(time.RFC3339)

if updated {
// Update last-sync annotation only when this reconcile actually changes managed annotations.
newAnnotations[AnnotationLastSync] = time.Now().UTC().Format(time.RFC3339)
svc.SetAnnotations(newAnnotations)
if err := r.Update(ctx, &svc); err != nil {
log.Error(err, "Failed to update Service annotations")
Expand Down
18 changes: 18 additions & 0 deletions internal/controller/servicesync_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ var _ = Describe("ServiceSync Controller", func() {

// Should have last-sync annotation
Expect(annotations).To(HaveKey(AnnotationLastSync))
lastSync := annotations[AnnotationLastSync]
resourceVersion := svc.GetResourceVersion()

// A steady-state reconcile must not rewrite Service annotations.
_, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: serviceName,
Namespace: serviceNS,
},
})
Expect(err).NotTo(HaveOccurred())

Expect(k8sClient.Get(ctx, types.NamespacedName{
Name: serviceName,
Namespace: serviceNS,
}, svc)).To(Succeed())
Expect(svc.GetResourceVersion()).To(Equal(resourceVersion))
Expect(svc.GetAnnotations()[AnnotationLastSync]).To(Equal(lastSync))
})

It("should preserve IPv4 addresses in dual-stack annotation", func() {
Expand Down
Loading