Skip to content

feat(console): VM power controls (start/stop/restart) + VNC fixes#27

Merged
Aleksei Sviridkin (lexfrei) merged 13 commits into
mainfrom
feat/vm-power-controls
Jun 2, 2026
Merged

feat(console): VM power controls (start/stop/restart) + VNC fixes#27
Aleksei Sviridkin (lexfrei) merged 13 commits into
mainfrom
feat/vm-power-controls

Conversation

@kvaps
Copy link
Copy Markdown
Member

@kvaps Andrei Kvapil (kvaps) commented Jun 1, 2026

image

Summary

Adds Start / Restart / Stop buttons to the VMInstance detail page, wired directly to the KubeVirt action subresources (subresources.kubevirt.iovirtualmachines/{name}/start|stop|restart). Button availability is driven by the backing VirtualMachine's printableStatus. Also fixes two related issues found while testing the feature.

Changes

  • k8s-client: generic subresource() client method + useK8sSubresource() mutation hook (invalidates the resource's get/list queries on success).
  • VMPowerControls: new component in the VMInstance detail toolbar. Resolves the backing VirtualMachine as <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.
  • fix(k8s-client): handle empty 2xx response bodies — KubeVirt action subresources return 202 with an empty body, which previously threw Unexpected end of JSON input and surfaced a spurious error toast even though the action succeeded.
  • fix(VNC): the VNC tab targeted the VMI by the cozystack app name (demo-vm) instead of the prefixed KubeVirt name (vm-instance-demo-vm), so it never connected; now resolved via release.prefix.
  • fix(VNC): don't open the VNC websocket when the VM is not running — show a "not running" notice instead of a dead console.

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 typecheck passes.
  • Verified end-to-end against a live cluster (vite dev → kubectl proxy): Restart issues PUT .../virtualmachines/{name}/restart202, status transitions Running → Starting → Running; the VNC tab shows the "not running" notice while the VM is stopped.
  • Pre-existing @typescript-eslint/no-explicit-any lint errors in VncTab.tsx and other files are unrelated to this change.

Summary by CodeRabbit

  • New Features

    • VM power controls (Start, Restart, Stop) added to application detail with status badge and confirmations
    • VNC console: reconnect button, fullscreen toggle, improved sizing/aspect handling, and clearer connection/loading/error UI
  • Bug Fixes

    • VNC now only initializes when the VM is running and handles connect/disconnect/security failures more robustly
    • Dev-server proxy updated to forward WebSocket upgrades for more reliable console streaming
    • API client now safely handles empty response bodies to avoid parse errors

Andrei Kvapil (kvaps) and others added 4 commits June 1, 2026 20:08
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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

VM Controls and VNC Enhancement

Layer / File(s) Summary
K8s Client subresource API
packages/k8s-client/src/client.ts, packages/k8s-client/src/hooks.ts, packages/k8s-client/src/index.ts, apps/console/src/__tests__/k8s-client/*
K8sClient.request reads response text and returns undefined for empty bodies. Added K8sClient.subresource for resource subresources (default PUT, conditional JSON body). useK8sSubresource hook wraps subresource mutations and invalidates both GET and LIST caches; tests added for URL/method and empty-body handling; hook tests validate invalidation behavior.
VM power controls component
apps/console/src/routes/detail/ApplicationDetailPage.tsx, apps/console/src/routes/detail/VMPowerControls.tsx, apps/console/src/routes/detail/VMPowerControls.test.tsx
New VMPowerControls computes VM name/namespace, polls VM status via useK8sList, shows a status badge, and provides Start/Restart/Stop buttons with confirmation, pending state, async subresource calls, cache invalidation, and error alerts. Integrated into ApplicationDetailPage header when kind === "VMInstance".
VNC console VM state gating
apps/console/src/routes/detail/VncTab.tsx, apps/console/src/routes/detail/VncTab.test.tsx
VncTab watches virtualmachines/<vmName> to derive powerStatus/isRunning and initializes noVNC only when running. Adds loading/not-running branches, guarded RFB lifecycle, reconnect/fullscreen behavior, canvas size measurement on connect, and tests validating gating and name-resolution agreement with VMPowerControls.
releasePrefix usage and Events tests
apps/console/src/routes/detail/EventsTab.tsx, apps/console/src/routes/detail/EventsTab.test.tsx, apps/console/src/lib/app-definitions.test.ts
EventsTab now derives Helm release name via shared releasePrefix(ad). Tests validate derived prefix when unset and honoring explicit spec.release.prefix.
SchemaForm typing update
apps/console/src/components/SchemaForm.tsx
Introduces a local SchemaNode interface and updates bindAdditionalProperties to use typed schema traversal instead of as any casts.
Dev proxy WebSocket support
apps/console/vite.config.ts
Vite dev-server proxy entries for "/apis" and "/api" now include ws: true so WebSocket upgrade requests (used by VNC) are forwarded to kubectl proxy; inline comment updated.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

size/XXL

Suggested reviewers

  • androndo
  • myasnikovdaniil

Poem

"I nibbled keys and nudged the wire,
Start, restart, stop — the rabbit's choir.
WebSockets hum, the canvas wakes,
VM badges blink for tiny stakes,
Hopping bytes to help your VMs conspire."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding VM power controls (start/stop/restart) and fixing VNC functionality for VMInstance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vm-power-controls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/console Issues or PRs related to apps/console — routes, detail pages, marketplace, command palette kind/feature Categorizes issue or PR as related to a new feature labels Jun 1, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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 ?? {},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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>
@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Jun 1, 2026
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>
@kvaps Andrei Kvapil (kvaps) marked this pull request as ready for review June 1, 2026 19:12
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/console/src/routes/detail/VMPowerControls.tsx (1)

38-50: ⚡ Quick win

Avoid refetchInterval polling; use a watch-based list query.

This polls the VirtualMachine every 5s. useK8sGet has no watch support, but useK8sList does (chunked-encoding watch), so the status can be obtained without polling by listing virtualmachines with a fieldSelector of metadata.name=${vmName}.

As per coding guidelines: "useK8sList already handles watches via chunked-encoding. 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/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 win

Avoid refetchInterval polling here too.

Same concern as VMPowerControls: this polls the VirtualMachine every 5s. Prefer a watch-based useK8sList with a fieldSelector on metadata.name to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 27e9b1b and 8e80930.

📒 Files selected for processing (7)
  • apps/console/src/routes/detail/ApplicationDetailPage.tsx
  • apps/console/src/routes/detail/VMPowerControls.tsx
  • apps/console/src/routes/detail/VncTab.tsx
  • apps/console/vite.config.ts
  • packages/k8s-client/src/client.ts
  • packages/k8s-client/src/hooks.ts
  • packages/k8s-client/src/index.ts

Comment thread apps/console/src/routes/detail/VMPowerControls.tsx Outdated
# Conflicts:
#	apps/console/src/routes/detail/VncTab.tsx
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels Jun 1, 2026
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>
@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Jun 2, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7397c74 and 8674911.

📒 Files selected for processing (11)
  • apps/console/src/__tests__/k8s-client/client.subresource.test.ts
  • apps/console/src/__tests__/k8s-client/useK8sSubresource.test.tsx
  • apps/console/src/components/SchemaForm.tsx
  • apps/console/src/lib/app-definitions.test.ts
  • apps/console/src/routes/detail/EventsTab.test.tsx
  • apps/console/src/routes/detail/EventsTab.tsx
  • apps/console/src/routes/detail/VMPowerControls.test.tsx
  • apps/console/src/routes/detail/VMPowerControls.tsx
  • apps/console/src/routes/detail/VncTab.test.tsx
  • apps/console/src/routes/detail/VncTab.tsx
  • packages/k8s-client/src/hooks.ts

Comment on lines 145 to +151
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +40 to 42
const powerStatus = vmList?.items[0]?.status?.printableStatus
const isRunning = powerStatus === "Running"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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).

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.

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.

@lexfrei Aleksei Sviridkin (lexfrei) merged commit 7c443a9 into main Jun 2, 2026
6 checks passed
@lexfrei Aleksei Sviridkin (lexfrei) deleted the feat/vm-power-controls branch June 2, 2026 09:54
Aleksei Sviridkin (lexfrei) added a commit to cozystack/cozystack that referenced this pull request Jun 2, 2026
<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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/console Issues or PRs related to apps/console — routes, detail pages, marketplace, command palette kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants