Skip to content

e2e: testing qemu provisioner + driver with jumpstarter-exec - #929

Open
mangelajo wants to merge 8 commits into
mainfrom
e2e-test-qemu
Open

e2e: testing qemu provisioner + driver with jumpstarter-exec#929
mangelajo wants to merge 8 commits into
mainfrom
e2e-test-qemu

Conversation

@mangelajo

@mangelajo mangelajo commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Fold ExporterSet QEMU e2e into the main e2e-tests job; recycle coverage runs j qemu power on / j qemu power off and re-checks the replacement. Flash/boot remains skipped until guest-disk capacity (Add storage/disk support for QEMU ExporterSet virtual targets #924).
  • Swap QEMU Pod roles so exporter is the main container and target-runtime (jumpstarter-exec) is a native sidecar: default kubectl logs, restartPolicy: Never, and exporter exit completes the Pod.
  • Add jumpstarter-exec shutdown and best-effort runtime teardown from the exporter on exitOnLeaseEnd.
  • Fix ExitAndReplace by deleting unleased exporters whose Pods are Succeeded/Failed, then refilling minAvailableReplicas (avoids Completed/Offline zombies and maxReplicas stalls).
  • Put QEMU cidata on the shared volume with world-traversable mode so the runtime sidecar (UID 65532) can mount vvfat across the exporter/runtime UID boundary.
  • Cover shutdown_runtime_sidecar error/fallback paths for diff-cover; share OIDC/legacy client–exporter create/login/delete helpers in e2e/test/utils.go.

Test plan

  • go test ./internal/exporterset/... (includes ExitAndReplace cleanup + RenderPod layout)
  • make e2e-exporterset-qemu — Online/Ready + power cycle + recycle; flash/boot skipped on 100Mi SizeLimit
  • make -C python pkg-test-jumpstarter — includes new shutdown_runtime_sidecar coverage
  • CI e2e-tests on this PR
  • Manual: apply controller/hack/sample-x86_64-kind.yaml, lease/release, confirm Completed Pod is deleted and a new Ready instance appears

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR changes QEMU Pods to use an exporter main container and native runtime sidecar. It adds runtime shutdown, terminal exporter recycling, image deployment wiring, shared e2e helpers, and ExporterSet QEMU tests.

Changes

QEMU ExporterSet lifecycle

Layer / File(s) Summary
Runtime shutdown protocol
rust/jumpstarter-exec/..., python/packages/jumpstarter/jumpstarter/exporter/...
Adds the shutdown protocol message, CLI command, server handling, exporter integration, and tests.
QEMU pod and driver model
controller/internal/exporterset/..., python/packages/jumpstarter-driver-qemu/...
Makes the exporter the main container, runs target-runtime as a native sidecar, normalizes QEMU sizes, and places cloud-init data on shared storage.
Terminal exporter recycling
controller/internal/exporterset/reconciler.go, controller/internal/exporterset/reconciler_test.go
Deletes unleased exporters with terminal Pods and validates ExitAndReplace replacement behavior.
ExporterSet QEMU e2e flow
.github/workflows/e2e.yaml, e2e/..., Makefile
Builds and loads QEMU images, prepares guest data, defines QEMU resources, and tests boot, power cycling, replacement, and diagnostics.
E2E provisioning and diagnostics helpers
e2e/test/utils.go, e2e/test/*_test.go
Adds reusable resource setup, cleanup, readiness, synchronization, and failure-log helpers.
Image build and deployment wiring
.dockerignore, controller/hack/..., .github/actions/..., controller/Containerfile.qemu-runtime
Adds local image detection and loading, workflow artifact handling, deployment variables, runtime image construction, and Docker context exclusions.
QEMU lifecycle documentation
docs/source/contributing/jeps/...
Documents the exporter-main/runtime-sidecar model, recycling semantics, image overrides, lease-end behavior, and QEMU test plan.

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

Possibly related issues

  • jumpstarter-dev/jumpstarter#924 — Adds QEMU disk-size support and flash/boot e2e coverage.

Possibly related PRs

Suggested labels: enhancement, go, python, build-pr-images, build-pr-images/jumpstarter

Suggested reviewers: kirkbrauer, raballew

Poem

A rabbit checks the runtime light,
The exporter starts and Pods turn right.
Sockets close, then tests renew,
Alpine boots in QEMU blue.
Each workflow hops through every stage.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% 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
Title check ✅ Passed The title clearly identifies the QEMU e2e testing and jumpstarter-exec changes, which are substantial parts of the pull request.
Description check ✅ Passed The description directly explains the QEMU pod changes, shutdown support, recycle behavior, e2e coverage, and test plan.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch e2e-test-qemu

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.

@mangelajo
mangelajo marked this pull request as ready for review July 29, 2026 15:47
@mangelajo mangelajo changed the title feat: exporter-as-main QEMU ExporterSet pods + ExitAndReplace e2e e2e: testing qemu provisioner + driver with jumpstarter-exec Jul 29, 2026
mangelajo and others added 3 commits July 29, 2026 15:51
Make the exporter the main container and jumpstarter-exec/runtime a native
sidecar so default logs and Pod completion follow the exporter. Add
jumpstarter-exec shutdown, terminal-Pod cleanup for ExitAndReplace, and
ExporterSet QEMU coverage in the main e2e suite (flash/boot gated on #924).

Co-authored-by: Cursor <cursoragent@cursor.com>
QEMU runs in the runtime container via jumpstarter-exec, so cloud-init
vvfat content must live under /shared rather than the exporter's /tmp.

Co-authored-by: Cursor <cursoragent@cursor.com>
… responsive

Exercise j qemu power on/off under jmp shell, verify ExitAndReplace rotation,
and chmod cidata dirs so the runtime sidecar can read them across UIDs.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

🧹 Nitpick comments (2)
e2e/scripts/ensure-qemu-guest-image.sh (1)

42-46: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Harden the download: timeouts, partial cleanup, and integrity check.

curl has no --max-time/--connect-timeout, so a stalled CDN can hang the job, the .partial file leaks on failure, and the "pinned" image is unverified.

♻️ Suggested hardening
 echo "Downloading Alpine guest image from ${ALPINE_IMAGE_URL}" >&2
 tmp="${DEST}.partial"
-curl -fL --retry 3 --retry-delay 2 -o "${tmp}" "${ALPINE_IMAGE_URL}"
+trap 'rm -f "${tmp}"' EXIT
+curl -fL --retry 3 --retry-delay 2 --connect-timeout 20 --max-time 900 \
+  -o "${tmp}" "${ALPINE_IMAGE_URL}"
+if [ -n "${JUMPSTARTER_E2E_QEMU_IMAGE_SHA256:-}" ]; then
+  echo "${JUMPSTARTER_E2E_QEMU_IMAGE_SHA256}  ${tmp}" | sha256sum -c -
+fi
 mv "${tmp}" "${DEST}"
🤖 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 `@e2e/scripts/ensure-qemu-guest-image.sh` around lines 42 - 46, Harden the
download flow around ALPINE_IMAGE_URL by adding curl connection and
total-transfer timeouts, ensuring the temporary ${DEST}.partial file is removed
whenever curl or the subsequent move fails, and verifying the downloaded image
against the expected pinned checksum before moving it to DEST. Preserve the
existing retry behavior and only output DEST after successful verification and
installation.
e2e/test/exporterset_qemu_test.go (1)

105-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Readiness assertion is weak; ContainSubstring("true") passes when some containers are not ready.

containerStatuses[*].ready yields a space-joined list (e.g. true false). Prefer kubectl wait --for=condition=Ready pod/<name>, which also fixes the "waiting for Pod Ready" label that currently only checks phase Running. Same pattern at Lines 205-208.

🤖 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 `@e2e/test/exporterset_qemu_test.go` around lines 105 - 116, Replace the
container readiness assertion in the pod-waiting flow with kubectl’s pod Ready
condition check, using the existing exporterName and namespace arguments via the
established Kubectl/Eventually pattern. Update the preceding phase-based wait
and its label so the readiness wait uses `kubectl wait --for=condition=Ready
pod/<name>`, and apply the same change to the corresponding flow around the
second readiness assertion.
🤖 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 286-292: Increase the GitHub Actions job timeout in
`.github/workflows/e2e.yaml` at lines 286-292 so it exceeds the 90-minute Ginkgo
suite timeout and includes setup and log collection time. Alternatively, reduce
the full-suite Ginkgo timeout in `.github/workflows/e2e.yaml` lines 286-292 and
`e2e/lib/common.sh` lines 74-82 to preserve equivalent buffer; update both
affected sites consistently if choosing this approach.
- Around line 189-200: Update the cache key for the exporter image near the
“Cache exporter image” step to hash the complete exporter build context,
including Python package source files, rather than only configuration and lock
files. Also update the QEMU-runtime cache key near the corresponding QEMU
runtime cache step to include the complete runtime build context, including
runtime and shutdown implementation inputs; apply the required changes at
.github/workflows/e2e.yaml lines 189-200 and 223-234.

In `@e2e/scripts/ensure-qemu-guest-image.sh`:
- Around line 4-8: Update the resolution-order header in
ensure-qemu-guest-image.sh to state that JUMPSTARTER_E2E_QEMU_IMAGE accepts only
an absolute path, removing the unsupported “or URL” claim while preserving the
existing behavior.

In `@e2e/scripts/qemu_flash_boot.py`:
- Around line 20-21: Update the documented invocation in the jmp shell command
to use uv run python3 instead of the system Python interpreter, and note that
users should run make sync first when dependencies are not installed.
- Around line 1-109: Move the executable script currently defining main() into
the repository’s python/ workspace, preserving its CLI behavior and executable
entry point. Update any callers, references, or invocation documentation to use
the new location, and ensure the relocated module remains exposed through the
Python workspace conventions.
- Around line 48-64: Update the positional image argument in the argument parser
so it is optional, allowing invocations with --skip-flash and no image path.
Preserve the existing image value and flashing behavior when an image is
provided, while keeping the skip-flash path independent of args.image.

In `@e2e/test/exporterset_qemu_test.go`:
- Around line 120-135: Update the shared volume capacity check in the
exporterSetQemu test to query the sizeLimit once, then branch on that result.
Remove the preceding Eventually assertion requiring a non-empty value so the
existing Skip behavior for an empty sizeLimit or 100Mi remains reachable.

In
`@python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py`:
- Around line 221-234: Update
test_cidata_uses_shared_work_dir_with_launcher_socket to rely on the
launcher_socket path directly: keep the socket under shared, remove the
Qemu._work_dir patch, and call driver.cidata() unchanged. Preserve assertions
that cidata resides under shared with the expected permissions and files.

In `@python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py`:
- Around line 540-548: Update the cidata method so the TemporaryDirectory
remains 0o700 in local mode and is chmodded to 0o755 only when sidecar mode
requires cross-UID access. Use the existing sidecar-mode configuration or helper
visible in the surrounding driver code to guard the chmod, while preserving the
shared _work_dir behavior.

In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 44-105: Offload the blocking shutdown_runtime_sidecar invocation
from serve() to a worker thread using the existing async/AnyIO thread-offloading
mechanism, at both shutdown call sites around the task-group exit. Preserve the
function’s arguments, return handling, and shutdown sequencing while ensuring
subprocess.run never executes directly on the event loop.
- Around line 85-92: Update the subprocess.run exception handling in
shutdown_runtime_sidecar to catch PermissionError and other relevant OSError
failures, log the shutdown failure with the executable context, and return False
consistently with the existing FileNotFoundError and TimeoutExpired paths.

In `@rust/jumpstarter-exec/tests/integration.rs`:
- Around line 452-458: Update the server shutdown handling around server.wait()
to enforce a deadline: repeatedly poll server.try_wait(), preserving the
successful exit assertion when the process exits, and kill the child if the
deadline expires before it terminates. Ensure the timeout path fails the test
rather than blocking indefinitely.

---

Nitpick comments:
In `@e2e/scripts/ensure-qemu-guest-image.sh`:
- Around line 42-46: Harden the download flow around ALPINE_IMAGE_URL by adding
curl connection and total-transfer timeouts, ensuring the temporary
${DEST}.partial file is removed whenever curl or the subsequent move fails, and
verifying the downloaded image against the expected pinned checksum before
moving it to DEST. Preserve the existing retry behavior and only output DEST
after successful verification and installation.

In `@e2e/test/exporterset_qemu_test.go`:
- Around line 105-116: Replace the container readiness assertion in the
pod-waiting flow with kubectl’s pod Ready condition check, using the existing
exporterName and namespace arguments via the established Kubectl/Eventually
pattern. Update the preceding phase-based wait and its label so the readiness
wait uses `kubectl wait --for=condition=Ready pod/<name>`, and apply the same
change to the corresponding flow around the second readiness assertion.
🪄 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: d5ba8535-d675-4bb6-b492-72c1c672a2c2

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb0364 and e546dcb.

📒 Files selected for processing (32)
  • .dockerignore
  • .github/actions/load-e2e-artifacts/action.yaml
  • .github/workflows/e2e.yaml
  • Makefile
  • controller/hack/deploy_vars
  • controller/hack/deploy_with_operator.sh
  • controller/hack/sample-x86_64-kind.yaml
  • controller/hack/utils
  • controller/internal/exporterset/exporterconfig.go
  • controller/internal/exporterset/provisioners/qemu/enrich_test.go
  • controller/internal/exporterset/provisioners/qemu/qemu.go
  • controller/internal/exporterset/provisioners/qemu/qemu_test.go
  • controller/internal/exporterset/reconciler.go
  • controller/internal/exporterset/reconciler_test.go
  • docs/source/contributing/jeps/JEP-0014-virtual-scalable-exporters.md
  • e2e/dex.values.yaml
  • e2e/lib/common.sh
  • e2e/manifests/exporterset-qemu-kind.yaml
  • e2e/run-e2e.sh
  • e2e/scripts/ensure-qemu-guest-image.sh
  • e2e/scripts/qemu_flash_boot.py
  • e2e/test/exporterset_qemu_test.go
  • e2e/testdata/.gitignore
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
  • rust/jumpstarter-exec/src/client.rs
  • rust/jumpstarter-exec/src/main.rs
  • rust/jumpstarter-exec/src/protocol.rs
  • rust/jumpstarter-exec/src/server.rs
  • rust/jumpstarter-exec/tests/integration.rs

Comment thread .github/workflows/e2e.yaml
Comment thread .github/workflows/e2e.yaml Outdated
Comment thread e2e/scripts/ensure-qemu-guest-image.sh
Comment thread e2e/scripts/qemu_flash_boot.py
Comment thread e2e/scripts/qemu_flash_boot.py
Comment thread python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py Outdated
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Comment thread rust/jumpstarter-exec/tests/integration.rs Outdated
Comment on lines +93 to +94
except Exception as exc: # noqa: BLE001 - best-effort cleanup
print(f"power off after failure also failed: {exc}", flush=True)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

poweroff should work, no best effort...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: success-path power.off() is no longer best-effort — failures propagate. Best-effort remains only on the failure/timeout cleanup branch.

@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)
.github/workflows/e2e.yaml (1)

184-187: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

New checkout steps don't set persist-credentials: false.

Both new jobs checkout the repo without persist-credentials: false while also uploading artifacts in the same job (artipacked pattern). The actual leak surface here is limited since only /tmp/*.tar (outside the checkout) is uploaded, but setting it explicitly costs nothing and matches supply-chain best practice.

🔒 Proposed fix
       - name: Checkout repository
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
         with:
           fetch-depth: 0
+          persist-credentials: false

Also applies to: 218-221

🤖 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/e2e.yaml around lines 184 - 187, Update both new
actions/checkout steps in the E2E workflow to set persist-credentials to false
alongside fetch-depth, covering the checkout steps near the artifact-uploading
jobs while preserving their existing checkout behavior.

Source: Linters/SAST tools

🤖 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 @.github/workflows/e2e.yaml:
- Around line 184-187: Update both new actions/checkout steps in the E2E
workflow to set persist-credentials to false alongside fetch-depth, covering the
checkout steps near the artifact-uploading jobs while preserving their existing
checkout behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f328255-7c8c-43bf-a872-3349d455d2b6

📥 Commits

Reviewing files that changed from the base of the PR and between e546dcb and 641a78b.

📒 Files selected for processing (32)
  • .dockerignore
  • .github/actions/load-e2e-artifacts/action.yaml
  • .github/workflows/e2e.yaml
  • Makefile
  • controller/hack/deploy_vars
  • controller/hack/deploy_with_operator.sh
  • controller/hack/sample-x86_64-kind.yaml
  • controller/hack/utils
  • controller/internal/exporterset/exporterconfig.go
  • controller/internal/exporterset/provisioners/qemu/enrich_test.go
  • controller/internal/exporterset/provisioners/qemu/qemu.go
  • controller/internal/exporterset/provisioners/qemu/qemu_test.go
  • controller/internal/exporterset/reconciler.go
  • controller/internal/exporterset/reconciler_test.go
  • docs/source/contributing/jeps/JEP-0014-virtual-scalable-exporters.md
  • e2e/dex.values.yaml
  • e2e/lib/common.sh
  • e2e/manifests/exporterset-qemu-kind.yaml
  • e2e/run-e2e.sh
  • e2e/scripts/ensure-qemu-guest-image.sh
  • e2e/scripts/qemu_flash_boot.py
  • e2e/test/exporterset_qemu_test.go
  • e2e/testdata/.gitignore
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
  • rust/jumpstarter-exec/src/client.rs
  • rust/jumpstarter-exec/src/main.rs
  • rust/jumpstarter-exec/src/protocol.rs
  • rust/jumpstarter-exec/src/server.rs
  • rust/jumpstarter-exec/tests/integration.rs
🚧 Files skipped from review as they are similar to previous changes (26)
  • e2e/testdata/.gitignore
  • controller/hack/deploy_with_operator.sh
  • controller/hack/deploy_vars
  • .dockerignore
  • .github/actions/load-e2e-artifacts/action.yaml
  • rust/jumpstarter-exec/src/protocol.rs
  • controller/hack/utils
  • e2e/lib/common.sh
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py
  • controller/internal/exporterset/reconciler_test.go
  • controller/hack/sample-x86_64-kind.yaml
  • rust/jumpstarter-exec/tests/integration.rs
  • e2e/test/exporterset_qemu_test.go
  • e2e/scripts/ensure-qemu-guest-image.sh
  • controller/internal/exporterset/exporterconfig.go
  • Makefile
  • e2e/scripts/qemu_flash_boot.py
  • rust/jumpstarter-exec/src/main.rs
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py
  • controller/internal/exporterset/reconciler.go
  • controller/internal/exporterset/provisioners/qemu/enrich_test.go
  • controller/internal/exporterset/provisioners/qemu/qemu_test.go
  • controller/internal/exporterset/provisioners/qemu/qemu.go
  • rust/jumpstarter-exec/src/server.rs
  • docs/source/contributing/jeps/JEP-0014-virtual-scalable-exporters.md

Raise diff-cover for exporter sidecar shutdown error/fallback branches, and
centralize OIDC/legacy client-exporter create/login/delete in utils.go.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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

Caution

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

⚠️ Outside diff range comments (2)
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py (2)

1200-1208: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that startup does not shut down the runtime.

Bind the mock and call shutdown.assert_not_called() before cancelling the task group. The current test only verifies _stop_requested, so it would pass if processing the initial unleased status incorrectly invoked the new shutdown helper.

As per coding guidelines, python/**/*_test.py must provide comprehensive package test coverage.

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py` around
lines 1200 - 1208, Update the test around exporter.serve to bind the
shutdown_runtime_sidecar patch as a mock, then assert
shutdown.assert_not_called() after the initial status is processed and before
cancelling the task group. Keep the existing _stop_requested assertion and
cancellation behavior unchanged.

Source: Coding guidelines


1186-1188: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert one shutdown request and remove the duplicate call.

serve() calls shutdown_runtime_sidecar() on the lease-end branch, then unconditionally calls it again after the task group. assert_called() permits this duplicate side effect; require assert_called_once() and make the cleanup path skip a shutdown already attempted for that transition.

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py` around
lines 1186 - 1188, Update the exporter.serve test and cleanup flow so
shutdown_runtime_sidecar is invoked exactly once when the lease-end branch has
already attempted shutdown. Replace the permissive assertion with
assert_called_once(), and track or otherwise reuse that shutdown state to skip
the unconditional post-task-group cleanup call for the same transition.
🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 1262-1278: Make test_falls_back_to_binary_beside_socket
deterministic by monkeypatching jumpstarter.exporter.exporter._DEFAULT_JMP_EXEC
to a nonexistent path under tmp_path before calling shutdown_runtime_sidecar.
Preserve the existing socket-adjacent binary setup and assertion while ensuring
the default binary path cannot exist in the test environment.

---

Outside diff comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 1200-1208: Update the test around exporter.serve to bind the
shutdown_runtime_sidecar patch as a mock, then assert
shutdown.assert_not_called() after the initial status is processed and before
cancelling the task group. Keep the existing _stop_requested assertion and
cancellation behavior unchanged.
- Around line 1186-1188: Update the exporter.serve test and cleanup flow so
shutdown_runtime_sidecar is invoked exactly once when the lease-end branch has
already attempted shutdown. Replace the permissive assertion with
assert_called_once(), and track or otherwise reuse that shutdown state to skip
the unconditional post-task-group cleanup call for the same transition.
🪄 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: 1d9534a8-b747-4b1b-9ff8-7be52f382155

📥 Commits

Reviewing files that changed from the base of the PR and between 641a78b and 6017640.

📒 Files selected for processing (6)
  • e2e/test/auth_logging_test.go
  • e2e/test/exit_on_lease_end_test.go
  • e2e/test/exporterset_qemu_test.go
  • e2e/test/hooks_test.go
  • e2e/test/utils.go
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/test/exporterset_qemu_test.go

Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
Comment on lines +41 to +42
minReplicas: 0
maxReplicas: 1

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 think this works well for testing the replacement mechanism on exit but doesn't cover the warm pool behavior, specifically maintaining min replicas while instances are leased and the scale-up and scale-down mechanism. I know it's all covered by unit testing but maybe it's worth adding it also as an e2e test with minReplicas: 2 / maxReplicas: 3 to verify the scaling engine works correctly against a real cluster. (can be done also as a followup)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes thanks!, I wanted to establish the base and then continue adding items like this. Also I will have to check how feasible it is without making the tests to way too heavy.

Adding this one to the list as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I makes total sense, I will work on a follow up to not make this bigger.

I want to verify how does it behave in terms of pressure for the CI, it probably shouldn't be too bad if we don't start (power on) all the qemus at once.

maxReplicas: 1
minAvailableReplicas: 1
scaleDownCooldown: 5m
recycleStrategy: ExitAndReplace

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.

do we also want to test the recycleStrategy: InPlaceReuse flow?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, and many other things. That's a good topic for a follow up PR if that sounds good.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, will do an an independent PR.

Comment thread controller/hack/sample-x86_64-kind.yaml Outdated
arch: x86_64
smp: 1
mem: 1G
disk_size: 10G

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
disk_size: 10G
disk_size: 2G

May be more conservative like this, to make sure CI is ok.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: sample uses disk_size: 2G.

// Exporter (cascading the Pod) so scale-up can refill minAvailableReplicas.
// Without this, Offline/Succeeded instances inflate replicas, block warm-buffer
// refill at maxReplicas, and leave Completed Pods behind.
func (r *ExporterSetReconciler) cleanupTerminalExporters(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Follow-up (not blocking this PR): disable-before-delete grace for ExitAndReplace.

Today we delete as soon as Pods are terminal and LeaseRef is nil. That can race with the lease controller (separate process): after release the exporter may still look Online/Available long enough to accept a new lease while we delete it (worse if unregister/Offline lags).

Plan (tracked in #935):

  1. On terminal + unleased + still enabled → set enabled=false, stamp a recycle annotation, RequeueAfter: 10s.
  2. Delete only from cleanupTerminalExporters once Offline or disabled ≥ 10s (and still unleased).
  3. cleanupDisabledExporters must skip recycle-stamped exporters so scale-down cleanup does not bypass the grace window.

Keeping that out of this PR to avoid growing the patch.

Comment thread e2e/scripts/qemu_flash_boot.py Outdated
)
parser.add_argument(
"--disk-size",
default=os.environ.get("JUMPSTARTER_E2E_DISK_SIZE", "10G"),

@mangelajo mangelajo Jul 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Suggested change
default=os.environ.get("JUMPSTARTER_E2E_DISK_SIZE", "10G"),
default=os.environ.get("JUMPSTARTER_E2E_DISK_SIZE", "2G"),

or consistent with any previous comments.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: default is now 2G (and the e2e flash invocation passes --disk-size 2G).

Comment thread rust/jumpstarter-exec/src/server.rs Outdated
Comment on lines +100 to +108
ClientMessage::Shutdown => {
log.info("shutdown requested", &[("socket", json!(socket_path))]);
// Acknowledge before exiting so the client can observe success.
send(&writer, &ServerMessage::Exit { code: Some(0) })?;
// Drop the writer to flush the ack, remove the socket, then exit
// so the container's PID 1 terminates and Kubernetes replaces the Pod.
drop(writer);
let _ = std::fs::remove_file(socket_path);
std::process::exit(0);

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.

std::process::exit(0) immediately terminates the process from a worker thread. Since serve_with spawns a new thread per connection, a Shutdown arriving while another connection is running an Exec session kills all child processes and I/O threads without signaling or draining.

Thus, if my brain does not trick me here Rust destructors are skipped, and since jumpstarter-exec runs as PID 1 in the container, orphaned children become zombies - I have seen entire clusters going doing from a misconfigured daemonset creating zombies ...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: Shutdown no longer calls process::exit. It acks the client, sets a shared flag, SIGTERMs tracked Exec children, and lets serve_with return cleanly so handlers can unwind.

@@ -47,7 +47,7 @@ const (
// configMountPath is where the ExporterConfig Secret is mounted.
configMountPath = "/etc/jumpstarter/exporters"

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.

Consider exporting the constant from the exporterset package (e.g. ExporterConfigMountPath) and importing it in the QEMU provisioner.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Exported ExporterConfigMountPath from exporterconfig.go in 45d69a6. The QEMU provisioner still uses a local exporterConfigPath constant (same value) — importing the parent exporterset package from provisioners/qemu creates a test import cycle (exporterset tests import qemu). Left a comment pointing at the shared constant.

}

// listPodsGroupedByExporter maps Exporter names to their owned Pods.
func (r *ExporterSetReconciler) listPodsGroupedByExporter(

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.

This new listPodsGroupedByExporter function (returns map[string][]corev1.Pod) issues the same List call with the same label selector and ownerReference filter as the existing listPodsByExporter (which returns map[string]struct{}). Both queries hit the API server or informer cache with identical parameters in a single reconcile cycle.

Keeping only listPodsGroupedByExporter and deriving the existence check at the call site would remove the duplication.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: listPodsByExporter now derives from listPodsGroupedByExporter (single List implementation).


if self.exit_on_lease_end and previous_leased:
logger.info("Exporter configured to exit after lease, shutting down")
shutdown_runtime_sidecar()

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.

Isnt this blocking for up to 10s?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes — fixed in 45d69a6 by running it via await anyio.to_thread.run_sync(shutdown_runtime_sidecar) at the call sites.

async with create_task_group() as tg:
tg.start_soon(exporter.serve)
# Give serve time to process the status
await anyio.sleep(0.1)

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.

Using an event or condition variable to synchronize (e.g. wire _report_status to set an event after processing, then await that event before asserting) would make the test deterministic.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: exit-on-lease tests synchronize with an anyio.Event after statuses are processed instead of fixed sleeps.

Comment thread e2e/test/exit_on_lease_end_test.go Outdated
DumpOnFailure(250, tracker.DumpLogs)
// Stop any running exporter between tests
tracker.StopAll()
time.Sleep(time.Second)

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.

Polling tracker.IsProcessRunning() with a short timeout via Eventually instead of a fixed sleep would be faster and more reliable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 45d69a6: AfterEach uses Eventually until !tracker.IsProcessRunning() after StopAll().

Harden CI cache/timeouts, flip runtime/exporter UIDs for cidata access,
make jumpstarter-exec Shutdown graceful, and tighten shutdown/e2e tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Caution

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

⚠️ Outside diff range comments (1)
controller/internal/exporterset/provisioners/qemu/qemu.go (1)

264-284: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Set a shared Pod fsGroup before the exporter writes /shared.

Qemu.cidata() creates a directory in /shared, but this Pod does not set spec.securityContext.fsGroup. When fsGroup is unset, Kubernetes does not modify volume ownership or permissions. The non-root exporter can then fail to create cidata in the emptyDir. Kubernetes also uses an implicit primary group when runAsGroup is unset. (kubernetes.io)

Set fsGroup: 65532 and an explicit shared runAsGroup: 65532 at Pod scope. Add assertions for both fields in qemu_test.go.

Proposed fix
+ sharedGroup := exporterNonRootUID
+
  pod := &corev1.Pod{
      ObjectMeta: podMeta,
      Spec: corev1.PodSpec{
+         SecurityContext: &corev1.PodSecurityContext{
+             RunAsGroup: &sharedGroup,
+             FSGroup:    &sharedGroup,
+         },
          RestartPolicy: corev1.RestartPolicyNever,
🤖 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/internal/exporterset/provisioners/qemu/qemu.go` around lines 264 -
284, Update the QEMU Pod security context to set pod-level fsGroup and
runAsGroup to 65532, ensuring the non-root exporter can create files under the
shared emptyDir volume. Locate the Pod construction containing the
sharedVolumeName volume and add assertions for both fields in qemu_test.go.
🤖 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 `@e2e/test/exit_on_lease_end_test.go`:
- Around line 62-67: Update the shutdown verification around tracker.StopAll so
it observes the processes that were tracked before StopAll clears pt.pids.
Preserve a snapshot of the tracked PIDs and poll their termination, or have
StopAll return a termination result and assert it, ensuring the Eventually
assertion genuinely verifies exporter shutdown.

In `@rust/jumpstarter-exec/src/server.rs`:
- Around line 143-149: Synchronize shutdown state and child registration with a
shared lifecycle lock across the Shutdown handler and Exec flow. In the shutdown
logic around the children snapshot and state.shutdown.store, hold the lock while
marking shutdown and collecting PIDs; after spawn, register the PID under the
same lock only when shutdown is inactive. If shutdown is already active,
terminate and reap the newly spawned child before Exec returns.

---

Outside diff comments:
In `@controller/internal/exporterset/provisioners/qemu/qemu.go`:
- Around line 264-284: Update the QEMU Pod security context to set pod-level
fsGroup and runAsGroup to 65532, ensuring the non-root exporter can create files
under the shared emptyDir volume. Locate the Pod construction containing the
sharedVolumeName volume and add assertions for both fields in qemu_test.go.
🪄 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: 780cb051-d4e1-4134-9b22-94e05753b4fd

📥 Commits

Reviewing files that changed from the base of the PR and between 6017640 and 45d69a6.

📒 Files selected for processing (18)
  • .github/workflows/e2e.yaml
  • controller/Containerfile.qemu-runtime
  • controller/hack/sample-x86_64-kind.yaml
  • controller/internal/exporterset/exporterconfig.go
  • controller/internal/exporterset/provisioners/qemu/qemu.go
  • controller/internal/exporterset/provisioners/qemu/qemu_test.go
  • controller/internal/exporterset/reconciler.go
  • e2e/lib/common.sh
  • e2e/scripts/ensure-qemu-guest-image.sh
  • e2e/scripts/qemu_flash_boot.py
  • e2e/test/exit_on_lease_end_test.go
  • e2e/test/exporterset_qemu_test.go
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
  • rust/jumpstarter-exec/src/server.rs
  • rust/jumpstarter-exec/tests/integration.rs
🚧 Files skipped from review as they are similar to previous changes (10)
  • rust/jumpstarter-exec/tests/integration.rs
  • python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py
  • .github/workflows/e2e.yaml
  • e2e/scripts/ensure-qemu-guest-image.sh
  • controller/internal/exporterset/provisioners/qemu/qemu_test.go
  • e2e/scripts/qemu_flash_boot.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • e2e/test/exporterset_qemu_test.go
  • controller/internal/exporterset/reconciler.go
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py

Comment on lines +62 to +67
// Stop any running exporter between tests and wait until it is gone.
tracker.StopAll()
time.Sleep(time.Second)
Eventually(func() bool {
return tracker.IsProcessRunning()
}, 10*time.Second, 100*time.Millisecond).Should(BeFalse(),
"exporter process should stop after StopAll")

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

Make the shutdown assertion observe the tracked processes.

tracker.StopAll() clears pt.pids in e2e/test/utils.go:621-640. tracker.IsProcessRunning() only checks pt.pids in e2e/test/utils.go:651-663. Therefore, this Eventually assertion returns false immediately and does not verify exporter termination.

Preserve the PID snapshot for verification, or make StopAll return a termination result and assert that result 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 `@e2e/test/exit_on_lease_end_test.go` around lines 62 - 67, Update the shutdown
verification around tracker.StopAll so it observes the processes that were
tracked before StopAll clears pt.pids. Preserve a snapshot of the tracked PIDs
and poll their termination, or have StopAll return a termination result and
assert it, ensuring the Eventually assertion genuinely verifies exporter
shutdown.

Comment on lines +143 to +149
let pids: Vec<u32> = state.children.lock().unwrap().iter().copied().collect();
for pid in pids {
unsafe {
kill(pid as i32, 15);
}
}
state.shutdown.store(true, Ordering::SeqCst);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Synchronize shutdown state and child registration.

A concurrent Exec can spawn a child after Shutdown snapshots children. It can then insert its PID after the shutdown handler and the accept loop have completed their cleanup snapshots. That child does not receive SIGTERM.

Use one lifecycle lock for both the shutdown state and PID set. Set the shutdown state while holding that lock. After spawn, register the PID only if shutdown is not active. Otherwise, terminate and reap the new child before returning.

Also applies to: 215-216

🤖 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 `@rust/jumpstarter-exec/src/server.rs` around lines 143 - 149, Synchronize
shutdown state and child registration with a shared lifecycle lock across the
Shutdown handler and Exec flow. In the shutdown logic around the children
snapshot and state.shutdown.store, hold the lock while marking shutdown and
collecting PIDs; after spawn, register the PID under the same lock only when
shutdown is inactive. If shutdown is already active, terminate and reap the
newly spawned child before Exec returns.

mangelajo and others added 2 commits July 31, 2026 18:24
Runtime-as-root was creating QMP sockets the non-root exporter could not
connect to. Clear umask / use 0666 on the launcher socket, and ship
jumpstarter-exec from the qemu-runtime image so the fix is easy to rebuild.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reverts embedding jumpstarter-exec in qemu-runtime. The binary is built
once in the exporter image and copied to /shared via init container,
avoiding a Rust rebuild for every new runtime image.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread .github/workflows/e2e.yaml Outdated
done

- name: Prefetch Alpine guest image for ExporterSet QEMU
run: bash e2e/scripts/ensure-qemu-guest-image.sh

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this must run from the setup-e2e make target, not be part of github ci yaml.

The ensure-qemu-guest-image.sh call now runs as part of setup-e2e.sh
so both CI and local `make e2e-setup` prefetch the image consistently.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mangelajo
mangelajo requested review from bkhizgiy and raballew August 1, 2026 14:53
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.

3 participants