feat(console): VM power controls (start/stop/restart) + VNC fixes#27
Conversation
Add Start/Restart/Stop buttons to the VMInstance detail page that call
the KubeVirt subresources.kubevirt.io virtualmachines/{name}/start|stop|
restart endpoints. The underlying VirtualMachine is resolved as
<release.prefix><app-name> and its printableStatus drives which buttons
are enabled. Adds a generic subresource() method to the k8s client and a
useK8sSubresource() mutation hook.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The VNC console targeted the VirtualMachineInstance by the cozystack app name (e.g. "demo-vm"), but KubeVirt names it "<release.prefix><name>" (e.g. "vm-instance-demo-vm"), so the websocket 404'd. Resolve the VMI name via release.prefix, the same way the VM power controls do. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
KubeVirt action subresources (virtualmachines/{name}/start|stop|restart)
return 202 Accepted with an empty body. request() called res.json()
unconditionally, throwing "Unexpected end of JSON input" and surfacing a
spurious error toast even though the action succeeded. Read the body as
text and return undefined when empty.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
VncTab connected the websocket whenever the app was a VMInstance, regardless of power state, so a stopped VM showed a dead console. Poll the VirtualMachine printableStatus and only attach when it is Running; otherwise show a 'not running' notice. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
📝 WalkthroughWalkthroughAdds a K8s subresource API and hook, a VMPowerControls UI integrated into ApplicationDetailPage, VncTab changes that gate noVNC initialization on VM running state, tests exercising name resolution and subresource invalidation, plus Vite dev-server WebSocket proxy support. ChangesVM Controls and VNC Enhancement
Sequence Diagram(s)sequenceDiagram
participant User
participant VMPowerControls
participant useK8sSubresource as Hook
participant K8sClient
participant KubernetesAPI as Kubernetes API
User->>VMPowerControls: click Start/Stop/Restart
VMPowerControls->>useK8sSubresource: mutate({ subresource, body?, method? })
useK8sSubresource->>K8sClient: subresource(apiGroup, apiVersion, plural, name, subresource, namespace, body, method)
K8sClient->>KubernetesAPI: PUT/POST /apis/{group}/{version}/namespaces/{ns}/{plural}/{name}/{subresource}
KubernetesAPI-->>K8sClient: response (possibly empty)
K8sClient-->>useK8sSubresource: result / error
useK8sSubresource->>useK8sSubresource: invalidateQueries(k8sResourceKey)
useK8sSubresource-->>VMPowerControls: settled result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Code Review
This pull request introduces VM power controls (start, stop, restart) for virtual machines and updates the VNC console tab to only attempt connection when the VM is running. It also extends the Kubernetes client and hooks to support subresource mutations and handle empty 2xx responses. Feedback highlights opportunities to improve the subresource hook by allowing empty request bodies and to reduce code duplication in the VNC tab by refactoring the reconnection logic using a state-based key.
| ref.name, | ||
| subresource, | ||
| ref.namespace, | ||
| body ?? {}, |
There was a problem hiding this comment.
The client.subresource method already handles undefined bodies gracefully by omitting the body from the request. Forcing body ?? {} here prevents callers from sending requests without a body, which might be required or preferred for certain subresources. Passing body directly preserves the flexibility of the underlying client method.
body,|
|
||
| const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:" | ||
| const wsUrl = `${wsProtocol}//${window.location.host}/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${instance.metadata.name}/vnc` | ||
| const wsUrl = `${wsProtocol}//${window.location.host}/apis/subresources.kubevirt.io/v1/namespaces/${ns}/virtualmachineinstances/${vmName}/vnc` |
There was a problem hiding this comment.
There is significant code duplication between the useEffect hook (lines 45-121) and the handleReconnect function (lines 173-224) for initializing the noVNC client.\n\nWe can eliminate this duplication entirely by introducing a reconnectKey state variable and adding it to the useEffect dependency array. This allows handleReconnect to simply increment the key, triggering the useEffect to clean up the old connection and establish a new one.\n\n### Suggested Refactoring\n\n1. Add a new state variable:\ntypescript\nconst [reconnectKey, setReconnectKey] = useState(0);\n\n\n2. Update the useEffect dependency array to include reconnectKey, and ensure it resets the container and states at the start:\ntypescript\nuseEffect(() => {\n if (appKind !== "VMInstance" || !isRunning || !vncContainerRef.current)\n return;\n\n let mounted = true;\n vncContainerRef.current.innerHTML = "";\n setConnecting(true);\n setError(null);\n\n // ... connection logic ...\n\n return () => {\n mounted = false;\n if (rfbRef.current) {\n rfbRef.current.disconnect();\n rfbRef.current = null;\n }\n };\n}, [appKind, ns, vmName, isRunning, reconnectKey]);\n\n\n3. Simplify handleReconnect to just increment the key:\ntypescript\nconst handleReconnect = () => {\n setReconnectKey((prev) => prev + 1);\n};\n
Restyle the VNC tab to mirror cozyportal-ui: a dark toolbar with a connection-status indicator (Monitor icon + Connected/Connecting/ Disconnected + green dot) and icon buttons for Ctrl+Alt+Del, fullscreen and reconnect; connecting/error overlays; aspect-ratio sizing with fullscreen support. Reconnect now runs through a connectionKey-driven effect instead of a duplicated RFB setup path. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The dev server's /apis (and /api) proxy lacked ws: true, so the VNC console's WebSocket upgrade was never forwarded to kubectl proxy and the console hung on 'Connecting…'. Enable ws on both proxy entries. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/console/src/routes/detail/VMPowerControls.tsx (1)
38-50: ⚡ Quick winAvoid
refetchIntervalpolling; use a watch-based list query.This polls the VirtualMachine every 5s.
useK8sGethas no watch support, butuseK8sListdoes (chunked-encoding watch), so the status can be obtained without polling by listingvirtualmachineswith afieldSelectorofmetadata.name=${vmName}.As per coding guidelines: "
useK8sListalready handles watches via chunked-encoding. Don't poll, don't addrefetchInterval, use{ watch: false }for one-shot queries".🤖 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 `@apps/console/src/routes/detail/VMPowerControls.tsx` around lines 38 - 50, Replace the polling get call with a watch-based list: stop using useK8sGet with refetchInterval and instead call useK8sList to list "virtualmachines" with a fieldSelector of metadata.name=${vmName} and namespace ns (this returns the VM status via the watch/chunked-encoding). Remove refetchInterval and enabled polling; rely on useK8sList's watch behavior (or pass { watch: false } only if you explicitly want a one-shot fetch). Keep references to vmName, ns and the plural "virtualmachines" to locate and update the existing call.apps/console/src/routes/detail/VncTab.tsx (1)
22-36: ⚡ Quick winAvoid
refetchIntervalpolling here too.Same concern as
VMPowerControls: this polls the VirtualMachine every 5s. Prefer a watch-baseduseK8sListwith afieldSelectoronmetadata.nameto gate on running state without polling.As per coding guidelines: "Don't poll, don't add
refetchInterval, use{ watch: false }for one-shot queries".🤖 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 `@apps/console/src/routes/detail/VncTab.tsx` around lines 22 - 36, The current useK8sGet call in VncTab.tsx (variables vm, vmLoading) uses refetchInterval to poll the VirtualMachine; remove the refetchInterval and replace this polling pattern with a watch-based query: use useK8sList instead of useK8sGet, set watch:true and provide a fieldSelector of `metadata.name=${vmName}` (and namespace `ns`) so the list watches only the specific VM and updates when its running state changes; if you only need a one-shot read instead, set `{ watch: false }` on the query rather than adding refetchInterval. Ensure the gating logic that uses appKind, vmName and ns remains the same.
🤖 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 `@apps/console/src/routes/detail/VMPowerControls.tsx`:
- Around line 52-77: The action's success currently invalidates caches for
KUBEVIRT_SUBRESOURCE_GROUP (subresources.kubevirt.io) so the VM status (queried
under KUBEVIRT_GROUP / kubevirt.io) is not refreshed; update the success handler
used by useK8sSubresource (or add a post-success step in run after
action.mutateAsync) to call the query invalidation for the VM status key under
KUBEVIRT_GROUP (the same key used to fetch vm?.status/printableStatus), so after
run (and action.mutateAsync) completes successfully the kubevirt.io VM query is
invalidated and the UI updates immediately. Make sure to reference the same
name/namespace (vmName / ns) when invalidating the kubevirt.io VM query.
---
Nitpick comments:
In `@apps/console/src/routes/detail/VMPowerControls.tsx`:
- Around line 38-50: Replace the polling get call with a watch-based list: stop
using useK8sGet with refetchInterval and instead call useK8sList to list
"virtualmachines" with a fieldSelector of metadata.name=${vmName} and namespace
ns (this returns the VM status via the watch/chunked-encoding). Remove
refetchInterval and enabled polling; rely on useK8sList's watch behavior (or
pass { watch: false } only if you explicitly want a one-shot fetch). Keep
references to vmName, ns and the plural "virtualmachines" to locate and update
the existing call.
In `@apps/console/src/routes/detail/VncTab.tsx`:
- Around line 22-36: The current useK8sGet call in VncTab.tsx (variables vm,
vmLoading) uses refetchInterval to poll the VirtualMachine; remove the
refetchInterval and replace this polling pattern with a watch-based query: use
useK8sList instead of useK8sGet, set watch:true and provide a fieldSelector of
`metadata.name=${vmName}` (and namespace `ns`) so the list watches only the
specific VM and updates when its running state changes; if you only need a
one-shot read instead, set `{ watch: false }` on the query rather than adding
refetchInterval. Ensure the gating logic that uses appKind, vmName and ns
remains the same.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9b2923a1-5a36-4bc5-8d9f-c9db4a2157bf
📒 Files selected for processing (7)
apps/console/src/routes/detail/ApplicationDetailPage.tsxapps/console/src/routes/detail/VMPowerControls.tsxapps/console/src/routes/detail/VncTab.tsxapps/console/vite.config.tspackages/k8s-client/src/client.tspackages/k8s-client/src/hooks.tspackages/k8s-client/src/index.ts
# Conflicts: # apps/console/src/routes/detail/VncTab.tsx
KubeVirt action subresources (virtualmachines/{name}/start|stop|restart)
answer 2xx with an empty body, so the request helper must return undefined
instead of letting JSON.parse("") throw. Pin that, the 204 short-circuit,
and the subresource path/method construction.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
A subresource action and the resource whose status it changes can live under different API groups: KubeVirt serves start/stop/restart under subresources.kubevirt.io but the VirtualMachine (with printableStatus) under kubevirt.io. The success handler built its invalidation key from the action ref, so it never matched the status query. Add an optional invalidate target ref and key off the resource prefix [k8s, group, version, plural, namespace], which React Query prefix-matches against the by-name GET and any field/label-selected LIST — so a watch-based status read is refreshed too. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
Several fixes to the VM power controls: - Resolve the KubeVirt VirtualMachine name from releasePrefix(ad) instead of an empty-string prefix fallback, so it targets the same object the VNC tab does and never queries a non-existent name when release.prefix is unset. - Read status via useK8sList with a metadata.name field-selector instead of a polled useK8sGet, so the watch layer streams printableStatus transitions live (the architecture forbids refetchInterval); the action also invalidates the resource for an instant refresh. - Drop the dead "Halted" branch (not a KubeVirt printableStatus value) and enable stop/restart for a Paused VM, which still has a running instance. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…lution The VNC tab hardcoded the "vm-instance-" prefix and polled the VirtualMachine power state with refetchInterval. Resolve the name through releasePrefix(ad) so it agrees with the power controls on the target object, and read status via useK8sList with a metadata.name field-selector so the watch layer tracks the power state live without polling. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…n events EventsTab derived the Helm release name with an empty-string prefix fallback, so when release.prefix was unset its app.kubernetes.io/instance selector queried the bare app name and matched none of the Helm-managed Pods/PVCs (which carry the "<singular>-<name>" instance label), silently dropping their events. Route it through releasePrefix(ad) like the VM tabs, and cover both releasePrefix and the resulting selector with tests. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
The helper that binds AdditionalPropertiesField walked schema nodes through an as-any cast. Route it through a small structural interface instead, narrowing unknown values explicitly. No behaviour change — the existing array-items map test still passes. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@apps/console/src/components/SchemaForm.tsx`:
- Around line 145-151: The walker currently skips tuple-style arrays due to the
"!Array.isArray(items)" check so tuple item schemas never get
bindAdditionalProperties applied; update the logic in SchemaForm.tsx where node,
items, bindAdditionalProperties, and uiNode?.items are used to handle both
cases: if items is an object (typeof items === "object" &&
!Array.isArray(items") keep the existing bindAdditionalProperties call for the
single items schema, and if Array.isArray(items) iterate over each tuple schema
calling bindAdditionalProperties(items[i] as RJSFSchema, uiNode?.items?.[i]) so
each tuple element recurses; ensure the uiNode item lookup aligns with tuple
indices and preserve existing behavior for non-array items.
In `@apps/console/src/routes/detail/VncTab.tsx`:
- Around line 40-42: The tab currently only treats VMs with powerStatus ===
"Running" as attachable; update the logic to treat "Paused" as attachable too
(since VMPowerControls already considers "Paused" a live state). Replace checks
using isRunning (or powerStatus === "Running") in VncTab (the declaration of
isRunning and the other usages around the other occurrences) with a new
predicate like isAttachable = powerStatus === "Running" || powerStatus ===
"Paused", and use isAttachable wherever the UI gates VNC/attach behavior
(including the other occurrences noted).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 74476f05-54b5-446f-8bb4-5b002e203264
📒 Files selected for processing (11)
apps/console/src/__tests__/k8s-client/client.subresource.test.tsapps/console/src/__tests__/k8s-client/useK8sSubresource.test.tsxapps/console/src/components/SchemaForm.tsxapps/console/src/lib/app-definitions.test.tsapps/console/src/routes/detail/EventsTab.test.tsxapps/console/src/routes/detail/EventsTab.tsxapps/console/src/routes/detail/VMPowerControls.test.tsxapps/console/src/routes/detail/VMPowerControls.tsxapps/console/src/routes/detail/VncTab.test.tsxapps/console/src/routes/detail/VncTab.tsxpackages/k8s-client/src/hooks.ts
| if ( | ||
| node.type === "array" && | ||
| node.items && | ||
| typeof node.items === "object" && | ||
| !Array.isArray(node.items) | ||
| items && | ||
| typeof items === "object" && | ||
| !Array.isArray(items) | ||
| ) { | ||
| const itemsUi = bindAdditionalProperties(node.items as RJSFSchema, (uiNode as any)?.items) | ||
| const itemsUi = bindAdditionalProperties(items as RJSFSchema, uiNode?.items) |
There was a problem hiding this comment.
Don't skip tuple-array item schemas.
!Array.isArray(items) means this walker no longer visits tuple-style arrays (items: [...]), so any additionalProperties map nested in those item schemas will miss the custom field binding and fall back to the broken native map rendering path. Please recurse through both object items and array items here. As per coding guidelines: "For form widget binding, walk both properties and items for arrays, but do not walk oneOf/anyOf/allOf unless a real chart needs it".
Suggested fix
const items = node.items
- if (
- node.type === "array" &&
- items &&
- typeof items === "object" &&
- !Array.isArray(items)
- ) {
- const itemsUi = bindAdditionalProperties(items as RJSFSchema, uiNode?.items)
+ if (node.type === "array" && items && typeof items === "object") {
+ if (Array.isArray(items)) {
+ const nextItems = items.map((item, index) =>
+ item && typeof item === "object"
+ ? bindAdditionalProperties(
+ item as RJSFSchema,
+ Array.isArray(uiNode?.items) ? (uiNode.items[index] as UiSchema | undefined) : undefined,
+ )
+ : undefined,
+ )
+ if (nextItems.some((itemUi) => itemUi !== undefined)) {
+ return { ...uiNode, items: nextItems }
+ }
+ return uiNode
+ }
+
+ const itemsUi = bindAdditionalProperties(items as RJSFSchema, uiNode?.items)
if (itemsUi !== undefined) {
return { ...uiNode, items: itemsUi }
}
}📝 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.
| if ( | |
| node.type === "array" && | |
| node.items && | |
| typeof node.items === "object" && | |
| !Array.isArray(node.items) | |
| items && | |
| typeof items === "object" && | |
| !Array.isArray(items) | |
| ) { | |
| const itemsUi = bindAdditionalProperties(node.items as RJSFSchema, (uiNode as any)?.items) | |
| const itemsUi = bindAdditionalProperties(items as RJSFSchema, uiNode?.items) | |
| if (node.type === "array" && items && typeof items === "object") { | |
| if (Array.isArray(items)) { | |
| const nextItems = items.map((item, index) => | |
| item && typeof item === "object" | |
| ? bindAdditionalProperties( | |
| item as RJSFSchema, | |
| Array.isArray(uiNode?.items) ? (uiNode.items[index] as UiSchema | undefined) : undefined, | |
| ) | |
| : undefined, | |
| ) | |
| if (nextItems.some((itemUi) => itemUi !== undefined)) { | |
| return { ...uiNode, items: nextItems } | |
| } | |
| return uiNode | |
| } | |
| const itemsUi = bindAdditionalProperties(items as RJSFSchema, uiNode?.items) | |
| if (itemsUi !== undefined) { | |
| return { ...uiNode, items: itemsUi } | |
| } | |
| } |
🤖 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 `@apps/console/src/components/SchemaForm.tsx` around lines 145 - 151, The
walker currently skips tuple-style arrays due to the "!Array.isArray(items)"
check so tuple item schemas never get bindAdditionalProperties applied; update
the logic in SchemaForm.tsx where node, items, bindAdditionalProperties, and
uiNode?.items are used to handle both cases: if items is an object (typeof items
=== "object" && !Array.isArray(items") keep the existing
bindAdditionalProperties call for the single items schema, and if
Array.isArray(items) iterate over each tuple schema calling
bindAdditionalProperties(items[i] as RJSFSchema, uiNode?.items?.[i]) so each
tuple element recurses; ensure the uiNode item lookup aligns with tuple indices
and preserve existing behavior for non-array items.
| const powerStatus = vmList?.items[0]?.status?.printableStatus | ||
| const isRunning = powerStatus === "Running" | ||
|
|
There was a problem hiding this comment.
Treat Paused VMs as attachable here too.
VMPowerControls already treats "Paused" as a live-VMI state, but this tab gates everything on powerStatus === "Running". That sends paused VMs down the "not running" path and prevents VNC from opening even though the virtualmachineinstances/{name}/vnc target still exists.
Suggested fix
- const powerStatus = vmList?.items[0]?.status?.printableStatus
- const isRunning = powerStatus === "Running"
+ const powerStatus = vmList?.items[0]?.status?.printableStatus
+ const hasRunningInstance =
+ powerStatus === "Running" || powerStatus === "Paused"
...
- if (!containerRef.current || appKind !== "VMInstance" || !isRunning) return
+ if (!containerRef.current || appKind !== "VMInstance" || !hasRunningInstance) return
...
- if (!isRunning) {
+ if (!hasRunningInstance) {Also applies to: 53-55, 160-168
🤖 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 `@apps/console/src/routes/detail/VncTab.tsx` around lines 40 - 42, The tab
currently only treats VMs with powerStatus === "Running" as attachable; update
the logic to treat "Paused" as attachable too (since VMPowerControls already
considers "Paused" a live state). Replace checks using isRunning (or powerStatus
=== "Running") in VncTab (the declaration of isRunning and the other usages
around the other occurrences) with a new predicate like isAttachable =
powerStatus === "Running" || powerStatus === "Paused", and use isAttachable
wherever the UI gates VNC/attach behavior (including the other occurrences
noted).
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
VM power controls (start/stop/restart) plus the bundled VNC, k8s-client subresource, and events-tab fixes. typecheck and the full vitest suite (281 tests) pass, and each behavior and fix is covered by a test. VM status is read through the watch layer (no polling), the cross-API-group cache invalidation is verified, and the events tab now matches Helm-managed resources by the prefixed release name. LGTM.
<img width="2770" height="1970" alt="image" src="https://github.com/user-attachments/assets/774c7c71-0e6a-496a-9898-8ad1ed848b0a" /> ## Summary Grants tenant users permission to call the KubeVirt VM power-control subresources (`subresources.kubevirt.io` → `virtualmachines/{name}/start|stop|restart`, verb `update`) by adding them to the `cozy:tenant:use:base` ClusterRole, which aggregates into `tenant-use`, `tenant-admin`, and `tenant-super-admin`. Without this, the dashboard VM power buttons return 403 even for tenant admins: `kubectl auth can-i update virtualmachines --subresource=start` is `no` for every tenant role today, while `virtualmachineinstances/console|vnc|portforward` are already granted at the same `use` level — so power control belongs there too. ## Companion PR UI that consumes this permission (VM power controls + VNC fixes): cozystack/cozystack-ui#27 ## Test - `helm template` renders `cozy:tenant:use:base` with the new `virtualmachines/start|stop|restart` rule. - After apply, `kubectl auth can-i update virtualmachines --subresource=start|stop|restart` flips from `no` to `yes` for the `tenant-*-use/admin/super-admin` groups. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Virtual Machine Power Control: Users with tenant-level access can now start, stop, and restart virtual machines, providing enhanced management capabilities for VM operations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
Adds Start / Restart / Stop buttons to the VMInstance detail page, wired directly to the KubeVirt action subresources (
subresources.kubevirt.io→virtualmachines/{name}/start|stop|restart). Button availability is driven by the backing VirtualMachine'sprintableStatus. Also fixes two related issues found while testing the feature.Changes
subresource()client method +useK8sSubresource()mutation hook (invalidates the resource's get/list queries on success).VirtualMachineas<release.prefix><name>(e.g.vm-instance-demo-vm), shows a power-status badge, enables Start only when stopped and Restart/Stop only when running, and confirms before restart/stop.2xxresponse bodies — KubeVirt action subresources return202with an empty body, which previously threwUnexpected end of JSON inputand surfaced a spurious error toast even though the action succeeded.demo-vm) instead of the prefixed KubeVirt name (vm-instance-demo-vm), so it never connected; now resolved viarelease.prefix.Requires
Tenant RBAC for the power subresources: cozystack/cozystack#2777. Without it the buttons return 403 for tenant users.
Screenshots
VM power controls on the VMInstance detail page (running):
TODO: attach screenshot
VNC tab when the VM is stopped:
TODO: attach screenshot
Test plan
pnpm -r typecheckpasses.kubectl proxy): Restart issuesPUT .../virtualmachines/{name}/restart→202, status transitionsRunning → Starting → Running; the VNC tab shows the "not running" notice while the VM is stopped.@typescript-eslint/no-explicit-anylint errors inVncTab.tsxand other files are unrelated to this change.Summary by CodeRabbit
New Features
Bug Fixes