feat: kubevirt provisioner - #927
Conversation
|
haha surprise provisioner, but honestly probably not a bad idea since now it's very easy to make, we can have a comparison point or alternative if the qemu is problematic at some point and the users don't need any of the special stuff like can or future bluetooth emulation. |
📝 WalkthroughWalkthroughAdds KubeVirt provisioning to the exporter-set controller, including remote VM and exporter Pod creation, parameter validation, driver enrichment, cleanup, and Python power, console, and storage drivers. ChangesKubeVirt exporter support
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ExporterSetController
participant KubevirtProvisioner
participant RemoteKubeVirtCluster
participant ExporterPod
participant PythonKubevirtDriver
ExporterSetController->>KubevirtProvisioner: RenderPod with exporter parameters
KubevirtProvisioner->>RemoteKubeVirtCluster: Create halted VirtualMachine
KubevirtProvisioner->>ExporterPod: Build exporter Pod and driver configuration
PythonKubevirtDriver->>RemoteKubeVirtCluster: Power, console, or storage operation
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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. Comment |
i wanted to play around with cuttlefish and it's easy getting kubevirt instances internally 😅 it also help find some issues in the general provisioner and also offer some insights into the storage handling |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
python/packages/jumpstarter-driver-kubevirt/README.md (1)
1-11: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winSymlink the KubeVirt README into the docs tree.
Driver package READMEs must also be linked from
docs/source/reference/package-apis/drivers/; addjumpstarter-driver-kubevirt.mdpointing topython/packages/jumpstarter-driver-kubevirt/README.md.🤖 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-driver-kubevirt/README.md` around lines 1 - 11, Add the required documentation symlink named jumpstarter-driver-kubevirt.md under the drivers package API reference directory, targeting python/packages/jumpstarter-driver-kubevirt/README.md. Preserve the existing README content and follow the established package README link convention.Source: Path instructions
🧹 Nitpick comments (1)
python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py (1)
254-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winHardcoded
20Gi/ReadWriteOncediverge from the configurable disk parameters used for VM creation.
kubevirt.go'sbuildVirtualMachinederivesDiskSize/AccessModesfrom provisioning parameters, butflashhere hardcodes"20Gi"and["ReadWriteOnce"], independent of those settings.Consider passing the configured disk size/access modes through the driver config (mirroring
kubeconfig/namespace/vm_name) soflashstays consistent with the VM's actual storage configuration.🤖 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-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py` around lines 254 - 266, Update the flash method’s PVC construction to use configurable disk size and access modes instead of hardcoded "20Gi" and ["ReadWriteOnce"]. Add these values to the driver configuration alongside kubeconfig, namespace, and vm_name, initialize them from the provisioning parameters used by buildVirtualMachine, and have flash reuse the configured values.
🤖 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/internal/exporterset/provisioners/kubevirt/kubevirt.go`:
- Line 247: Replace the resource.MustParse call in the disk-size handling with
fallible quantity parsing, propagate or return the parse error consistently with
the Memory handling in ParseParams, and ensure invalid DiskSize values are
rejected without panicking.
- Around line 161-181: Normalize nil Exporter handling across the KubeVirt
provisioner: update buildVirtualMachine, RenderPod, and EnrichExporterExport so
nil input never dereferences exporter.Name or panics. Preserve the existing
GenerateName behavior in buildExporterPod, or remove it only if all KubeVirt
callers are guaranteed to provide a non-nil Exporter; keep behavior consistent
with the QEMU implementation.
In `@controller/internal/exporterset/provisioners/kubevirt/params.go`:
- Around line 94-122: Update the numeric validation in the resources CPU
handling and forwardPorts loop to reject fractional values rather than allowing
toInt truncation. When cpu or a forwarded port is supplied but is not a valid
positive integer within the existing port range, return a validation error
instead of silently ignoring or truncating it; preserve valid integer handling
and existing field assignments.
In `@controller/internal/exporterset/provisioners/kubevirt/remote.go`:
- Around line 77-79: Update the cached-client lookup in the remote client
provisioning flow to detect changes to the referenced Secret or VTC credentials
reference before returning the cached client. Evict and recreate the entry when
credentials change, or key the cache by a credential revision, while preserving
reuse for unchanged credentials.
In
`@python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/client.py`:
- Around line 1-14: Run make lint-fix to resolve the I001 import-order violation
in client.py, ensuring the import block is formatted according to Ruff’s
automatic sorting rules without manually reordering imports.
In
`@python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py`:
- Around line 1-17: Fix the I001 import-order violation in driver.py by running
make lint-fix, ensuring Ruff separates the jumpstarter_driver_* first-party
imports from the third-party websockets imports and applies the canonical
ordering.
- Line 140: Fix the ruff E501 violation on the _build_console_ws_url function
signature by formatting its parameters across multiple lines while preserving
the existing type annotations and return type.
- Around line 119-137: Update the PowerReading construction in read to use only
fields defined by PowerReading, replacing the invalid online argument with the
appropriate existing voltage/current representation. Also update the MockPower
examples to avoid the invalid status field, keeping all PowerReading call sites
consistent with the model definition.
- Around line 271-279: Update the DataVolume replacement flow around
client.replace to catch lightkube.ApiError only, call client.create solely when
the error status code is 404, and re-raise all other API errors unchanged.
- Around line 19-26: Update the optional lightkube import block around
AsyncClient so the ImportError fallback does not rebind the imported class name
to None; use a separate availability marker or consistently typed alias, and
adjust downstream checks to use it while preserving behavior when lightkube is
unavailable. Confirm the change with make pkg-ty-jumpstarter-driver-kubevirt.
In `@python/pyproject.toml`:
- Line 28: Reorder the jumpstarter-driver-kubevirt entry in the
[tool.uv.sources] configuration so it appears before jumpstarter-driver-network,
preserving alphabetical ordering of driver package registrations.
---
Outside diff comments:
In `@python/packages/jumpstarter-driver-kubevirt/README.md`:
- Around line 1-11: Add the required documentation symlink named
jumpstarter-driver-kubevirt.md under the drivers package API reference
directory, targeting python/packages/jumpstarter-driver-kubevirt/README.md.
Preserve the existing README content and follow the established package README
link convention.
---
Nitpick comments:
In
`@python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py`:
- Around line 254-266: Update the flash method’s PVC construction to use
configurable disk size and access modes instead of hardcoded "20Gi" and
["ReadWriteOnce"]. Add these values to the driver configuration alongside
kubeconfig, namespace, and vm_name, initialize them from the provisioning
parameters used by buildVirtualMachine, and have flash reuse the configured
values.
🪄 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: 071b1318-a165-4f4d-aba3-a482868c9c93
⛔ Files ignored due to path filters (3)
controller/deploy/operator/go.sumis excluded by!**/*.sumcontroller/go.sumis excluded by!**/*.sumpython/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (18)
controller/cmd/exporter-set-controller/main.gocontroller/deploy/operator/go.modcontroller/go.modcontroller/internal/exporterset/exporterconfig.gocontroller/internal/exporterset/provisioner.gocontroller/internal/exporterset/provisioners/kubevirt/kubevirt.gocontroller/internal/exporterset/provisioners/kubevirt/kubevirt_test.gocontroller/internal/exporterset/provisioners/kubevirt/params.gocontroller/internal/exporterset/provisioners/kubevirt/remote.gocontroller/internal/exporterset/provisioners/qemu/enrich_test.gocontroller/internal/exporterset/provisioners/qemu/qemu.gopython/packages/jumpstarter-driver-kubevirt/README.mdpython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/__init__.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/client.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/py.typedpython/packages/jumpstarter-driver-kubevirt/pyproject.tomlpython/pyproject.toml
| func (p *Provisioner) buildVirtualMachine( | ||
| exporter *jumpstarterdevv1alpha1.Exporter, | ||
| params *KubevirtParams, | ||
| ) (*kubevirtv1.VirtualMachine, error) { | ||
| runStrategy := kubevirtv1.RunStrategyHalted | ||
|
|
||
| memory, err := resource.ParseQuantity(params.Memory) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("parse memory %q: %w", params.Memory, err) | ||
| } | ||
|
|
||
| vm := &kubevirtv1.VirtualMachine{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: exporter.Name, | ||
| Namespace: params.Namespace, | ||
| Labels: map[string]string{ | ||
| labelManagedBy: "jumpstarter", | ||
| labelExporter: exporter.Name, | ||
| labelProvenance: ProvisionerName, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'kubevirt.go|kubevirt.*|params.go|exporterset' . | sed 's#^\./##' | head -100
echo "== file outline =="
ast-grep outline controller/internal/exporterset/provisioners/kubevirt/kubevirt.go --view expanded || true
echo "== relevant buildVirtualMachine/RenderPod/EnrichExporterExport sections =="
cat -n controller/internal/exporterset/provisioners/kubevirt/kubevirt.go | sed -n '1,470p'
echo "== search RenderPod/EnrichExporterExport usages and nil exporter patterns =="
rg -n "RenderPod|EnrichExporterExport|buildExporterPod|exporter == nil|GenerateName|Name:" controller/internal/exporterset -SRepository: jumpstarter-dev/jumpstarter
Length of output: 45672
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate interface definitions and provisioner implementations =="
rg -n "type Provisioner|\bRenderPod\b|\bEnrichExporterExport\b|\bEnrichExporterExport\b" controller/internal/exporterset -S
echo "== git diff stat/name-only =="
git diff --stat -- controller/internal/exporterset/provisioners/kubevirt/kubevirt.go
git diff -- controller/internal/exporterset/provisioners/kubevirt/kubevirt.go | sed -n '1,240p'Repository: jumpstarter-dev/jumpstarter
Length of output: 5441
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== renderer interfaces and comments =="
cat -n controller/internal/exporterset/provisioner.go | sed -n '1,80p'
echo "== controller reconciler RenderPod path =="
cat -n controller/internal/exporterset/reconciler.go | sed -n '400,470p'
echo "== controller reconciler EnrichExporterExport path =="
cat -n controller/internal/exporterset/exporterconfig.go | sed -n '80,145p'
echo "== qemu provisioner nil exporter paths for comparison =="
cat -n controller/internal/exporterset/provisioners/qemu/qemu.go | sed -n '133,205p'
cat -n controller/internal/exporterset/provisioners/qemu/qemu.go | sed -n '275,335p'
cat -n controller/internal/exporterset/provisioners/qemu/enrich_test.go | sed -n '25,50p'Repository: jumpstarter-dev/jumpstarter
Length of output: 15215
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== kubevirt tests around nil exporter/vm name =="
cat -n controller/internal/exporterset/provisioners/kubevirt/kubevirt_test.go | sed -n '1,245p'
echo "== direct semantic check: nil exporter untyped dereference would panic if reached =="
python3 - <<'PY'
print("Programmatic evidence: buildExporterPod nil guard exists while buildVirtualMachine and EnrichExporterExport dereference exporter.Name unconditionally.")
print("Call path creates exporter CR; nil path is not exercised in kubevirt tests.")
PYRepository: jumpstarter-dev/jumpstarter
Length of output: 8281
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== createExporterPod callers around exporter lifecycle =="
cat -n controller/internal/exporterset/reconciler.go | sed -n '240,500p'
echo "== all direct nil exporter construction/tests outside kubevirt =="
rg -n "new.*Exporter\b|\bExporter\\b|render Pod|enrich drivers|EnrichExporterExport|RenderPod" controller/internal/exporterset/provisioners controller/internal/exporterset -SRepository: jumpstarter-dev/jumpstarter
Length of output: 37121
Normalize kubevirt’s nil-exporter handling.
RenderPod/EnrichExporterExport currently receive a non-nil Exporter from the reconciler’s normal scale-up path, but EnrichExporterExport can be called with nil (and the QEMU implementor treats nil as allowed), while buildVirtualMachine and EnrichExporterExport dereference exporter.Name unconditionally. Either update the provisioner interface/consumers to pass a non-nil Exporter for KubeVirt and remove this defensive GenerateName branch in buildExporterPod, or handle nil exporter consistently across RenderPod and EnrichExporterExport without panicking.
🤖 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/kubevirt/kubevirt.go` around
lines 161 - 181, Normalize nil Exporter handling across the KubeVirt
provisioner: update buildVirtualMachine, RenderPod, and EnrichExporterExport so
nil input never dereferences exporter.Name or panics. Preserve the existing
GenerateName behavior in buildExporterPod, or remove it only if all KubeVirt
callers are guaranteed to provide a non-nil Exporter; keep behavior consistent
with the QEMU implementation.
| } | ||
| } | ||
|
|
||
| diskSize := resource.MustParse(params.DiskSize) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
resource.MustParse panics on invalid DiskSize.
Memory uses the fallible resource.ParseQuantity with error handling (line 167), but DiskSize uses resource.MustParse, which panics on malformed quantities. Test coverage only validates memory (TestParseParams_rejectsInvalidMemory), not disk size, so this input's validity isn't demonstrably guaranteed before reaching this call.
🛡️ Proposed fix
- diskSize := resource.MustParse(params.DiskSize)
+ diskSize, err := resource.ParseQuantity(params.DiskSize)
+ if err != nil {
+ return nil, fmt.Errorf("parse disk size %q: %w", params.DiskSize, err)
+ }📝 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.
| diskSize := resource.MustParse(params.DiskSize) | |
| diskSize, err := resource.ParseQuantity(params.DiskSize) | |
| if err != nil { | |
| return nil, fmt.Errorf("parse disk size %q: %w", params.DiskSize, err) | |
| } |
🤖 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/kubevirt/kubevirt.go` at line
247, Replace the resource.MustParse call in the disk-size handling with fallible
quantity parsing, propagate or return the parse error consistently with the
Memory handling in ParseParams, and ensure invalid DiskSize values are rejected
without panicking.
| if resources, ok := params["resources"].(map[string]any); ok { | ||
| if cpu, ok := toInt(resources["cpu"]); ok && cpu > 0 { | ||
| p.CPU = cpu | ||
| } | ||
| if mem, ok := resources["memory"].(string); ok && mem != "" { | ||
| p.Memory = mem | ||
| } | ||
| if disk, ok := resources["storage"].(string); ok && disk != "" { | ||
| p.DiskSize = disk | ||
| } | ||
| } | ||
|
|
||
| if ssh, ok := params["sshAccess"].(string); ok { | ||
| switch ssh { | ||
| case "", "NodePort", "LoadBalancer": | ||
| p.SSHAccess = ssh | ||
| default: | ||
| return nil, fmt.Errorf("invalid sshAccess %q; must be \"NodePort\", \"LoadBalancer\", or empty", ssh) | ||
| } | ||
| } | ||
|
|
||
| if fwd, ok := params["forwardPorts"].(map[string]any); ok { | ||
| p.ForwardPorts = make(map[string]int, len(fwd)) | ||
| for name, v := range fwd { | ||
| if port, ok := toInt(v); ok && port > 0 && port <= 65535 { | ||
| p.ForwardPorts[name] = port | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject fractional numeric parameters instead of truncating them.
resources.cpu: 1.9 becomes 1, and forwardPorts.<name>: 2222.5 becomes 2222. Reject non-integral values explicitly; otherwise provisioning uses values the user did not request.
Proposed fix
+import "math"
+
func toInt(v any) (int, bool) {
switch n := v.(type) {
case int:
return n, true
case int64:
return int(n), true
case float64:
+ if math.Trunc(n) != n {
+ return 0, false
+ }
return int(n), true
default:
return 0, false
}
}Also return a validation error when cpu or a forwarded port is supplied but cannot be converted to a valid positive integer.
Also applies to: 220-232
🤖 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/kubevirt/params.go` around lines
94 - 122, Update the numeric validation in the resources CPU handling and
forwardPorts loop to reject fractional values rather than allowing toInt
truncation. When cpu or a forwarded port is supplied but is not a valid positive
integer within the existing port range, return a validation error instead of
silently ignoring or truncating it; preserve valid integer handling and existing
field assignments.
| if cl, ok := p.remoteClients.get(vtc.Name); ok { | ||
| return cl, nil | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Invalidate cached clients when remote credentials change.
This early return bypasses the Secret permanently after first use. Kubeconfig rotation or a changed credential reference will keep remote provisioning on stale credentials until the controller restarts. Evict this entry when the referenced Secret or VTC credentials reference changes, or key cached clients by a credential revision.
🤖 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/kubevirt/remote.go` around lines
77 - 79, Update the cached-client lookup in the remote client provisioning flow
to detect changes to the referenced Secret or VTC credentials reference before
returning the cached client. Evict and recreate the entry when credentials
change, or key the cache by a credential revision, while preserving reuse for
unchanged credentials.
| try: | ||
| from lightkube import AsyncClient | ||
| from lightkube.config.kubeconfig import KubeConfig | ||
| from lightkube.generic_resource import create_namespaced_resource | ||
| from lightkube.models.meta_v1 import ObjectMeta | ||
| from lightkube.types import PatchType | ||
| except ImportError: | ||
| AsyncClient = None |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Fix ty type-check failure: implicit shadowing of AsyncClient.
CI's type checker reports AsyncClient is implicitly shadowed — the try branch binds it to the lightkube.AsyncClient class, while except ImportError rebinds it to None, which ty sees as an inconsistent type for the name.
🛠️ Proposed fix
+from typing import Any
+
try:
from lightkube import AsyncClient
from lightkube.config.kubeconfig import KubeConfig
from lightkube.generic_resource import create_namespaced_resource
from lightkube.models.meta_v1 import ObjectMeta
from lightkube.types import PatchType
except ImportError:
- AsyncClient = None
+ AsyncClient: Any = NoneAs per coding guidelines, run make pkg-ty-<package_name> to confirm the fix rather than invoking a type checker manually.
📝 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.
| try: | |
| from lightkube import AsyncClient | |
| from lightkube.config.kubeconfig import KubeConfig | |
| from lightkube.generic_resource import create_namespaced_resource | |
| from lightkube.models.meta_v1 import ObjectMeta | |
| from lightkube.types import PatchType | |
| except ImportError: | |
| AsyncClient = None | |
| from typing import Any | |
| try: | |
| from lightkube import AsyncClient | |
| from lightkube.config.kubeconfig import KubeConfig | |
| from lightkube.generic_resource import create_namespaced_resource | |
| from lightkube.models.meta_v1 import ObjectMeta | |
| from lightkube.types import PatchType | |
| except ImportError: | |
| AsyncClient: Any = None |
🧰 Tools
🪛 GitHub Actions: Linters / 2_type-check-python.txt
[error] 26-26: ty error[invalid-assignment]: Implicit shadowing of class AsyncClient. AsyncClient is set to None in except ImportError. Rule: invalid-assignment.
🪛 GitHub Actions: Linters / type-check-python
[error] 26-26: ty error[invalid-assignment]: Implicit shadowing of class AsyncClient. Suggested to annotate to make it explicit. (rule invalid-assignment)
🤖 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-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py`
around lines 19 - 26, Update the optional lightkube import block around
AsyncClient so the ImportError fallback does not rebind the imported class name
to None; use a separate availability marker or consistently typed alias, and
adjust downstream checks to use it while preserving behavior when lightkube is
unavailable. Confirm the change with make pkg-ty-jumpstarter-driver-kubevirt.
Sources: Coding guidelines, Pipeline failures
| jumpstarter-driver-ridesx = { workspace = true } | ||
| jumpstarter-driver-network = { workspace = true } | ||
| jumpstarter-driver-obd = { workspace = true } | ||
| jumpstarter-driver-kubevirt = { workspace = true } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Place this source entry in alphabetical order.
Move jumpstarter-driver-kubevirt before jumpstarter-driver-network. As per coding guidelines, new driver packages must be registered under [tool.uv.sources] “in alphabetical order.”
🤖 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/pyproject.toml` at line 28, Reorder the jumpstarter-driver-kubevirt
entry in the [tool.uv.sources] configuration so it appears before
jumpstarter-driver-network, preserving alphabetical ordering of driver package
registrations.
Source: Coding guidelines
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
15a5693 to
a535e5c
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
controller/internal/exporterset/provisioners/kubevirt/kubevirt.go (1)
161-181: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winInconsistent nil-
exporterhandling within the same provisioner.
buildVirtualMachineandEnrichExporterExportdereferenceexporter.Nameunconditionally, whilebuildExporterPodin the same file defensively guards against a nilexporter(L356-360). TheProvisionerinterface'sEnrichExporterExportappears to tolerate nil in some flows (qemu's implementation ignores the param, and its tests passnil). If a nilExporterever reaches these KubeVirt paths, it will panic. Either guarantee callers always pass a non-nil Exporter for KubeVirt (and drop the defensive branch inbuildExporterPodfor consistency), or handle nil consistently across all three methods.Also applies to: 425-436
🤖 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/kubevirt/kubevirt.go` around lines 161 - 181, Handle nil exporters consistently across KubeVirt provisioner methods: update buildVirtualMachine, EnrichExporterExport, and buildExporterPod to follow the same nil-safe behavior. Preserve existing behavior for non-nil exporters and avoid dereferencing exporter.Name when the exporter is nil.controller/internal/exporterset/provisioners/kubevirt/remote.go (1)
77-79: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winStale cached client survives credential rotation.
Once cached, this bypasses the Secret permanently. Kubeconfig rotation or a changed
CredentialsSecretRefwill keep provisioning on stale credentials until the controller restarts. Evict the cache entry when the referenced Secret/credentials reference changes, or key the cache by a credential revision (e.g. Secret resourceVersion).🤖 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/kubevirt/remote.go` around lines 77 - 79, Update the cached-client lookup in the provisioner method around remoteClients.get and associate each client with the current credentials reference or Secret resourceVersion. Reuse the cached client only when that credential identity is unchanged; otherwise evict or replace the entry and create a client from the current Secret so credential rotation takes effect without restarting the controller.
🧹 Nitpick comments (1)
python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py (1)
20-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
_HAS_LIGHTKUBEis dead code that doesn't actually make lightkube optional.The
AsyncClientshadowing bug is fixed, but_HAS_LIGHTKUBEis set and never read anywhere in the file. Sincecreate_namespaced_resource(...)at module scope (lines 37-56) runs unconditionally, a missinglightkubeinstall would still raiseNameErrorat import time regardless of_HAS_LIGHTKUBE. Givenlightkube>=0.15is a required dependency inpyproject.toml, thetry/except ImportErroris effectively unreachable and misleading — it suggests lightkube is optional when it isn't.♻️ Proposed simplification (since lightkube is a hard dependency)
-if TYPE_CHECKING: - from lightkube import AsyncClient - -try: - from lightkube import AsyncClient - from lightkube.config.kubeconfig import KubeConfig - from lightkube.core.exceptions import ApiError - from lightkube.generic_resource import create_namespaced_resource - from lightkube.models.meta_v1 import ObjectMeta - from lightkube.types import PatchType - - _HAS_LIGHTKUBE = True -except ImportError: - _HAS_LIGHTKUBE = False +from lightkube import AsyncClient +from lightkube.config.kubeconfig import KubeConfig +from lightkube.core.exceptions import ApiError +from lightkube.generic_resource import create_namespaced_resource +from lightkube.models.meta_v1 import ObjectMeta +from lightkube.types import PatchType🤖 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-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py` around lines 20 - 34, Remove the unused _HAS_LIGHTKUBE flag and the surrounding optional-import try/except in driver.py. Import AsyncClient, KubeConfig, ApiError, create_namespaced_resource, ObjectMeta, and PatchType directly, preserving the existing module-scope resource setup and treating lightkube as the required dependency declared in pyproject.toml.
🤖 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/internal/exporterset/provisioners/kubevirt/params.go`:
- Around line 220-232: Update toInt to accept float64 values only when they
represent whole numbers, returning false for fractional values instead of
truncating them. Ensure callers handling resources.cpu and forwardPorts
propagate this failure so fractional inputs are rejected rather than silently
dropped.
- Around line 94-97: Add a reasonable maximum CPU core bound to the
resources.cpu validation in params.go, accepting values only when they are
positive and within that limit before assigning p.CPU. Ensure
buildVirtualMachine receives a value that cannot unexpectedly wrap during its
uint32(params.CPU) conversion.
In
`@python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py`:
- Around line 131-143: Update the exception handling in read() so unexpected
client errors are logged with their details instead of being silently swallowed.
Preserve the zero-valued PowerReading fallback only if that behavior is
required, while ensuring authentication, RBAC, network, and other failures are
distinguishable from a non-Running VMI.
In `@python/packages/jumpstarter-driver-kubevirt/pyproject.toml`:
- Line 27: Update the KubeVirt package’s dev dependencies to include
pytest-asyncio (at least version 0.26.0), and add asyncio_mode = "auto" under
its [tool.pytest.ini_options] configuration. Keep the existing pytest and
pytest-cov dependencies unchanged.
- Around line 11-18: Add the direct jumpstarter workspace mapping to the
[tool.uv.sources] section in pyproject.toml, using the same workspace
configuration as python/pyproject.toml. Keep the existing child-driver source
mappings unchanged.
---
Duplicate comments:
In `@controller/internal/exporterset/provisioners/kubevirt/kubevirt.go`:
- Around line 161-181: Handle nil exporters consistently across KubeVirt
provisioner methods: update buildVirtualMachine, EnrichExporterExport, and
buildExporterPod to follow the same nil-safe behavior. Preserve existing
behavior for non-nil exporters and avoid dereferencing exporter.Name when the
exporter is nil.
In `@controller/internal/exporterset/provisioners/kubevirt/remote.go`:
- Around line 77-79: Update the cached-client lookup in the provisioner method
around remoteClients.get and associate each client with the current credentials
reference or Secret resourceVersion. Reuse the cached client only when that
credential identity is unchanged; otherwise evict or replace the entry and
create a client from the current Secret so credential rotation takes effect
without restarting the controller.
---
Nitpick comments:
In
`@python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py`:
- Around line 20-34: Remove the unused _HAS_LIGHTKUBE flag and the surrounding
optional-import try/except in driver.py. Import AsyncClient, KubeConfig,
ApiError, create_namespaced_resource, ObjectMeta, and PatchType directly,
preserving the existing module-scope resource setup and treating lightkube as
the required dependency declared in pyproject.toml.
🪄 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: 008da57d-386b-4b61-965e-71e1beda22a7
⛔ Files ignored due to path filters (3)
controller/deploy/operator/go.sumis excluded by!**/*.sumcontroller/go.sumis excluded by!**/*.sumpython/uv.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
controller/cmd/exporter-set-controller/main.gocontroller/deploy/operator/go.modcontroller/go.modcontroller/internal/exporterset/exporterconfig.gocontroller/internal/exporterset/provisioner.gocontroller/internal/exporterset/provisioners/kubevirt/kubevirt.gocontroller/internal/exporterset/provisioners/kubevirt/kubevirt_test.gocontroller/internal/exporterset/provisioners/kubevirt/params.gocontroller/internal/exporterset/provisioners/kubevirt/remote.gocontroller/internal/exporterset/provisioners/qemu/enrich_test.gocontroller/internal/exporterset/provisioners/qemu/qemu.gopython/packages/jumpstarter-driver-kubevirt/README.mdpython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/__init__.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/client.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/py.typedpython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/test_client.pypython/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/test_driver.pypython/packages/jumpstarter-driver-kubevirt/pyproject.tomlpython/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (2)
- python/packages/jumpstarter-driver-kubevirt/README.md
- controller/internal/exporterset/exporterconfig.go
| if resources, ok := params["resources"].(map[string]any); ok { | ||
| if cpu, ok := toInt(resources["cpu"]); ok && cpu > 0 { | ||
| p.CPU = cpu | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
No upper bound on resources.cpu.
Only cpu > 0 is enforced; a very large user-supplied value flows unchecked into uint32(params.CPU) in kubevirt.go (buildVirtualMachine, L187), which static analysis flags as a narrowing conversion that can silently wrap. Add a reasonable upper bound (e.g., a sane max core count) here to prevent downstream truncation to an unexpected value.
🤖 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/kubevirt/params.go` around lines
94 - 97, Add a reasonable maximum CPU core bound to the resources.cpu validation
in params.go, accepting values only when they are positive and within that limit
before assigning p.CPU. Ensure buildVirtualMachine receives a value that cannot
unexpectedly wrap during its uint32(params.CPU) conversion.
Source: Linters/SAST tools
| // toInt converts a numeric interface{} (float64 from JSON, int, etc.) to int. | ||
| func toInt(v any) (int, bool) { | ||
| switch n := v.(type) { | ||
| case int: | ||
| return n, true | ||
| case int64: | ||
| return int(n), true | ||
| case float64: | ||
| return int(n), true | ||
| default: | ||
| return 0, false | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fractional numeric params are silently truncated.
toInt truncates float64 (e.g., resources.cpu: 1.9 → 1) instead of rejecting non-integral values. This still affects resources.cpu (L95) and forwardPorts (L118), which silently drops invalid ports rather than erroring.
🐛 Proposed fix
+import "math"
+
func toInt(v any) (int, bool) {
switch n := v.(type) {
case int:
return n, true
case int64:
return int(n), true
case float64:
+ if math.Trunc(n) != n {
+ return 0, false
+ }
return int(n), true
default:
return 0, false
}
}🤖 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/kubevirt/params.go` around lines
220 - 232, Update toInt to accept float64 values only when they represent whole
numbers, returning false for fractional values instead of truncating them.
Ensure callers handling resources.cpu and forwardPorts propagate this failure so
fractional inputs are rejected rather than silently dropped.
| try: | ||
| vmi = await client.get( | ||
| VirtualMachineInstance, | ||
| name=name, | ||
| namespace=self.namespace, | ||
| ) | ||
| phase = vmi.get("status", {}).get("phase", "Unknown") | ||
| if phase == "Running": | ||
| yield PowerReading(voltage=5.0, current=1.0) | ||
| else: | ||
| yield PowerReading(voltage=0.0, current=0.0) | ||
| except Exception: | ||
| yield PowerReading(voltage=0.0, current=0.0) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Silently swallowing all exceptions in read() hides real failures.
Any error (auth failure, RBAC denial, network issue) is reported identically to "VM stopped" with no logging, making it hard to distinguish a genuinely powered-off VM from a broken connection to the cluster.
🩹 Proposed fix
- except Exception:
+ except Exception:
+ logger.warning("failed to read VMI status for %s/%s", self.namespace, name, exc_info=True)
yield PowerReading(voltage=0.0, current=0.0)📝 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.
| try: | |
| vmi = await client.get( | |
| VirtualMachineInstance, | |
| name=name, | |
| namespace=self.namespace, | |
| ) | |
| phase = vmi.get("status", {}).get("phase", "Unknown") | |
| if phase == "Running": | |
| yield PowerReading(voltage=5.0, current=1.0) | |
| else: | |
| yield PowerReading(voltage=0.0, current=0.0) | |
| except Exception: | |
| yield PowerReading(voltage=0.0, current=0.0) | |
| try: | |
| vmi = await client.get( | |
| VirtualMachineInstance, | |
| name=name, | |
| namespace=self.namespace, | |
| ) | |
| phase = vmi.get("status", {}).get("phase", "Unknown") | |
| if phase == "Running": | |
| yield PowerReading(voltage=5.0, current=1.0) | |
| else: | |
| yield PowerReading(voltage=0.0, current=0.0) | |
| except Exception: | |
| logger.warning("failed to read VMI status for %s/%s", self.namespace, name, exc_info=True) | |
| yield PowerReading(voltage=0.0, current=0.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
`@python/packages/jumpstarter-driver-kubevirt/jumpstarter_driver_kubevirt/driver.py`
around lines 131 - 143, Update the exception handling in read() so unexpected
client errors are logged with their details instead of being silently swallowed.
Preserve the zero-valued PowerReading fallback only if that behavior is
required, while ensuring authentication, RBAC, network, and other failures are
distinguishable from a non-Running VMI.
| dependencies = [ | ||
| "jumpstarter", | ||
| "jumpstarter-driver-composite", | ||
| "jumpstarter-driver-power", | ||
| "jumpstarter-driver-network", | ||
| "lightkube>=0.15", | ||
| "websockets>=13.0", | ||
| ] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd pyproject.toml python/packages/jumpstarter-driver-network --exec cat -n {}
fd pyproject.toml python/packages/jumpstarter-driver-power --exec cat -n {}Repository: jumpstarter-dev/jumpstarter
Length of output: 3422
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List workspace/package pyproject files and inspect exact relevant sections.
printf 'workspace pyproject relevant sections:\n'
sed -n '1,140p' python/pyproject.toml
printf '\nkubevirt pyproject relevant sections:\n'
sed -n '1,90p' python/packages/jumpstarter-driver-kubevirt/pyproject.toml
printf '\nother relevant driver/package pyproject files with sources/deps:\n'
for f in $(fd 'pyproject\.toml$' python/packages | sort); do
if rg -q '^\[tool\.uv\.sources\]|dependencies =|dependency-groups' "$f"; then
echo "===== $f ====="
rg -n '(\[project\]|\[tool\.uv\.sources\]|\[dependency-groups\]|name = |dependencies = "jumpstarter"|jumpstarter = { workspace = true })' "$f"
echo
fi
done
# Programmatic check: packages with direct "jumpstarter" dependency but no local workspace source mapping.
python3 - <<'PY'
from pathlib import Path
import re
pkg = Path('python/packages')
for tomli_f in sorted(pkg.rglob('pyproject.toml')):
text = tomli_f.read_text()
deps = re.search(r'^dependencies\s*=\s*\[(.*?)\]', text, re.S)
source = re.search(r'^\[tool\.uv\.sources\]\s*\n((?:(?:\S+\s*=\s*\{[^}]+\}[*\n])*)?)', text, re.S)
has_jumpstarter_dep = deps and bool(re.search(r'"jumpstarter"[^";]+|jumpstarter', deps.group(1)))
has_jumpstarter_ws = source and 'jumpstarter' in source.group(1)
if has_jumpstarter_dep:
print(f"{tomli_f}: has_jumpstarter_dep={has_jumpstarter_dep} has_jumpstarter_ws={has_jumpstarter_ws} has_uv_sources={bool(source)}")
PYRepository: jumpstarter-dev/jumpstarter
Length of output: 6170
Add jumpstarter = { workspace = true } to [tool.uv.sources].
jumpstarter is a direct dependency, while the child driver dependencies are mapped to the workspace. python/pyproject.toml also declares jumpstarter as a workspace source, so omitting it here leaves this package with inconsistent resolution behavior.
🔧 Proposed fix
[tool.uv.sources]
+jumpstarter = { workspace = true }
jumpstarter-driver-composite = { workspace = true }
jumpstarter-driver-network = { workspace = true }
jumpstarter-driver-power = { workspace = true }📝 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.
| dependencies = [ | |
| "jumpstarter", | |
| "jumpstarter-driver-composite", | |
| "jumpstarter-driver-power", | |
| "jumpstarter-driver-network", | |
| "lightkube>=0.15", | |
| "websockets>=13.0", | |
| ] | |
| [tool.uv.sources] | |
| jumpstarter = { workspace = true } | |
| jumpstarter-driver-composite = { workspace = true } | |
| jumpstarter-driver-network = { workspace = true } | |
| jumpstarter-driver-power = { workspace = true } |
🤖 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-driver-kubevirt/pyproject.toml` around lines 11 -
18, Add the direct jumpstarter workspace mapping to the [tool.uv.sources]
section in pyproject.toml, using the same workspace configuration as
python/pyproject.toml. Keep the existing child-driver source mappings unchanged.
| KubevirtStorage = "jumpstarter_driver_kubevirt.driver:KubevirtStorage" | ||
|
|
||
| [dependency-groups] | ||
| dev = ["pytest>=8.3.2", "pytest-cov>=5.0.0"] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "== search for pytest-asyncio across the workspace =="
rg -n "pytest-asyncio|asyncio_mode" python -g '*.toml' -g '*.cfg' -g '*.ini'Repository: jumpstarter-dev/jumpstarter
Length of output: 3394
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== kubevirt marker usages =="
rg -n "`@pytest`\.mark\.asyncio|async def " python/packages/jumpstarter-driver-kubevirt || true
echo
echo "== kubevirt test files =="
fd -a 'test_.*\.py|.*_test\.py' python/packages/jumpstarter-driver-kubevirt | sed 's#^\./##'
echo
echo "== kubevirt pyproject =="
cat -n python/packages/jumpstarter-driver-kubevirt/pyproject.toml | sed -n '1,120p'
echo
echo "== any pytest config files =="
fd -a 'pytest.ini|conftest.py|pyproject.toml' python/packages/jumpstarter-driver-kubevirt . | sort | xargs -r -n1 sh -c 'echo "--- $0"; sed -n "/pytest\|asyncio_mode\|\\[tool\.pytest\\]|\\[tool.hatch|\\[project|\\[build-system\\]|dev =/p" "$0"'Repository: jumpstarter-dev/jumpstarter
Length of output: 22021
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== python workspace pytest/asyncio deps =="
cat -n python/pyproject.toml | sed -n '1,140p'
echo
echo "== workspace root pytest options and deps =="
rg -n "pytest-asyncio|asyncio_mode|pytest-cov|pytest>=" python/pyproject.tomlRepository: jumpstarter-dev/jumpstarter
Length of output: 5110
Add pytest-asyncio and configure asyncio_mode for the KubeVirt tests.
test_driver.py and test_client.py mark async tests with @pytest.mark.asyncio, but python/packages/jumpstarter-driver-kubevirt/pyproject.toml only declares pytest and pytest-cov in dev; root python/pyproject.toml has no pytest-asyncio dependency or asyncio_mode option. Add the plugin, e.g. pytest-asyncio>=0.26.0, and asyncio_mode = "auto" under [tool.pytest.ini_options] so these tests run correctly.
🤖 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-driver-kubevirt/pyproject.toml` at line 27,
Update the KubeVirt package’s dev dependencies to include pytest-asyncio (at
least version 0.26.0), and add asyncio_mode = "auto" under its
[tool.pytest.ini_options] configuration. Keep the existing pytest and pytest-cov
dependencies unchanged.
experimenting