@@ -20,6 +20,59 @@ import (
2020 "sigs.k8s.io/controller-runtime/pkg/log"
2121)
2222
23+ // ReconcileLeaseTimeFields calculates missing time fields and validates consistency
24+ // between BeginTime, EndTime, and Duration. Modifies pointers in place.
25+ //
26+ // Supported lease specification patterns:
27+ // 1. Duration only (no BeginTime/EndTime): immediate start, runs for Duration
28+ // - BeginTime set by controller when exporter acquired
29+ // - EndTime = Status.BeginTime + Duration (calculated at runtime)
30+ //
31+ // 2. EndTime only: INVALID - cannot infer Duration without BeginTime or explicit Duration
32+ // - Returns error: "duration is required (must specify Duration, or both BeginTime and EndTime)"
33+ //
34+ // 3. BeginTime + Duration: scheduled start at BeginTime, runs for Duration
35+ // - Lease waits until BeginTime, then acquires exporter
36+ // - EndTime = BeginTime + Duration (calculated at runtime)
37+ //
38+ // 4. BeginTime + EndTime: scheduled window, Duration computed from times
39+ // - Duration = EndTime - BeginTime (auto-calculated here)
40+ // - Validates EndTime > BeginTime (positive duration)
41+ //
42+ // 5. EndTime + Duration: scheduled end, BeginTime computed as EndTime - Duration
43+ // - BeginTime = EndTime - Duration (auto-calculated here)
44+ // - Useful for "finish by" scheduling
45+ //
46+ // 6. BeginTime + EndTime + Duration: all three specified, validates consistency
47+ // - Validates Duration == EndTime - BeginTime
48+ // - Returns error if inconsistent: "duration conflicts with begin_time and end_time"
49+ //
50+ // Note: The controller never auto-populates Spec.EndTime. It calculates expiration time
51+ // on-demand from available fields, keeping Spec.EndTime meaningful only when explicitly
52+ // set by the user. See lease_controller.go reconcileStatusEnded for expiration logic.
53+ func ReconcileLeaseTimeFields (beginTime , endTime * * metav1.Time , duration * * metav1.Duration ) error {
54+ if * beginTime != nil && * endTime != nil {
55+ // Calculate duration from explicit begin/end times
56+ calculated := (* endTime ).Sub ((* beginTime ).Time )
57+ if * duration != nil && (* duration ).Duration > 0 && (* duration ).Duration != calculated {
58+ return fmt .Errorf ("duration conflicts with begin_time and end_time" )
59+ }
60+ * duration = & metav1.Duration {Duration : calculated }
61+ } else if * endTime != nil && * duration != nil && (* duration ).Duration > 0 {
62+ // Calculate BeginTime from EndTime - Duration (scheduled lease ending at specific time)
63+ * beginTime = & metav1.Time {Time : (* endTime ).Add (- (* duration ).Duration )}
64+ }
65+
66+ // Validate final duration is positive (rejects nil, negative, zero)
67+ if * duration == nil {
68+ return fmt .Errorf ("duration is required (must specify Duration, or both BeginTime and EndTime)" )
69+ }
70+ if (* duration ).Duration <= 0 {
71+ return fmt .Errorf ("duration must be positive, got %v" , (* duration ).Duration )
72+ }
73+ return nil
74+ }
75+
2376func LeaseFromProtobuf (
2477 req * cpb.Lease ,
2578 key types.NamespacedName ,
@@ -30,15 +83,33 @@ func LeaseFromProtobuf(
3083 return nil , err
3184 }
3285
86+ var beginTime , endTime * metav1.Time
87+ var duration * metav1.Duration
88+
89+ if req .BeginTime != nil {
90+ beginTime = & metav1.Time {Time : req .BeginTime .AsTime ()}
91+ }
92+ if req .EndTime != nil {
93+ endTime = & metav1.Time {Time : req .EndTime .AsTime ()}
94+ }
95+ if req .Duration != nil {
96+ duration = & metav1.Duration {Duration : req .Duration .AsDuration ()}
97+ }
98+ if err := ReconcileLeaseTimeFields (& beginTime , & endTime , & duration ); err != nil {
99+ return nil , err
100+ }
101+
33102 return & Lease {
34103 ObjectMeta : metav1.ObjectMeta {
35104 Namespace : key .Namespace ,
36105 Name : key .Name ,
37106 },
38107 Spec : LeaseSpec {
39108 ClientRef : clientRef ,
40- Duration : metav1. Duration { Duration : req . Duration . AsDuration ()} ,
109+ Duration : duration ,
41110 Selector : * selector ,
111+ BeginTime : beginTime ,
112+ EndTime : endTime ,
42113 },
43114 }, nil
44115}
@@ -60,22 +131,34 @@ func (l *Lease) ToProtobuf() *cpb.Lease {
60131 }
61132
62133 lease := cpb.Lease {
63- Name : fmt .Sprintf ("namespaces/%s/leases/%s" , l .Namespace , l .Name ),
64- Selector : metav1 .FormatLabelSelector (& l .Spec .Selector ),
65- Duration : durationpb .New (l .Spec .Duration .Duration ),
66- EffectiveDuration : durationpb .New (l .Spec .Duration .Duration ), // TODO: implement lease renewal
67- Client : ptr .To (fmt .Sprintf ("namespaces/%s/clients/%s" , l .Namespace , l .Spec .ClientRef .Name )),
68- Conditions : conditions ,
69- // TODO: implement scheduled leases
70- BeginTime : nil ,
71- EndTime : nil ,
134+ Name : fmt .Sprintf ("namespaces/%s/leases/%s" , l .Namespace , l .Name ),
135+ Selector : metav1 .FormatLabelSelector (& l .Spec .Selector ),
136+ Client : ptr .To (fmt .Sprintf ("namespaces/%s/clients/%s" , l .Namespace , l .Spec .ClientRef .Name )),
137+ Conditions : conditions ,
138+ }
139+ if l .Spec .Duration != nil {
140+ lease .Duration = durationpb .New (l .Spec .Duration .Duration )
72141 }
73142
143+ // Requested/planned times from Spec
144+ if l .Spec .BeginTime != nil {
145+ lease .BeginTime = timestamppb .New (l .Spec .BeginTime .Time )
146+ }
147+ if l .Spec .EndTime != nil {
148+ lease .EndTime = timestamppb .New (l .Spec .EndTime .Time )
149+ }
150+
151+ // Actual times from Status
74152 if l .Status .BeginTime != nil {
75153 lease .EffectiveBeginTime = timestamppb .New (l .Status .BeginTime .Time )
76- }
77- if l .Status .EndTime != nil {
78- lease .EffectiveEndTime = timestamppb .New (l .Status .EndTime .Time )
154+ endTime := time .Now ()
155+ if l .Status .EndTime != nil {
156+ endTime = l .Status .EndTime .Time
157+ lease .EffectiveEndTime = timestamppb .New (endTime )
158+ }
159+ // Final effective duration or current one so far while active. Non-negative to handle clock skew.
160+ effectiveDuration := max (endTime .Sub (l .Status .BeginTime .Time ), 0 )
161+ lease .EffectiveDuration = durationpb .New (effectiveDuration )
79162 }
80163 if l .Status .ExporterRef != nil {
81164 lease .Exporter = ptr .To (utils .UnparseExporterIdentifier (kclient.ObjectKey {
0 commit comments