@@ -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,98 @@ 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+ })
1123+
10301124var _ = Describe ("Scheduled Leases" , func () {
10311125 BeforeEach (func () {
10321126 createExporters (context .Background (), testExporter1DutA , testExporter2DutA , testExporter3DutB )
@@ -1709,6 +1803,65 @@ var _ = Describe("Scheduled Leases", func() {
17091803 })
17101804 })
17111805
1806+ When ("an ended lease has the ended label" , func () {
1807+ It ("should be a no-op on subsequent reconciles" , func () {
1808+ lease := leaseDutA2Sec .DeepCopy ()
1809+ lease .Spec .Duration = & metav1.Duration {Duration : 100 * time .Millisecond }
1810+
1811+ ctx := context .Background ()
1812+ Expect (k8sClient .Create (ctx , lease )).To (Succeed ())
1813+ _ = reconcileLease (ctx , lease )
1814+
1815+ Eventually (func () bool {
1816+ _ = reconcileLease (ctx , lease )
1817+ return getLease (ctx , lease .Name ).Status .Ended
1818+ }).WithTimeout (500 * time .Millisecond ).WithPolling (50 * time .Millisecond ).Should (BeTrue ())
1819+
1820+ updatedLease := getLease (ctx , lease .Name )
1821+ Expect (updatedLease .Labels ).To (HaveKeyWithValue (
1822+ string (jumpstarterdevv1alpha1 .LeaseLabelEnded ),
1823+ jumpstarterdevv1alpha1 .LeaseLabelEndedValue ,
1824+ ))
1825+
1826+ // Reconciling again should be a no-op
1827+ result := reconcileLease (ctx , lease )
1828+ Expect (result .RequeueAfter ).To (BeZero ())
1829+ })
1830+ })
1831+
1832+ When ("an ended lease is missing the ended label" , func () {
1833+ It ("should backfill the label on the next reconcile" , func () {
1834+ lease := leaseDutA2Sec .DeepCopy ()
1835+ lease .Spec .Duration = & metav1.Duration {Duration : 100 * time .Millisecond }
1836+
1837+ ctx := context .Background ()
1838+ Expect (k8sClient .Create (ctx , lease )).To (Succeed ())
1839+ _ = reconcileLease (ctx , lease )
1840+
1841+ Eventually (func () bool {
1842+ _ = reconcileLease (ctx , lease )
1843+ return getLease (ctx , lease .Name ).Status .Ended
1844+ }).WithTimeout (500 * time .Millisecond ).WithPolling (50 * time .Millisecond ).Should (BeTrue ())
1845+
1846+ // Simulate split-brain: remove the ended label as if metadata update failed
1847+ updatedLease := getLease (ctx , lease .Name )
1848+ delete (updatedLease .Labels , string (jumpstarterdevv1alpha1 .LeaseLabelEnded ))
1849+ Expect (k8sClient .Update (ctx , updatedLease )).To (Succeed ())
1850+
1851+ // Verify label is gone
1852+ updatedLease = getLease (ctx , lease .Name )
1853+ Expect (updatedLease .Labels ).NotTo (HaveKey (string (jumpstarterdevv1alpha1 .LeaseLabelEnded )))
1854+
1855+ // Reconcile should backfill the label
1856+ _ = reconcileLease (ctx , lease )
1857+ updatedLease = getLease (ctx , lease .Name )
1858+ Expect (updatedLease .Labels ).To (HaveKeyWithValue (
1859+ string (jumpstarterdevv1alpha1 .LeaseLabelEnded ),
1860+ jumpstarterdevv1alpha1 .LeaseLabelEndedValue ,
1861+ ))
1862+ })
1863+ })
1864+
17121865 // UpdateLease mutation tests
17131866 // Note: These tests simulate what UpdateLease does via gRPC by directly
17141867 // modifying the lease spec and calling ReconcileLeaseTimeFields
0 commit comments