Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 441f24d

Browse files
authored
Merge pull request #168 from michalskrivanek/endtime
support future leases, calculate effective begin/end/duration lease times
2 parents d2a9335 + cacdf9e commit 441f24d

13 files changed

Lines changed: 2096 additions & 263 deletions

File tree

api/v1alpha1/lease_helpers.go

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
2376
func 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{

api/v1alpha1/lease_types.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,26 @@ import (
2525
type LeaseSpec struct {
2626
// The client that is requesting the lease
2727
ClientRef corev1.LocalObjectReference `json:"clientRef"`
28-
// The desired duration of the lease
29-
Duration metav1.Duration `json:"duration"`
28+
// Duration of the lease. Must be positive when provided.
29+
// Can be omitted (nil) when both BeginTime and EndTime are provided,
30+
// in which case it's calculated as EndTime - BeginTime.
31+
Duration *metav1.Duration `json:"duration,omitempty"`
3032
// The selector for the exporter to be used
3133
Selector metav1.LabelSelector `json:"selector"`
3234
// The release flag requests the controller to end the lease now
3335
Release bool `json:"release,omitempty"`
36+
// Requested start time. If omitted, lease starts when exporter is acquired.
37+
// Immutable after lease starts (cannot change the past).
38+
BeginTime *metav1.Time `json:"beginTime,omitempty"`
39+
// Requested end time. If specified with BeginTime, Duration is calculated.
40+
// Can be updated to extend or shorten active leases.
41+
EndTime *metav1.Time `json:"endTime,omitempty"`
3442
}
3543

3644
// LeaseStatus defines the observed state of Lease
3745
type LeaseStatus struct {
3846
// If the lease has been acquired an exporter name is assigned
39-
// and then and then it can be used, it will be empty while still pending
47+
// and then it can be used, it will be empty while still pending
4048
BeginTime *metav1.Time `json:"beginTime,omitempty"`
4149
EndTime *metav1.Time `json:"endTime,omitempty"`
4250
ExporterRef *corev1.LocalObjectReference `json:"exporterRef,omitempty"`

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/lease_controller.go

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (r *LeaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
8282
return result, err
8383
}
8484

85-
if err := r.reconcileStatusBeginTime(ctx, &lease); err != nil {
85+
if err := r.reconcileStatusBeginEndTimes(ctx, &lease); err != nil {
8686
return result, err
8787
}
8888

@@ -141,34 +141,41 @@ func (r *LeaseReconciler) reconcileStatusEnded(
141141
lease.Release(ctx)
142142
return nil
143143
} else if lease.Status.BeginTime != nil {
144-
expiration := lease.Status.BeginTime.Add(lease.Spec.Duration.Duration)
144+
var expiration time.Time
145+
if lease.Spec.EndTime != nil {
146+
// expires at Spec.EndTime when specified
147+
expiration = lease.Spec.EndTime.Time
148+
} else if lease.Spec.BeginTime != nil && lease.Spec.Duration != nil {
149+
// expires at Spec.BeginTime + Spec.Duration - scheduled lease
150+
expiration = lease.Spec.BeginTime.Add(lease.Spec.Duration.Duration)
151+
} else if lease.Spec.Duration != nil {
152+
// expires at actual BeginTime + Spec.Duration - immediate lease
153+
expiration = lease.Status.BeginTime.Add(lease.Spec.Duration.Duration)
154+
}
155+
145156
if expiration.Before(now) {
146157
lease.Expire(ctx)
147158
return nil
148-
} else {
149-
result.RequeueAfter = expiration.Sub(now)
150-
return nil
151159
}
160+
result.RequeueAfter = expiration.Sub(now)
161+
return nil
152162
}
153163

154164
}
155165
return nil
156166
}
157167

158168
// nolint:unparam
159-
func (r *LeaseReconciler) reconcileStatusBeginTime(
169+
func (r *LeaseReconciler) reconcileStatusBeginEndTimes(
160170
ctx context.Context,
161171
lease *jumpstarterdevv1alpha1.Lease,
162172
) error {
163-
logger := log.FromContext(ctx)
164-
165-
now := time.Now()
166173
if lease.Status.BeginTime == nil && lease.Status.ExporterRef != nil {
174+
logger := log.FromContext(ctx)
167175
logger.Info("Updating begin time for lease", "lease", lease.Name, "exporter", lease.GetExporterName(), "client", lease.GetClientName())
176+
now := time.Now()
177+
lease.Status.BeginTime = &metav1.Time{Time: now}
168178
lease.SetStatusReady(true, "Ready", "An exporter has been acquired for the client")
169-
lease.Status.BeginTime = &metav1.Time{
170-
Time: now,
171-
}
172179
}
173180

174181
return nil
@@ -188,6 +195,20 @@ func (r *LeaseReconciler) reconcileStatusExporterRef(
188195
}
189196

190197
if lease.Status.ExporterRef == nil {
198+
// For scheduled leases: only assign exporter if requested BeginTime has arrived
199+
if lease.Spec.BeginTime != nil {
200+
now := time.Now()
201+
if lease.Spec.BeginTime.After(now) {
202+
// Requested BeginTime is in the future, wait until then
203+
waitDuration := lease.Spec.BeginTime.Sub(now)
204+
logger.Info("Lease is scheduled for the future, waiting",
205+
"lease", lease.Name,
206+
"requestedBeginTime", lease.Spec.BeginTime,
207+
"waitDuration", waitDuration)
208+
result.RequeueAfter = waitDuration
209+
return nil
210+
}
211+
}
191212
logger.Info("Looking for a matching exporter for lease", "lease", lease.Name, "client", lease.GetClientName(), "selector", lease.Spec.Selector)
192213

193214
selector, err := lease.GetExporterSelector()
@@ -338,7 +359,14 @@ func (r *LeaseReconciler) attachMatchingPolicies(ctx context.Context, lease *jum
338359
}
339360
if clientSelector.Matches(labels.Set(jclient.Labels)) {
340361
if p.MaximumDuration != nil {
341-
if lease.Spec.Duration.Duration > p.MaximumDuration.Duration {
362+
// Calculate requested duration (may be from explicit Duration or computed from times)
363+
requestedDuration := time.Duration(0)
364+
if lease.Spec.Duration != nil {
365+
requestedDuration = lease.Spec.Duration.Duration
366+
} else if lease.Spec.BeginTime != nil && lease.Spec.EndTime != nil {
367+
requestedDuration = lease.Spec.EndTime.Sub(lease.Spec.BeginTime.Time)
368+
}
369+
if requestedDuration > p.MaximumDuration.Duration {
342370
// TODO: we probably should keep this on the list of approved exporters
343371
// but mark as excessive duration so we can report it on the status
344372
// of lease if no other options exist

0 commit comments

Comments
 (0)