Skip to content

Commit c73f1c9

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 c73f1c9

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

controller/internal/controller/lease_controller.go

Lines changed: 35 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,38 @@ 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+
602+
// skipEndedPredicate returns a predicate that filters out leases carrying the
603+
// ended label. Leases without the label are admitted so the reconciler can
604+
// backfill it when Status.Ended is true but the label write was lost.
605+
func skipEndedPredicate() predicate.Funcs {
606+
return predicate.Funcs{
607+
CreateFunc: func(e event.CreateEvent) bool {
608+
return !isLeaseEnded(e.Object)
609+
},
610+
UpdateFunc: func(e event.UpdateEvent) bool {
611+
return !isLeaseEnded(e.ObjectNew)
612+
},
613+
DeleteFunc: func(e event.DeleteEvent) bool {
614+
return true
615+
},
616+
GenericFunc: func(e event.GenericEvent) bool {
617+
return !isLeaseEnded(e.Object)
618+
},
619+
}
620+
}
621+
588622
// SetupWithManager sets up the controller with the Manager.
589623
func (r *LeaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
590624
return ctrl.NewControllerManagedBy(mgr).
591625
For(&jumpstarterdevv1alpha1.Lease{}).
626+
WithEventFilter(skipEndedPredicate()).
592627
Complete(r)
593628
}

controller/internal/controller/lease_controller_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"k8s.io/apimachinery/pkg/api/meta"
3232
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3333
"k8s.io/apimachinery/pkg/types"
34+
"sigs.k8s.io/controller-runtime/pkg/event"
35+
"sigs.k8s.io/controller-runtime/pkg/predicate"
3436
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3537
)
3638

@@ -1027,6 +1029,97 @@ var _ = Describe("orderApprovedExporters", func() {
10271029
})
10281030
})
10291031

1032+
var _ = Describe("isLeaseEnded", func() {
1033+
It("should return true when the ended label is present", func() {
1034+
lease := &jumpstarterdevv1alpha1.Lease{
1035+
ObjectMeta: metav1.ObjectMeta{
1036+
Labels: map[string]string{
1037+
string(jumpstarterdevv1alpha1.LeaseLabelEnded): jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1038+
},
1039+
},
1040+
}
1041+
Expect(isLeaseEnded(lease)).To(BeTrue())
1042+
})
1043+
1044+
It("should return false when the ended label is missing", func() {
1045+
lease := &jumpstarterdevv1alpha1.Lease{
1046+
ObjectMeta: metav1.ObjectMeta{
1047+
Labels: map[string]string{},
1048+
},
1049+
}
1050+
Expect(isLeaseEnded(lease)).To(BeFalse())
1051+
})
1052+
1053+
It("should return false when labels are nil", func() {
1054+
lease := &jumpstarterdevv1alpha1.Lease{}
1055+
Expect(isLeaseEnded(lease)).To(BeFalse())
1056+
})
1057+
})
1058+
1059+
var _ = Describe("skipEndedPredicate", func() {
1060+
var skipEnded predicate.Funcs
1061+
1062+
BeforeEach(func() {
1063+
skipEnded = skipEndedPredicate()
1064+
})
1065+
1066+
It("should admit creates for non-ended leases", func() {
1067+
lease := &jumpstarterdevv1alpha1.Lease{}
1068+
Expect(skipEnded.Create(event.CreateEvent{Object: lease})).To(BeTrue())
1069+
})
1070+
1071+
It("should reject creates for ended leases", func() {
1072+
lease := &jumpstarterdevv1alpha1.Lease{
1073+
ObjectMeta: metav1.ObjectMeta{
1074+
Labels: map[string]string{
1075+
string(jumpstarterdevv1alpha1.LeaseLabelEnded): jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1076+
},
1077+
},
1078+
}
1079+
Expect(skipEnded.Create(event.CreateEvent{Object: lease})).To(BeFalse())
1080+
})
1081+
1082+
It("should reject updates where new object has ended label", func() {
1083+
oldLease := &jumpstarterdevv1alpha1.Lease{}
1084+
newLease := &jumpstarterdevv1alpha1.Lease{
1085+
ObjectMeta: metav1.ObjectMeta{
1086+
Labels: map[string]string{
1087+
string(jumpstarterdevv1alpha1.LeaseLabelEnded): jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1088+
},
1089+
},
1090+
}
1091+
Expect(skipEnded.Update(event.UpdateEvent{ObjectOld: oldLease, ObjectNew: newLease})).To(BeFalse())
1092+
})
1093+
1094+
It("should admit updates for non-ended leases", func() {
1095+
oldLease := &jumpstarterdevv1alpha1.Lease{}
1096+
newLease := &jumpstarterdevv1alpha1.Lease{}
1097+
Expect(skipEnded.Update(event.UpdateEvent{ObjectOld: oldLease, ObjectNew: newLease})).To(BeTrue())
1098+
})
1099+
1100+
It("should admit updates for ended-in-status but unlabeled leases so label can be backfilled", func() {
1101+
oldLease := &jumpstarterdevv1alpha1.Lease{}
1102+
newLease := &jumpstarterdevv1alpha1.Lease{
1103+
Status: jumpstarterdevv1alpha1.LeaseStatus{
1104+
Ended: true,
1105+
},
1106+
}
1107+
// No ended label → predicate admits → reconciler runs → backfills label
1108+
Expect(skipEnded.Update(event.UpdateEvent{ObjectOld: oldLease, ObjectNew: newLease})).To(BeTrue())
1109+
})
1110+
1111+
It("should always admit deletes", func() {
1112+
lease := &jumpstarterdevv1alpha1.Lease{
1113+
ObjectMeta: metav1.ObjectMeta{
1114+
Labels: map[string]string{
1115+
string(jumpstarterdevv1alpha1.LeaseLabelEnded): jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1116+
},
1117+
},
1118+
}
1119+
Expect(skipEnded.Delete(event.DeleteEvent{Object: lease})).To(BeTrue())
1120+
})
1121+
})
1122+
10301123
var _ = Describe("Scheduled Leases", func() {
10311124
BeforeEach(func() {
10321125
createExporters(context.Background(), testExporter1DutA, testExporter2DutA, testExporter3DutB)
@@ -1709,6 +1802,65 @@ var _ = Describe("Scheduled Leases", func() {
17091802
})
17101803
})
17111804

1805+
When("an ended lease has the ended label", func() {
1806+
It("should short-circuit without running reconciliation logic", func() {
1807+
lease := leaseDutA2Sec.DeepCopy()
1808+
lease.Spec.Duration = &metav1.Duration{Duration: 100 * time.Millisecond}
1809+
1810+
ctx := context.Background()
1811+
Expect(k8sClient.Create(ctx, lease)).To(Succeed())
1812+
_ = reconcileLease(ctx, lease)
1813+
1814+
Eventually(func() bool {
1815+
_ = reconcileLease(ctx, lease)
1816+
return getLease(ctx, lease.Name).Status.Ended
1817+
}).WithTimeout(500 * time.Millisecond).WithPolling(50 * time.Millisecond).Should(BeTrue())
1818+
1819+
updatedLease := getLease(ctx, lease.Name)
1820+
Expect(updatedLease.Labels).To(HaveKeyWithValue(
1821+
string(jumpstarterdevv1alpha1.LeaseLabelEnded),
1822+
jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1823+
))
1824+
1825+
// Reconciling again should be a no-op (short-circuit)
1826+
result := reconcileLease(ctx, lease)
1827+
Expect(result.RequeueAfter).To(BeZero())
1828+
})
1829+
})
1830+
1831+
When("an ended lease is missing the ended label", func() {
1832+
It("should backfill the label on the next reconcile", func() {
1833+
lease := leaseDutA2Sec.DeepCopy()
1834+
lease.Spec.Duration = &metav1.Duration{Duration: 100 * time.Millisecond}
1835+
1836+
ctx := context.Background()
1837+
Expect(k8sClient.Create(ctx, lease)).To(Succeed())
1838+
_ = reconcileLease(ctx, lease)
1839+
1840+
Eventually(func() bool {
1841+
_ = reconcileLease(ctx, lease)
1842+
return getLease(ctx, lease.Name).Status.Ended
1843+
}).WithTimeout(500 * time.Millisecond).WithPolling(50 * time.Millisecond).Should(BeTrue())
1844+
1845+
// Simulate split-brain: remove the ended label as if metadata update failed
1846+
updatedLease := getLease(ctx, lease.Name)
1847+
delete(updatedLease.Labels, string(jumpstarterdevv1alpha1.LeaseLabelEnded))
1848+
Expect(k8sClient.Update(ctx, updatedLease)).To(Succeed())
1849+
1850+
// Verify label is gone
1851+
updatedLease = getLease(ctx, lease.Name)
1852+
Expect(updatedLease.Labels).NotTo(HaveKey(string(jumpstarterdevv1alpha1.LeaseLabelEnded)))
1853+
1854+
// Reconcile should backfill the label
1855+
_ = reconcileLease(ctx, lease)
1856+
updatedLease = getLease(ctx, lease.Name)
1857+
Expect(updatedLease.Labels).To(HaveKeyWithValue(
1858+
string(jumpstarterdevv1alpha1.LeaseLabelEnded),
1859+
jumpstarterdevv1alpha1.LeaseLabelEndedValue,
1860+
))
1861+
})
1862+
})
1863+
17121864
// UpdateLease mutation tests
17131865
// Note: These tests simulate what UpdateLease does via gRPC by directly
17141866
// modifying the lease spec and calling ReconcileLeaseTimeFields

0 commit comments

Comments
 (0)