Skip to content

feat: add lockOnly support for sentinel cluster-lock jobs - #89

Merged
MML-coder merged 2 commits into
mainfrom
feat/lock-only
May 19, 2026
Merged

feat: add lockOnly support for sentinel cluster-lock jobs#89
MML-coder merged 2 commits into
mainfrom
feat/lock-only

Conversation

@MML-coder

@MML-coder MML-coder commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds lockOnly boolean field to FournosJob CRD — sentinel jobs that hold cluster-slot quota without running a pipeline
  • Adds lockOnly validation and lock workload creation in lifecycle.py (skips Forge/execution engine resolution)
  • Adds early return in execution.py::reconcile_admitted() so lockOnly jobs stay Admitted indefinitely
  • Removes executionEngine from CRD required fields (lockOnly jobs don't need one)

Context: The hearth controller (cluster lifecycle operator) creates sentinel FournosJobs with lockOnly: true to implement human-driven cluster locking. Without this change, the execution controller tries to create a PipelineRun for the sentinel and fails with "Failed to fetch Pipeline".

Test plan

  • Verify existing integration tests still pass
  • Lock a cluster via hearth: oc patch fournoscluster <name> -n hearth --type=merge -p '{"spec":{"owner":"test"}}'
  • Verify sentinel FournosJob transitions to Admitted (not Failed)
  • Verify Kueue Workload is created and admitted
  • Verify new jobs targeting the locked cluster stay Pending
  • Unlock cluster: oc patch fournoscluster <name> -n hearth --type=merge -p '{"spec":{"owner":""}}'
  • Verify pending jobs get admitted after unlock

Summary by CodeRabbit

  • New Features

    • Added support for "lock-only" jobs that reserve cluster resources without executing pipelines.
    • Jobs with lockOnly mode require cluster configuration and remain in Admitted phase until deletion.
  • Chores

    • Updated CRD schema to support lock-only job mode with updated field requirements.

Review Change Stack

Hearth creates sentinel FournosJobs with lockOnly: true to hold
cluster-slot quota for human-driven cluster ownership. Without this,
the execution controller tries to create a PipelineRun and fails.

- CRD: add lockOnly boolean field, remove executionEngine from required
- lifecycle: validate lockOnly, create lock Workload skipping Forge
- execution: early return in reconcile_admitted for lockOnly jobs
@openshift-ci

openshift-ci Bot commented May 14, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign sjmonson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@MML-coder has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 20 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eec96fbe-17f6-4520-a0f8-045c0c342c0e

📥 Commits

Reviewing files that changed from the base of the PR and between 31d6467 and c220a29.

📒 Files selected for processing (1)
  • fournos/handlers/lifecycle.py
📝 Walkthrough

Walkthrough

The PR introduces a lock-only mode for FournosJob that reserves exclusive cluster capacity via Kueue without launching pipelines. The CRD schema adds a spec.lockOnly field and relaxes executionEngine requirement. The lifecycle handler validates the mode and creates exclusive Kueue Workloads. The execution handler short-circuits admitted lock-only jobs, avoiding pipeline creation.

Changes

Lock-Only Mode for FournosJob

Layer / File(s) Summary
CRD Contract for Lock-Only Mode
manifests/crd.yaml
spec adds explicit required: [] (allowing jobs without executionEngine), introduces spec.lockOnly boolean field (default false) with documentation indicating exclusive cluster-locking behavior that skips execution-engine resolution and PipelineRun creation.
Lifecycle Handler: Lock Workload Creation and Validation
fournos/handlers/lifecycle.py
Kubernetes client imports updated to use k8s_client alias; imports extended to include owner_ref for workload metadata. Validation added: lockOnly=true requires cluster, non-lockOnly jobs require spec.executionEngine. New _create_lock_workload helper creates exclusive Kueue Workloads with idempotent 409 handling and sets Job to PENDING with lock-admission message. ApiException handling updated in cluster-listing and exclusive-locker-querying code paths to use k8s_client.exceptions.ApiException.
Execution Handler: Lock-Only Short-Circuit
fournos/handlers/execution.py
reconcile_admitted detects spec.lockOnly=true and returns early after setting "Cluster lock held" status message, bypassing kubeconfig resolution and PipelineRun creation. Non-lock-only jobs continue normal reconciliation flow.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A lock to hold the cluster tight,
No pipeline runs, just slots to fight!
Kueue keeps the queue in line,
While lock-only jobs reserved shine.
Watch them wait in PENDING grace,
Till their lock can find its place! 🔐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding support for a lockOnly feature to create sentinel cluster-lock jobs that hold quota without running pipelines.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lock-only

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
fournos/handlers/lifecycle.py (2)

1-269: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix ruff format lint failure.

CI is failing on ruff format --check. Run ruff format fournos/handlers/lifecycle.py (or ruff format fournos/ tests/) before pushing so the lint job stays green.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fournos/handlers/lifecycle.py` around lines 1 - 269, The file fails ruff
format checks; run `ruff format fournos/handlers/lifecycle.py` (or the
project-wide `ruff format`) and reformat the file, then re-run the linter before
committing; ensure the formatted changes cover the top-level functions and
helpers such as on_create, _create_lock_workload, _find_exclusive_locker,
_pending_status, and reconcile_pending so imports, spacing, and string
concatenations match ruff's style rules and the CI `ruff format --check` job
passes.

53-88: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Edge case: lockOnly: true combined with exclusive: false is silently inconsistent.

When a user submits lockOnly: true with exclusive: false explicitly set, the validation at lines 55-63 passes, the LABEL_EXCLUSIVE_CLUSTER label at line 83-84 is not applied, but _create_lock_workload unconditionally passes exclusive=True to Kueue (line 104). This produces:

  1. A Workload that exclusively locks the cluster slot, but
  2. No LABEL_EXCLUSIVE_CLUSTER label on the FournosJob, so _find_exclusive_locker (line 144) cannot identify it as the locker for other pending jobs.

The CRD description states lockOnly "Implies exclusive: true", so enforce that invariant explicitly in validation (or normalize it) so the label and the workload stay in sync.

🛡️ Proposed fix — force exclusive=True when lockOnly is set
     cluster = spec.get("cluster")
     exclusive = spec["exclusive"]
     lock_only = spec.get("lockOnly", False)
 
     if lock_only and not cluster:
         patch.status["phase"] = Phase.FAILED
         patch.status["message"] = "lockOnly: true requires 'cluster' to be set"
         return
 
     if not lock_only and not spec.get("executionEngine"):
         patch.status["phase"] = Phase.FAILED
         patch.status["message"] = "spec.executionEngine is required for non-lockOnly jobs"
         return
 
+    # lockOnly implies exclusive (per CRD contract).
+    if lock_only:
+        exclusive = True
+
     if exclusive and not cluster:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fournos/handlers/lifecycle.py` around lines 53 - 88, The validation allows
lock_only True while exclusive is False, but _create_lock_workload always
creates an exclusive Kueue Workload and LABEL_EXCLUSIVE_CLUSTER isn't applied,
breaking _find_exclusive_locker; fix by normalizing/enforcing exclusive when
lock_only is set: when lock_only (spec.get("lockOnly")) is True, set exclusive =
True (or set spec["exclusive"]=True) and ensure patch.meta.setdefault("labels",
{})[LABEL_EXCLUSIVE_CLUSTER] = cluster is applied (reuse LABEL_EXCLUSIVE_CLUSTER
and the same branch that currently adds the label), so the label and created
exclusive workload stay in sync with _create_lock_workload and
_find_exclusive_locker.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@fournos/handlers/lifecycle.py`:
- Around line 1-269: The file fails ruff format checks; run `ruff format
fournos/handlers/lifecycle.py` (or the project-wide `ruff format`) and reformat
the file, then re-run the linter before committing; ensure the formatted changes
cover the top-level functions and helpers such as on_create,
_create_lock_workload, _find_exclusive_locker, _pending_status, and
reconcile_pending so imports, spacing, and string concatenations match ruff's
style rules and the CI `ruff format --check` job passes.
- Around line 53-88: The validation allows lock_only True while exclusive is
False, but _create_lock_workload always creates an exclusive Kueue Workload and
LABEL_EXCLUSIVE_CLUSTER isn't applied, breaking _find_exclusive_locker; fix by
normalizing/enforcing exclusive when lock_only is set: when lock_only
(spec.get("lockOnly")) is True, set exclusive = True (or set
spec["exclusive"]=True) and ensure patch.meta.setdefault("labels",
{})[LABEL_EXCLUSIVE_CLUSTER] = cluster is applied (reuse LABEL_EXCLUSIVE_CLUSTER
and the same branch that currently adds the label), so the label and created
exclusive workload stay in sync with _create_lock_workload and
_find_exclusive_locker.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 475f73ca-c672-415b-92e6-0bbcae958473

📥 Commits

Reviewing files that changed from the base of the PR and between 80f13c8 and 31d6467.

📒 Files selected for processing (3)
  • fournos/handlers/execution.py
  • fournos/handlers/lifecycle.py
  • manifests/crd.yaml

Comment thread manifests/crd.yaml
@kpouget

kpouget commented May 18, 2026

Copy link
Copy Markdown
Collaborator

thanks @MML-coder , looks good. Please fix the linter and remove the Hearth reference

@MML-coder

Copy link
Copy Markdown
Collaborator Author

@kpouget - this PR still needs lgtm :) Thanks

@kpouget

kpouget commented May 19, 2026

Copy link
Copy Markdown
Collaborator

@kpouget - this PR still needs lgtm :) Thanks

when OCPCI is in charge of the labels, you can't get LGTM if there are pending changes 🙃 (the label would be removed with your push)

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label May 19, 2026
@MML-coder
MML-coder merged commit 216e292 into main May 19, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants