Skip to content

Commit 35002c5

Browse files
committed
fix: avoid reconciling ended leases
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com> Assited-by: claude-opus-4.6
1 parent 26aee22 commit 35002c5

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

controller/internal/controller/lease_controller.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import (
3434
ctrl "sigs.k8s.io/controller-runtime"
3535
"sigs.k8s.io/controller-runtime/pkg/client"
3636
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
37+
"sigs.k8s.io/controller-runtime/pkg/event"
3738
"sigs.k8s.io/controller-runtime/pkg/log"
39+
"sigs.k8s.io/controller-runtime/pkg/predicate"
3840
)
3941

4042
// LeaseReconciler reconciles a Lease object
@@ -77,6 +79,10 @@ func (r *LeaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
7779
)
7880
}
7981

82+
if lease.Status.Ended && isLeaseEnded(&lease) {
83+
return ctrl.Result{}, nil
84+
}
85+
8086
var result ctrl.Result
8187
if err := r.reconcileStatusExporterRef(ctx, &result, &lease); err != nil {
8288
return result, err
@@ -585,9 +591,36 @@ func filterOutOfflineExporters(approvedExporters []ApprovedExporter) []ApprovedE
585591
return onlineExporters
586592
}
587593

594+
// isLeaseEnded checks whether a lease object carries the ended label.
595+
func isLeaseEnded(obj client.Object) bool {
596+
if v, ok := obj.GetLabels()[string(jumpstarterdevv1alpha1.LeaseLabelEnded)]; ok {
597+
return v == jumpstarterdevv1alpha1.LeaseLabelEndedValue
598+
}
599+
return false
600+
}
601+
588602
// SetupWithManager sets up the controller with the Manager.
589603
func (r *LeaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
604+
// Skip ended leases: they are immutable and never need further reconciliation.
605+
// The Update funnel still admits the transition into ended so the label
606+
// and status update written by the reconciler itself is not swallowed.
607+
skipEnded := predicate.Funcs{
608+
CreateFunc: func(e event.CreateEvent) bool {
609+
return !isLeaseEnded(e.Object)
610+
},
611+
UpdateFunc: func(e event.UpdateEvent) bool {
612+
return !isLeaseEnded(e.ObjectNew)
613+
},
614+
DeleteFunc: func(e event.DeleteEvent) bool {
615+
return true
616+
},
617+
GenericFunc: func(e event.GenericEvent) bool {
618+
return !isLeaseEnded(e.Object)
619+
},
620+
}
621+
590622
return ctrl.NewControllerManagedBy(mgr).
591623
For(&jumpstarterdevv1alpha1.Lease{}).
624+
WithEventFilter(skipEnded).
592625
Complete(r)
593626
}

controller/internal/controller/lease_controller_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,65 @@ var _ = Describe("Scheduled Leases", func() {
17091709
})
17101710
})
17111711

1712+
When("an ended lease has the ended label", func() {
1713+
It("should short-circuit without running reconciliation logic", func() {
1714+
lease := leaseDutA2Sec.DeepCopy()
1715+
lease.Spec.Duration = &metav1.Duration{Duration: 100 * time.Millisecond}
1716+
1717+
ctx := context.Background()
1718+
Expect(k8sClient.Create(ctx, lease)).To(Succeed())
1719+
_ = reconcileLease(ctx, lease)
1720+
1721+
Eventually(func() bool {
1722+
_ = reconcileLease(ctx, lease)
1723+
return getLease(ctx, lease.Name).Status.Ended
1724+
}).WithTimeout(500 * time.Millisecond).WithPolling(50 * time.Millisecond).Should(BeTrue())
1725+
1726+
updatedLease := getLease(ctx, lease.Name)
1727+
Expect(updatedLease.Labels).To(HaveKeyWithValue(
1728+
string(jumpstarterdevv1alpha1.LeaseLabelEnded),
1729+
jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1730+
))
1731+
1732+
// Reconciling again should be a no-op (short-circuit)
1733+
result := reconcileLease(ctx, lease)
1734+
Expect(result.RequeueAfter).To(BeZero())
1735+
})
1736+
})
1737+
1738+
When("an ended lease is missing the ended label", func() {
1739+
It("should backfill the label on the next reconcile", func() {
1740+
lease := leaseDutA2Sec.DeepCopy()
1741+
lease.Spec.Duration = &metav1.Duration{Duration: 100 * time.Millisecond}
1742+
1743+
ctx := context.Background()
1744+
Expect(k8sClient.Create(ctx, lease)).To(Succeed())
1745+
_ = reconcileLease(ctx, lease)
1746+
1747+
Eventually(func() bool {
1748+
_ = reconcileLease(ctx, lease)
1749+
return getLease(ctx, lease.Name).Status.Ended
1750+
}).WithTimeout(500 * time.Millisecond).WithPolling(50 * time.Millisecond).Should(BeTrue())
1751+
1752+
// Simulate split-brain: remove the ended label as if metadata update failed
1753+
updatedLease := getLease(ctx, lease.Name)
1754+
delete(updatedLease.Labels, string(jumpstarterdevv1alpha1.LeaseLabelEnded))
1755+
Expect(k8sClient.Update(ctx, updatedLease)).To(Succeed())
1756+
1757+
// Verify label is gone
1758+
updatedLease = getLease(ctx, lease.Name)
1759+
Expect(updatedLease.Labels).NotTo(HaveKey(string(jumpstarterdevv1alpha1.LeaseLabelEnded)))
1760+
1761+
// Reconcile should backfill the label
1762+
_ = reconcileLease(ctx, lease)
1763+
updatedLease = getLease(ctx, lease.Name)
1764+
Expect(updatedLease.Labels).To(HaveKeyWithValue(
1765+
string(jumpstarterdevv1alpha1.LeaseLabelEnded),
1766+
jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1767+
))
1768+
})
1769+
})
1770+
17121771
// UpdateLease mutation tests
17131772
// Note: These tests simulate what UpdateLease does via gRPC by directly
17141773
// modifying the lease spec and calling ReconcileLeaseTimeFields

0 commit comments

Comments
 (0)