From c27807952aa07b6a174cc97c67dff0061d58011f Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Wed, 25 Mar 2026 09:52:43 +0500 Subject: [PATCH 1/5] [vm-instance] Rename subnets to networks and add dropdown selector Rename the misleading 'subnets' field to 'networks' in VMInstance to better reflect that it attaches the VM to VPC network attachments, not creates subnets. The old 'subnets' field is kept as deprecated with fallback logic for backward compatibility. Add an API-backed dropdown (listInput) for networks[].name that lists available NetworkAttachmentDefinitions in the namespace, and hide the deprecated subnets field from the dashboard UI. Assisted-By: Claude AI Signed-off-by: Kirill Ilin --- .../dashboard/customformsoverride.go | 26 ++++++++++ .../dashboard/customformsoverride_test.go | 50 +++++++++++++++++++ packages/apps/vm-instance/templates/vm.yaml | 16 +++--- packages/apps/vm-instance/values.yaml | 14 +++--- 4 files changed, 91 insertions(+), 15 deletions(-) diff --git a/internal/controller/dashboard/customformsoverride.go b/internal/controller/dashboard/customformsoverride.go index 8fb65de7e..907ebedd6 100644 --- a/internal/controller/dashboard/customformsoverride.go +++ b/internal/controller/dashboard/customformsoverride.go @@ -61,6 +61,9 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph // Override specific fields with API-backed dropdowns (listInput type) applyListInputOverrides(schema, kind, openAPIProps) + // Hide deprecated fields from the UI + hidden = append(hidden, hiddenDeprecatedFields(kind)...) + spec := map[string]any{ "customizationId": customizationID, "hidden": hidden, @@ -216,6 +219,17 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma }, } + // Override networks[].name to be an API-backed dropdown listing NetworkAttachmentDefinitions + networksItemProps := ensureArrayItemProps(specProps, "networks") + networksItemProps["name"] = map[string]any{ + "type": "listInput", + "customProps": map[string]any{ + "valueUri": "/api/clusters/{cluster}/k8s/apis/k8s.cni.cncf.io/v1/namespaces/{namespace}/network-attachment-definitions", + "keysToValue": []any{"metadata", "name"}, + "keysToLabel": []any{"metadata", "name"}, + }, + } + case "ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB", "NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk": specProps := ensureSchemaPath(schema, "spec") @@ -237,6 +251,18 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma } } +// hiddenDeprecatedFields returns hidden paths for deprecated fields that should not +// appear in the dashboard UI for the given kind. +func hiddenDeprecatedFields(kind string) []any { + switch kind { + case "VMInstance": + return []any{ + []any{"spec", "subnets"}, + } + } + return nil +} + // storageClassListInput returns a listInput field config for a storageClass dropdown // backed by the cluster's available StorageClasses. func storageClassListInput() map[string]any { diff --git a/internal/controller/dashboard/customformsoverride_test.go b/internal/controller/dashboard/customformsoverride_test.go index d0a34abc6..7833d2e96 100644 --- a/internal/controller/dashboard/customformsoverride_test.go +++ b/internal/controller/dashboard/customformsoverride_test.go @@ -234,6 +234,56 @@ func TestApplyListInputOverrides_VMInstance(t *testing.T) { if diskCustomProps["valueUri"] != expectedDiskURI { t.Errorf("expected disks valueUri %s, got %v", expectedDiskURI, diskCustomProps["valueUri"]) } + + // Check networks[].name is a listInput + networks, ok := specProps["networks"].(map[string]any) + if !ok { + t.Fatal("networks not found in schema.properties.spec.properties") + } + netItems, ok := networks["items"].(map[string]any) + if !ok { + t.Fatal("networks.items not found") + } + netItemProps, ok := netItems["properties"].(map[string]any) + if !ok { + t.Fatal("networks.items.properties not found") + } + netName, ok := netItemProps["name"].(map[string]any) + if !ok { + t.Fatal("networks.items.properties.name not found") + } + if netName["type"] != "listInput" { + t.Errorf("expected networks name type listInput, got %v", netName["type"]) + } + netCustomProps, ok := netName["customProps"].(map[string]any) + if !ok { + t.Fatal("networks name customProps not found") + } + expectedNetURI := "/api/clusters/{cluster}/k8s/apis/k8s.cni.cncf.io/v1/namespaces/{namespace}/network-attachment-definitions" + if netCustomProps["valueUri"] != expectedNetURI { + t.Errorf("expected networks valueUri %s, got %v", expectedNetURI, netCustomProps["valueUri"]) + } +} + +func TestHiddenDeprecatedFields_VMInstance(t *testing.T) { + hidden := hiddenDeprecatedFields("VMInstance") + if len(hidden) != 1 { + t.Fatalf("expected 1 hidden path, got %d", len(hidden)) + } + path, ok := hidden[0].([]any) + if !ok { + t.Fatal("hidden path is not []any") + } + if len(path) != 2 || path[0] != "spec" || path[1] != "subnets" { + t.Errorf("expected [spec subnets], got %v", path) + } +} + +func TestHiddenDeprecatedFields_UnknownKind(t *testing.T) { + hidden := hiddenDeprecatedFields("SomeOtherKind") + if hidden != nil { + t.Errorf("expected nil for unknown kind, got %v", hidden) + } } func TestApplyListInputOverrides_StorageClassSimple(t *testing.T) { diff --git a/packages/apps/vm-instance/templates/vm.yaml b/packages/apps/vm-instance/templates/vm.yaml index 0a7eb7c5e..d5c51ac96 100644 --- a/packages/apps/vm-instance/templates/vm.yaml +++ b/packages/apps/vm-instance/templates/vm.yaml @@ -86,12 +86,11 @@ spec: interfaces: - name: default bridge: {} - {{- if .Values.subnets }} - {{- range $i, $subnet := .Values.subnets }} - - name: {{ $subnet.name }} + {{- $networks := .Values.networks | default .Values.subnets }} + {{- range $i, $net := $networks }} + - name: {{ $net.name }} bridge: {} {{- end }} - {{- end }} machine: type: "" {{- with .Values.sshKeys }} @@ -123,10 +122,9 @@ spec: networks: - name: default pod: {} - {{- if .Values.subnets }} - {{- range $i, $subnet := .Values.subnets }} - - name: {{ $subnet.name }} + {{- $networks := .Values.networks | default .Values.subnets }} + {{- range $i, $net := $networks }} + - name: {{ $net.name }} multus: - networkName: {{ $.Release.Namespace }}/{{ $subnet.name }} - {{- end }} + networkName: {{ $.Release.Namespace }}/{{ $net.name }} {{- end }} diff --git a/packages/apps/vm-instance/values.yaml b/packages/apps/vm-instance/values.yaml index d5ed5679b..f07b75d39 100644 --- a/packages/apps/vm-instance/values.yaml +++ b/packages/apps/vm-instance/values.yaml @@ -10,8 +10,8 @@ ## @field {string} name - Disk name. ## @field {string} [bus] - Disk bus type (e.g. "sata"). -## @typedef {struct} Subnet - Additional subnets -## @field {string} [name] - Subnet name +## @typedef {struct} Network - Network to attach the VM to. +## @field {string} [name] - Network attachment name. ## @typedef {struct} GPU - GPU device configuration. ## @field {string} name - The name of the GPU resource to attach. @@ -55,13 +55,15 @@ disks: [] ## - name: example-data ## bus: sata -## @param {[]Subnet} subnets - Additional subnets -subnets: [] +## @param {[]Network} networks - Networks to attach the VM to. +networks: [] ## Example: -## subnets: +## networks: ## - name: subnet-84dbec17 ## - name: subnet-aa8896b5 -## - name: subnet-e9b97196 + +## @param {[]Network} subnets - Deprecated: use networks instead. +subnets: [] ## @param {[]GPU} gpus - List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). gpus: [] From a56ef30df653d31b5c48c2ebaa7dfd9971d41e53 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Wed, 25 Mar 2026 09:56:29 +0500 Subject: [PATCH 2/5] [vm-instance] Fix review issues: validation, DRY, and regenerate CRD Add fail validation when both 'networks' and 'subnets' are set to prevent silent data loss during migration. Compute $networks variable once at template top level to eliminate DRY violation. Regenerate CRD via make generate instead of manual editing. Assisted-By: Claude AI Signed-off-by: Kirill Ilin --- packages/apps/vm-instance/README.md | 6 +++-- packages/apps/vm-instance/templates/vm.yaml | 6 +++-- packages/apps/vm-instance/values.schema.json | 28 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/apps/vm-instance/README.md b/packages/apps/vm-instance/README.md index 11d1441db..a2b6603e6 100644 --- a/packages/apps/vm-instance/README.md +++ b/packages/apps/vm-instance/README.md @@ -47,8 +47,10 @@ virtctl ssh @ | `disks` | List of disks to attach. | `[]object` | `[]` | | `disks[i].name` | Disk name. | `string` | `""` | | `disks[i].bus` | Disk bus type (e.g. "sata"). | `string` | `""` | -| `subnets` | Additional subnets | `[]object` | `[]` | -| `subnets[i].name` | Subnet name | `string` | `""` | +| `networks` | Networks to attach the VM to. | `[]object` | `[]` | +| `networks[i].name` | Network attachment name. | `string` | `""` | +| `subnets` | Deprecated: use networks instead. | `[]object` | `[]` | +| `subnets[i].name` | Network attachment name. | `string` | `""` | | `gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` | | `gpus[i].name` | The name of the GPU resource to attach. | `string` | `""` | | `cpuModel` | Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map | `string` | `""` | diff --git a/packages/apps/vm-instance/templates/vm.yaml b/packages/apps/vm-instance/templates/vm.yaml index d5c51ac96..23d9a1c4e 100644 --- a/packages/apps/vm-instance/templates/vm.yaml +++ b/packages/apps/vm-instance/templates/vm.yaml @@ -7,6 +7,10 @@ {{- if and (not .Values.instanceType) (not (and .Values.resources .Values.resources.cpu .Values.resources.sockets .Values.resources.memory)) }} {{- fail "Either instanceType or resources (cpu, sockets, memory) must be specified" }} {{- end }} +{{- if and .Values.networks .Values.subnets }} +{{- fail "Both 'networks' and 'subnets' are set. Use only 'networks'; 'subnets' is deprecated." }} +{{- end }} +{{- $networks := .Values.networks | default .Values.subnets }} apiVersion: kubevirt.io/v1 kind: VirtualMachine @@ -86,7 +90,6 @@ spec: interfaces: - name: default bridge: {} - {{- $networks := .Values.networks | default .Values.subnets }} {{- range $i, $net := $networks }} - name: {{ $net.name }} bridge: {} @@ -122,7 +125,6 @@ spec: networks: - name: default pod: {} - {{- $networks := .Values.networks | default .Values.subnets }} {{- range $i, $net := $networks }} - name: {{ $net.name }} multus: diff --git a/packages/apps/vm-instance/values.schema.json b/packages/apps/vm-instance/values.schema.json index 1b2b3515f..eae70e328 100644 --- a/packages/apps/vm-instance/values.schema.json +++ b/packages/apps/vm-instance/values.schema.json @@ -150,6 +150,20 @@ "type": "string", "default": "" }, + "networks": { + "description": "Networks to attach the VM to.", + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "description": "Network attachment name.", + "type": "string" + } + } + } + }, "resources": { "description": "Resource configuration for the virtual machine.", "type": "object", @@ -204,6 +218,20 @@ "type": "string" } }, + "subnets": { + "description": "Deprecated: use networks instead.", + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "description": "Network attachment name.", + "type": "string" + } + } + } + }, "cloudInit": { "description": "Cloud-init user data.", "type": "string", From cb79c5fac73787ab07caf4334f76a46541f4eb00 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Mon, 30 Mar 2026 10:55:26 +0500 Subject: [PATCH 3/5] chore(vm-instance): regenerate CRD and schema via make generate Assisted-By: Claude AI Signed-off-by: Kirill Ilin --- api/apps/v1alpha1/vminstance/types.go | 17 ++++--- packages/apps/vm-instance/values.schema.json | 48 +++++++------------ .../vm-instance-rd/cozyrds/vm-instance.yaml | 4 +- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/api/apps/v1alpha1/vminstance/types.go b/api/apps/v1alpha1/vminstance/types.go index 33f58b86f..2bb059c6b 100644 --- a/api/apps/v1alpha1/vminstance/types.go +++ b/api/apps/v1alpha1/vminstance/types.go @@ -38,9 +38,12 @@ type ConfigSpec struct { // List of disks to attach. // +kubebuilder:default:={} Disks []Disk `json:"disks,omitempty"` - // Additional subnets + // Networks to attach the VM to. // +kubebuilder:default:={} - Subnets []Subnet `json:"subnets,omitempty"` + Networks []Network `json:"networks,omitempty"` + // Deprecated: use networks instead. + // +kubebuilder:default:={} + Subnets []Network `json:"subnets,omitempty"` // List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). // +kubebuilder:default:={} Gpus []GPU `json:"gpus,omitempty"` @@ -73,6 +76,11 @@ type GPU struct { Name string `json:"name"` } +type Network struct { + // Network attachment name. + Name string `json:"name,omitempty"` +} + type Resources struct { // Number of CPU cores allocated. Cpu resource.Quantity `json:"cpu,omitempty"` @@ -82,11 +90,6 @@ type Resources struct { Sockets resource.Quantity `json:"sockets,omitempty"` } -type Subnet struct { - // Subnet name - Name string `json:"name,omitempty"` -} - // +kubebuilder:validation:Enum="PortList";"WholeIP" type ExternalMethod string diff --git a/packages/apps/vm-instance/values.schema.json b/packages/apps/vm-instance/values.schema.json index eae70e328..34f7f6344 100644 --- a/packages/apps/vm-instance/values.schema.json +++ b/packages/apps/vm-instance/values.schema.json @@ -114,15 +114,29 @@ } } }, - "subnets": { - "description": "Additional subnets", + "networks": { + "description": "Networks to attach the VM to.", "type": "array", "default": [], "items": { "type": "object", "properties": { "name": { - "description": "Subnet name", + "description": "Network attachment name.", + "type": "string" + } + } + } + }, + "subnets": { + "description": "Deprecated: use networks instead.", + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "description": "Network attachment name.", "type": "string" } } @@ -150,20 +164,6 @@ "type": "string", "default": "" }, - "networks": { - "description": "Networks to attach the VM to.", - "type": "array", - "default": [], - "items": { - "type": "object", - "properties": { - "name": { - "description": "Network attachment name.", - "type": "string" - } - } - } - }, "resources": { "description": "Resource configuration for the virtual machine.", "type": "object", @@ -218,20 +218,6 @@ "type": "string" } }, - "subnets": { - "description": "Deprecated: use networks instead.", - "type": "array", - "default": [], - "items": { - "type": "object", - "properties": { - "name": { - "description": "Network attachment name.", - "type": "string" - } - } - } - }, "cloudInit": { "description": "Cloud-init user data.", "type": "string", diff --git a/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml b/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml index 3cfbf4ef8..e93fa86b3 100644 --- a/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml +++ b/packages/system/vm-instance-rd/cozyrds/vm-instance.yaml @@ -8,7 +8,7 @@ spec: singular: vminstance plural: vminstances openAPISchema: |- - {"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"externalMethod":{"description":"Method to pass through traffic to the VM.","type":"string","default":"PortList","enum":["PortList","WholeIP"]},"externalPorts":{"description":"Ports to forward from outside the cluster.","type":"array","default":[22],"items":{"type":"integer"}},"runStrategy":{"description":"Requested running state of the VirtualMachineInstance","type":"string","default":"Always","enum":["Always","Halted","Manual","RerunOnFailure","Once"]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"instanceProfile":{"description":"Virtual Machine preferences profile.","type":"string","default":"ubuntu","enum":["alpine","centos.7","centos.7.desktop","centos.stream10","centos.stream10.desktop","centos.stream8","centos.stream8.desktop","centos.stream8.dpdk","centos.stream9","centos.stream9.desktop","centos.stream9.dpdk","cirros","fedora","fedora.arm64","opensuse.leap","opensuse.tumbleweed","rhel.10","rhel.10.arm64","rhel.7","rhel.7.desktop","rhel.8","rhel.8.desktop","rhel.8.dpdk","rhel.9","rhel.9.arm64","rhel.9.desktop","rhel.9.dpdk","rhel.9.realtime","sles","ubuntu","windows.10","windows.10.virtio","windows.11","windows.11.virtio","windows.2k16","windows.2k16.virtio","windows.2k19","windows.2k19.virtio","windows.2k22","windows.2k22.virtio","windows.2k25","windows.2k25.virtio",""]},"disks":{"description":"List of disks to attach.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"bus":{"description":"Disk bus type (e.g. \"sata\").","type":"string"},"name":{"description":"Disk name.","type":"string"}}}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}},"gpus":{"description":"List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"The name of the GPU resource to attach.","type":"string"}}}},"cpuModel":{"description":"Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map","type":"string","default":""},"resources":{"description":"Resource configuration for the virtual machine.","type":"object","default":{},"properties":{"cpu":{"description":"Number of CPU cores allocated.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Amount of memory allocated.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"sockets":{"description":"Number of CPU sockets (vCPU topology).","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""}}} + {"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"externalMethod":{"description":"Method to pass through traffic to the VM.","type":"string","default":"PortList","enum":["PortList","WholeIP"]},"externalPorts":{"description":"Ports to forward from outside the cluster.","type":"array","default":[22],"items":{"type":"integer"}},"runStrategy":{"description":"Requested running state of the VirtualMachineInstance","type":"string","default":"Always","enum":["Always","Halted","Manual","RerunOnFailure","Once"]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"instanceProfile":{"description":"Virtual Machine preferences profile.","type":"string","default":"ubuntu","enum":["alpine","centos.7","centos.7.desktop","centos.stream10","centos.stream10.desktop","centos.stream8","centos.stream8.desktop","centos.stream8.dpdk","centos.stream9","centos.stream9.desktop","centos.stream9.dpdk","cirros","fedora","fedora.arm64","opensuse.leap","opensuse.tumbleweed","rhel.10","rhel.10.arm64","rhel.7","rhel.7.desktop","rhel.8","rhel.8.desktop","rhel.8.dpdk","rhel.9","rhel.9.arm64","rhel.9.desktop","rhel.9.dpdk","rhel.9.realtime","sles","ubuntu","windows.10","windows.10.virtio","windows.11","windows.11.virtio","windows.2k16","windows.2k16.virtio","windows.2k19","windows.2k19.virtio","windows.2k22","windows.2k22.virtio","windows.2k25","windows.2k25.virtio",""]},"disks":{"description":"List of disks to attach.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"bus":{"description":"Disk bus type (e.g. \"sata\").","type":"string"},"name":{"description":"Disk name.","type":"string"}}}},"networks":{"description":"Networks to attach the VM to.","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Network attachment name.","type":"string"}}}},"subnets":{"description":"Deprecated: use networks instead.","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Network attachment name.","type":"string"}}}},"gpus":{"description":"List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"The name of the GPU resource to attach.","type":"string"}}}},"cpuModel":{"description":"Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map","type":"string","default":""},"resources":{"description":"Resource configuration for the virtual machine.","type":"object","default":{},"properties":{"cpu":{"description":"Number of CPU cores allocated.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Amount of memory allocated.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"sockets":{"description":"Number of CPU sockets (vCPU topology).","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""}}} release: prefix: vm-instance- labels: @@ -26,7 +26,7 @@ spec: tags: - compute icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODdfMzQ1NCkiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY4N18zNDU0KSI+CjxwYXRoIGQ9Ik04OS41MDM5IDExMS43MDdINTQuNDk3QzU0LjE3MjcgMTExLjcwNyA1NC4wMTA4IDExMS4yMjEgNTQuMzM0OSAxMTEuMDU5TDU3LjI1MjIgMTA4Ljk1MkM2MC4zMzE0IDEwNi42ODMgNjEuOTUyMiAxMDIuNjMxIDYwLjk3OTcgOTguNzQxMkg4My4wMjFDODIuMDQ4NSAxMDIuNjMxIDgzLjY2OTMgMTA2LjY4MyA4Ni43NDg1IDEwOC45NTJMODkuNjY1OCAxMTEuMDU5Qzg5Ljk5IDExMS4yMjEgODkuODI3OSAxMTEuNzA3IDg5LjUwMzkgMTExLjcwN1oiIGZpbGw9IiNCMEI2QkIiLz4KPHBhdGggZD0iTTExMy4zMjggOTguNzQxSDMwLjY3MjVDMjcuNTkzMSA5OC43NDEgMjUgOTYuMTQ4IDI1IDkzLjA2ODdWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJWOTMuMDY4N0MxMTkgOTYuMTQ4IDExNi40MDcgOTguNzQxIDExMy4zMjggOTguNzQxWiIgZmlsbD0iI0U4RURFRSIvPgo8cGF0aCBkPSJNMTE5IDg0LjE1NDlIMjVWMzMuMTAzMkMyNSAzMC4wMjM5IDI3LjU5MzEgMjcuNDMwNyAzMC42NzI1IDI3LjQzMDdIMTEzLjMyOEMxMTYuNDA3IDI3LjQzMDcgMTE5IDMwLjAyMzcgMTE5IDMzLjEwMzJMMTE5IDg0LjE1NDlaIiBmaWxsPSIjMDBCM0ZGIi8+CjxwYXRoIGQ9Ik05MC42Mzc0IDExNi41NjlINTMuMzYxNkM1Mi4wNjUxIDExNi41NjkgNTAuOTMwNyAxMTUuNDM1IDUwLjkzMDcgMTE0LjEzOEM1MC45MzA3IDExMi44NDEgNTIuMDY1MSAxMTEuNzA3IDUzLjM2MTYgMTExLjcwN0g5MC42Mzc0QzkxLjkzMzkgMTExLjcwNyA5My4wNjg0IDExMi44NDEgOTMuMDY4NCAxMTQuMTM4QzkzLjA2ODQgMTE1LjQzNSA5MS45MzM5IDExNi41NjkgOTAuNjM3NCAxMTYuNTY5WiIgZmlsbD0iI0U4RURFRSIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi41Mjc1IDUzLjgzNjdDNzIuNDQzMSA1My44MzUxIDcyLjM2MDUgNTMuODEyMiA3Mi4yODczIDUzLjc3MDFMNTYuNDY5OSA0NC43OTM0QzU2LjM5ODMgNDQuNzUxOSA1Ni4zMzg4IDQ0LjY5MjMgNTYuMjk3MyA0NC42MjA3QzU2LjI1NTkgNDQuNTQ5IDU2LjIzMzggNDQuNDY3OCA1Ni4yMzM0IDQ0LjM4NUM1Ni4yMzM0IDQ0LjIxNjkgNTYuMzI1OCA0NC4wNjE3IDU2LjQ2OTkgNDMuOTc4NUw3Mi4xOTEyIDM1LjA2MDlDNzIuMjYzNyAzNS4wMjEgNzIuMzQ1IDM1IDcyLjQyNzcgMzVDNzIuNTEwNSAzNSA3Mi41OTE4IDM1LjAyMSA3Mi42NjQzIDM1LjA2MDlMODguNDg3MiA0NC4wMzk1Qzg4LjU1OTEgNDQuMDgwMSA4OC42MTg4IDQ0LjEzOTIgODguNjYgNDQuMjEwN0M4OC43MDEzIDQ0LjI4MjIgODguNzIyNyA0NC4zNjM1IDg4LjcyMTkgNDQuNDQ2Qzg4LjcyMjUgNDQuNTI4NSA4OC43MDEgNDQuNjA5NyA4OC42NTk4IDQ0LjY4MTJDODguNjE4NSA0NC43NTI2IDg4LjU1ODkgNDQuODExOCA4OC40ODcyIDQ0Ljg1MjVMNzIuNzcxNCA1My43NjgzQzcyLjY5NzIgNTMuODExNCA3Mi42MTMzIDUzLjgzNDkgNzIuNTI3NSA1My44MzY3IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBvcGFjaXR5PSIwLjciIGQ9Ik03MC4yNTUzIDc1LjY1MTdDNzAuMTcxIDc1LjY1MzUgNzAuMDg3OCA3NS42MzE3IDcwLjAxNTEgNzUuNTg4OEw1NC4yNDU4IDY2LjY0MTdDNTQuMTcxNSA2Ni42MDI0IDU0LjEwOTUgNjYuNTQzNiA1NC4wNjYxIDY2LjQ3MTZDNTQuMDIyOCA2Ni4zOTk3IDU0IDY2LjMxNzMgNTQgNjYuMjMzM1Y0OC4yNzhDNTQgNDguMTA4IDU0LjA5MjQgNDcuOTU0NiA1NC4yNDM5IDQ3Ljg2OTZDNTQuMzE3MiA0Ny44MjcxIDU0LjQwMDQgNDcuODA0NyA1NC40ODUxIDQ3LjgwNDdDNTQuNTY5NyA0Ny44MDQ3IDU0LjY1MjkgNDcuODI3MSA1NC43MjYyIDQ3Ljg2OTZMNzAuNDkzNyA1Ni44MTMxQzcwLjU2NDIgNTYuODU2NSA3MC42MjI1IDU2LjkxNyA3MC42NjMyIDU2Ljk4OTFDNzAuNzAzOSA1Ny4wNjEyIDcwLjcyNTcgNTcuMTQyNCA3MC43MjY1IDU3LjIyNTFWNzUuMTgwNUM3MC43MjU5IDc1LjI2MjggNzAuNzA0MiA3NS4zNDM2IDcwLjY2MzUgNzUuNDE1MUM3MC42MjI3IDc1LjQ4NjYgNzAuNTY0MiA3NS41NDY0IDcwLjQ5MzcgNzUuNTg4OEM3MC40MjA2IDc1LjYyOTEgNzAuMzM4NyA3NS42NTA3IDcwLjI1NTMgNzUuNjUxNyIgZmlsbD0id2hpdGUiLz4KPHBhdGggb3BhY2l0eT0iMC40IiBkPSJNNzQuNzE5OCA3NS42NTExQzc0LjYzMzMgNzUuNjUxMiA3NC41NDgyIDc1LjYyOTYgNzQuNDcyMiA3NS41ODgzQzc0LjQwMTYgNzUuNTQ2MSA3NC4zNDMyIDc1LjQ4NjIgNzQuMzAyNyA3NS40MTQ3Qzc0LjI2MjMgNzUuMzQzMSA3NC4yNDExIDc1LjI2MjIgNzQuMjQxMiA3NS4xOFY1Ny4zMzczQzc0LjI0MTIgNTcuMTcxIDc0LjMzMzYgNTcuMDE1OCA3NC40NzIyIDU2LjkyOUw5MC4yMzk3IDQ3Ljk4NTVDOTAuMzExOSA0Ny45NDM4IDkwLjM5MzggNDcuOTIxOSA5MC40NzcxIDQ3LjkyMTlDOTAuNTYwNSA0Ny45MjE5IDkwLjY0MjQgNDcuOTQzOCA5MC43MTQ2IDQ3Ljk4NTVDOTAuNzg3NiA0OC4wMjU1IDkwLjg0ODUgNDguMDg0MiA5MC44OTExIDQ4LjE1NTdDOTAuOTMzNyA0OC4yMjcyIDkwLjk1NjMgNDguMzA4OCA5MC45NTY2IDQ4LjM5MlY2Ni4yMzI4QzkwLjk1NyA2Ni4zMTY0IDkwLjkzNDcgNjYuMzk4NSA5MC44OTIxIDY2LjQ3MDRDOTAuODQ5NSA2Ni41NDI0IDkwLjc4ODEgNjYuNjAxNCA5MC43MTQ2IDY2LjY0MTFMNzQuOTUyNiA3NS41ODgzQzc0Ljg4MjUgNzUuNjMwNyA3NC44MDE4IDc1LjY1MjUgNzQuNzE5OCA3NS42NTExIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4N18zNDU0IiB4MT0iMTYxIiB5MT0iMTgwIiB4Mj0iMy41OTI4NGUtMDciIHkyPSI0Ljk5OTk4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNTk1NjU2Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNjg3XzM0NTQiPgo8cmVjdCB3aWR0aD0iOTQiIGhlaWdodD0iOTQiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNSAyNSkiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K - keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "external"], ["spec", "externalMethod"], ["spec", "externalPorts"], ["spec", "runStrategy"], ["spec", "instanceType"], ["spec", "instanceProfile"], ["spec", "disks"], ["spec", "subnets"], ["spec", "gpus"], ["spec", "cpuModel"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]] + keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "external"], ["spec", "externalMethod"], ["spec", "externalPorts"], ["spec", "runStrategy"], ["spec", "instanceType"], ["spec", "instanceProfile"], ["spec", "disks"], ["spec", "networks"], ["spec", "subnets"], ["spec", "gpus"], ["spec", "cpuModel"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]] secrets: exclude: [] include: [] From 149cb67692e2031c39bda68e5e6b1e94b8a93f19 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Mon, 30 Mar 2026 10:56:09 +0500 Subject: [PATCH 4/5] feat(platform): add migration 36 to copy subnets to networks in VMInstance Existing VMInstance resources store network attachments in the deprecated spec.subnets field. This migration copies subnets to the new spec.networks field so the dashboard UI correctly displays attached networks. The old subnets field is kept intact because migrations run before the new CRD is applied. Assisted-By: Claude AI Signed-off-by: Kirill Ilin --- .../platform/images/migrations/migrations/36 | 18 ++++++++++++++++++ packages/core/platform/values.yaml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 packages/core/platform/images/migrations/migrations/36 diff --git a/packages/core/platform/images/migrations/migrations/36 b/packages/core/platform/images/migrations/migrations/36 new file mode 100755 index 000000000..744a24804 --- /dev/null +++ b/packages/core/platform/images/migrations/migrations/36 @@ -0,0 +1,18 @@ +#!/bin/sh +# Migration 36 --> 37 +# +# Copy spec.subnets to spec.networks in VMInstance resources. +# The old "subnets" field is kept intact because migrations run before +# the new CRD is applied; removing it now would lose data. + +set -e + +kubectl get vminstances.apps.cozystack.io -A -o json | jq -r ' + .items[] + | select(.spec.subnets != null and (.spec.subnets | length) > 0) + | select(.spec.networks == null or (.spec.networks | length) == 0) + | "kubectl -n \(.metadata.namespace) patch vminstances.apps.cozystack.io \(.metadata.name) --type=json -p '\''[{\"op\":\"add\",\"path\":\"/spec/networks\",\"value\":\(.spec.subnets|tojson)}]'\''" +' | sh -ex + +# Write version to cozystack-version config +kubectl create configmap -n cozy-system cozystack-version --from-literal=version=37 --dry-run=client -o yaml | kubectl apply -f- diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 8baa68451..c9af0e5e9 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -6,7 +6,7 @@ sourceRef: migrations: enabled: false image: ghcr.io/cozystack/cozystack/platform-migrations:v1.2.0@sha256:3a3d8cfa4323d8023b7c7800d8b552b250bc2de01ca7550fd024a10b12324f6d - targetVersion: 36 + targetVersion: 37 # Bundle deployment configuration bundles: system: From c73f677c798c809cd4939aa8ad93a56256418021 Mon Sep 17 00:00:00 2001 From: Kirill Ilin Date: Mon, 30 Mar 2026 11:11:56 +0500 Subject: [PATCH 5/5] fix(vm-instance): remove fail on both networks and subnets set After migration 36 copies subnets to networks, both fields are populated. The fail guard would break Helm reconciliation. Instead, networks simply takes priority via the existing default fallback. Also handle missing VMInstance CRD gracefully in migration 36. Assisted-By: Claude AI Signed-off-by: Kirill Ilin --- packages/apps/vm-instance/templates/vm.yaml | 3 --- packages/core/platform/images/migrations/migrations/36 | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/apps/vm-instance/templates/vm.yaml b/packages/apps/vm-instance/templates/vm.yaml index 23d9a1c4e..55f278c1a 100644 --- a/packages/apps/vm-instance/templates/vm.yaml +++ b/packages/apps/vm-instance/templates/vm.yaml @@ -7,9 +7,6 @@ {{- if and (not .Values.instanceType) (not (and .Values.resources .Values.resources.cpu .Values.resources.sockets .Values.resources.memory)) }} {{- fail "Either instanceType or resources (cpu, sockets, memory) must be specified" }} {{- end }} -{{- if and .Values.networks .Values.subnets }} -{{- fail "Both 'networks' and 'subnets' are set. Use only 'networks'; 'subnets' is deprecated." }} -{{- end }} {{- $networks := .Values.networks | default .Values.subnets }} apiVersion: kubevirt.io/v1 diff --git a/packages/core/platform/images/migrations/migrations/36 b/packages/core/platform/images/migrations/migrations/36 index 744a24804..0f0a94dd7 100755 --- a/packages/core/platform/images/migrations/migrations/36 +++ b/packages/core/platform/images/migrations/migrations/36 @@ -7,12 +7,16 @@ set -e +if ! kubectl get crd vminstances.apps.cozystack.io >/dev/null 2>&1; then + echo "VMInstance CRD not found, skipping migration" +else kubectl get vminstances.apps.cozystack.io -A -o json | jq -r ' .items[] | select(.spec.subnets != null and (.spec.subnets | length) > 0) | select(.spec.networks == null or (.spec.networks | length) == 0) | "kubectl -n \(.metadata.namespace) patch vminstances.apps.cozystack.io \(.metadata.name) --type=json -p '\''[{\"op\":\"add\",\"path\":\"/spec/networks\",\"value\":\(.spec.subnets|tojson)}]'\''" ' | sh -ex +fi # Write version to cozystack-version config kubectl create configmap -n cozy-system cozystack-version --from-literal=version=37 --dry-run=client -o yaml | kubectl apply -f-