Skip to content

feat: reconcile ExporterSet provisioner controllers in operator#908

Merged
mangelajo merged 4 commits into
jumpstarter-dev:mainfrom
maboras-rh:exporterset-controller
Jul 24, 2026
Merged

feat: reconcile ExporterSet provisioner controllers in operator#908
mangelajo merged 4 commits into
jumpstarter-dev:mainfrom
maboras-rh:exporterset-controller

Conversation

@maboras-rh

@maboras-rh maboras-rh commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

This PR extends the Jumpstarter operator to deploy and manage ExporterSet provisioner controllers as Kubernetes Deployments, as defined in JEP-0014. It builds on the ExporterSet controller bootstrap in #864.

When spec.exporterSets.provisioners is configured, the operator creates one Deployment per enabled provisioner, running the shared jumpstarter-exporterset-controller image with --provisioner=<name>. Each provisioner gets its own ServiceAccount, Role, and RoleBinding.

API changes

Adds spec.exporterSets to the Jumpstarter CR:

  • Global settings: image, imagePullPolicy, resources
  • Per-provisioner config (provisioners[]): name, enabled, optional image override, replicas, optional resources override
  • ProvisionerConfig.name validation: DNS-subdomain-safe pattern (^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$); names that collide after sanitization are rejected at reconcile time

Adds status condition ExporterSetControllersReady, included in overall Ready only when at least one provisioner is enabled.

Operator behavior

  • Reconciles Deployment + RBAC (SA, Role, RoleBinding) for each enabled provisioner
  • Cleans up Deployments and owned Role/RoleBinding when a provisioner is disabled or removed (ServiceAccounts are preserved, matching existing operator SA patterns)
  • Reports ExporterSetControllersReady based on each enabled provisioner Deployment's Available condition
  • Extends operator ClusterRole with roles delete/patch, pods get/list/watch, and virtualtarget.jumpstarter.dev read/status permissions (required to create provisioner Roles with those rules)
  • Default provisioner controller resources when unset: 100m CPU / 256Mi memory requests, 500m CPU / 512Mi memory limits

Exporter-set-controller runtime fix

The provisioner controller binary is updated to work with namespaced Roles:

  • Requires NAMESPACE env var (set via downward API in the Deployment)
  • Restricts the controller-runtime cache to that namespace
  • Sets LeaderElectionNamespace to the pod namespace

Without this, informers issue cluster-scoped List/Watch calls that are rejected by the API server.

Dev / deploy tooling

  • make deploy and make deploy-operator now also build jumpstarter-exporterset-controller
  • hack/deploy_with_operator.sh deploys qemu.jumpstarter.dev as a default enabled provisioner in the generated Jumpstarter CR

Example CR snippet

spec:
  exporterSets:
    image: quay.io/jumpstarter-dev/jumpstarter-exporterset-controller:latest
    imagePullPolicy: IfNotPresent
    resources:
      requests:
        cpu: 100m
        memory: 256Mi
    provisioners:
      - name: qemu.jumpstarter.dev
        enabled: true
        replicas: 1
        # optional per-provisioner overrides:
        # image: ...
        # resources: ...

Test plan

  • Unit tests for resource constructors, RBAC rules, resource defaults, and provisioner name sanitization (exporterset_test.go)
  • Controller integration tests for full lifecycle: create, per-provisioner image/resources override, disable/cleanup, multiple provisioners, mixed enabled/disabled, spec removal, name collision rejection, and ExporterSetControllersReady status (jumpstarter_controller_test.go)
  • make -C controller/deploy/operator test
  • Manual: make deploy and verify qemu.jumpstarter.dev provisioner Deployment, SA, and Role come up in jumpstarter-lab

Related

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds optional exporter-set provisioner configuration to the Jumpstarter API and CRD, reconciles per-provisioner Deployments and RBAC resources, cleans up disabled controllers, and includes exporter-set availability in Jumpstarter readiness status.

Changes

ExporterSet controller support

Layer / File(s) Summary
ExporterSet configuration contract
controller/deploy/operator/api/v1alpha1/..., controller/deploy/operator/config/crd/..., controller/deploy/operator/config/rbac/role.yaml
Adds exporter-set configuration types, CRD validation, generated deep-copy methods, readiness condition typing, and RBAC permissions.
Provisioner resource reconciliation
controller/deploy/operator/internal/controller/jumpstarter/exporterset.go, controller/deploy/operator/internal/controller/jumpstarter/exporterset_test.go
Reconciles provisioner-specific ServiceAccounts, Roles, RoleBindings, and Deployments, including cleanup, defaults, policy rules, image overrides, replicas, probes, and security settings.
Operator wiring and readiness lifecycle
controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go, controller/deploy/operator/internal/controller/jumpstarter/status.go, controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller_test.go
Runs exporter-set reconciliation and reports readiness from Deployment availability, with lifecycle and sanitization tests.
Controller runtime and deployment integration
controller/cmd/exporter-set-controller/main.go, controller/Makefile, controller/hack/*, .github/actions/load-e2e-artifacts/action.yaml, .github/workflows/e2e.yaml
Restricts controller caching to its namespace and builds, loads, configures, and distributes the exporter-set-controller image.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JumpstarterCR
  participant JumpstarterReconciler
  participant KubernetesAPI
  participant ExporterSetController
  JumpstarterCR->>JumpstarterReconciler: Provide exporterSets configuration
  JumpstarterReconciler->>KubernetesAPI: Reconcile RBAC and provisioner Deployments
  KubernetesAPI->>ExporterSetController: Start configured controller
  ExporterSetController->>KubernetesAPI: Report Deployment availability
  JumpstarterReconciler->>JumpstarterCR: Update readiness conditions
Loading

Possibly related PRs

Suggested labels: operator, build-pr-images

Suggested reviewers: bennyz, bkhizgiy, kirkbrauer

Poem

I’m a rabbit with controllers to deploy,
Provisioners bloom with Kubernetes joy.
Roles bind, probes gleam, replicas grow,
Readiness tells when all systems glow.
Hop, hop—the exporter sets now flow!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: reconciling ExporterSet provisioner controllers in the operator.
Description check ✅ Passed The description is directly related to the PR and accurately summarizes the API, controller, tooling, and test changes.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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)
controller/deploy/operator/internal/controller/jumpstarter/exporterset.go (1)

542-546: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Harden provisioner-name sanitization (and validate Name).

sanitizeProvisionerName only lowercases and replaces .; any other character invalid in a DNS-1123 resource name (e.g. _, /, spaces) passes through into %s-exporterset-%s names and label values, causing CreateOrUpdate to be rejected by the API server and the whole reconcile to fail. ProvisionerConfig.Name has only MinLength=1 (no Pattern), so nothing upstream prevents such input. Additionally, distinct names can collapse to the same sanitized suffix (e.g. qemu.dev and qemu-dev), colliding on Deployment/Role names and in enabledProvisioners (last-write-wins).

Consider adding a +kubebuilder:validation:Pattern on ProvisionerConfig.Name (in jumpstarter_types.go) to constrain input to a DNS-subdomain-like form, and/or reject/normalize remaining invalid characters here.

🤖 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 `@controller/deploy/operator/internal/controller/jumpstarter/exporterset.go`
around lines 542 - 546, Harden sanitizeProvisionerName and
ProvisionerConfig.Name validation so provisioner names produce valid,
collision-resistant DNS-1123 resource and label suffixes. Add the appropriate
kubebuilder Pattern to ProvisionerConfig.Name in jumpstarter_types.go, and
update sanitizeProvisionerName to validate or reject characters and
transformations that could create invalid or colliding names; ensure invalid
input cannot reach the %s-exporterset-%s resources or enabledProvisioners.
🤖 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 `@controller/deploy/operator/internal/controller/jumpstarter/exporterset.go`:
- Around line 542-546: Harden sanitizeProvisionerName and ProvisionerConfig.Name
validation so provisioner names produce valid, collision-resistant DNS-1123
resource and label suffixes. Add the appropriate kubebuilder Pattern to
ProvisionerConfig.Name in jumpstarter_types.go, and update
sanitizeProvisionerName to validate or reject characters and transformations
that could create invalid or colliding names; ensure invalid input cannot reach
the %s-exporterset-%s resources or enabledProvisioners.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f6bdaccc-67e6-4acf-8265-82e2bd593128

📥 Commits

Reviewing files that changed from the base of the PR and between 069b889 and 8a03cd9.

📒 Files selected for processing (9)
  • controller/deploy/operator/api/v1alpha1/jumpstarter_types.go
  • controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go
  • controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml
  • controller/deploy/operator/config/rbac/role.yaml
  • controller/deploy/operator/internal/controller/jumpstarter/exporterset.go
  • controller/deploy/operator/internal/controller/jumpstarter/exporterset_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/status.go

@mangelajo
mangelajo marked this pull request as draft July 22, 2026 12:40

@mangelajo mangelajo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I didn't really read everything, but some overall things I saw.

You may want to extend this to deploy the qemu provisioner even if currently does nothing: https://github.com/jumpstarter-dev/jumpstarter/blob/main/controller/hack/deploy_with_operator.sh#L174

Comment thread controller/deploy/operator/api/v1alpha1/jumpstarter_types.go
Comment thread controller/deploy/operator/internal/controller/jumpstarter/exporterset.go Outdated
{
APIGroups: []string{"virtualtarget.jumpstarter.dev"},
Resources: []string{"exportersets"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we double check if we need update/patch/delete here and why? we may need to update the status, which is a different policy rule, but the object itself?

@maboras-rh maboras-rh Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've checked this. for the resource exportersets the only read verbs get list watch are sufficient for controller.
addressed in bc26068

@maboras-rh maboras-rh changed the title WIP: feat: reconcile ExporterSet provisioner controllers in operator feat: reconcile ExporterSet provisioner controllers in operator Jul 23, 2026
@maboras-rh
maboras-rh marked this pull request as ready for review July 23, 2026 16:33

@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: 1

🤖 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 `@controller/hack/deploy_with_operator.sh`:
- Line 35: Make exporter image loading in deploy_with_operator.sh conditional on
DEPLOY_JUMPSTARTER=true, so operator-only deployment does not require it. In
controller/Makefile lines 198-199, remove docker-build-exporter-set-controller
from the operator-only target.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 13010112-d351-4a8f-9978-1b7972a4b506

📥 Commits

Reviewing files that changed from the base of the PR and between 8a03cd9 and cdc5b16.

📒 Files selected for processing (12)
  • controller/Makefile
  • controller/cmd/exporter-set-controller/main.go
  • controller/deploy/operator/api/v1alpha1/jumpstarter_types.go
  • controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go
  • controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml
  • controller/deploy/operator/config/rbac/role.yaml
  • controller/deploy/operator/internal/controller/jumpstarter/exporterset.go
  • controller/deploy/operator/internal/controller/jumpstarter/exporterset_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller.go
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller_test.go
  • controller/hack/deploy_vars
  • controller/hack/deploy_with_operator.sh
🚧 Files skipped from review as they are similar to previous changes (6)
  • controller/deploy/operator/config/crd/bases/operator.jumpstarter.dev_jumpstarters.yaml
  • controller/deploy/operator/api/v1alpha1/jumpstarter_types.go
  • controller/deploy/operator/internal/controller/jumpstarter/exporterset_test.go
  • controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go
  • controller/deploy/operator/internal/controller/jumpstarter/jumpstarter_controller_test.go
  • controller/deploy/operator/internal/controller/jumpstarter/exporterset.go

# load the container images into the cluster
load_image "${IMG}"
load_image "${OPERATOR_IMG}"
load_image "${EXPORTER_SET_CONTROLLER_IMG}"

@coderabbitai coderabbitai Bot Jul 23, 2026

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not require the exporter image for operator-only deployment.

deploy-operator sets DEPLOY_JUMPSTARTER=false, but deploy_with_operator.sh loads the exporter image before checking that flag. This makes the operator-only target unnecessarily build and require an exporter image, and can fail even though no Jumpstarter CR is deployed.

  • controller/hack/deploy_with_operator.sh#L35-L35: load the exporter image only when DEPLOY_JUMPSTARTER=true.
  • controller/Makefile#L198-L199: remove docker-build-exporter-set-controller from the operator-only target.
📍 Affects 2 files
  • controller/hack/deploy_with_operator.sh#L35-L35 (this comment)
  • controller/Makefile#L198-L199
🤖 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 `@controller/hack/deploy_with_operator.sh` at line 35, Make exporter image
loading in deploy_with_operator.sh conditional on DEPLOY_JUMPSTARTER=true, so
operator-only deployment does not require it. In controller/Makefile lines
198-199, remove docker-build-exporter-set-controller from the operator-only
target.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the operator will deploy jumpstarter and the exporterset controller as well.. (see lines 207-212). so we will need this image.

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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- deploy-operator target context ---'
rg -n -A12 -B4 '^[[:space:]]*deploy-operator[[:space:]]*:' controller/Makefile

printf '%s\n' '--- DEPLOY_JUMPSTARTER assignments and calls ---'
rg -n -A3 -B3 '\bDEPLOY_JUMPSTARTER\b|deploy_with_operator\.sh' controller

Length of output: 2960


@mangelajo, you’re right for the normal deploy flow: Lines 207-212 configure the Jumpstarter CR, which causes the operator to create the exporter-set controller Deployment, so that image must be built and loaded there.

However, the finding is specifically about deploy-operator. That target invokes the script with DEPLOY_JUMPSTARTER=false (Makefile Line 200), and the script exits at Lines 58-60 before generating or applying the CR. Thus, in this path, no exporter-set controller is deployed and the image build/load is unnecessary.

The scope should remain narrow: conditionally build/load EXPORTER_SET_CONTROLLER_IMG only for the full Jumpstarter deployment path.

@maboras-rh

Copy link
Copy Markdown
Contributor Author

PTAL - @mangelajo @bennyz @bkhizgiy @evakhoni

@mangelajo
mangelajo self-requested a review July 24, 2026 08:41
The deploy_with_operator.sh script loads the exporterset-controller
image into the kind cluster, but the E2E workflow never built it.
With SKIP_BUILD=true in CI, kind_load_image fails because the image
does not exist locally.

Add a build-exporterset-controller-image job (parallel with the
existing controller/operator builds) that builds, caches, and uploads
the image as an artifact. Update load-e2e-artifacts to download and
docker-load it alongside the other images.

@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: 1

🤖 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/e2e.yaml:
- Around line 145-148: Update the checkout steps in the build-controller-image,
build-operator-image, and build-exporterset-controller-image jobs to set
persist-credentials to false alongside fetch-depth, preventing checkout
credentials from entering build contexts or uploaded image artifacts.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b9a37bdb-9bf2-4005-9e63-ea30abb68bb7

📥 Commits

Reviewing files that changed from the base of the PR and between cdc5b16 and 2cdfd6e.

📒 Files selected for processing (2)
  • .github/actions/load-e2e-artifacts/action.yaml
  • .github/workflows/e2e.yaml

Comment thread .github/workflows/e2e.yaml
@mangelajo

Copy link
Copy Markdown
Member

Added a commit to handle the new image E2E setup and caching

@mangelajo
mangelajo added this pull request to the merge queue Jul 24, 2026
Merged via the queue into jumpstarter-dev:main with commit 8bae226 Jul 24, 2026
25 checks passed
mangelajo added a commit that referenced this pull request Jul 24, 2026
The operator now deploys the exporter-set-controller (#908), so remove
the standalone YAML and Makefile targets. Keep sample CR manifests for
local kind testing after operator install.

Co-authored-by: Cursor <cursoragent@cursor.com>
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