Skip to content

feat: hearth controller — FournosCluster lifecycle operator - #1

Merged
MML-coder merged 10 commits into
mainfrom
hearth-controller
May 19, 2026
Merged

feat: hearth controller — FournosCluster lifecycle operator#1
MML-coder merged 10 commits into
mainfrom
hearth-controller

Conversation

@MML-coder

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

Copy link
Copy Markdown
Collaborator

Summary

  • Migrated FournosCluster controller from fournos/fournos-cluster/ (PR #74) to standalone repo
  • Renamed package fournos_clusterhearth, env prefix HEARTH_, namespace hearth
  • Separate ClusterRole hearth (no more ArgoCD fights with shared fournos-cluster role)
  • Container image: quay.io/rh_perfscale/hearth
  • Kueue backfill fix: new GPU types are added to all flavors to prevent 422 errors
  • CI workflow: tests + container image build/push to quay.io on merge

What's included

  • hearth/ — kopf operator (auto-discovery, GPU detection, Kueue management, locking, TTL)
  • tests/ — 87 unit tests
  • deploy/ — Kustomize manifests (namespace, SA, ClusterRole, Deployment)
  • manifests/crd.yaml — FournosCluster CRD
  • .github/workflows/ci.yaml — CI pipeline

Verified on psap-automation cluster

  • Pod running in hearth namespace
  • Auto-discovered fournos-fg (2x NVIDIA-L40S) and athena-fire (8x NVIDIA)
  • Kubeconfig validation, GPU discovery, ResourceFlavor creation all working
  • CQ updates pending ArgoCD ignoreDifferences PR (openshift-psap/fournos-gitops#10)

Test plan

  • 87 unit tests passing
  • Container builds and runs on OCP cluster
  • Auto-discovery from labeled kubeconfig secrets
  • GPU discovery finds correct hardware
  • Delete + re-onboard cycle tested (fournos-fg)
  • CQ flavor/quota updates persist (blocked on fournos-gitops#10)

Summary by CodeRabbit

  • New Features

    • Kubernetes operator for GPU cluster discovery, inventory reporting, ownership/locking with TTL, and Kueue integration for dynamic flavors/quotas
    • Auto-discovery of clusters from kubeconfig secrets
    • Custom resource definition and deployment-ready manifests for installing the operator (including health/liveness checks)
  • Chores

    • CI workflow to lint, test, build and publish container images; image stream and ArgoCD application manifests
    • Project packaging, dev tooling and .gitignore updates
  • Tests

    • Extensive unit tests covering handlers, GPU discovery, and Kueue operations
  • Documentation

    • Expanded README with deployment, configuration, and troubleshooting guidance

Review Change Stack

Migrated from fournos/fournos-cluster/ (PR #74) to standalone repo.

- kopf operator watching kubeconfig secrets for auto-discovery
- GPU discovery via target cluster node labels
- Kueue ResourceFlavor and ClusterQueue management
- Cluster locking with sentinel FournosJob and TTL expiry
- Backfill fix: new GPU types added to all flavors (prevents 422)
- Deployment manifests (namespace, SA, ClusterRole, Deployment)
- CI workflow for tests and container image build/push
- 87 unit tests passing
@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@MML-coder has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 50 minutes and 30 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: 1fe75e23-9332-4c6f-841a-e01d18f49127

📥 Commits

Reviewing files that changed from the base of the PR and between ca38f5e and f45049e.

📒 Files selected for processing (2)
  • deploy/secrets-role.yaml
  • deploy/secrets-rolebinding.yaml
📝 Walkthrough

Walkthrough

This pull request adds a Kopf-based Kubernetes operator (Hearth) that auto-discovers remote clusters from kubeconfig Secrets, performs GPU discovery, manages Kueue ResourceFlavors/ClusterQueues, enforces sentinel-based cluster locks with optional TTLs, provides CRD/Deployment/RBAC manifests, CI, container build, and unit tests.

Changes

Core Operator Implementation

Layer / File(s) Summary
Configuration and constants
hearth/settings.py, hearth/constants.py, hearth/state.py, hearth/handlers/__init__.py, hearth/__main__.py
Settings class with environment-driven config (HEARTH_ prefix), module-level constants for labels/conditions/CRD identifiers, shared controller state container, public handler exports, and CLI entrypoint.
GPU discovery client
hearth/core/gpu_discovery.py
GPU discovery queries remote cluster nodes via kubeconfig secrets, decodes/parses YAML, normalizes GPU model names from vendor labels, aggregates counts by vendor/model, and returns timestamped inventory with total counts.
Kueue client module
hearth/core/kueue.py
Kueue client manages ResourceFlavor creation, addition to ClusterQueues, and quota updates with automatic backfilling of missing resource entries across all flavors.
Cluster reconciliation handlers
hearth/handlers/cluster.py
Cluster handlers implement lifecycle callbacks (create/owner-change/hardware-change) and reconciliation loop: validates kubeconfig, enforces TTL-based lock expiry, runs GPU discovery with interval/backoff, and reconciles sentinel lock job.
Secret-based cluster auto-discovery
hearth/handlers/secret.py
Secret handler watches kubeconfig secrets, extracts cluster names via configurable regex pattern, and auto-creates FournosCluster custom resources with labels and optional owner from annotations.
Operator entrypoint and wiring
hearth/operator.py, hearth/__main__.py
Operator module loads kubeconfig, initializes Kubernetes API clients, instantiates GPU discovery and Kueue clients into shared state, registers Kopf handlers for events and timers, and provides the entrypoint to start the reconciliation loop.

Kubernetes Resources and Deployment

Layer / File(s) Summary
FournosCluster CRD
manifests/crd.yaml
Defines the FournosCluster custom resource with spec fields for kubeconfig secret reference, ownership, GPU discovery interval, discovered hardware, and status fields for kubeconfig validity, lock state, GPU summary, and transition conditions.
Operator namespace and RBAC
deploy/namespace.yaml, deploy/sa.yaml, deploy/clusterrole.yaml, deploy/clusterrolebinding.yaml, deploy/secrets-role.yaml, deploy/secrets-rolebinding.yaml
Creates operator namespace, service account, ClusterRole with permissions for CRD and Kueue, ClusterRoleBinding, and separate Role/RoleBinding for scoped secret access.
Operator deployment and kustomization
deploy/deployment.yaml, deploy/imagestream.yaml, deploy/kustomization.yaml
Deployment runs operator with hardened security context (drop caps, non-root, seccomp), environment variables, liveness probe, resource requests/limits, ImageStream and kustomization that references all manifests.

Container Build and CI

Layer / File(s) Summary
Container image
Containerfile, pyproject.toml
Multi-stage Python 3.12 UBI build with dependency installation from pyproject.toml (runtime: kopf/kubernetes/pydantic-settings/pyyaml; dev: pytest/ruff), hardened user/capabilities, and kopf entrypoint with liveness endpoint.
GitHub Actions CI
.github/workflows/ci.yaml, .gitignore
Workflow tests on push/PR (Python linting/formatting via Ruff, pytest with coverage), builds container image with podman, and pushes tagged/latest images to Quay on main-branch pushes using secrets; gitignore patterns added for Python/build/test artifacts.

Test Suite

Layer / File(s) Summary
Cluster handler tests
tests/unit/test_cluster_handlers.py
Comprehensive tests cover duration parsing, GPU summary formatting, creation/owner/hardware callbacks, TTL expiry behavior, GPU discovery interval/backoff/failure tracking, lock job reconciliation, and full reconciliation step order.
GPU discovery tests
tests/unit/test_gpu_discovery.py
Tests validate GPU model normalization, secret-based kubeconfig loading (base64/YAML handling), node listing and aggregation, error handling (missing secret, invalid YAML, no contexts), timeout config, and dataclass immutability.
Kueue client tests
tests/unit/test_kueue.py
Tests verify flavor creation, addition to queues, quota updates with cross-flavor backfilling, and flavor listing against mocked Kubernetes custom-object API.
Secret handler tests
tests/unit/test_secret_handlers.py
Tests verify cluster name extraction from secret names, FournosCluster auto-creation with labels/ownership, existing-cluster detection, race condition tolerance (409), and annotation-based owner assignment.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped through clusters near and far,
Found GPUs shining like a star,
I stitched up flavors, guarded each lock,
Timed my discoveries — tick-tock-tock,
Reconciled states and rolled out the chart.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: hearth controller — FournosCluster lifecycle operator' clearly summarizes the main change: introducing a Kubernetes operator for managing FournosCluster resources. It directly matches the changeset's core contribution.
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 hearth-controller

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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (9)
Containerfile (1)

5-12: 💤 Low value

Consider documenting the two-stage pip install pattern.

The build creates a temporary hearth/__init__.py stub to install dependencies (lines 5-9), then copies the real source and installs with --no-deps (lines 11-12). While this pattern caches dependencies in a separate layer for faster rebuilds, it may not be immediately clear to maintainers why two install steps are necessary.

If this pattern is retained, a brief inline comment explaining the caching benefit would improve clarity.

🤖 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 `@Containerfile` around lines 5 - 12, Add a concise inline comment above the
two-stage pip install sequence explaining the purpose: that a temporary stub
hearth/__init__.py is created so dependencies from pyproject.toml can be
installed and cached in an earlier layer (pip install --no-cache-dir .), then
the real source is copied and installed with pip install --no-cache-dir
--no-deps . This comment should reference the temporary stub creation (mkdir -p
hearth && touch hearth/__init__.py), the initial install (pip install
--no-cache-dir .), the removal (rm -rf hearth), and the final install after COPY
hearth/ to clarify the caching/fast-rebuild intent for future maintainers.
pyproject.toml (2)

10-15: ⚡ Quick win

Consider tightening dependency version constraints.

All dependencies use >= constraints, which allow any future major version. This can introduce breaking changes unexpectedly. Consider using ~= (compatible release) or explicit upper bounds to improve reproducibility and reduce risk of runtime breakage.

For example:

dependencies = [
    "kopf~=1.37",
    "kubernetes~=31.0",
    "pydantic-settings~=2.7",
    "pyyaml~=6.0",
]
🤖 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 `@pyproject.toml` around lines 10 - 15, The dependency entries using open-ended
">=" constraints (kopf, kubernetes, pydantic-settings, pyyaml) should be
tightened to prevent inadvertent breaking upgrades; update the four entries
("kopf", "kubernetes", "pydantic-settings", "pyyaml") to use compatible-release
operator "~=" or explicit upper bounds (e.g., "~=1.37", "~=31.0", "~=2.7",
"~=6.0") in pyproject.toml so installs are reproducible and limited to
compatible minor/patch releases.

5-8: 💤 Low value

Consider adding license and author metadata.

The project metadata lacks license and authors/maintainers fields. Adding these would improve the package's completeness, especially if it may be published or shared outside the immediate team.

🤖 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 `@pyproject.toml` around lines 5 - 8, Add missing package metadata to
pyproject.toml by adding a license field and an authors (or maintainers) list
under the [project] table; specifically update the existing [project] block
(name, version, description) to include e.g. license = "MIT" (or SPDX
identifier) and authors = [{name = "Your Name", email = "you@example.com"}] or a
maintainers = [...] entry so the package has proper license and contact metadata
for distribution and discovery.
.github/workflows/ci.yaml (1)

40-55: ⚡ Quick win

Consider adding container security scanning before push.

The build-and-push job builds and pushes the container image without running a vulnerability scan. Adding a scan step (e.g., Trivy, Grype, or Quay's built-in scanning) would help catch known CVEs before images reach the registry, improving the security posture of deployed operators.

Example addition after line 42:

      - name: Scan container
        run: |
          podman run --rm -v /var/run/docker.sock:/var/run/docker.sock \
            aquasec/trivy image --severity HIGH,CRITICAL hearth:${{ github.sha }}
🤖 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 @.github/workflows/ci.yaml around lines 40 - 55, Add a container scan step
between the "Build container" and "Push container" steps so images are
vulnerability-scanned before being pushed; create a step named "Scan container"
(or similar) that runs a scanner such as Trivy or Grype against hearth:${{
github.sha }} (e.g., aquasec/trivy image --severity HIGH,CRITICAL hearth:${{
github.sha }}), mount any required socket/dirs or use the scanner's CLI
container, and configure the command to exit non-zero on findings (so the job
fails on HIGH/CRITICAL CVEs) before the existing "Login to Quay" / "Push
container" steps.
manifests/crd.yaml (1)

90-104: 💤 Low value

Consider adding validation for GPU inventory items.

The hardware.gpus array items have no required fields, allowing empty objects. While the operator controls these writes, adding required: ["vendor", "model", "count"] would provide schema-level validation and clearer API contracts.

📋 Suggested validation enhancement
                       items:
                         type: object
+                        required:
+                          - vendor
+                          - model
+                          - count
                         properties:
                           vendor:
                             type: string
🤖 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 `@manifests/crd.yaml` around lines 90 - 104, The CRD schema for hardware.gpus
currently allows empty objects; add schema-level validation by specifying
required fields for each GPU item (e.g., required: ["vendor","model","count"])
under the gpus.items properties so hardware.gpus entries must include vendor,
model and count; update the gpus array item definition (the gpus.items object in
the CRD) to include the required list to enforce this constraint.
deploy/deployment.yaml (2)

15-16: 💤 Low value

Consider adding pod-level security context for defense in depth.

While the container security context is well configured (lines 35-41), adding a pod-level securityContext would provide an additional layer of security constraints.

🛡️ Optional pod security context
     spec:
       serviceAccountName: hearth
+      securityContext:
+        runAsNonRoot: true
+        seccompProfile:
+          type: RuntimeDefault
       containers:
         - name: hearth
🤖 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 `@deploy/deployment.yaml` around lines 15 - 16, Add a pod-level securityContext
under the same spec that contains serviceAccountName (the block with "spec:
serviceAccountName: hearth") to enforce cluster-wide pod constraints; define
fields such as runAsNonRoot: true, fsGroup (e.g., 1000), readOnlyRootFilesystem:
true, and seccompProfile/runtimeClass as appropriate to your cluster, while
keeping the existing container-level securityContext intact (the container's
securityContext settings should remain unchanged) so the pod-wide policies apply
in addition to per-container controls.

29-34: ⚡ Quick win

Add a readiness probe to prevent premature traffic.

The deployment defines a liveness probe but no readiness probe. Without a readiness probe, the pod may be marked as ready and receive traffic before the operator is fully initialized, potentially causing request failures during startup.

🏥 Add readiness probe
           livenessProbe:
             httpGet:
               path: /healthz
               port: 8080
             initialDelaySeconds: 10
             periodSeconds: 30
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: 8080
+            initialDelaySeconds: 5
+            periodSeconds: 10
🤖 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 `@deploy/deployment.yaml` around lines 29 - 34, Add a readinessProbe alongside
the existing livenessProbe to prevent the pod from receiving traffic before
initialization completes: in the same container spec that contains
livenessProbe, add a readinessProbe block (httpGet to /healthz on port 8080 or
another readiness-specific endpoint) with sensible timings (e.g.,
initialDelaySeconds a bit longer or tuned, periodSeconds/failureThreshold) so
readiness checks start after startup and only mark the pod ready when the app is
truly ready; reference the existing livenessProbe and ensure readinessProbe uses
the same port and path (or a dedicated readiness path) and appropriate
initialDelaySeconds/periodSeconds/failureThreshold values.
hearth/operator.py (1)

26-26: 💤 Low value

Consider using force=True or checking existing handlers.

Calling logging.basicConfig() at runtime may be silently ignored if logging handlers are already configured (e.g., by Kopf or test frameworks). This could lead to unexpected log-level behavior.

Proposed fix to ensure configuration applies
-    logging.basicConfig(level=log_level, format="%(asctime)s %(name)s %(levelname)s %(message)s")
+    logging.basicConfig(
+        level=log_level, 
+        format="%(asctime)s %(name)s %(levelname)s %(message)s",
+        force=True
+    )

Note: force=True requires Python 3.8+. If you need Python 3.7 compatibility, clear existing handlers first.

🤖 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 `@hearth/operator.py` at line 26, The logging setup call in hearth.operator.py
uses logging.basicConfig(...) which can be ignored if handlers already exist;
update the configuration to ensure it takes effect by either adding force=True
to the logging.basicConfig call or, for Python <3.8 compatibility, clear
existing handlers first (e.g., inspect logging.root.handlers and remove them)
before calling logging.basicConfig; update the single call where
logging.basicConfig is invoked so the intended log_level and format are reliably
applied at runtime.
hearth/__main__.py (1)

6-9: 💤 Low value

Consider making the liveness endpoint configurable.

The liveness endpoint is hardcoded to http://0.0.0.0:8080/healthz. If the port needs to change (e.g., for local development or different deployment environments), it requires a code change.

Proposed refactor to make it configurable

In hearth/settings.py, add:

liveness_port: int = 8080

Then update hearth/__main__.py:

 kopf.run(
     namespaces=[settings.namespace, settings.secrets_namespace],
-    liveness_endpoint="http://0.0.0.0:8080/healthz",
+    liveness_endpoint=f"http://0.0.0.0:{settings.liveness_port}/healthz",
 )
🤖 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 `@hearth/__main__.py` around lines 6 - 9, Make the liveness endpoint
configurable by adding a setting (e.g., liveness_port: int = 8080) in
hearth/settings.py and updating the call to kopf.run in hearth/__main__.py to
construct the endpoint from that setting instead of hardcoding it; replace the
literal "http://0.0.0.0:8080/healthz" with a generated string like
"http://0.0.0.0:{settings.liveness_port}/healthz" (or allow an override via
settings.liveness_endpoint if you prefer full-URL configurability) so kopf.run
uses the configured port at runtime.
🤖 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.

Inline comments:
In @.github/workflows/ci.yaml:
- Around line 1-8: The workflow titled "CI" lacks explicit GitHub Actions
permissions; add a permissions block either at the workflow top-level (under the
existing "name: CI") or per-job to enforce least-privilege, e.g., set contents:
read and packages: write only if you push to GitHub Container Registry,
otherwise just contents: read for external registries like Quay.io; ensure the
permissions entry is present alongside the existing on/push/pull_request
configuration so jobs like the main CI job obey least-privilege.

In `@deploy/clusterrole.yaml`:
- Around line 22-24: The ClusterRole currently grants cluster-wide access to
"secrets"; remove the secrets rule from the ClusterRole and instead create a
namespaced Role named "hearth-secrets" in the "psap-secrets" namespace with
verbs ["get","list","watch","patch"] on resources ["secrets"], then create a
RoleBinding named "hearth-secrets" in the "psap-secrets" namespace that binds
that Role to the "hearth" ServiceAccount (subject: kind ServiceAccount, name
hearth, namespace hearth); finally, keep all other non-secrets rules in the
existing ClusterRole and add the new Role and RoleBinding to your manifest
collection (kustomization) so they are applied together.

In `@deploy/deployment.yaml`:
- Around line 19-20: Replace the non-deterministic image tag
"quay.io/rh_perfscale/hearth:latest" with a specific semantic version or
immutable digest (e.g., "quay.io/rh_perfscale/hearth:v0.1.0" or a commit
SHA/digest) and ensure the corresponding imagePullPolicy is appropriate (you may
change imagePullPolicy from Always to IfNotPresent when using immutable tags);
update the "image: quay.io/rh_perfscale/hearth:latest" entry and adjust
"imagePullPolicy" accordingly.

In `@hearth/core/gpu_discovery.py`:
- Around line 74-102: Wrap all failure paths in the kubeconfig-fetch/parse logic
so they raise GPUDiscoveryError instead of letting raw exceptions escape: when
catching client.exceptions.ApiException, convert non-404 ApiException into
GPUDiscoveryError with context (include secret_name and namespace) instead of
re-raising; when decoding secret.data["kubeconfig"] (base64 decode and
yaml.safe_load) catch decoding/yaml exceptions and raise GPUDiscoveryError
describing the failure; similarly, if the parsed value is a base64-encoded
string and the second decode/safe_load fails, catch and raise GPUDiscoveryError;
ensure the final type check for parsed still raises GPUDiscoveryError on invalid
YAML so all paths consistently produce GPUDiscoveryError rather than raw
exceptions.

In `@hearth/core/kueue.py`:
- Around line 86-106: In add_flavor_to_cluster_queue, when you inject
CLUSTER_SLOT_RESOURCE into the flavor's resources (the block building resources
and appending to flavors), also update the corresponding rg["coveredResources"]
to include CLUSTER_SLOT_RESOURCE (if not already present) so coveredResources
stays consistent with the flavor resources used by update_flavor_quotas; ensure
you dedupe (no duplicate entries) and modify resource_groups/rg before calling
self._k8s.patch_cluster_custom_object so the patched spec contains the updated
coveredResources.

In `@manifests/crd.yaml`:
- Around line 10-11: The CRD's plural name is incorrect: change the `plural`
value from "fournoscluster" to the proper Kubernetes plural "fournosclusters" so
API paths and kubectl resource names are correct; update the `plural` field in
the CRD definition (the entry named `plural`) accordingly to match the resource
kind/CRD `singular` and follow convention.

---

Nitpick comments:
In @.github/workflows/ci.yaml:
- Around line 40-55: Add a container scan step between the "Build container" and
"Push container" steps so images are vulnerability-scanned before being pushed;
create a step named "Scan container" (or similar) that runs a scanner such as
Trivy or Grype against hearth:${{ github.sha }} (e.g., aquasec/trivy image
--severity HIGH,CRITICAL hearth:${{ github.sha }}), mount any required
socket/dirs or use the scanner's CLI container, and configure the command to
exit non-zero on findings (so the job fails on HIGH/CRITICAL CVEs) before the
existing "Login to Quay" / "Push container" steps.

In `@Containerfile`:
- Around line 5-12: Add a concise inline comment above the two-stage pip install
sequence explaining the purpose: that a temporary stub hearth/__init__.py is
created so dependencies from pyproject.toml can be installed and cached in an
earlier layer (pip install --no-cache-dir .), then the real source is copied and
installed with pip install --no-cache-dir --no-deps . This comment should
reference the temporary stub creation (mkdir -p hearth && touch
hearth/__init__.py), the initial install (pip install --no-cache-dir .), the
removal (rm -rf hearth), and the final install after COPY hearth/ to clarify the
caching/fast-rebuild intent for future maintainers.

In `@deploy/deployment.yaml`:
- Around line 15-16: Add a pod-level securityContext under the same spec that
contains serviceAccountName (the block with "spec: serviceAccountName: hearth")
to enforce cluster-wide pod constraints; define fields such as runAsNonRoot:
true, fsGroup (e.g., 1000), readOnlyRootFilesystem: true, and
seccompProfile/runtimeClass as appropriate to your cluster, while keeping the
existing container-level securityContext intact (the container's securityContext
settings should remain unchanged) so the pod-wide policies apply in addition to
per-container controls.
- Around line 29-34: Add a readinessProbe alongside the existing livenessProbe
to prevent the pod from receiving traffic before initialization completes: in
the same container spec that contains livenessProbe, add a readinessProbe block
(httpGet to /healthz on port 8080 or another readiness-specific endpoint) with
sensible timings (e.g., initialDelaySeconds a bit longer or tuned,
periodSeconds/failureThreshold) so readiness checks start after startup and only
mark the pod ready when the app is truly ready; reference the existing
livenessProbe and ensure readinessProbe uses the same port and path (or a
dedicated readiness path) and appropriate
initialDelaySeconds/periodSeconds/failureThreshold values.

In `@hearth/__main__.py`:
- Around line 6-9: Make the liveness endpoint configurable by adding a setting
(e.g., liveness_port: int = 8080) in hearth/settings.py and updating the call to
kopf.run in hearth/__main__.py to construct the endpoint from that setting
instead of hardcoding it; replace the literal "http://0.0.0.0:8080/healthz" with
a generated string like "http://0.0.0.0:{settings.liveness_port}/healthz" (or
allow an override via settings.liveness_endpoint if you prefer full-URL
configurability) so kopf.run uses the configured port at runtime.

In `@hearth/operator.py`:
- Line 26: The logging setup call in hearth.operator.py uses
logging.basicConfig(...) which can be ignored if handlers already exist; update
the configuration to ensure it takes effect by either adding force=True to the
logging.basicConfig call or, for Python <3.8 compatibility, clear existing
handlers first (e.g., inspect logging.root.handlers and remove them) before
calling logging.basicConfig; update the single call where logging.basicConfig is
invoked so the intended log_level and format are reliably applied at runtime.

In `@manifests/crd.yaml`:
- Around line 90-104: The CRD schema for hardware.gpus currently allows empty
objects; add schema-level validation by specifying required fields for each GPU
item (e.g., required: ["vendor","model","count"]) under the gpus.items
properties so hardware.gpus entries must include vendor, model and count; update
the gpus array item definition (the gpus.items object in the CRD) to include the
required list to enforce this constraint.

In `@pyproject.toml`:
- Around line 10-15: The dependency entries using open-ended ">=" constraints
(kopf, kubernetes, pydantic-settings, pyyaml) should be tightened to prevent
inadvertent breaking upgrades; update the four entries ("kopf", "kubernetes",
"pydantic-settings", "pyyaml") to use compatible-release operator "~=" or
explicit upper bounds (e.g., "~=1.37", "~=31.0", "~=2.7", "~=6.0") in
pyproject.toml so installs are reproducible and limited to compatible
minor/patch releases.
- Around line 5-8: Add missing package metadata to pyproject.toml by adding a
license field and an authors (or maintainers) list under the [project] table;
specifically update the existing [project] block (name, version, description) to
include e.g. license = "MIT" (or SPDX identifier) and authors = [{name = "Your
Name", email = "you@example.com"}] or a maintainers = [...] entry so the package
has proper license and contact metadata for distribution and discovery.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 455d227b-8a4f-47b0-9d69-7530269a228f

📥 Commits

Reviewing files that changed from the base of the PR and between c0b5c27 and 3819320.

📒 Files selected for processing (29)
  • .github/workflows/ci.yaml
  • .gitignore
  • Containerfile
  • deploy/clusterrole.yaml
  • deploy/clusterrolebinding.yaml
  • deploy/deployment.yaml
  • deploy/kustomization.yaml
  • deploy/namespace.yaml
  • deploy/sa.yaml
  • hearth/__init__.py
  • hearth/__main__.py
  • hearth/constants.py
  • hearth/core/__init__.py
  • hearth/core/gpu_discovery.py
  • hearth/core/kueue.py
  • hearth/handlers/__init__.py
  • hearth/handlers/cluster.py
  • hearth/handlers/secret.py
  • hearth/operator.py
  • hearth/settings.py
  • hearth/state.py
  • manifests/crd.yaml
  • pyproject.toml
  • tests/__init__.py
  • tests/unit/__init__.py
  • tests/unit/test_cluster_handlers.py
  • tests/unit/test_gpu_discovery.py
  • tests/unit/test_kueue.py
  • tests/unit/test_secret_handlers.py

Comment thread .github/workflows/ci.yaml
Comment on lines +1 to +8
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit permissions to workflow jobs.

The workflow does not specify explicit permissions for either job. GitHub Actions best practice recommends setting least-privilege permissions to reduce the attack surface if the workflow is compromised.

Consider adding at the workflow or job level:

permissions:
  contents: read
  packages: write  # if pushing to GitHub Container Registry

For Quay.io, contents: read is sufficient since authentication uses external secrets.

🤖 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 @.github/workflows/ci.yaml around lines 1 - 8, The workflow titled "CI" lacks
explicit GitHub Actions permissions; add a permissions block either at the
workflow top-level (under the existing "name: CI") or per-job to enforce
least-privilege, e.g., set contents: read and packages: write only if you push
to GitHub Container Registry, otherwise just contents: read for external
registries like Quay.io; ensure the permissions entry is present alongside the
existing on/push/pull_request configuration so jobs like the main CI job obey
least-privilege.

Comment thread deploy/clusterrole.yaml Outdated
Comment thread deploy/deployment.yaml
Comment on lines +19 to +20
image: quay.io/rh_perfscale/hearth:latest
imagePullPolicy: Always

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace :latest image tag with a specific version.

Using :latest with imagePullPolicy: Always creates non-deterministic deployments and makes rollbacks difficult. Production deployments should use semantic version tags (e.g., v0.1.0 or commit SHAs) for reproducibility and proper version tracking.

🏷️ Recommended fix
       containers:
         - name: hearth
-          image: quay.io/rh_perfscale/hearth:latest
-          imagePullPolicy: Always
+          image: quay.io/rh_perfscale/hearth:v0.1.0  # or commit SHA
+          imagePullPolicy: IfNotPresent
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
image: quay.io/rh_perfscale/hearth:latest
imagePullPolicy: Always
image: quay.io/rh_perfscale/hearth:v0.1.0 # or commit SHA
imagePullPolicy: IfNotPresent
🤖 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 `@deploy/deployment.yaml` around lines 19 - 20, Replace the non-deterministic
image tag "quay.io/rh_perfscale/hearth:latest" with a specific semantic version
or immutable digest (e.g., "quay.io/rh_perfscale/hearth:v0.1.0" or a commit
SHA/digest) and ensure the corresponding imagePullPolicy is appropriate (you may
change imagePullPolicy from Always to IfNotPresent when using immutable tags);
update the "image: quay.io/rh_perfscale/hearth:latest" entry and adjust
"imagePullPolicy" accordingly.

Comment thread hearth/core/gpu_discovery.py
Comment thread hearth/core/kueue.py Outdated
Comment thread manifests/crd.yaml Outdated
MML-coder added 5 commits May 14, 2026 15:14
📝 CodeRabbit Chat: Implement requested code changes
- Scope secrets access to psap-secrets namespace via Role/RoleBinding
  instead of cluster-wide ClusterRole (least privilege)
- Normalize all kubeconfig-read failures to GPUDiscoveryError so the
  handler's except path always fires (non-404 API errors, base64/yaml
  decode failures)
- Keep coveredResources in sync when add_flavor_to_cluster_queue
  injects cluster-slot resource
- Add troubleshooting entry for sentinel "Pipeline not found" error
- Fix workload namespace in troubleshooting section
- Add unit tests for all three code fixes (91 tests passing)
Run ruff format across all source and test files to pass CI format
check. Also adds ImageStream trigger annotation to deployment and
execution_namespace setting for sentinel job namespace configuration.
CI uses --cov flags but pytest-cov was missing from pyproject.toml,
causing pytest to fail with "unrecognized arguments" (exit code 4).
Self-contained ArgoCD setup — no fournos-gitops changes needed.
AppProject restricts hearth to its own namespace and resource types.
ImageStream polls quay.io for new images; trigger annotation on
Deployment handles automatic rollouts.

Bootstrap: oc apply -f argocd/ -n openshift-gitops
Comment thread hearth/core/gpu_discovery.py
Comment thread README.md Outdated
@kpouget

kpouget commented May 18, 2026

Copy link
Copy Markdown
Collaborator

thanks Mehul, just need to remove the paragraph on PipelineNotFound, and fix the plural name of FournosCluster and will be good 👍🏻

Comment thread hearth/handlers/cluster.py Outdated
MML-coder added 2 commits May 18, 2026 16:31
The CRD plural field must end with 's' to follow Kubernetes naming
conventions. Updates metadata.name, RBAC resources, Python constant,
README, and sentinel job prefix (hearth-lock- instead of cluster-lock-).
Set kubernetes.client.rest and urllib3 loggers to WARNING regardless of
HEARTH_LOG_LEVEL, preventing full HTTP response bodies (including
kubeconfig secrets) from being dumped at DEBUG level.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
README.md (1)

32-38: 💤 Low value

Add language identifier to fenced code block.

The CD flow diagram lacks a language identifier. Adding text satisfies linters and improves rendering consistency.

Suggested fix
-```
+```text
 PR merged to main
   -> GitHub Actions builds + pushes quay.io/rh_perfscale/hearth:latest
     -> OpenShift ImageStream detects new image (~15 min poll)
       -> image trigger rolls out new pod
         -> ArgoCD ignores image field (ignoreDifferences)
-```
+```
🤖 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 `@README.md` around lines 32 - 38, The fenced code block in README.md that
contains the CD flow diagram is missing a language identifier; update the
opening fence for that block in the README (the block that starts with ```) to
use ```text so linters and renderers treat it as plain text and rendering is
consistent.
🤖 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.

Nitpick comments:
In `@README.md`:
- Around line 32-38: The fenced code block in README.md that contains the CD
flow diagram is missing a language identifier; update the opening fence for that
block in the README (the block that starts with ```) to use ```text so linters
and renderers treat it as plain text and rendering is consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1dab1b9-6341-41ed-af98-486e27c17e7f

📥 Commits

Reviewing files that changed from the base of the PR and between 0bcec9e and ca38f5e.

📒 Files selected for processing (21)
  • README.md
  • argocd/app-hearth.yaml
  • argocd/appproject.yaml
  • deploy/clusterrole.yaml
  • deploy/deployment.yaml
  • deploy/imagestream.yaml
  • deploy/kustomization.yaml
  • deploy/secrets-role.yaml
  • deploy/secrets-rolebinding.yaml
  • hearth/constants.py
  • hearth/core/gpu_discovery.py
  • hearth/core/kueue.py
  • hearth/handlers/cluster.py
  • hearth/operator.py
  • hearth/settings.py
  • manifests/crd.yaml
  • pyproject.toml
  • tests/unit/test_cluster_handlers.py
  • tests/unit/test_gpu_discovery.py
  • tests/unit/test_kueue.py
  • tests/unit/test_secret_handlers.py
✅ Files skipped from review due to trivial changes (5)
  • deploy/imagestream.yaml
  • deploy/secrets-rolebinding.yaml
  • deploy/secrets-role.yaml
  • argocd/app-hearth.yaml
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (9)
  • hearth/constants.py
  • manifests/crd.yaml
  • hearth/settings.py
  • tests/unit/test_secret_handlers.py
  • tests/unit/test_gpu_discovery.py
  • hearth/core/gpu_discovery.py
  • hearth/handlers/cluster.py
  • tests/unit/test_kueue.py
  • tests/unit/test_cluster_handlers.py

kopf watches all registered resource types in both configured namespaces.
Add Role/RoleBinding for secrets in the hearth namespace so kopf's watch
loop doesn't spam APIForbiddenError on startup.
@kpouget

kpouget commented May 19, 2026

Copy link
Copy Markdown
Collaborator

/lgtm

@MML-coder
MML-coder merged commit 7d76e22 into main May 19, 2026
3 checks passed
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