Skip to content

Conversation

@michalskrivanek
Copy link
Contributor

@michalskrivanek michalskrivanek commented Oct 10, 2025

lease's endTime was incorrectly using beginTime. This is only for the leases that effectively ended already, so not really used. But still should be fixed...

Summary by CodeRabbit

  • New Features

    • Specs may include optional BeginTime, EndTime, and nullable Duration; API now emits spec values and computed effective begin/end times and durations.
    • Controller supports scheduled leases (future BeginTime), initializes BeginTime, marks leases Ready, and schedules expiration or waits as appropriate.
    • Update operations validate/reconcile time fields and prevent changing BeginTime after a lease has started.
  • Bug Fixes

    • Corrected EndTime handling and conditional Duration handling so lease retrieval and serialization return consistent times and durations.
  • Tests

    • Expanded tests for scheduled leases, timing interactions, edge cases, releases, protobuf serialization, and competing leases.

lease's endTime was incorrectly using beginTime. This is only for the leases that effectively ended already, so not really used. But still should be fixed...
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (6)
  • internal/protocol/jumpstarter/client/v1/client.pb.go is excluded by !**/*.pb.go
  • internal/protocol/jumpstarter/v1/common.pb.go is excluded by !**/*.pb.go
  • internal/protocol/jumpstarter/v1/jumpstarter.pb.go is excluded by !**/*.pb.go
  • internal/protocol/jumpstarter/v1/jumpstarter_grpc.pb.go is excluded by !**/*.pb.go
  • internal/protocol/jumpstarter/v1/kubernetes.pb.go is excluded by !**/*.pb.go
  • internal/protocol/jumpstarter/v1/router.pb.go is excluded by !**/*.pb.go

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds BeginTime and EndTime to LeaseSpec, makes Duration nullable, introduces ReconcileLeaseTimeFields and protobuf emission changes, fixes GetLease/RequestLease handling, forbids BeginTime changes after start, enhances reconciler for scheduled starts/expirations and requeueing, and expands controller tests for scheduled and edge cases.

Changes

Cohort / File(s) Summary
API types & deepcopy
api/v1alpha1/lease_types.go, api/v1alpha1/zz_generated.deepcopy.go
LeaseSpec.Duration changed from metav1.Duration to *metav1.Duration (omitempty); added BeginTime *metav1.Time and EndTime *metav1.Time; deepcopy updated to nil-guarded copies for Duration, BeginTime, EndTime.
API helpers & protobuf I/O
api/v1alpha1/lease_helpers.go
Added ReconcileLeaseTimeFields(beginTime, endTime **metav1.Time, duration **metav1.Duration) error; LeaseFromProtobuf initializes Spec Begin/End/Duration and calls reconciler; ToProtobuf emits Spec Begin/End/Duration (when present) and computes/emits EffectiveBeginTime/EffectiveEndTime/EffectiveDuration from Status.
Controller reconciliation & tests
internal/controller/lease_controller.go, internal/controller/lease_controller_test.go
reconcileStatusBeginTimereconcileStatusBeginEndTimes; reconciler supports scheduled future Spec.BeginTime (requeue), sets Status.BeginTime when acquiring exporter, computes expiration from Spec.EndTime or Spec.BeginTime+Duration or Status.BeginTime+Duration, and guards duration checks; tests add scheduling/edge-case coverage and use polling.
Service server bug fix
internal/service/controller_service.go
Fixed GetLease EndTime population to use Spec.EndTime.Time when present; response construction refactored; RequestLease now only sets spec.Duration when incoming req.Duration is non-nil.
Client service update flow
internal/service/client/v1/client_service.go
UpdateLease now conditionally updates BeginTime, EndTime, and pointer Duration; forbids changing BeginTime if lease already started (errors unless identical); calls ReconcileLeaseTimeFields before patching.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant ClientSvc as Client Service
  participant API as K8s API
  participant Helper as ReconcileLeaseTimeFields

  Client->>ClientSvc: UpdateLease(desired Spec: Begin/End/Duration?)
  ClientSvc->>API: GET Lease
  API-->>ClientSvc: Lease (Spec, Status)
  alt Lease already started & BeginTime changed
    ClientSvc-->>Client: error (cannot update BeginTime: lease has already started)
  else
    ClientSvc->>Helper: Reconcile(spec.BeginTime, spec.EndTime, spec.Duration)
    Helper-->>ClientSvc: ok / error
    alt ok
      ClientSvc->>API: PATCH Lease (updated Spec fields if provided)
      API-->>ClientSvc: Updated Lease
      ClientSvc-->>Client: success
    else
      ClientSvc-->>Client: error (inconsistent times)
    end
  end
Loading
sequenceDiagram
  autonumber
  participant Controller as LeaseReconciler
  participant API as K8s API
  participant Clock as Now()

  Controller->>API: Fetch Lease
  alt Spec.BeginTime exists && Spec.BeginTime > Now && no ExporterRef
    Controller->>Clock: now
    Controller-->>Controller: Requeue after (Spec.BeginTime - now)
  else
    opt Status.BeginTime nil
      Controller->>API: Set Status.BeginTime, mark Ready
    end
    Controller->>Clock: now
    alt Spec.EndTime set
      Controller-->>Controller: expiration = Spec.EndTime
    else Spec.BeginTime & Spec.Duration set
      Controller-->>Controller: expiration = Spec.BeginTime + Spec.Duration
    else Spec.Duration set
      Controller-->>Controller: expiration = Status.BeginTime + Spec.Duration
    end
    alt expiration > now
      Controller-->>Controller: RequeueAfter = expiration - now
    else
      Controller->>API: Expire lease (mark ended)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo
  • NickCao

Poem

"I hop to fix a mismatched end,
I stitch Begin and End into spec and send.
I guard the start from sideways change,
I requeue clocks across a scheduled range.
Thump-thump — the rabbit signs and grins." 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly captures the primary change by indicating support for future leases and the calculation of effective begin, end, and duration fields which aligns with the core functionality added in this PR.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my proposal

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
api/v1alpha1/lease_types.go (1)

34-37: Add validation and clarify precedence for BeginTime/EndTime vs Duration.

  • Validate invariants (e.g., EndTime >= BeginTime; and EndTime should not be before BeginTime+Duration if both provided, or define precedence/min logic).
  • Consider kubebuilder validation or an admission webhook, and document mutability (e.g., BeginTime immutable after start).
internal/controller/lease_controller.go (2)

144-156: Honor Spec.EndTime as an upper bound for expiration.

If EndTime is requested, expire no later than that.

-			expiration := beginTime.Add(lease.Spec.Duration.Duration)
+			expiration := beginTime.Add(lease.Spec.Duration.Duration)
+			if lease.Spec.EndTime != nil && lease.Spec.EndTime.Time.Before(expiration) {
+				expiration = lease.Spec.EndTime.Time
+			}

Ensure intended precedence between Duration and EndTime matches API expectations.


192-206: Set Pending("Scheduled") while waiting for future BeginTime.

Improves visibility to clients that the lease is scheduled.

-				result.RequeueAfter = waitDuration
+				lease.SetStatusPending("Scheduled", "Lease scheduled to begin at %s", lease.Spec.BeginTime.Time.Format(time.RFC3339))
+				result.RequeueAfter = waitDuration
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74fa578 and e48ccb6.

📒 Files selected for processing (5)
  • api/v1alpha1/lease_helpers.go (3 hunks)
  • api/v1alpha1/lease_types.go (1 hunks)
  • api/v1alpha1/zz_generated.deepcopy.go (1 hunks)
  • internal/controller/lease_controller.go (3 hunks)
  • internal/service/client/v1/client_service.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/controller/lease_controller.go (1)
api/v1alpha1/lease_types.go (1)
  • Lease (76-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: e2e-tests (ubuntu-24.04-arm)
  • GitHub Check: e2e-tests (ubuntu-24.04)
  • GitHub Check: e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
  • GitHub Check: lint-go
  • GitHub Check: tests
  • GitHub Check: deploy-kind
🔇 Additional comments (3)
api/v1alpha1/zz_generated.deepcopy.go (1)

500-507: DeepCopy for BeginTime/EndTime looks correct.

Nil-guarded pointer DeepCopy matches existing patterns. No issues.

api/v1alpha1/lease_helpers.go (1)

76-83: ToProtobuf time mapping looks good.

Emits planned Begin/End from Spec and effective times/duration from Status with a sensible fallback to now. LGTM.

Also applies to: 84-94

internal/controller/lease_controller.go (1)

85-88: Rename to reconcileStatusBeginEndTimes is clear.

Call-site update looks correct.

@michalskrivanek michalskrivanek changed the title use the right time in endTime support future leases, calculate effective begin/end/duration lease times Oct 10, 2025
@michalskrivanek
Copy link
Contributor Author

michalskrivanek commented Oct 11, 2025

# | BeginTime | EndTime | Duration       | Result                   | Use Case
1 | ❌ | ❌ | ✅ 1h                       | Immediate, duration=1h   | "I need device for 1 hour"
2 | ❌ | ✅ 5pm | ❌ 0                    | Immediate, ends at 5pm   | "I need device until 5pm"
3 | ✅ 2pm | ❌ | ✅ 1h                   | Scheduled 2pm-3pm        | "I need device from 2pm for 1 hour"
4 | ✅ 2pm | ✅ 5pm | ❌ 0                | Scheduled 2pm-5pm        | "I need device from 2pm to 5pm"
5 | ❌ | ✅ 5pm | ✅ 1h                   | Scheduled 4pm-5pm        | "I need 1-hour lease ending at 5pm" ✅
6 | ✅ 2pm | ✅ 5pm | ✅ 3h               | Scheduled 2pm-5pm (validated) | "I need device 2pm-5pm (confirm 3h)"

@mangelajo
Copy link
Member

# | BeginTime | EndTime | Duration       | Result                   | Use Case
1 | ❌ | ❌ | ✅ 1h                       | Immediate, duration=1h   | "I need device for 1 hour"
2 | ❌ | ✅ 5pm | ❌ 0                    | Immediate, ends at 5pm   | "I need device until 5pm"
3 | ✅ 2pm | ❌ | ✅ 1h                   | Scheduled 2pm-3pm        | "I need device from 2pm for 1 hour"
4 | ✅ 2pm | ✅ 5pm | ❌ 0                | Scheduled 2pm-5pm        | "I need device from 2pm to 5pm"
5 | ❌ | ✅ 5pm | ✅ 1h                   | Scheduled 4pm-5pm        | "I need 1-hour lease ending at 5pm" ✅
6 | ✅ 2pm | ✅ 5pm | ✅ 3h               | Scheduled 2pm-5pm (validated) | "I need device 2pm-5pm (confirm 3h)"

can you make this table part of the comments, or perhaps we should use it for the jumpstarter.dev documentation somewhere? :)

It was hard for me to think about some of those combinations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9e6c97 and 913d59c.

📒 Files selected for processing (1)
  • internal/controller/lease_controller_test.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/controller/lease_controller_test.go (1)
api/v1alpha1/lease_helpers.go (2)
  • LeaseFromProtobuf (47-86)
  • ReconcileLeaseTimeFields (26-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: e2e-tests (ubuntu-24.04-arm)
  • GitHub Check: e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
  • GitHub Check: tests
  • GitHub Check: e2e-tests (ubuntu-24.04)
  • GitHub Check: lint-go
  • GitHub Check: deploy-kind

@michalskrivanek michalskrivanek force-pushed the endtime branch 4 times, most recently from e7c838f to 7b67d4f Compare October 13, 2025 13:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
api/v1alpha1/lease_types.go (1)

31-31: Consider making Duration optional in the API surface

If Duration can be omitted (per comments), prefer a pointer with omitempty to avoid serializing 0 durations and to reflect optionality.

Example:

- Duration metav1.Duration `json:"duration"`
+ Duration *metav1.Duration `json:"duration,omitempty"`

This will require adapting call sites and ToProtobuf to handle nil safely.

🧹 Nitpick comments (4)
api/v1alpha1/lease_types.go (1)

36-41: BeginTime/EndTime mutability guarantees need enforcement across entry points

Docs say BeginTime is immutable after start and EndTime can be updated. Service layer enforces some of this; direct CR updates currently bypass it. Consider a validating webhook to enforce the same constraints when the CRD is used directly.

internal/controller/lease_controller.go (1)

197-211: Surface “scheduled” state to users

While requeueing until future BeginTime, consider setting Pending with a “Scheduled” reason to improve observability for clients (e.g., SetStatusPending("Scheduled", ...)).

api/v1alpha1/lease_helpers.go (2)

101-107: Only emit Duration when it’s specified (> 0)

ToProtobuf always sets Duration, producing a 0s duration when it’s unspecified (e.g., EndTime‑only). Prefer omitting it to signal “unspecified”.

Apply:

-	lease := cpb.Lease{
-		Name:       fmt.Sprintf("namespaces/%s/leases/%s", l.Namespace, l.Name),
-		Selector:   metav1.FormatLabelSelector(&l.Spec.Selector),
-		Duration:   durationpb.New(l.Spec.Duration.Duration),
-		Client:     ptr.To(fmt.Sprintf("namespaces/%s/clients/%s", l.Namespace, l.Spec.ClientRef.Name)),
-		Conditions: conditions,
-	}
+	lease := cpb.Lease{
+		Name:       fmt.Sprintf("namespaces/%s/leases/%s", l.Namespace, l.Name),
+		Selector:   metav1.FormatLabelSelector(&l.Spec.Selector),
+		Client:     ptr.To(fmt.Sprintf("namespaces/%s/clients/%s", l.Namespace, l.Spec.ClientRef.Name)),
+		Conditions: conditions,
+	}
+	if l.Spec.Duration.Duration > 0 {
+		lease.Duration = durationpb.New(l.Spec.Duration.Duration)
+	}

117-127: Inject clock for testability (optional)

EffectiveDuration uses time.Now(). Consider injecting a clock (e.g., controller‑runtime clock) to make tests deterministic and enable time travel without sleeps.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7c838f and 7b67d4f.

📒 Files selected for processing (6)
  • api/v1alpha1/lease_helpers.go (3 hunks)
  • api/v1alpha1/lease_types.go (1 hunks)
  • api/v1alpha1/zz_generated.deepcopy.go (1 hunks)
  • internal/controller/lease_controller.go (3 hunks)
  • internal/controller/lease_controller_test.go (6 hunks)
  • internal/service/client/v1/client_service.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/service/client/v1/client_service.go
  • api/v1alpha1/zz_generated.deepcopy.go
🧰 Additional context used
🧬 Code graph analysis (3)
internal/controller/lease_controller.go (1)
api/v1alpha1/lease_types.go (1)
  • Lease (80-86)
api/v1alpha1/lease_helpers.go (1)
api/v1alpha1/lease_types.go (2)
  • Lease (80-86)
  • LeaseSpec (25-42)
internal/controller/lease_controller_test.go (3)
internal/service/helpers.go (1)
  • MatchLabels (3-13)
api/v1alpha1/lease_types.go (1)
  • Lease (80-86)
api/v1alpha1/lease_helpers.go (2)
  • LeaseFromProtobuf (44-83)
  • ReconcileLeaseTimeFields (25-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: e2e-tests (ubuntu-24.04)
  • GitHub Check: e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
  • GitHub Check: e2e-tests (ubuntu-24.04-arm)
  • GitHub Check: lint-go
  • GitHub Check: deploy-kind
  • GitHub Check: tests
🔇 Additional comments (4)
api/v1alpha1/lease_types.go (1)

28-31: Duration doc vs validation mismatch (EndTime‑only)

Comment says Duration can be omitted if EndTime is specified (incl. “EndTime − EffectiveBeginTime”). However, ReconcileLeaseTimeFields (api/v1alpha1/lease_helpers.go Lines 23–42) rejects Duration <= 0 unless both BeginTime and EndTime are provided. gRPC path will reject EndTime‑only, but controller tests accept it via direct K8s create. Please align semantics (either allow EndTime‑only in validation, or update docs/tests).

internal/controller/lease_controller.go (1)

145-154: Correct expiration precedence (EndTime > Begin+Duration > Status.Begin+Duration)

This addresses the earlier bug and aligns with expected semantics. LGTM.

internal/controller/lease_controller_test.go (1)

805-840: EndTime‑only test conflicts with validation helpers

This test allows EndTime with Duration=0 and no BeginTime. ReconcileLeaseTimeFields (used by gRPC/service) rejects that (requires positive duration unless both Begin+End provided). Please align semantics across CR and gRPC (allow EndTime‑only in validator, or mark this path invalid and adjust the test once a webhook enforces validation).

api/v1alpha1/lease_helpers.go (1)

23-42: Clarify and reconcile timing semantics

Function enforces Duration > 0 unless both BeginTime and EndTime are provided (then it derives Duration). This makes EndTime‑only invalid via gRPC, while controller/tests accept it via direct CR. Decide and unify:

  • If EndTime‑only should be valid: relax the final check when EndTime != nil (allow Duration==0 to mean “until EndTime”).
  • If it must be invalid: update API docs/tests to reflect that Duration is required unless both Begin and End are set.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/service/controller_service.go (1)

612-621: Nil-deref risk: req.Duration may be nil

Calling AsDuration() on a nil req.Duration will panic. Guard it.

Apply this diff:

-        Spec: jumpstarterdevv1alpha1.LeaseSpec{
-            ClientRef: corev1.LocalObjectReference{
-                Name: client.Name,
-            },
-            Duration: &metav1.Duration{Duration: req.Duration.AsDuration()},
-            Selector: metav1.LabelSelector{
-                MatchLabels:      matchLabels,
-                MatchExpressions: matchExpressions,
-            },
-        },
+        Spec: jumpstarterdevv1alpha1.LeaseSpec{
+            ClientRef: corev1.LocalObjectReference{
+                Name: client.Name,
+            },
+            Selector: metav1.LabelSelector{
+                MatchLabels:      matchLabels,
+                MatchExpressions: matchExpressions,
+            },
+        },
     }
+    if req.Duration != nil {
+        lease.Spec.Duration = &metav1.Duration{Duration: req.Duration.AsDuration()}
+    }
🧹 Nitpick comments (5)
api/v1alpha1/lease_types.go (1)

28-41: API shape LGTM; add validation/immutability guards

Consider enforcing invariants via validation:

  • Reject zero/negative duration; ensure EndTime > BeginTime when both set.
  • Make BeginTime immutable after start; allow EndTime edits for extend/shorten.
    Implement via admission webhook or CRD XValidation where feasible.
internal/controller/lease_controller.go (1)

145-154: Defensive guard for unset expiration

If no EndTime and no Duration, expiration stays zero and the lease would end immediately. Add a zero check to avoid accidental expiry.

-           if expiration.Before(now) {
+           if expiration.IsZero() {
+               return nil
+           }
+           if expiration.Before(now) {
                lease.Expire(ctx)
                return nil
            }
            result.RequeueAfter = expiration.Sub(now)
            return nil

Also applies to: 160-162

internal/controller/lease_controller_test.go (1)

757-766: Slightly increase timeouts or avoid Truncate to reduce flakes

Using Truncate(time.Second) plus ~1s timeouts is tight under CI jitter. Consider bumping the Eventually timeout (e.g., +500ms) or avoid truncation where not required.

api/v1alpha1/lease_helpers.go (2)

23-43: Consider adding the 6-case matrix as function documentation.

The function logic is sound, but as mentioned in past review comments, adding the 6-case matrix from the PR description would improve maintainability and help future developers understand the expected behavior for different input combinations.

Example documentation structure:

 // ReconcileLeaseTimeFields calculates missing time fields and validates consistency
 // between BeginTime, EndTime, and Duration. Modifies pointers in place.
+//
+// Handles six cases:
+// 1. Duration only (no begin/end): immediate start, runs for duration
+// 2. EndTime only: invalid - cannot infer duration without BeginTime or explicit Duration
+// 3. BeginTime + Duration: scheduled start at BeginTime, runs for duration
+// 4. BeginTime + EndTime: scheduled window, duration computed from times
+// 5. EndTime + Duration: scheduled end, BeginTime computed as EndTime - Duration
+// 6. BeginTime + EndTime + Duration: validates all three are consistent
 func ReconcileLeaseTimeFields(beginTime, endTime **metav1.Time, duration **metav1.Duration) error {

38-41: Consider clarifying the error message for nil duration.

The error message "duration must be positive" is technically correct but might be clearer if it distinguishes between missing and non-positive duration values.

-	if *duration == nil || (*duration).Duration <= 0 {
-		return fmt.Errorf("duration must be positive")
+	if *duration == nil {
+		return fmt.Errorf("duration is required")
+	}
+	if (*duration).Duration <= 0 {
+		return fmt.Errorf("duration must be positive, got %v", (*duration).Duration)
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b67d4f and 250bd4f.

📒 Files selected for processing (7)
  • api/v1alpha1/lease_helpers.go (3 hunks)
  • api/v1alpha1/lease_types.go (1 hunks)
  • api/v1alpha1/zz_generated.deepcopy.go (1 hunks)
  • internal/controller/lease_controller.go (4 hunks)
  • internal/controller/lease_controller_test.go (9 hunks)
  • internal/service/client/v1/client_service.go (1 hunks)
  • internal/service/controller_service.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/service/client/v1/client_service.go
🧰 Additional context used
🧬 Code graph analysis (4)
internal/controller/lease_controller.go (1)
api/v1alpha1/lease_types.go (1)
  • Lease (80-86)
internal/service/controller_service.go (2)
internal/protocol/jumpstarter/v1/kubernetes.pb.go (6)
  • Time (140-146)
  • Time (159-159)
  • Time (174-176)
  • LabelSelector (87-93)
  • LabelSelector (106-106)
  • LabelSelector (121-123)
internal/protocol/jumpstarter/v1/jumpstarter.pb.go (3)
  • GetLeaseResponse (1168-1178)
  • GetLeaseResponse (1191-1191)
  • GetLeaseResponse (1206-1208)
internal/controller/lease_controller_test.go (2)
api/v1alpha1/lease_types.go (1)
  • Lease (80-86)
api/v1alpha1/lease_helpers.go (2)
  • LeaseFromProtobuf (45-84)
  • ReconcileLeaseTimeFields (25-43)
api/v1alpha1/lease_helpers.go (1)
api/v1alpha1/lease_types.go (2)
  • Lease (80-86)
  • LeaseSpec (25-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: e2e-tests (ubuntu-24.04-arm)
  • GitHub Check: e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
  • GitHub Check: e2e-tests (ubuntu-24.04)
  • GitHub Check: deploy-kind
  • GitHub Check: lint-go
  • GitHub Check: tests
🔇 Additional comments (10)
api/v1alpha1/zz_generated.deepcopy.go (1)

498-511: DeepCopy for pointer fields is correct

Nil-safe copies for Duration, BeginTime, EndTime look good and avoid aliasing.

internal/service/controller_service.go (2)

536-537: Correct EndTime source fixed

Using lease.Status.EndTime for endTime is the right fix.


566-576: Response construction improvement is clean

Building resp and setting Duration only when non‑nil is correct.

internal/controller/lease_controller.go (3)

169-179: Setting Status.BeginTime on acquisition is correct

Updates begin time once exporter is assigned and marks Ready. Good.


199-211: Scheduled lease gating is correct

Requeues until Spec.BeginTime, preventing premature exporter assignment. Good.


361-368: Nil-safe policy duration check

Guarding both MaximumDuration and Spec.Duration prevents nil deref and false comparisons. LGTM.

internal/controller/lease_controller_test.go (1)

112-131: Replaced fixed sleeps with Eventually—nice

This makes tests faster and less flaky. Good use of polling and shorter durations.

api/v1alpha1/lease_helpers.go (3)

26-32: Verify case 4 behavior matches intended design.

The PR description states case 4 (BeginTime + EndTime without Duration) should be "invalid (duration computed as 0)". However, the current implementation computes the duration as EndTime - BeginTime (Line 28), which for the example "2pm to 5pm" would be 3 hours (positive), passing validation at Line 39.

Please confirm whether:

  1. Case 4 should be accepted (current behavior), or
  2. The code should reject providing both BeginTime and EndTime without an explicit Duration, or
  3. The PR matrix describes pre-fix behavior and is now outdated

55-82: LGTM! Properly parses and reconciles time fields.

The refactored logic correctly:

  • Parses BeginTime, EndTime, and Duration from protobuf when present
  • Calls ReconcileLeaseTimeFields to ensure internal consistency and derive missing fields
  • Assigns the reconciled values to Lease.Spec
  • Propagates reconciliation errors appropriately

The earlier issue with missing EndTime parsing (noted in past review comments) has been addressed.


102-130: LGTM! Well-designed distinction between requested and actual times.

The protobuf serialization correctly distinguishes between:

  • Spec times (Lines 108-118): Requested/planned BeginTime, EndTime, and Duration
  • Effective times (Lines 121-130): Actual Status.BeginTime, Status.EndTime, and computed EffectiveDuration

This dual representation allows API consumers to understand both the original request and what actually occurred. The computation of EffectiveDuration (Line 129) correctly handles:

  • Active leases: uses time.Now() to show current elapsed time
  • Completed leases: uses Status.EndTime to show final duration

The bug mentioned in the PR description (endTime incorrectly using beginTime) has been properly fixed at Line 125.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
internal/service/client/v1/client_service.go (1)

222-234: Fix BeginTime update check and avoid unintended clears; gate updates by request field presence

  • Pointer comparison (Line 223) is wrong; different pointers with same timestamp will be treated as changes.
  • Omitting fields in the request currently clears Spec fields (Lines 227-229), which can break reconciliation (e.g., nil Duration).
  • Update BeginTime/EndTime/Duration only when explicitly present in the request; compare times, not pointers.

Apply this diff in this block:

-	// BeginTime can only be updated before lease starts
-	if jlease.Status.ExporterRef != nil && desired.Spec.BeginTime != jlease.Spec.BeginTime {
-		return nil, fmt.Errorf("cannot update BeginTime: lease has already started")
-	}
-
-	jlease.Spec.BeginTime = desired.Spec.BeginTime
-	jlease.Spec.Duration = desired.Spec.Duration
-	jlease.Spec.EndTime = desired.Spec.EndTime
+	// BeginTime can only be updated before lease starts; only if explicitly provided
+	if req.Lease.BeginTime != nil {
+		if jlease.Status.ExporterRef != nil {
+			// value-compare to avoid pointer inequality false positives
+			if jlease.Spec.BeginTime == nil || !jlease.Spec.BeginTime.Equal(desired.Spec.BeginTime) {
+				return nil, fmt.Errorf("cannot update BeginTime: lease has already started")
+			}
+		}
+		jlease.Spec.BeginTime = desired.Spec.BeginTime
+	}
+	// Update Duration only if provided; preserve existing otherwise
+	if req.Lease.Duration != nil {
+		jlease.Spec.Duration = desired.Spec.Duration
+	}
+	// Update EndTime only if provided; preserve existing otherwise
+	if req.Lease.EndTime != nil {
+		jlease.Spec.EndTime = desired.Spec.EndTime
+	}

This prevents accidental clears and enforces BeginTime immutability after start without blocking no-op updates.

🧹 Nitpick comments (4)
internal/controller/lease_controller.go (2)

144-154: Defensive: handle “no expiration computed” explicitly

If neither Spec.EndTime nor any Duration is set, expiration stays zero and the lease will be expired immediately. If you want to tolerate such CRs (e.g., misconfigurations outside gRPC), guard it:

-			if expiration.Before(now) {
+			if expiration.IsZero() {
+				// No expiration computed; do not end or requeue
+				return nil
+			}
+			if expiration.Before(now) {
 				lease.Expire(ctx)
 				return nil
 			}
 			result.RequeueAfter = expiration.Sub(now)
 			return nil

Optional, since service-level validation normally prevents this state.

Also applies to: 160-162


361-368: Enforce MaximumDuration when BeginTime+EndTime are provided (Duration nil)

Current check only applies when Spec.Duration is set. If the request uses BeginTime+EndTime with nil Duration, MaximumDuration is skipped. Consider deriving requested duration:

-							if p.MaximumDuration != nil && lease.Spec.Duration != nil {
-								if lease.Spec.Duration.Duration > p.MaximumDuration.Duration {
+							// Determine requested duration: explicit Duration or EndTime-BeginTime
+							var reqDur *time.Duration
+							if lease.Spec.Duration != nil {
+								d := lease.Spec.Duration.Duration
+								reqDur = &d
+							} else if lease.Spec.BeginTime != nil && lease.Spec.EndTime != nil {
+								d := lease.Spec.EndTime.Sub(lease.Spec.BeginTime.Time)
+								if d > 0 {
+									reqDur = &d
+								}
+							}
+							if p.MaximumDuration != nil && reqDur != nil {
+								if *reqDur > p.MaximumDuration.Duration {
 									// TODO: we probably should keep this on the list of approved exporters
 									// but mark as excessive duration so we can report it on the status
 									// of lease if no other options exist
 									continue
 								}
 							}
api/v1alpha1/lease_helpers.go (2)

54-74: Tight reconciliation logic; consider richer conflict/error messages.

Logic is solid and matches the documented cases. Optionally improve debuggability by including values in the conflict error and by being explicit when end <= begin.

Apply this diff to enhance errors:

-		calculated := (*endTime).Sub((*beginTime).Time)
-		if *duration != nil && (*duration).Duration > 0 && (*duration).Duration != calculated {
-			return fmt.Errorf("duration conflicts with begin_time and end_time")
-		}
+		calculated := (*endTime).Sub((*beginTime).Time)
+		if calculated <= 0 {
+			return fmt.Errorf("end_time must be after begin_time (begin=%s, end=%s)", (*beginTime).Time.Format(time.RFC3339), (*endTime).Time.Format(time.RFC3339))
+		}
+		if *duration != nil && (*duration).Duration > 0 && (*duration).Duration != calculated {
+			return fmt.Errorf("duration %v conflicts with begin_time=%s and end_time=%s",
+				(*duration).Duration,
+				(*beginTime).Time.Format(time.RFC3339),
+				(*endTime).Time.Format(time.RFC3339))
+		}

151-161: Clamp EffectiveDuration to non-negative to resist clock skew.

If controller time is slightly behind Status.BeginTime (NTP skew), endTime.Sub(begin) can be negative. Safer to clamp at zero.

Apply this diff:

-		// Final effective duration or current duration while active
-		lease.EffectiveDuration = durationpb.New(endTime.Sub(l.Status.BeginTime.Time))
+		// Final effective duration or current duration while active; clamp to >= 0 for clock skew safety
+		if dur := endTime.Sub(l.Status.BeginTime.Time); dur > 0 {
+			lease.EffectiveDuration = durationpb.New(dur)
+		} else {
+			lease.EffectiveDuration = durationpb.New(0)
+		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 250bd4f and 35b8498.

📒 Files selected for processing (7)
  • api/v1alpha1/lease_helpers.go (3 hunks)
  • api/v1alpha1/lease_types.go (1 hunks)
  • api/v1alpha1/zz_generated.deepcopy.go (1 hunks)
  • internal/controller/lease_controller.go (4 hunks)
  • internal/controller/lease_controller_test.go (9 hunks)
  • internal/service/client/v1/client_service.go (1 hunks)
  • internal/service/controller_service.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/v1alpha1/zz_generated.deepcopy.go
🧰 Additional context used
🧬 Code graph analysis (5)
internal/controller/lease_controller.go (1)
api/v1alpha1/lease_types.go (1)
  • Lease (80-86)
internal/service/controller_service.go (2)
internal/protocol/jumpstarter/v1/kubernetes.pb.go (6)
  • Time (140-146)
  • Time (159-159)
  • Time (174-176)
  • LabelSelector (87-93)
  • LabelSelector (106-106)
  • LabelSelector (121-123)
internal/protocol/jumpstarter/v1/jumpstarter.pb.go (3)
  • GetLeaseResponse (1168-1178)
  • GetLeaseResponse (1191-1191)
  • GetLeaseResponse (1206-1208)
internal/service/client/v1/client_service.go (1)
api/v1alpha1/lease_helpers.go (1)
  • ReconcileLeaseTimeFields (53-74)
api/v1alpha1/lease_helpers.go (1)
api/v1alpha1/lease_types.go (2)
  • Lease (80-86)
  • LeaseSpec (25-42)
internal/controller/lease_controller_test.go (1)
api/v1alpha1/lease_helpers.go (2)
  • LeaseFromProtobuf (76-115)
  • ReconcileLeaseTimeFields (53-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: deploy-kind
  • GitHub Check: e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
  • GitHub Check: e2e-tests (ubuntu-24.04-arm)
  • GitHub Check: lint-go
  • GitHub Check: e2e-tests (ubuntu-24.04)
  • GitHub Check: tests
🔇 Additional comments (8)
internal/service/controller_service.go (1)

535-537: LGTM: EndTime fix and conditional Duration are correct

  • Correctly populates EndTime from Status.EndTime.
  • Duration is included only when set, matching nullable spec.

Also applies to: 566-576

api/v1alpha1/lease_types.go (1)

28-31: Docs/semantics look good—clarify EndTime-only acceptance across paths

The nullable Duration and added BeginTime/EndTime align with behavior. One clarification: gRPC path validates Duration or Begin+End, while the controller tolerates EndTime-only (immediate until EndTime). Consider documenting whether EndTime-only is supported at the API level or only via direct CRD usage, for consistency.

Also applies to: 36-41

internal/controller/lease_controller.go (1)

197-211: LGTM: proper gating for future BeginTime

Requeues until requested BeginTime arrives, avoiding premature exporter assignment. Good.

internal/controller/lease_controller_test.go (1)

125-131: Tests: good move to Eventually and thorough scheduling coverage

  • Replacing fixed sleeps with Eventually improves reliability and speed.
  • Scenarios for BeginTime/EndTime/Duration interactions are well covered.

Also applies to: 737-766, 842-874

api/v1alpha1/lease_helpers.go (4)

23-53: Great addition: clear spec matrix in code.

The embedded 6-case matrix makes the logic easy to follow and aligns with the PR discussion.


86-101: Protobuf → API parsing and reconciliation look correct.

Begin/End/Duration are parsed, then reconciled to ensure consistency. This enables the supported patterns and blocks invalid ones as intended.


133-141: Good: Duration emitted only when specified/derived in Spec.

Name, Selector, Client, Conditions, and Spec.Duration serialization look correct.


143-150: Spec times serialization LGTM.

BeginTime/EndTime are emitted only when present in Spec, keeping Spec.EndTime meaningful when explicitly set.

…ease requests

This effectively implements scheduled leases.
- set EffectiveBeginTime once the lease is aquired, optionally wait with acquisition for after the BeginTime passes.
- populate the EffectiveDuration before lease is ended so that clients see the duration so far (equivalent to: now - EffectiveBeginTime).
- add BeginTime initialization to CreateLease request.
- allow updating BeginTime by UpdateLease before the lease is acquired
- lease can be created with acombination of BeginTime/EndTime/Duration, where missing BeginTime means "immediately".

When querying leases that are pending or active the clients can see:
BeginTime - if requested in CreateLease
EffectiveBeginTime - the time lease become active. Either immediately or at(after) the requested BeginTime
Duration - as requested
EffectiveDuration - calculated, could be affected by subsequent lease extenstions as well as it could be shorten by acquisition happening later than requested in BeginTime
validation and behavior tests for Lease creation with BeginTime,EndTime,Duration
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
api/v1alpha1/lease_helpers.go (1)

53-74: Time-field reconciliation is consistent with the 6-case matrix

Rules for Begin/End/Duration cover all combinations, including validation for conflicts and positivity. LGTM.

🧹 Nitpick comments (5)
internal/service/controller_service.go (1)

566-576: Populate Duration conditionally — consider deriving when absent

Current logic sets Duration only if Spec.Duration is set. If users specify BeginTime+EndTime (with nil Duration), consider also deriving Duration = EndTime−BeginTime for completeness in the response.

api/v1alpha1/lease_types.go (1)

28-41: API shape is good; consider CRD validations/immutability hints

  • Duration as pointer with omitempty is right.
  • Consider adding validation to reject zero/negative durations at the CRD level (+webhook or kubebuilder markers).
  • If BeginTime must be immutable after start, a validation webhook check would help enforce it.
internal/controller/lease_controller.go (1)

144-162: Expiration logic is sound; consider early end when EndTime already past before start

Currently, if Spec.EndTime is in the past and the lease hasn’t started, we still acquire and then immediately expire. Consider ending immediately without acquiring to avoid transient exporter assignment:

+        // End immediately if EndTime already in the past and we haven't started
+        if lease.Spec.EndTime != nil && lease.Status.BeginTime == nil &&
+           (lease.Spec.BeginTime == nil || !lease.Spec.BeginTime.After(now)) &&
+           lease.Spec.EndTime.Time.Before(now) {
+            lease.Expire(ctx)
+            return nil
+        }

This reduces churn and avoids momentary exporter flips.

internal/controller/lease_controller_test.go (1)

1101-1131: Good switch to Eventually; pad tight timeouts to reduce CI flakes

Some waits (e.g., 1200ms for 1s events) are tight. Consider bumping timeouts to 1.5–2s in those cases to reduce flakiness on slow CI.

Also applies to: 1535-1548, 1650-1655

api/v1alpha1/lease_helpers.go (1)

133-149: ToProtobuf: includes requested and effective times

Emitting Spec times and effective Status-derived times is useful. After fixing the clamp above, this is solid.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35b8498 and a5e3403.

📒 Files selected for processing (7)
  • api/v1alpha1/lease_helpers.go (3 hunks)
  • api/v1alpha1/lease_types.go (1 hunks)
  • api/v1alpha1/zz_generated.deepcopy.go (1 hunks)
  • internal/controller/lease_controller.go (4 hunks)
  • internal/controller/lease_controller_test.go (9 hunks)
  • internal/service/client/v1/client_service.go (1 hunks)
  • internal/service/controller_service.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/service/client/v1/client_service.go
🧰 Additional context used
🧬 Code graph analysis (4)
internal/controller/lease_controller_test.go (1)
api/v1alpha1/lease_helpers.go (2)
  • LeaseFromProtobuf (76-115)
  • ReconcileLeaseTimeFields (53-74)
api/v1alpha1/lease_helpers.go (1)
api/v1alpha1/lease_types.go (2)
  • Lease (80-86)
  • LeaseSpec (25-42)
internal/controller/lease_controller.go (1)
api/v1alpha1/lease_types.go (1)
  • Lease (80-86)
internal/service/controller_service.go (2)
internal/protocol/jumpstarter/v1/kubernetes.pb.go (6)
  • Time (140-146)
  • Time (159-159)
  • Time (174-176)
  • LabelSelector (87-93)
  • LabelSelector (106-106)
  • LabelSelector (121-123)
internal/protocol/jumpstarter/v1/jumpstarter.pb.go (3)
  • GetLeaseResponse (1168-1178)
  • GetLeaseResponse (1191-1191)
  • GetLeaseResponse (1206-1208)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: tests
  • GitHub Check: e2e-tests-28d6b1cc3b49ab9ae176918ab9709a2e2522c97e
  • GitHub Check: lint-go
  • GitHub Check: e2e-tests (ubuntu-24.04)
  • GitHub Check: e2e-tests (ubuntu-24.04-arm)
  • GitHub Check: deploy-kind
🔇 Additional comments (7)
api/v1alpha1/zz_generated.deepcopy.go (1)

498-512: DeepCopy for pointer fields looks correct

Nil-guarded allocations for Duration/BeginTime/EndTime are proper and consistent with controller-gen patterns. LGTM.

internal/service/controller_service.go (2)

536-537: Bugfix: EndTime now sourced from Status.EndTime

Using lease.Status.EndTime.Time (instead of BeginTime) is the correct fix. LGTM.


621-623: RequestLease: Duration set only when provided

This change aligns with making Duration optional and avoids forcing zero durations. LGTM.

internal/controller/lease_controller.go (3)

169-179: Begin time stamping on first acquisition

Setting Status.BeginTime when ExporterRef appears and marking Ready is correct and minimal. LGTM.


198-211: Scheduled leases: defer acquisition until BeginTime

Gating exporter assignment on future BeginTime and using RequeueAfter is correct and prevents premature allocation. LGTM.


363-369: Policy MaximumDuration guard handles all patterns

Computing requestedDuration from either Spec.Duration or EndTime−BeginTime avoids nil derefs and enforces caps. LGTM.

api/v1alpha1/lease_helpers.go (1)

86-101: LeaseFromProtobuf: good—ensures consistency before persisting

Parsing all three fields and calling ReconcileLeaseTimeFields avoids invalid specs, and sets Begin/End/Duration coherently. LGTM.

Also applies to: 108-114

_ = reconcileLease(ctx, lease2)
updatedLease = getLease(ctx, lease2Name)
return updatedLease.Status.ExporterRef != nil
}).WithTimeout(2500 * time.Millisecond).WithPolling(50 * time.Millisecond).Should(BeTrue())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice improvements :)

Copy link
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some of the tests belong to client_service tests, but we can reorg that later.

It looks great

@mangelajo mangelajo merged commit 441f24d into jumpstarter-dev:main Oct 14, 2025
8 checks passed
@michalskrivanek michalskrivanek deleted the endtime branch October 14, 2025 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants