feat(platform): add migration to convert virtual-machine to vm-disk and vm-instance (#2040)

## Summary

Add migration script (#28) that converts virtual-machine HelmReleases
into separate vm-disk + vm-instance HelmReleases, preserving disk data,
kube-ovn IPs, and LoadBalancer IPs.

## Changes

- Add migration 28 that discovers all VirtualMachine HelmReleases and
for each:
  - Extracts values, saves kube-ovn IP and LoadBalancer IP
  - Switches PV reclaim to Retain, stops and deletes the VM
  - Creates new PVC bound to existing PV (with volumeMode detection)
  - Creates vm-disk and vm-instance HelmReleases with proper values
- Pre-creates LoadBalancer service with metallb annotation if
external=true
  - Cleans up old resources including protected services
- Temporarily disables CDI operator and apiserver webhooks during
DataVolume creation to bypass PVC existence validation
- Script is fully idempotent (safe to re-run if interrupted)
This commit is contained in:
Andrei Kvapil authored and GitHub committed 2026-02-14 03:03:20 +01:00
commit ef48d74c5a
34 files changed
+911 -1471

No files matched your search

-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env bats
@test "Create a Virtual Machine" {
name='test'
kubectl apply -f - <<EOF
apiVersion: apps.cozystack.io/v1alpha1
kind: VirtualMachine
metadata:
name: $name
namespace: tenant-test
spec:
external: false
externalMethod: PortList
externalPorts:
- 22
instanceType: "u1.medium"
instanceProfile: ubuntu
systemDisk:
image: ubuntu
storage: 5Gi
storageClass: replicated
gpus: []
resources: {}
sshKeys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
test@test
cloudInit: |
#cloud-config
users:
- name: test
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD: ALL']
groups: sudo
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF test@test
cloudInitSeed: ""
EOF
sleep 5
kubectl -n tenant-test wait hr virtual-machine-$name --timeout=10s --for=condition=ready
kubectl -n tenant-test wait dv virtual-machine-$name --timeout=150s --for=condition=ready
kubectl -n tenant-test wait pvc virtual-machine-$name --timeout=100s --for=jsonpath='{.status.phase}'=Bound
kubectl -n tenant-test wait vm virtual-machine-$name --timeout=100s --for=condition=ready
timeout 120 sh -ec "until kubectl -n tenant-test get vmi virtual-machine-$name -o jsonpath='{.status.interfaces[0].ipAddress}' | grep -q '[0-9]'; do sleep 10; done"
kubectl -n tenant-test delete virtualmachines.apps.cozystack.io $name
}
@@ -1,3 +0,0 @@
.helmignore
/logos
/Makefile
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v2
name: virtual-machine
description: Virtual Machine (simple)
icon: /logos/vm.svg
type: application
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
appVersion: 0.14.0
-10
View File
@@ -1,10 +0,0 @@
include ../../../hack/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
yq -o json -i '.properties.gpus.items.type = "object" | .properties.gpus.default = []' values.schema.json
# INSTANCE_TYPES=$$(yq e '.metadata.name' -o=json -r ../../system/kubevirt-instancetypes/templates/instancetypes.yaml | yq 'split(" ") | . + [""]' -o json) \
# && yq -i -o json ".properties.instanceType.enum = $${INSTANCE_TYPES}" values.schema.json
PREFERENCES=$$(yq e '.metadata.name' -o=json -r ../../system/kubevirt-instancetypes/templates/preferences.yaml | yq 'split(" ") | . + [""]' -o json) \
&& yq -i -o json ".properties.instanceProfile.enum = $${PREFERENCES}" values.schema.json
../../../hack/update-crd.sh
-278
View File
@@ -1,278 +0,0 @@
# Virtual Machine (simple)
A Virtual Machine (VM) simulates computer hardware, enabling various operating systems and applications to run in an isolated environment.
## Deployment Details
The virtual machine is managed and hosted through KubeVirt, allowing you to harness the benefits of virtualization within your Kubernetes ecosystem.
- Docs: [KubeVirt User Guide](https://kubevirt.io/user-guide/)
- GitHub: [KubeVirt Repository](https://github.com/kubevirt/kubevirt)
## Accessing virtual machine
You can access the virtual machine using the virtctl tool:
- [KubeVirt User Guide - Virtctl Client Tool](https://kubevirt.io/user-guide/user_workloads/virtctl_client_tool/)
To access the serial console:
```
virtctl console <vm>
```
To access the VM using VNC:
```
virtctl vnc <vm>
```
To SSH into the VM:
```
virtctl ssh <user>@<vm>
```
## Parameters
### Common parameters
| Name | Description | Type | Value |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------ |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` |
| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` |
| `runStrategy` | Requested running state of the VirtualMachineInstance | `string` | `Always` |
| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` |
| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` |
| `systemDisk` | System disk configuration. | `object` | `{}` |
| `systemDisk.image` | The base image for the virtual machine. | `string` | `ubuntu` |
| `systemDisk.storage` | The size of the disk allocated for the virtual machine. | `string` | `5Gi` |
| `systemDisk.storageClass` | StorageClass used to store the data. | `string` | `replicated` |
| `subnets` | Additional subnets | `[]object` | `[]` |
| `subnets[i].name` | Subnet name | `string` | `""` |
| `gpus` | List of GPUs to attach. | `[]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` | `""` |
| `resources` | Resource configuration for the virtual machine. | `object` | `{}` |
| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` |
| `resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` |
| `cloudInit` | Cloud-init user data. | `string` | `""` |
| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` |
## U Series
The U Series is quite neutral and provides resources for
general purpose applications.
*U* is the abbreviation for "Universal", hinting at the universal
attitude towards workloads.
VMs of instance types will share physical CPU cores on a
time-slice basis with other VMs.
### U Series Characteristics
Specific characteristics of this series are:
- *Burstable CPU performance* - The workload has a baseline compute
performance but is permitted to burst beyond this baseline, if
excess compute resources are available.
- *vCPU-To-Memory Ratio (1:4)* - A vCPU-to-Memory ratio of 1:4, for less
noise per node.
## O Series
The O Series is based on the U Series, with the only difference
being that memory is overcommitted.
*O* is the abbreviation for "Overcommitted".
### UO Series Characteristics
Specific characteristics of this series are:
- *Burstable CPU performance* - The workload has a baseline compute
performance but is permitted to burst beyond this baseline, if
excess compute resources are available.
- *Overcommitted Memory* - Memory is over-committed in order to achieve
a higher workload density.
- *vCPU-To-Memory Ratio (1:4)* - A vCPU-to-Memory ratio of 1:4, for less
noise per node.
## CX Series
The CX Series provides exclusive compute resources for compute
intensive applications.
*CX* is the abbreviation of "Compute Exclusive".
The exclusive resources are given to the compute threads of the
VM. In order to ensure this, some additional cores (depending
on the number of disks and NICs) will be requested to offload
the IO threading from cores dedicated to the workload.
In addition, in this series, the NUMA topology of the used
cores is provided to the VM.
### CX Series Characteristics
Specific characteristics of this series are:
- *Hugepages* - Hugepages are used in order to improve memory
performance.
- *Dedicated CPU* - Physical cores are exclusively assigned to every
vCPU in order to provide fixed and high compute guarantees to the
workload.
- *Isolated emulator threads* - Hypervisor emulator threads are isolated
from the vCPUs in order to reduce emaulation related impact on the
workload.
- *vNUMA* - Physical NUMA topology is reflected in the guest in order to
optimize guest sided cache utilization.
- *vCPU-To-Memory Ratio (1:2)* - A vCPU-to-Memory ratio of 1:2.
## M Series
The M Series provides resources for memory intensive
applications.
*M* is the abbreviation of "Memory".
### M Series Characteristics
Specific characteristics of this series are:
- *Hugepages* - Hugepages are used in order to improve memory
performance.
- *Burstable CPU performance* - The workload has a baseline compute
performance but is permitted to burst beyond this baseline, if
excess compute resources are available.
- *vCPU-To-Memory Ratio (1:8)* - A vCPU-to-Memory ratio of 1:8, for much
less noise per node.
## RT Series
The RT Series provides resources for realtime applications, like Oslat.
*RT* is the abbreviation for "realtime".
This series of instance types requires nodes capable of running
realtime applications.
### RT Series Characteristics
Specific characteristics of this series are:
- *Hugepages* - Hugepages are used in order to improve memory
performance.
- *Dedicated CPU* - Physical cores are exclusively assigned to every
vCPU in order to provide fixed and high compute guarantees to the
workload.
- *Isolated emulator threads* - Hypervisor emulator threads are isolated
from the vCPUs in order to reduce emaulation related impact on the
workload.
- *vCPU-To-Memory Ratio (1:4)* - A vCPU-to-Memory ratio of 1:4 starting from
the medium size.
## Development
To get started with customizing or creating your own instancetypes and preferences
see [DEVELOPMENT.md](./DEVELOPMENT.md).
## Resources
The following instancetype resources are provided by Cozystack:
Name | vCPUs | Memory
-----|-------|-------
cx1.2xlarge | 8 | 16Gi
cx1.4xlarge | 16 | 32Gi
cx1.8xlarge | 32 | 64Gi
cx1.large | 2 | 4Gi
cx1.medium | 1 | 2Gi
cx1.xlarge | 4 | 8Gi
gn1.2xlarge | 8 | 32Gi
gn1.4xlarge | 16 | 64Gi
gn1.8xlarge | 32 | 128Gi
gn1.xlarge | 4 | 16Gi
m1.2xlarge | 8 | 64Gi
m1.4xlarge | 16 | 128Gi
m1.8xlarge | 32 | 256Gi
m1.large | 2 | 16Gi
m1.xlarge | 4 | 32Gi
n1.2xlarge | 16 | 32Gi
n1.4xlarge | 32 | 64Gi
n1.8xlarge | 64 | 128Gi
n1.large | 4 | 8Gi
n1.medium | 4 | 4Gi
n1.xlarge | 8 | 16Gi
o1.2xlarge | 8 | 32Gi
o1.4xlarge | 16 | 64Gi
o1.8xlarge | 32 | 128Gi
o1.large | 2 | 8Gi
o1.medium | 1 | 4Gi
o1.micro | 1 | 1Gi
o1.nano | 1 | 512Mi
o1.small | 1 | 2Gi
o1.xlarge | 4 | 16Gi
rt1.2xlarge | 8 | 32Gi
rt1.4xlarge | 16 | 64Gi
rt1.8xlarge | 32 | 128Gi
rt1.large | 2 | 8Gi
rt1.medium | 1 | 4Gi
rt1.micro | 1 | 1Gi
rt1.small | 1 | 2Gi
rt1.xlarge | 4 | 16Gi
u1.2xlarge | 8 | 32Gi
u1.2xmedium | 2 | 4Gi
u1.4xlarge | 16 | 64Gi
u1.8xlarge | 32 | 128Gi
u1.large | 2 | 8Gi
u1.medium | 1 | 4Gi
u1.micro | 1 | 1Gi
u1.nano | 1 | 512Mi
u1.small | 1 | 2Gi
u1.xlarge | 4 | 16Gi
The following preference resources are provided by Cozystack:
Name | Guest OS
-----|---------
alpine | Alpine
centos.7 | CentOS 7
centos.7.desktop | CentOS 7
centos.stream10 | CentOS Stream 10
centos.stream10.desktop | CentOS Stream 10
centos.stream8 | CentOS Stream 8
centos.stream8.desktop | CentOS Stream 8
centos.stream8.dpdk | CentOS Stream 8
centos.stream9 | CentOS Stream 9
centos.stream9.desktop | CentOS Stream 9
centos.stream9.dpdk | CentOS Stream 9
cirros | Cirros
fedora | Fedora (amd64)
fedora.arm64 | Fedora (arm64)
opensuse.leap | OpenSUSE Leap
opensuse.tumbleweed | OpenSUSE Tumbleweed
rhel.10 | Red Hat Enterprise Linux 10 Beta (amd64)
rhel.10.arm64 | Red Hat Enterprise Linux 10 Beta (arm64)
rhel.7 | Red Hat Enterprise Linux 7
rhel.7.desktop | Red Hat Enterprise Linux 7
rhel.8 | Red Hat Enterprise Linux 8
rhel.8.desktop | Red Hat Enterprise Linux 8
rhel.8.dpdk | Red Hat Enterprise Linux 8
rhel.9 | Red Hat Enterprise Linux 9 (amd64)
rhel.9.arm64 | Red Hat Enterprise Linux 9 (arm64)
rhel.9.desktop | Red Hat Enterprise Linux 9 Desktop (amd64)
rhel.9.dpdk | Red Hat Enterprise Linux 9 DPDK (amd64)
rhel.9.realtime | Red Hat Enterprise Linux 9 Realtime (amd64)
sles | SUSE Linux Enterprise Server
ubuntu | Ubuntu
windows.10 | Microsoft Windows 10
windows.10.virtio | Microsoft Windows 10 (virtio)
windows.11 | Microsoft Windows 11
windows.11.virtio | Microsoft Windows 11 (virtio)
windows.2k16 | Microsoft Windows Server 2016
windows.2k16.virtio | Microsoft Windows Server 2016 (virtio)
windows.2k19 | Microsoft Windows Server 2019
windows.2k19.virtio | Microsoft Windows Server 2019 (virtio)
windows.2k22 | Microsoft Windows Server 2022
windows.2k22.virtio | Microsoft Windows Server 2022 (virtio)
windows.2k25 | Microsoft Windows Server 2025
windows.2k25.virtio | Microsoft Windows Server 2025 (virtio)
@@ -1 +0,0 @@
../../../library/cozy-lib
@@ -1 +0,0 @@
#!/bin/sh
@@ -1,21 +0,0 @@
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="144" height="144" rx="24" fill="url(#paint0_linear_687_3454)"/>
<g clip-path="url(#clip0_687_3454)">
<path d="M89.5039 111.707H54.497C54.1727 111.707 54.0108 111.221 54.3349 111.059L57.2522 108.952C60.3314 106.683 61.9522 102.631 60.9797 98.7412H83.021C82.0485 102.631 83.6693 106.683 86.7485 108.952L89.6658 111.059C89.99 111.221 89.8279 111.707 89.5039 111.707Z" fill="#B0B6BB"/>
<path d="M113.328 98.741H30.6725C27.5931 98.741 25 96.148 25 93.0687V33.1032C25 30.0239 27.5931 27.4307 30.6725 27.4307H113.328C116.407 27.4307 119 30.0237 119 33.1032V93.0687C119 96.148 116.407 98.741 113.328 98.741Z" fill="#E8EDEE"/>
<path d="M119 84.1549H25V33.1032C25 30.0239 27.5931 27.4307 30.6725 27.4307H113.328C116.407 27.4307 119 30.0237 119 33.1032L119 84.1549Z" fill="#00B3FF"/>
<path d="M90.6374 116.569H53.3616C52.0651 116.569 50.9307 115.435 50.9307 114.138C50.9307 112.841 52.0651 111.707 53.3616 111.707H90.6374C91.9339 111.707 93.0684 112.841 93.0684 114.138C93.0684 115.435 91.9339 116.569 90.6374 116.569Z" fill="#E8EDEE"/>
</g>
<path d="M72.5275 53.8367C72.4431 53.8351 72.3605 53.8122 72.2873 53.7701L56.4699 44.7934C56.3983 44.7519 56.3388 44.6923 56.2973 44.6207C56.2559 44.549 56.2338 44.4678 56.2334 44.385C56.2334 44.2169 56.3258 44.0617 56.4699 43.9785L72.1912 35.0609C72.2637 35.021 72.345 35 72.4277 35C72.5105 35 72.5918 35.021 72.6643 35.0609L88.4872 44.0395C88.5591 44.0801 88.6188 44.1392 88.66 44.2107C88.7013 44.2822 88.7227 44.3635 88.7219 44.446C88.7225 44.5285 88.701 44.6097 88.6598 44.6812C88.6185 44.7526 88.5589 44.8118 88.4872 44.8525L72.7714 53.7683C72.6972 53.8114 72.6133 53.8349 72.5275 53.8367" fill="white"/>
<path opacity="0.7" d="M70.2553 75.6517C70.171 75.6535 70.0878 75.6317 70.0151 75.5888L54.2458 66.6417C54.1715 66.6024 54.1095 66.5436 54.0661 66.4716C54.0228 66.3997 54 66.3173 54 66.2333V48.278C54 48.108 54.0924 47.9546 54.2439 47.8696C54.3172 47.8271 54.4004 47.8047 54.4851 47.8047C54.5697 47.8047 54.6529 47.8271 54.7262 47.8696L70.4937 56.8131C70.5642 56.8565 70.6225 56.917 70.6632 56.9891C70.7039 57.0612 70.7257 57.1424 70.7265 57.2251V75.1805C70.7259 75.2628 70.7042 75.3436 70.6635 75.4151C70.6227 75.4866 70.5642 75.5464 70.4937 75.5888C70.4206 75.6291 70.3387 75.6507 70.2553 75.6517" fill="white"/>
<path opacity="0.4" d="M74.7198 75.6511C74.6333 75.6512 74.5482 75.6296 74.4722 75.5883C74.4016 75.5461 74.3432 75.4862 74.3027 75.4147C74.2623 75.3431 74.2411 75.2622 74.2412 75.18V57.3373C74.2412 57.171 74.3336 57.0158 74.4722 56.929L90.2397 47.9855C90.3119 47.9438 90.3938 47.9219 90.4771 47.9219C90.5605 47.9219 90.6424 47.9438 90.7146 47.9855C90.7876 48.0255 90.8485 48.0842 90.8911 48.1557C90.9337 48.2272 90.9563 48.3088 90.9566 48.392V66.2328C90.957 66.3164 90.9347 66.3985 90.8921 66.4704C90.8495 66.5424 90.7881 66.6014 90.7146 66.6411L74.9526 75.5883C74.8825 75.6307 74.8018 75.6525 74.7198 75.6511" fill="white"/>
<defs>
<linearGradient id="paint0_linear_687_3454" x1="161" y1="180" x2="3.59284e-07" y2="4.99998" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#595656"/>
</linearGradient>
<clipPath id="clip0_687_3454">
<rect width="94" height="94" fill="white" transform="translate(25 25)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

@@ -1,126 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "virtual-machine.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "virtual-machine.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "virtual-machine.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "virtual-machine.labels" -}}
helm.sh/chart: {{ include "virtual-machine.chart" . }}
{{ include "virtual-machine.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "virtual-machine.selectorLabels" -}}
app.kubernetes.io/name: {{ include "virtual-machine.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Generate a stable UUID for cloud-init re-initialization upon upgrade.
*/}}
{{- define "virtual-machine.stableUuid" -}}
{{- $source := printf "%s-%s-%s" .Release.Namespace (include "virtual-machine.fullname" .) .Values.cloudInitSeed }}
{{- $hash := sha256sum $source }}
{{- $uuid := printf "%s-%s-4%s-9%s-%s" (substr 0 8 $hash) (substr 8 12 $hash) (substr 13 16 $hash) (substr 17 20 $hash) (substr 20 32 $hash) }}
{{- if eq .Values.cloudInitSeed "" }}
{{- /* Try to save previous uuid to not trigger full cloud-init again if user decided to remove the seed. */}}
{{- $vmResource := lookup "kubevirt.io/v1" "VirtualMachine" .Release.Namespace (include "virtual-machine.fullname" .) -}}
{{- if $vmResource }}
{{- $existingUuid := $vmResource | dig "spec" "template" "spec" "domain" "firmware" "uuid" "" }}
{{- if $existingUuid }}
{{- $uuid = $existingUuid }}
{{- end }}
{{- end }}
{{- end }}
{{- $uuid }}
{{- end }}
{{/*
Domain resources (cpu, memory) as a JSON object.
Used in vm.yaml for rendering and in the update hook for merge patches.
*/}}
{{- define "virtual-machine.domainResources" -}}
{{- $result := dict -}}
{{- if or .Values.cpuModel (and .Values.resources .Values.resources.cpu .Values.resources.sockets) -}}
{{- $cpu := dict -}}
{{- if and .Values.resources .Values.resources.cpu .Values.resources.sockets -}}
{{- $_ := set $cpu "cores" (.Values.resources.cpu | int64) -}}
{{- $_ := set $cpu "sockets" (.Values.resources.sockets | int64) -}}
{{- end -}}
{{- if .Values.cpuModel -}}
{{- $_ := set $cpu "model" .Values.cpuModel -}}
{{- end -}}
{{- $_ := set $result "cpu" $cpu -}}
{{- end -}}
{{- if and .Values.resources .Values.resources.memory -}}
{{- $_ := set $result "resources" (dict "requests" (dict "memory" .Values.resources.memory)) -}}
{{- end -}}
{{- $result | toJson -}}
{{- end -}}
{{/*
Node Affinity for Windows VMs
*/}}
{{- define "virtual-machine.nodeAffinity" -}}
{{- if .Values._cluster.scheduling -}}
{{- $dedicatedNodesForWindowsVMs := get .Values._cluster.scheduling "dedicatedNodesForWindowsVMs" -}}
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
affinity:
nodeAffinity:
{{- if $isWindows }}
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: scheduling.cozystack.io/vm-windows
operator: In
values:
- "true"
{{- else }}
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: scheduling.cozystack.io/vm-windows
operator: NotIn
values:
- "true"
{{- end }}
{{- end -}}
{{- end -}}
{{- end -}}
@@ -1,43 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Release.Name }}-dashboard-resources
rules:
- apiGroups:
- ""
resources:
- services
resourceNames:
- {{ include "virtual-machine.fullname" . }}
verbs: ["get", "list", "watch"]
- apiGroups:
- cozystack.io
resources:
- workloadmonitors
resourceNames:
- {{ .Release.Name }}
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ .Release.Name }}-dashboard-resources
subjects:
{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }}
roleRef:
kind: Role
name: {{ .Release.Name }}-dashboard-resources
apiGroup: rbac.authorization.k8s.io
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
spec:
replicas: 1
minReplicas: 1
kind: virtual-machine
type: virtual-machine
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
version: {{ $.Chart.Version }}
@@ -1,40 +0,0 @@
# if subnets are specified and image is either ubunu or alpine
{{- if and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine")) }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "virtual-machine.fullname" $ }}-network-config
stringData:
networkData: |
network:
version: 1
config:
{{- if eq .Values.systemDisk.image "ubuntu" }}
# main iface
- type: physical
name: enp1s0
subnets:
- type: dhcp
# additional ifaces attached to subnets
{{- range $i, $subnet := .Values.subnets }}
- type: physical
name: enp{{ add 2 $i }}s0
subnets:
- type: dhcp
{{- end }}
{{- else if eq .Values.systemDisk.image "alpine" }}
#main iface
- type: physical
name: eth0
subnets:
- type: dhcp
# additional ifaces attached to subnets
{{- range $i, $subnet := .Values.subnets }}
- type: physical
name: eth{{ add1 $i }}
subnets:
- type: dhcp
{{- end }}
{{- end }}
{{- end }}
@@ -1,33 +0,0 @@
{{- if .Values.sshKeys }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "virtual-machine.fullname" $ }}-ssh-keys
stringData:
{{- range $k, $v := .Values.sshKeys }}
key{{ $k }}: {{ quote $v }}
{{- end }}
{{- end }}
{{- if or .Values.cloudInit .Values.sshKeys }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "virtual-machine.fullname" . }}-cloud-init
stringData:
userdata: |
{{- if .Values.cloudInit }}
{{- .Values.cloudInit | nindent 4 }}
{{- else if and (.Values.sshKeys) (not .Values.cloudInit) }}
{{- /*
We usually provide ssh keys in cloud-init metadata, because userdata it not typed and can be used for any purpose.
However, if user provides ssh keys but not cloud-init, we still need to provide a minimal cloud-init config to avoid errors.
*/}}
#cloud-config
ssh_authorized_keys:
{{- range .Values.sshKeys }}
- {{ quote . }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,38 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "virtual-machine.fullname" . }}
labels:
apps.cozystack.io/user-service: "true"
{{- include "virtual-machine.labels" . | nindent 4 }}
{{- if .Values.external }}
annotations:
networking.cozystack.io/wholeIP: "true"
{{- end }}
spec:
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
{{- if .Values.external }}
externalTrafficPolicy: Local
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
allocateLoadBalancerNodePorts: false
{{- end }}
{{- else }}
clusterIP: None
{{- end }}
selector:
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
ports:
{{- if .Values.external }}
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
- port: 65535
{{- else }}
{{- range .Values.externalPorts }}
- name: port-{{ . }}
port: {{ . }}
targetPort: {{ . }}
{{- end }}
{{- end }}
{{- else }}
- port: 65535
{{- end }}
@@ -1,170 +0,0 @@
{{- $vmName := include "virtual-machine.fullname" . -}}
{{- $namespace := .Release.Namespace -}}
{{- $existingVM := lookup "kubevirt.io/v1" "VirtualMachine" $namespace $vmName -}}
{{- $existingPVC := lookup "v1" "PersistentVolumeClaim" $namespace $vmName -}}
{{- $existingService := lookup "v1" "Service" $namespace $vmName -}}
{{- $instanceType := .Values.instanceType | default "" -}}
{{- $instanceProfile := .Values.instanceProfile | default "" -}}
{{- $desiredStorage := .Values.systemDisk.storage | default "" -}}
{{- $desiredServiceType := ternary "LoadBalancer" "ClusterIP" .Values.external -}}
{{- $needUpdateType := false -}}
{{- $needUpdateProfile := false -}}
{{- $needResizePVC := false -}}
{{- $needRecreateService := false -}}
{{- $needRemoveInstanceType := false -}}
{{- $needRemoveCustomResources := false -}}
{{- $existingHasInstanceType := and $existingVM $existingVM.spec.instancetype -}}
{{- if and $existingHasInstanceType (not $instanceType) -}}
{{- $needRemoveInstanceType = true -}}
{{- else if and $existingHasInstanceType $instanceType -}}
{{- if not (eq $existingVM.spec.instancetype.name $instanceType) -}}
{{- $needUpdateType = true -}}
{{- end -}}
{{- else if and $existingVM (not $existingHasInstanceType) $instanceType -}}
{{- $needRemoveCustomResources = true -}}
{{- end -}}
{{- if and $existingVM $existingVM.spec.preference $instanceProfile -}}
{{- if not (eq $existingVM.spec.preference.name $instanceProfile) -}}
{{- $needUpdateProfile = true -}}
{{- end -}}
{{- end -}}
{{- if and $existingPVC $desiredStorage -}}
{{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}}
{{- if not (eq $currentStorage $desiredStorage) -}}
{{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}}
{{- $newSize := (include "cozy-lib.resources.toFloat" $desiredStorage) | float64 -}}
{{- if gt $newSize $oldSize -}}
{{- $needResizePVC = true -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $existingService -}}
{{- $currentServiceType := $existingService.spec.type -}}
{{- if ne $currentServiceType $desiredServiceType -}}
{{- $needRecreateService = true -}}
{{- end -}}
{{- end -}}
{{- if or $needUpdateType $needUpdateProfile $needResizePVC $needRecreateService $needRemoveInstanceType $needRemoveCustomResources }}
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ $.Release.Name }}-update-hook"
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: hook-succeeded,before-hook-creation
spec:
template:
metadata:
labels:
app: "{{ $.Release.Name }}-update-hook"
policy.cozystack.io/allow-to-apiserver: "true"
spec:
serviceAccountName: {{ $.Release.Name }}-update-hook
restartPolicy: Never
containers:
- name: update-resources
image: docker.io/alpine/k8s:1.33.4
resources:
requests:
memory: "16Mi"
cpu: "10m"
limits:
memory: "128Mi"
cpu: "100m"
command: ["sh", "-exc"]
args:
- |
{{- if $needUpdateType }}
echo "Patching VirtualMachine for instancetype update..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"instancetype":{"name": "{{ $instanceType }}", "revisionName": null}}}'
{{- end }}
{{- if $needUpdateProfile }}
echo "Patching VirtualMachine for preference update..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"preference":{"name": "{{ $instanceProfile }}", "revisionName": null}}}'
{{- end }}
{{- if $needRemoveInstanceType }}
echo "Removing instancetype from VM (switching to custom resources)..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"instancetype":null{{- if not $instanceProfile }},"preference":null{{- end }},"template":{"spec":{"domain":{{ include "virtual-machine.domainResources" . }}}}}}'
{{- end }}
{{- if $needRemoveCustomResources }}
echo "Removing custom CPU/memory from domain (switching to instancetype)..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"instancetype":{"name":"{{ $instanceType }}","revisionName":null},"template":{"spec":{"domain":{"cpu":null,"resources":null}}}}}'
{{- end }}
{{- if $needResizePVC }}
echo "Patching PVC for storage resize..."
kubectl patch pvc {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"resources":{"requests":{"storage":"{{ $desiredStorage }}"}}}}'
{{- end }}
{{- if $needRecreateService }}
echo "Removing Service..."
kubectl delete service --cascade=orphan -n {{ $namespace }} {{ $vmName }}
{{- end }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ $.Release.Name }}-update-hook
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $.Release.Name }}-update-hook
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation
rules:
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachines"]
verbs: ["patch", "get", "list", "watch"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["patch", "get", "list", "watch"]
- apiGroups: [""]
resources: ["services"]
resourceNames: ["{{ $vmName }}"]
verbs: ["delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $.Release.Name }}-update-hook
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation
subjects:
- kind: ServiceAccount
name: {{ $.Release.Name }}-update-hook
roleRef:
kind: Role
name: {{ $.Release.Name }}-update-hook
apiGroup: rbac.authorization.k8s.io
{{- end }}
@@ -1,159 +0,0 @@
{{- if and .Values.instanceType (not (lookup "instancetype.kubevirt.io/v1beta1" "VirtualMachineClusterInstancetype" "" .Values.instanceType)) }}
{{- fail (printf "Specified instancetype not exists in cluster: %s" .Values.instanceType) }}
{{- end }}
{{- if and .Values.instanceProfile (not (lookup "instancetype.kubevirt.io/v1beta1" "VirtualMachineClusterPreference" "" .Values.instanceProfile)) }}
{{- fail (printf "Specified profile not exists in cluster: %s" .Values.instanceProfile) }}
{{- end }}
{{- 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 }}
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: {{ include "virtual-machine.fullname" . }}
labels:
{{- include "virtual-machine.labels" . | nindent 4 }}
spec:
{{- if hasKey .Values "runStrategy" }}
runStrategy: {{ .Values.runStrategy }}
{{- else }}
runStrategy: {{ ternary "Always" "Halted" (.Values.running | default true) }}
{{- end }}
{{- with .Values.instanceType }}
instancetype:
kind: VirtualMachineClusterInstancetype
name: {{ . }}
{{- end }}
{{- with .Values.instanceProfile }}
preference:
kind: VirtualMachineClusterPreference
name: {{ . }}
{{- end }}
dataVolumeTemplates:
- metadata:
name: {{ include "virtual-machine.fullname" . }}
labels:
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
storage:
resources:
requests:
storage: {{ .Values.systemDisk.storage | quote }}
{{- with .Values.systemDisk.storageClass }}
storageClassName: {{ . }}
{{- end }}
source:
{{- $dv := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" "cozy-public" (printf "vm-image-%s" .Values.systemDisk.image) }}
{{- if $dv }}
pvc:
name: vm-image-{{ .Values.systemDisk.image }}
namespace: cozy-public
{{- else }}
http:
{{- if eq .Values.systemDisk.image "cirros" }}
url: https://download.cirros-cloud.net/0.6.2/cirros-0.6.2-x86_64-disk.img
{{- else if eq .Values.systemDisk.image "ubuntu" }}
url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
{{- else if eq .Values.systemDisk.image "fedora" }}
url: https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2
{{- else if eq .Values.systemDisk.image "alpine" }}
url: https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/cloud/nocloud_alpine-3.20.8-x86_64-bios-cloudinit-r0.qcow2
{{- else if eq .Values.systemDisk.image "talos" }}
url: https://github.com/siderolabs/talos/releases/download/v1.7.6/nocloud-amd64.raw.xz
{{- end }}
{{- end }}
template:
metadata:
annotations:
kubevirt.io/allow-pod-bridge-network-live-migration: "true"
labels:
{{- include "virtual-machine.labels" . | nindent 8 }}
spec:
domain:
{{- $domainRes := include "virtual-machine.domainResources" . | fromJson -}}
{{- with $domainRes.cpu }}
cpu: {{- . | toYaml | nindent 10 }}
{{- end }}
{{- with $domainRes.resources }}
resources: {{- . | toYaml | nindent 10 }}
{{- end }}
firmware:
uuid: {{ include "virtual-machine.stableUuid" . }}
devices:
{{- if .Values.gpus }}
gpus:
{{- range $i, $gpu := .Values.gpus }}
- name: gpu{{ add $i 1 }}
deviceName: {{ $gpu.name }}
{{- end }}
{{- end }}
disks:
- disk:
bus: scsi
name: systemdisk
{{- if or .Values.cloudInit .Values.sshKeys (and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine"))) }}
- disk:
bus: virtio
name: cloudinitdisk
{{- end }}
interfaces:
- name: default
bridge: {}
{{- if .Values.subnets }}
{{- range $i, $subnet := .Values.subnets }}
- name: {{ $subnet.name }}
bridge: {}
{{- end }}
{{- end }}
machine:
type: ""
{{- if .Values.sshKeys }}
accessCredentials:
- sshPublicKey:
source:
secret:
secretName: {{ include "virtual-machine.fullname" $ }}-ssh-keys
propagationMethod:
# keys will be injected into metadata part of cloud-init disk
noCloud: {}
{{- end }}
terminationGracePeriodSeconds: 30
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
volumes:
- name: systemdisk
dataVolume:
name: {{ include "virtual-machine.fullname" . }}
{{- if or .Values.cloudInit .Values.sshKeys (and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine"))) }}
- name: cloudinitdisk
cloudInitNoCloud:
{{- if or .Values.cloudInit .Values.sshKeys }}
secretRef:
name: {{ include "virtual-machine.fullname" . }}-cloud-init
{{- end }}
{{- if and .Values.subnets (or (eq .Values.systemDisk.image "ubuntu") (eq .Values.systemDisk.image "alpine")) }}
networkDataSecretRef:
name: {{ include "virtual-machine.fullname" . }}-network-config
{{- end }}
{{- end }}
networks:
- name: default
pod: {}
{{- if .Values.subnets }}
{{- range $i, $subnet := .Values.subnets }}
- name: {{ $subnet.name }}
multus:
networkName: {{ $.Release.Namespace }}/{{ $subnet.name }}
{{- end }}
{{- end }}
@@ -1,230 +0,0 @@
{
"title": "Chart Values",
"type": "object",
"properties": {
"cloudInit": {
"description": "Cloud-init user data.",
"type": "string",
"default": ""
},
"cloudInitSeed": {
"description": "Seed string to generate SMBIOS UUID for the VM.",
"type": "string",
"default": ""
},
"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": ""
},
"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"
}
},
"gpus": {
"description": "List of GPUs to attach.",
"type": "array",
"default": [],
"items": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "The name of the GPU resource to attach.",
"type": "string"
}
}
}
},
"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",
""
]
},
"instanceType": {
"description": "Virtual Machine instance type.",
"type": "string",
"default": "u1.medium"
},
"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
}
}
},
"runStrategy": {
"description": "Requested running state of the VirtualMachineInstance",
"type": "string",
"default": "Always",
"enum": [
"Always",
"Halted",
"Manual",
"RerunOnFailure",
"Once"
]
},
"sshKeys": {
"description": "List of SSH public keys for authentication.",
"type": "array",
"default": [],
"items": {
"type": "string"
}
},
"subnets": {
"description": "Additional subnets",
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"name": {
"description": "Subnet name",
"type": "string"
}
}
}
},
"systemDisk": {
"description": "System disk configuration.",
"type": "object",
"default": {},
"required": [
"image",
"storage"
],
"properties": {
"image": {
"description": "The base image for the virtual machine.",
"type": "string",
"default": "ubuntu",
"enum": [
"ubuntu",
"cirros",
"alpine",
"fedora",
"talos"
]
},
"storage": {
"description": "The size of the disk allocated for the virtual machine.",
"type": "string",
"default": "5Gi"
},
"storageClass": {
"description": "StorageClass used to store the data.",
"type": "string",
"default": "replicated"
}
}
}
}
}
-108
View File
@@ -1,108 +0,0 @@
##
## @section Common parameters
##
## @enum {string} ExternalMethod - Method to pass through traffic to the VM.
## @value PortList - Forward selected ports only.
## @value WholeIP - Forward all traffic for the IP.
## @enum {string} SystemImage - The base image for the virtual machine.
## @value ubuntu
## @value cirros
## @value alpine
## @value fedora
## @value talos
## @typedef {struct} SystemDisk - System disk configuration.
## @field {SystemImage} image - The base image for the virtual machine.
## @field {string} storage - The size of the disk allocated for the virtual machine.
## @field {string} [storageClass] - StorageClass used to store the data.
## @typedef {struct} Subnet - Additional subnets
## @field {string} [name] - Subnet name
## @typedef {struct} GPU - GPU device configuration.
## @field {string} name - The name of the GPU resource to attach.
## @typedef {struct} Resources - Resource configuration for the virtual machine.
## @field {quantity} [cpu] - Number of CPU cores allocated.
## @field {quantity} [sockets] - Number of CPU sockets (vCPU topology).
## @field {quantity} [memory] - Amount of memory allocated.
## @param {bool} external - Enable external access from outside the cluster.
external: false
## @param {ExternalMethod} externalMethod - Method to pass through traffic to the VM.
externalMethod: "PortList"
## @param {[]int} externalPorts - Ports to forward from outside the cluster.
externalPorts:
- 22
## @enum {string} RunStrategy - Requested running state of the VirtualMachineInstance
## @value Always - VMI should always be running
## @value Halted - VMI should never be running
## @value Manual - VMI can be started/stopped using API endpoints
## @value RerunOnFailure - VMI will initially be running and restarted if a failure occurs, but will not be restarted upon successful completion
## @value Once - VMI will run once and not be restarted upon completion regardless if the completion is of phase Failure or Success
## @param {RunStrategy} runStrategy - Requested running state of the VirtualMachineInstance
runStrategy: Always
## @param {string} instanceType - Virtual Machine instance type.
## @param {string} instanceProfile - Virtual Machine preferences profile.
instanceType: "u1.medium"
instanceProfile: ubuntu
## @param {SystemDisk} systemDisk - System disk configuration.
systemDisk:
image: ubuntu
storage: 5Gi
storageClass: replicated
## @param {[]Subnet} subnets - Additional subnets
subnets: []
## Example:
## subnets:
## - name: subnet-84dbec17
## - name: subnet-aa8896b5
## - name: subnet-e9b97196
## @param {[]GPU} gpus - List of GPUs to attach.
gpus: []
## Example:
## gpus:
## - name: nvidia.com/GA102GL_A10
## @param {string} cpuModel - Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map
cpuModel: ""
## @param {Resources} [resources] - Resource configuration for the virtual machine.
resources: {}
## Example:
## resources:
## cpu: "4"
## sockets: "1"
## memory: "8Gi"
## @param {[]string} sshKeys - List of SSH public keys for authentication.
sshKeys: []
## Example:
## sshKeys:
## - ssh-rsa ...
## - ssh-ed25519 ...
##
## @param {string} cloudInit - Cloud-init user data.
cloudInit: ""
## Example:
## cloudInit: |
## #cloud-config
## password: ubuntu
## chpasswd: { expire: False }
##
## @param {string} cloudInitSeed - Seed string to generate SMBIOS UUID for the VM.
cloudInitSeed: ""
## Example:
## cloudInitSeed: "upd1"
+23 -22
View File
@@ -36,28 +36,29 @@ virtctl ssh <user>@<vm>
### Common parameters
| Name | Description | Type | Value |
| ------------------- | ------------------------------------------------------------------- | ---------- | ----------- |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` |
| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` |
| `running` | Determines if the virtual machine should be running. | `bool` | `true` |
| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` |
| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` |
| `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` | `""` |
| `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` | `""` |
| `resources` | Resource configuration for the virtual machine. | `object` | `{}` |
| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` |
| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` |
| `cloudInit` | Cloud-init user data. | `string` | `""` |
| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` |
| Name | Description | Type | Value |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------- |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` |
| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` |
| `runStrategy` | Requested running state of the VirtualMachineInstance | `string` | `Always` |
| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` |
| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` |
| `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` | `""` |
| `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` | `""` |
| `resources` | Resource configuration for the virtual machine. | `object` | `{}` |
| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` |
| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` |
| `cloudInit` | Cloud-init user data. | `string` | `""` |
| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` |
## U Series
@@ -70,6 +70,29 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
{{- $uuid }}
{{- end }}
{{/*
Domain resources (cpu, memory) as a JSON object.
Used in vm.yaml for rendering and in the update hook for merge patches.
*/}}
{{- define "virtual-machine.domainResources" -}}
{{- $result := dict -}}
{{- if or .Values.cpuModel (and .Values.resources .Values.resources.cpu .Values.resources.sockets) -}}
{{- $cpu := dict -}}
{{- if and .Values.resources .Values.resources.cpu .Values.resources.sockets -}}
{{- $_ := set $cpu "cores" (.Values.resources.cpu | int64) -}}
{{- $_ := set $cpu "sockets" (.Values.resources.sockets | int64) -}}
{{- end -}}
{{- if .Values.cpuModel -}}
{{- $_ := set $cpu "model" .Values.cpuModel -}}
{{- end -}}
{{- $_ := set $result "cpu" $cpu -}}
{{- end -}}
{{- if and .Values.resources .Values.resources.memory -}}
{{- $_ := set $result "resources" (dict "requests" (dict "memory" .Values.resources.memory)) -}}
{{- end -}}
{{- $result | toJson -}}
{{- end -}}
{{/*
Node Affinity for Windows VMs
*/}}
@@ -2,26 +2,43 @@
{{- $namespace := .Release.Namespace -}}
{{- $existingVM := lookup "kubevirt.io/v1" "VirtualMachine" $namespace $vmName -}}
{{- $existingService := lookup "v1" "Service" $namespace $vmName -}}
{{- $instanceType := .Values.instanceType | default "" -}}
{{- $instanceProfile := .Values.instanceProfile | default "" -}}
{{- $desiredServiceType := ternary "LoadBalancer" "ClusterIP" .Values.external -}}
{{- $needUpdateType := false -}}
{{- $needUpdateProfile := false -}}
{{- $needRecreateService := false -}}
{{- $needRemoveInstanceType := false -}}
{{- $needRemoveCustomResources := false -}}
{{- if and $existingVM $instanceType -}}
{{- $existingHasInstanceType := and $existingVM $existingVM.spec.instancetype -}}
{{- if and $existingHasInstanceType (not $instanceType) -}}
{{- $needRemoveInstanceType = true -}}
{{- else if and $existingHasInstanceType $instanceType -}}
{{- if not (eq $existingVM.spec.instancetype.name $instanceType) -}}
{{- $needUpdateType = true -}}
{{- end -}}
{{- else if and $existingVM (not $existingHasInstanceType) $instanceType -}}
{{- $needRemoveCustomResources = true -}}
{{- end -}}
{{- if and $existingVM $instanceProfile -}}
{{- if and $existingVM $existingVM.spec.preference $instanceProfile -}}
{{- if not (eq $existingVM.spec.preference.name $instanceProfile) -}}
{{- $needUpdateProfile = true -}}
{{- end -}}
{{- end -}}
{{- if or $needUpdateType $needUpdateProfile }}
{{- if $existingService -}}
{{- $currentServiceType := $existingService.spec.type -}}
{{- if ne $currentServiceType $desiredServiceType -}}
{{- $needRecreateService = true -}}
{{- end -}}
{{- end -}}
{{- if or $needUpdateType $needUpdateProfile $needRecreateService $needRemoveInstanceType $needRemoveCustomResources }}
apiVersion: batch/v1
kind: Job
metadata:
@@ -58,13 +75,32 @@ spec:
--type merge \
-p '{"spec":{"instancetype":{"name": "{{ $instanceType }}", "revisionName": null}}}'
{{- end }}
{{- if $needUpdateProfile }}
echo "Patching VirtualMachine for preference update..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"preference":{"name": "{{ $instanceProfile }}", "revisionName": null}}}'
{{- end }}
{{- if $needRemoveInstanceType }}
echo "Removing instancetype from VM (switching to custom resources)..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"instancetype":null{{- if not $instanceProfile }},"preference":null{{- end }},"template":{"spec":{"domain":{{ include "virtual-machine.domainResources" . }}}}}}'
{{- end }}
{{- if $needRemoveCustomResources }}
echo "Removing custom CPU/memory from domain (switching to instancetype)..."
kubectl patch virtualmachines.kubevirt.io {{ $vmName }} -n {{ $namespace }} \
--type merge \
-p '{"spec":{"instancetype":{"name":"{{ $instanceType }}","revisionName":null},"template":{"spec":{"domain":{"cpu":null,"resources":null}}}}}'
{{- end }}
{{- if $needRecreateService }}
echo "Removing Service..."
kubectl delete service --cascade=orphan -n {{ $namespace }} {{ $vmName }}
{{- end }}
---
apiVersion: v1
kind: ServiceAccount
@@ -87,6 +123,10 @@ rules:
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachines"]
verbs: ["patch", "get", "list", "watch"]
- apiGroups: [""]
resources: ["services"]
resourceNames: ["{{ $vmName }}"]
verbs: ["delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
+13 -9
View File
@@ -4,6 +4,9 @@
{{- if and .Values.instanceProfile (not (lookup "instancetype.kubevirt.io/v1beta1" "VirtualMachineClusterPreference" "" .Values.instanceProfile)) }}
{{- fail (printf "Specified instanceProfile does not exist in the cluster: %s" .Values.instanceProfile) }}
{{- end }}
{{- 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 }}
apiVersion: kubevirt.io/v1
kind: VirtualMachine
@@ -12,7 +15,11 @@ metadata:
labels:
{{- include "virtual-machine.labels" . | nindent 4 }}
spec:
running: {{ .Values.running }}
{{- if hasKey .Values "runStrategy" }}
runStrategy: {{ .Values.runStrategy }}
{{- else }}
runStrategy: {{ ternary "Always" "Halted" (.Values.running | default true) }}
{{- end }}
{{- with .Values.instanceType }}
instancetype:
kind: VirtualMachineClusterInstancetype
@@ -31,15 +38,12 @@ spec:
{{- include "virtual-machine.labels" . | nindent 8 }}
spec:
domain:
{{- if and .Values.resources .Values.resources.cpu .Values.resources.sockets }}
cpu:
cores: {{ .Values.resources.cpu }}
sockets: {{ .Values.resources.sockets }}
{{- $domainRes := include "virtual-machine.domainResources" . | fromJson -}}
{{- with $domainRes.cpu }}
cpu: {{- . | toYaml | nindent 10 }}
{{- end }}
{{- if and .Values.resources .Values.resources.memory }}
resources:
requests:
memory: {{ .Values.resources.memory | quote }}
{{- with $domainRes.resources }}
resources: {{- . | toYaml | nindent 10 }}
{{- end }}
firmware:
uuid: {{ include "virtual-machine.stableUuid" . }}
+16 -4
View File
@@ -12,6 +12,11 @@
"type": "string",
"default": ""
},
"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": ""
},
"disks": {
"description": "List of disks to attach.",
"type": "array",
@@ -175,10 +180,17 @@
}
}
},
"running": {
"description": "Determines if the virtual machine should be running.",
"type": "boolean",
"default": true
"runStrategy": {
"description": "Requested running state of the VirtualMachineInstance",
"type": "string",
"default": "Always",
"enum": [
"Always",
"Halted",
"Manual",
"RerunOnFailure",
"Once"
]
},
"sshKeys": {
"description": "List of SSH public keys for authentication.",
+12 -2
View File
@@ -31,8 +31,15 @@ externalMethod: PortList
externalPorts:
- 22
## @param {bool} running - Determines if the virtual machine should be running.
running: true
## @enum {string} RunStrategy - Requested running state of the VirtualMachineInstance
## @value Always - VMI should always be running
## @value Halted - VMI should never be running
## @value Manual - VMI can be started/stopped using API endpoints
## @value RerunOnFailure - VMI will initially be running and restarted if a failure occurs, but will not be restarted upon successful completion
## @value Once - VMI will run once and not be restarted upon completion regardless if the completion is of phase Failure or Success
## @param {RunStrategy} runStrategy - Requested running state of the VirtualMachineInstance
runStrategy: Always
## @param {string} instanceType - Virtual Machine instance type.
instanceType: "u1.medium"
@@ -62,6 +69,9 @@ gpus: []
## gpus:
## - name: nvidia.com/GA102GL_A10
## @param {string} cpuModel - Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map
cpuModel: ""
## @param {Resources} [resources] - Resource configuration for the virtual machine.
resources: {}
+777
View File
@@ -0,0 +1,777 @@
#!/bin/bash
# Migration 29 --> 30
# Convert virtual-machine HelmReleases to vm-disk + vm-instance.
# Discovers all VirtualMachine HelmReleases by label, migrates each instance.
# Idempotent: safe to re-run.
set -euo pipefail
OLD_PREFIX="virtual-machine"
NEW_DISK_PREFIX="vm-disk"
NEW_INSTANCE_PREFIX="vm-instance"
PROTECTION_WEBHOOK_NAME="protection-webhook"
PROTECTION_WEBHOOK_NS="protection-webhook"
CDI_APISERVER_NS="cozy-kubevirt-cdi"
CDI_APISERVER_DEPLOY="cdi-apiserver"
CDI_VALIDATING_WEBHOOKS="cdi-api-datavolume-validate cdi-api-dataimportcron-validate cdi-api-populator-validate cdi-api-validate"
CDI_MUTATING_WEBHOOKS="cdi-api-datavolume-mutate"
# ============================================================
# Helper functions
# ============================================================
resource_exists() {
local ns="$1" type="$2" name="$3"
kubectl -n "$ns" get "$type" "$name" --no-headers 2>/dev/null | grep -q .
}
delete_resource() {
local ns="$1" type="$2" name="$3"
if resource_exists "$ns" "$type" "$name"; then
echo " [DELETE] ${type}/${name}"
kubectl -n "$ns" delete "$type" "$name" --wait=false
else
echo " [SKIP] ${type}/${name} already gone"
fi
}
clone_resource() {
local ns="$1" type="$2" old_name="$3" new_name="$4" old_instance="$5" new_instance="$6"
if resource_exists "$ns" "$type" "$new_name"; then
echo " [SKIP] ${type}/${new_name} already exists"
return 0
fi
if ! resource_exists "$ns" "$type" "$old_name"; then
echo " [SKIP] ${type}/${old_name} not found"
return 0
fi
echo " [CREATE] ${type}/${new_name} from ${old_name}"
kubectl -n "$ns" get "$type" "$old_name" -o json | \
jq --arg new_name "$new_name" --arg old "$old_instance" --arg new "$new_instance" '
.metadata.name = $new_name |
del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp,
.metadata.generation, .metadata.managedFields, .metadata.selfLink,
.metadata.ownerReferences) |
del(.status) |
.metadata.labels = ((.metadata.labels // {}) | with_entries(
if (.value | type) == "string" then .value |= gsub($old; $new) else . end)) |
.metadata.annotations = ((.metadata.annotations // {}) | with_entries(
select(.key != "kubectl.kubernetes.io/last-applied-configuration") |
if (.value | type) == "string" then .value |= gsub($old; $new) else . end))
' | kubectl -n "$ns" apply -f -
}
# ============================================================
# STEP 1: Discover all VirtualMachine instances
# ============================================================
echo "=== Discovering VirtualMachine HelmReleases ==="
INSTANCES=()
while IFS=/ read -r ns name; do
[ -z "$ns" ] && continue
instance="${name#${OLD_PREFIX}-}"
INSTANCES+=("${ns}/${instance}")
echo " Found: ${ns}/${name} (instance=${instance})"
done < <(kubectl get hr -A -l "apps.cozystack.io/application.kind=VirtualMachine" \
-o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}' 2>/dev/null)
if [ ${#INSTANCES[@]} -eq 0 ]; then
echo " No VirtualMachine HelmReleases found. Nothing to migrate."
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=30 --dry-run=client -o yaml | kubectl apply -f-
exit 0
fi
echo " Total: ${#INSTANCES[@]} instance(s)"
# ============================================================
# STEP 2: Migrate each instance
# ============================================================
ALL_PV_NAMES=()
ALL_PROTECTED_RESOURCES=()
for entry in "${INSTANCES[@]}"; do
NAMESPACE="${entry%%/*}"
INSTANCE="${entry#*/}"
OLD_NAME="${OLD_PREFIX}-${INSTANCE}"
NEW_DISK_NAME="${NEW_DISK_PREFIX}-${INSTANCE}"
NEW_INSTANCE_NAME="${NEW_INSTANCE_PREFIX}-${INSTANCE}"
echo ""
echo "======================================================================"
echo "=== Migrating: ${OLD_NAME} -> ${NEW_DISK_NAME} + ${NEW_INSTANCE_NAME} in ${NAMESPACE}"
echo "======================================================================"
# --- 2a: Suspend old HelmRelease ---
echo " --- Suspend HelmRelease ---"
if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then
suspended=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false")
if [ "$suspended" != "true" ]; then
echo " [SUSPEND] hr/${OLD_NAME}"
kubectl -n "$NAMESPACE" patch hr "$OLD_NAME" --type=merge -p '{"spec":{"suspend":true}}'
else
echo " [SKIP] hr/${OLD_NAME} already suspended"
fi
else
echo " [SKIP] hr/${OLD_NAME} not found"
fi
# --- 2b: Extract values from HelmRelease ---
echo " --- Extract values ---"
VALUES_SECRET=""
VALUES_KEY="values.yaml"
if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then
VALUES_SECRET=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o json | \
jq -r '.spec.valuesFrom // [] | map(select(.kind == "Secret" and (.name | test("cozystack-values") | not))) | .[0].name // ""')
if [ -n "$VALUES_SECRET" ]; then
VALUES_KEY=$(kubectl -n "$NAMESPACE" get hr "$OLD_NAME" -o json | \
jq -r '.spec.valuesFrom // [] | map(select(.kind == "Secret" and (.name | test("cozystack-values") | not))) | .[0].valuesKey // "values.yaml"')
fi
fi
# Extract values from secret
EXTERNAL="false"
EXTERNAL_METHOD="PortList"
EXTERNAL_PORTS='[22]'
INSTANCE_TYPE="u1.medium"
INSTANCE_PROFILE="ubuntu"
RUN_STRATEGY="Always"
SYSTEM_DISK_IMAGE="ubuntu"
SYSTEM_DISK_STORAGE="5Gi"
SYSTEM_DISK_STORAGE_CLASS="replicated"
SSH_KEYS="[]"
CLOUD_INIT=""
CLOUD_INIT_SEED=""
SUBNETS="[]"
GPUS="[]"
CPU_MODEL=""
RESOURCES="{}"
if [ -n "$VALUES_SECRET" ] && resource_exists "$NAMESPACE" "secret" "$VALUES_SECRET"; then
echo " Reading values from secret: ${VALUES_SECRET}"
VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.data.${VALUES_KEY}}" 2>/dev/null | base64 -d 2>/dev/null || true)
if [ -z "$VALUES_YAML" ]; then
VALUES_YAML=$(kubectl -n "$NAMESPACE" get secret "$VALUES_SECRET" -o jsonpath="{.stringData.${VALUES_KEY}}" 2>/dev/null || true)
fi
if [ -n "$VALUES_YAML" ]; then
EXTERNAL=$(echo "$VALUES_YAML" | yq -r '.external // false')
EXTERNAL_METHOD=$(echo "$VALUES_YAML" | yq -r '.externalMethod // "PortList"')
EXTERNAL_PORTS=$(echo "$VALUES_YAML" | yq -c '.externalPorts // [22]')
INSTANCE_TYPE=$(echo "$VALUES_YAML" | yq -r '.instanceType // "u1.medium"')
INSTANCE_PROFILE=$(echo "$VALUES_YAML" | yq -r '.instanceProfile // "ubuntu"')
RUN_STRATEGY=$(echo "$VALUES_YAML" | yq -r '.runStrategy // (if .running == false then "Halted" else "Always" end)')
SYSTEM_DISK_IMAGE=$(echo "$VALUES_YAML" | yq -r '.systemDisk.image // "ubuntu"')
SYSTEM_DISK_STORAGE=$(echo "$VALUES_YAML" | yq -r '.systemDisk.storage // "5Gi"')
SYSTEM_DISK_STORAGE_CLASS=$(echo "$VALUES_YAML" | yq -r '.systemDisk.storageClass // "replicated"')
SSH_KEYS=$(echo "$VALUES_YAML" | yq -c '.sshKeys // []')
CLOUD_INIT=$(echo "$VALUES_YAML" | yq -r '.cloudInit // ""')
CLOUD_INIT_SEED=$(echo "$VALUES_YAML" | yq -r '.cloudInitSeed // ""')
SUBNETS=$(echo "$VALUES_YAML" | yq -c '.subnets // []')
GPUS=$(echo "$VALUES_YAML" | yq -c '.gpus // []')
CPU_MODEL=$(echo "$VALUES_YAML" | yq -r '.cpuModel // ""')
RESOURCES=$(echo "$VALUES_YAML" | yq -c '.resources // {}')
fi
fi
echo " external=${EXTERNAL}, instanceType=${INSTANCE_TYPE}, image=${SYSTEM_DISK_IMAGE}"
# --- 2c: Save kube-ovn IP ---
echo " --- Save kube-ovn IP ---"
OVN_IP=""
OVN_MAC=""
OVN_SUBNET="ovn-default"
OVN_IP_NAME="${OLD_NAME}.${NAMESPACE}"
if kubectl get ip.kubeovn.io "$OVN_IP_NAME" --no-headers 2>/dev/null | grep -q .; then
OVN_IP=$(kubectl get ip.kubeovn.io "$OVN_IP_NAME" -o jsonpath='{.spec.ipAddress}')
OVN_MAC=$(kubectl get ip.kubeovn.io "$OVN_IP_NAME" -o jsonpath='{.spec.macAddress}')
OVN_SUBNET=$(kubectl get ip.kubeovn.io "$OVN_IP_NAME" -o jsonpath='{.spec.subnet}')
echo " kube-ovn IP: ${OVN_IP}, MAC: ${OVN_MAC}, subnet: ${OVN_SUBNET}"
else
echo " [SKIP] No kube-ovn IP resource found"
fi
# --- 2d: Save LoadBalancer IP (if external=true) ---
echo " --- Save LoadBalancer IP ---"
LB_IP=""
if [ "$EXTERNAL" = "true" ] && resource_exists "$NAMESPACE" "svc" "$OLD_NAME"; then
LB_IP=$(kubectl -n "$NAMESPACE" get svc "$OLD_NAME" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
if [ -n "$LB_IP" ]; then
echo " LoadBalancer IP: ${LB_IP}"
else
echo " [WARN] external=true but no LoadBalancer IP assigned"
fi
else
echo " [SKIP] Not external or no service"
fi
# --- 2e: Switch PV reclaim policy to Retain ---
echo " --- Switch PV reclaim to Retain ---"
PV_NAME=""
if resource_exists "$NAMESPACE" "pvc" "$OLD_NAME"; then
PV_NAME=$(kubectl -n "$NAMESPACE" get pvc "$OLD_NAME" -o jsonpath='{.spec.volumeName}')
if [ -n "$PV_NAME" ]; then
ALL_PV_NAMES+=("$PV_NAME")
current_policy=$(kubectl get pv "$PV_NAME" -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')
if [ "$current_policy" != "Retain" ]; then
echo " [PATCH] PV ${PV_NAME}: ${current_policy} -> Retain"
kubectl patch pv "$PV_NAME" -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
else
echo " [SKIP] PV ${PV_NAME} already Retain"
fi
fi
elif resource_exists "$NAMESPACE" "pvc" "$NEW_DISK_NAME"; then
PV_NAME=$(kubectl -n "$NAMESPACE" get pvc "$NEW_DISK_NAME" -o jsonpath='{.spec.volumeName}')
ALL_PV_NAMES+=("$PV_NAME")
echo " [SKIP] New PVC ${NEW_DISK_NAME} already exists, PV=${PV_NAME}"
else
echo " [WARN] No PVC found for ${OLD_NAME} or ${NEW_DISK_NAME}"
fi
# --- 2f: Stop and delete the VirtualMachine ---
echo " --- Stop VirtualMachine ---"
if resource_exists "$NAMESPACE" "vm" "$OLD_NAME"; then
echo " [PATCH] Stop VM ${OLD_NAME}"
kubectl -n "$NAMESPACE" patch vm "$OLD_NAME" --type merge -p '{"spec":{"runStrategy":"Halted"}}' 2>/dev/null || \
kubectl -n "$NAMESPACE" patch vm "$OLD_NAME" --type merge -p '{"spec":{"running":false}}' 2>/dev/null || true
echo " Waiting for VMI to terminate..."
kubectl -n "$NAMESPACE" wait --for=delete vmi "$OLD_NAME" --timeout=120s 2>/dev/null || true
fi
# --- 2g: Delete VirtualMachine CR (cascades to DataVolume and PVC) ---
echo " --- Delete VirtualMachine CR ---"
if resource_exists "$NAMESPACE" "vm" "$OLD_NAME"; then
echo " [DELETE] vm/${OLD_NAME}"
kubectl -n "$NAMESPACE" delete vm "$OLD_NAME" --wait=true --timeout=60s 2>/dev/null || true
fi
echo " Waiting for old PVC to be deleted..."
for i in $(seq 1 30); do
if ! resource_exists "$NAMESPACE" "pvc" "$OLD_NAME"; then
echo " [OK] PVC ${OLD_NAME} deleted"
break
fi
sleep 2
done
# If PVC still exists (orphaned), delete it manually
if resource_exists "$NAMESPACE" "pvc" "$OLD_NAME"; then
echo " [DELETE] Orphaned pvc/${OLD_NAME}"
kubectl -n "$NAMESPACE" delete pvc "$OLD_NAME" --wait=false 2>/dev/null || true
fi
# Delete orphaned DataVolume if still present
if resource_exists "$NAMESPACE" "dv" "$OLD_NAME"; then
echo " [DELETE] Orphaned dv/${OLD_NAME}"
kubectl -n "$NAMESPACE" delete dv "$OLD_NAME" --wait=false 2>/dev/null || true
fi
# --- 2h: Create new PVC for vm-disk ---
echo " --- Create new PVC for vm-disk ---"
if [ -n "$PV_NAME" ]; then
if resource_exists "$NAMESPACE" "pvc" "$NEW_DISK_NAME"; then
new_phase=$(kubectl -n "$NAMESPACE" get pvc "$NEW_DISK_NAME" \
-o jsonpath='{.status.phase}' 2>/dev/null || echo "unknown")
if [ "$new_phase" = "Bound" ]; then
echo " [SKIP] PVC ${NEW_DISK_NAME} already Bound"
fi
else
PV_VOLUME_MODE=$(kubectl get pv "$PV_NAME" -o jsonpath='{.spec.volumeMode}' 2>/dev/null || echo "Filesystem")
echo " [CREATE] PVC ${NEW_DISK_NAME} -> PV ${PV_NAME} (volumeMode=${PV_VOLUME_MODE})"
cat <<PVCEOF | kubectl -n "$NAMESPACE" apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${NEW_DISK_NAME}
namespace: ${NAMESPACE}
annotations:
cdi.kubevirt.io/allowClaimAdoption: "true"
labels:
app.kubernetes.io/instance: ${NEW_DISK_NAME}
spec:
accessModes: ["ReadWriteMany"]
volumeMode: ${PV_VOLUME_MODE}
storageClassName: "${SYSTEM_DISK_STORAGE_CLASS}"
volumeName: "${PV_NAME}"
resources:
requests:
storage: "${SYSTEM_DISK_STORAGE}"
PVCEOF
new_pvc_uid=$(kubectl -n "$NAMESPACE" get pvc "$NEW_DISK_NAME" -o jsonpath='{.metadata.uid}')
echo " [PATCH] Rebind PV ${PV_NAME} -> ${NEW_DISK_NAME}"
kubectl patch pv "$PV_NAME" --type=merge -p "{
\"spec\":{\"claimRef\":{\"apiVersion\":\"v1\",\"kind\":\"PersistentVolumeClaim\",
\"name\":\"${NEW_DISK_NAME}\",\"namespace\":\"${NAMESPACE}\",\"uid\":\"${new_pvc_uid}\"}}
}"
echo " Waiting for PVC ${NEW_DISK_NAME} to bind..."
kubectl -n "$NAMESPACE" wait pvc "$NEW_DISK_NAME" \
--for=jsonpath='{.status.phase}'=Bound --timeout=60s
echo " [OK] PVC ${NEW_DISK_NAME} is Bound"
fi
fi
# --- 2i: Clone Secrets ---
echo " --- Clone Secrets ---"
kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \
| grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values" \
| while IFS= read -r secret; do
old_secret_name="${secret#secret/}"
suffix="${old_secret_name#${OLD_NAME}}"
new_secret_name="${NEW_INSTANCE_NAME}${suffix}"
clone_resource "$NAMESPACE" "secret" "$old_secret_name" "$new_secret_name" "$OLD_NAME" "$NEW_INSTANCE_NAME"
done
# --- 2j: Clone WorkloadMonitor ---
echo " --- Clone WorkloadMonitor ---"
clone_resource "$NAMESPACE" "workloadmonitor.cozystack.io" "$OLD_NAME" "$NEW_INSTANCE_NAME" "$OLD_NAME" "$NEW_INSTANCE_NAME"
# --- 2k: Migrate kube-ovn IP ---
echo " --- Migrate kube-ovn IP ---"
NEW_OVN_IP_NAME="${NEW_INSTANCE_NAME}.${NAMESPACE}"
if [ -n "$OVN_IP" ]; then
if kubectl get ip.kubeovn.io "$NEW_OVN_IP_NAME" --no-headers 2>/dev/null | grep -q .; then
echo " [SKIP] ip.kubeovn.io/${NEW_OVN_IP_NAME} already exists"
else
echo " [CREATE] ip.kubeovn.io/${NEW_OVN_IP_NAME}"
cat <<IPEOF | kubectl apply -f -
apiVersion: kubeovn.io/v1
kind: IP
metadata:
name: ${NEW_OVN_IP_NAME}
labels:
${OVN_SUBNET}: ""
ovn.kubernetes.io/ip_reserved: "true"
ovn.kubernetes.io/subnet: ${OVN_SUBNET}
spec:
ipAddress: "${OVN_IP}"
v4IpAddress: "${OVN_IP}"
v6IpAddress: ""
macAddress: "${OVN_MAC}"
namespace: "${NAMESPACE}"
podName: "${NEW_INSTANCE_NAME}"
podType: "VirtualMachine"
subnet: "${OVN_SUBNET}"
attachIps: []
attachMacs: []
attachSubnets: []
IPEOF
fi
# Delete old kube-ovn IP resource
if kubectl get ip.kubeovn.io "$OVN_IP_NAME" --no-headers 2>/dev/null | grep -q .; then
echo " [DELETE] ip.kubeovn.io/${OVN_IP_NAME}"
kubectl delete ip.kubeovn.io "$OVN_IP_NAME" 2>/dev/null || true
fi
fi
# --- 2l: Create vm-disk values secret ---
echo " --- Create vm-disk values secret ---"
DISK_VALUES_SECRET="${NEW_DISK_NAME}-values"
if ! resource_exists "$NAMESPACE" "secret" "$DISK_VALUES_SECRET"; then
echo " [CREATE] secret/${DISK_VALUES_SECRET}"
cat <<DSEOF | kubectl -n "$NAMESPACE" apply -f -
apiVersion: v1
kind: Secret
metadata:
name: ${DISK_VALUES_SECRET}
namespace: ${NAMESPACE}
stringData:
values.yaml: |
source:
image:
name: "${SYSTEM_DISK_IMAGE}"
optical: false
storage: "${SYSTEM_DISK_STORAGE}"
storageClass: "${SYSTEM_DISK_STORAGE_CLASS}"
DSEOF
else
echo " [SKIP] secret/${DISK_VALUES_SECRET} already exists"
fi
# --- 2m: Create vm-disk HelmRelease (suspended) ---
echo " --- Create vm-disk HelmRelease (suspended) ---"
if resource_exists "$NAMESPACE" "hr" "$NEW_DISK_NAME"; then
echo " [SKIP] hr/${NEW_DISK_NAME} already exists"
else
echo " [CREATE] hr/${NEW_DISK_NAME}"
cat <<DHREOF | kubectl -n "$NAMESPACE" apply -f -
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ${NEW_DISK_NAME}
namespace: ${NAMESPACE}
labels:
sharding.fluxcd.io/key: tenants
apps.cozystack.io/application.kind: VMDisk
apps.cozystack.io/application.name: vm-disk
spec:
suspend: true
chartRef:
kind: ExternalArtifact
name: cozystack-vm-disk-application-kubevirt-vm-disk
namespace: cozy-system
interval: 5m
timeout: 10m
install:
remediation:
retries: -1
upgrade:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: ${DISK_VALUES_SECRET}
valuesKey: values.yaml
DHREOF
fi
# --- 2n: Create vm-instance values secret ---
echo " --- Create vm-instance values secret ---"
INSTANCE_VALUES_SECRET="${NEW_INSTANCE_NAME}-values"
if ! resource_exists "$NAMESPACE" "secret" "$INSTANCE_VALUES_SECRET"; then
echo " [CREATE] secret/${INSTANCE_VALUES_SECRET}"
cat <<ISEOF | kubectl -n "$NAMESPACE" apply -f -
apiVersion: v1
kind: Secret
metadata:
name: ${INSTANCE_VALUES_SECRET}
namespace: ${NAMESPACE}
stringData:
values.yaml: |
external: ${EXTERNAL}
externalMethod: "${EXTERNAL_METHOD}"
externalPorts: ${EXTERNAL_PORTS}
runStrategy: "${RUN_STRATEGY}"
instanceType: "${INSTANCE_TYPE}"
instanceProfile: "${INSTANCE_PROFILE}"
disks:
- name: "${INSTANCE}"
subnets: ${SUBNETS}
gpus: ${GPUS}
cpuModel: "${CPU_MODEL}"
resources: ${RESOURCES}
sshKeys: ${SSH_KEYS}
cloudInit: $(printf '%s' "$CLOUD_INIT" | jq -Rs .)
cloudInitSeed: "${CLOUD_INIT_SEED}"
ISEOF
else
echo " [SKIP] secret/${INSTANCE_VALUES_SECRET} already exists"
fi
# --- 2o: Create vm-instance HelmRelease (suspended) ---
echo " --- Create vm-instance HelmRelease (suspended) ---"
if resource_exists "$NAMESPACE" "hr" "$NEW_INSTANCE_NAME"; then
echo " [SKIP] hr/${NEW_INSTANCE_NAME} already exists"
else
# Build valuesFrom list - include cozystack-values if the old HR had it
COZYSTACK_VALUES_FROM=""
if resource_exists "$NAMESPACE" "secret" "cozystack-values"; then
COZYSTACK_VALUES_FROM=' - kind: Secret
name: cozystack-values'
fi
echo " [CREATE] hr/${NEW_INSTANCE_NAME}"
cat <<IHREOF | kubectl -n "$NAMESPACE" apply -f -
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ${NEW_INSTANCE_NAME}
namespace: ${NAMESPACE}
labels:
sharding.fluxcd.io/key: tenants
apps.cozystack.io/application.kind: VMInstance
apps.cozystack.io/application.name: vm-instance
spec:
suspend: true
chartRef:
kind: ExternalArtifact
name: cozystack-vm-instance-application-kubevirt-vm-instance
namespace: cozy-system
interval: 5m
timeout: 10m
install:
remediation:
retries: -1
upgrade:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: ${INSTANCE_VALUES_SECRET}
valuesKey: values.yaml
${COZYSTACK_VALUES_FROM}
IHREOF
fi
# --- 2p: If external=true, pre-create LoadBalancer service with metallb annotation ---
echo " --- Handle external service ---"
if [ "$EXTERNAL" = "true" ] && [ -n "$LB_IP" ]; then
if resource_exists "$NAMESPACE" "svc" "$NEW_INSTANCE_NAME"; then
echo " [SKIP] svc/${NEW_INSTANCE_NAME} already exists"
else
echo " [CREATE] svc/${NEW_INSTANCE_NAME} with metallb IP ${LB_IP}"
cat <<SVCEOF | kubectl -n "$NAMESPACE" apply -f -
apiVersion: v1
kind: Service
metadata:
name: ${NEW_INSTANCE_NAME}
namespace: ${NAMESPACE}
labels:
apps.cozystack.io/user-service: "true"
app.kubernetes.io/name: vm-instance
app.kubernetes.io/instance: ${NEW_INSTANCE_NAME}
annotations:
networking.cozystack.io/wholeIP: "true"
metallb.universe.tf/loadBalancerIPs: "${LB_IP}"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: vm-instance
app.kubernetes.io/instance: ${NEW_INSTANCE_NAME}
ports:
- port: 65535
SVCEOF
fi
fi
# --- 2q: Delete old resources ---
echo " --- Delete old resources ---"
kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \
| grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values" \
| while IFS= read -r secret; do
old_secret_name="${secret#secret/}"
delete_resource "$NAMESPACE" "secret" "$old_secret_name"
done
delete_resource "$NAMESPACE" "workloadmonitor.cozystack.io" "$OLD_NAME"
if resource_exists "$NAMESPACE" "hr" "$OLD_NAME"; then
kubectl -n "$NAMESPACE" patch hr "$OLD_NAME" --type=json \
-p='[{"op":"remove","path":"/metadata/finalizers"}]' 2>/dev/null || true
delete_resource "$NAMESPACE" "hr" "$OLD_NAME"
fi
echo " [DELETE] secrets with label owner=helm,name=${OLD_NAME}"
kubectl -n "$NAMESPACE" delete secret -l "owner=helm,name=${OLD_NAME}" 2>/dev/null || true
# Delete old values secret
if [ -n "$VALUES_SECRET" ]; then
delete_resource "$NAMESPACE" "secret" "$VALUES_SECRET"
fi
# Collect protected resources for batch deletion
if resource_exists "$NAMESPACE" "svc" "$OLD_NAME"; then
ALL_PROTECTED_RESOURCES+=("${NAMESPACE}:svc/${OLD_NAME}")
fi
done
# ============================================================
# STEP 3: Delete protected resources (Services)
# ============================================================
echo ""
echo "--- Step 3: Delete protected resources ---"
if [ ${#ALL_PROTECTED_RESOURCES[@]} -gt 0 ]; then
WEBHOOK_EXISTS=false
if kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" --no-headers 2>/dev/null | grep -q .; then
WEBHOOK_EXISTS=true
fi
if [ "$WEBHOOK_EXISTS" = "true" ]; then
echo " --- Temporarily disabling protection-webhook ---"
WEBHOOK_REPLICAS=$(kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" \
-o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1")
echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> 0 (was ${WEBHOOK_REPLICAS})"
kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" --replicas=0
echo " [PATCH] Set failurePolicy=Ignore on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}"
kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \
jq '.webhooks[].failurePolicy = "Ignore"' | \
kubectl apply -f - 2>/dev/null || true
echo " Waiting for webhook pods to terminate..."
kubectl -n "$PROTECTION_WEBHOOK_NS" wait --for=delete pod \
-l app.kubernetes.io/name=protection-webhook --timeout=60s 2>/dev/null || true
sleep 3
fi
for entry in "${ALL_PROTECTED_RESOURCES[@]}"; do
ns="${entry%%:*}"
res="${entry#*:}"
echo " [DELETE] ${ns}/${res}"
kubectl -n "$ns" delete "$res" --wait=false 2>/dev/null || true
done
if [ "$WEBHOOK_EXISTS" = "true" ]; then
echo " [PATCH] Set failurePolicy=Fail on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}"
kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \
jq '.webhooks[].failurePolicy = "Fail"' | \
kubectl apply -f - 2>/dev/null || true
echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> ${WEBHOOK_REPLICAS}"
kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" \
--replicas="$WEBHOOK_REPLICAS"
echo " --- protection-webhook restored ---"
fi
else
echo " [SKIP] No protected resources to delete"
fi
# ============================================================
# STEP 4: Restore PV reclaim policies
# ============================================================
echo ""
echo "--- Step 4: Restore PV reclaim policies ---"
for pv_name in "${ALL_PV_NAMES[@]}"; do
if [ -n "$pv_name" ]; then
current_policy=$(kubectl get pv "$pv_name" \
-o jsonpath='{.spec.persistentVolumeReclaimPolicy}' 2>/dev/null || echo "unknown")
if [ "$current_policy" = "Retain" ]; then
echo " [PATCH] PV ${pv_name}: Retain -> Delete"
kubectl patch pv "$pv_name" -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
else
echo " [SKIP] PV ${pv_name} already ${current_policy}"
fi
fi
done
# ============================================================
# STEP 5: Temporarily disable CDI datavolume webhooks
# ============================================================
# CDI's datavolume-validate webhook rejects DataVolume creation when a PVC
# with the same name already exists. We must disable it so that vm-disk
# HelmReleases can reconcile and adopt the pre-created PVCs.
# We scale down both cdi-operator (which recreates webhook configs) and
# cdi-apiserver (which serves the webhooks), then delete webhook configs.
# Both are restored after vm-disk HRs reconcile.
echo ""
echo "--- Step 5: Temporarily disable CDI webhooks ---"
CDI_OPERATOR_REPLICAS=$(kubectl -n "$CDI_APISERVER_NS" get deploy cdi-operator \
-o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1")
CDI_APISERVER_REPLICAS=$(kubectl -n "$CDI_APISERVER_NS" get deploy "$CDI_APISERVER_DEPLOY" \
-o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1")
echo " [SCALE] cdi-operator -> 0 (was ${CDI_OPERATOR_REPLICAS})"
kubectl -n "$CDI_APISERVER_NS" scale deploy cdi-operator --replicas=0
echo " [SCALE] ${CDI_APISERVER_DEPLOY} -> 0 (was ${CDI_APISERVER_REPLICAS})"
kubectl -n "$CDI_APISERVER_NS" scale deploy "$CDI_APISERVER_DEPLOY" --replicas=0
echo " Waiting for cdi pods to terminate..."
kubectl -n "$CDI_APISERVER_NS" wait --for=delete pod \
-l cdi.kubevirt.io=cdi-apiserver --timeout=60s 2>/dev/null || true
kubectl -n "$CDI_APISERVER_NS" wait --for=delete pod \
-l name=cdi-operator --timeout=60s 2>/dev/null || true
for wh in $CDI_VALIDATING_WEBHOOKS; do
if kubectl get validatingwebhookconfiguration "$wh" >/dev/null 2>&1; then
echo " [DELETE] validatingwebhookconfiguration/${wh}"
kubectl delete validatingwebhookconfiguration "$wh" 2>/dev/null || true
fi
done
for wh in $CDI_MUTATING_WEBHOOKS; do
if kubectl get mutatingwebhookconfiguration "$wh" >/dev/null 2>&1; then
echo " [DELETE] mutatingwebhookconfiguration/${wh}"
kubectl delete mutatingwebhookconfiguration "$wh" 2>/dev/null || true
fi
done
sleep 2
# ============================================================
# STEP 6: Unsuspend vm-disk HelmReleases first
# ============================================================
echo ""
echo "--- Step 6: Unsuspend vm-disk HelmReleases ---"
for entry in "${INSTANCES[@]}"; do
ns="${entry%%/*}"
instance="${entry#*/}"
disk_name="${NEW_DISK_PREFIX}-${instance}"
if resource_exists "$ns" "hr" "$disk_name"; then
suspended=$(kubectl -n "$ns" get hr "$disk_name" \
-o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false")
if [ "$suspended" = "true" ]; then
echo " [UNSUSPEND] ${ns}/hr/${disk_name}"
kubectl -n "$ns" patch hr "$disk_name" --type=merge -p '{"spec":{"suspend":false}}'
else
echo " [SKIP] ${ns}/hr/${disk_name} already not suspended"
fi
# Force immediate reconciliation
echo " [TRIGGER] Reconcile ${ns}/hr/${disk_name}"
kubectl -n "$ns" annotate hr "$disk_name" --overwrite \
"reconcile.fluxcd.io/requestedAt=$(date +%s)" 2>/dev/null || true
fi
done
# Wait for disk HelmReleases to become ready
echo " Waiting for vm-disk HelmReleases to reconcile..."
for entry in "${INSTANCES[@]}"; do
ns="${entry%%/*}"
instance="${entry#*/}"
disk_name="${NEW_DISK_PREFIX}-${instance}"
for i in $(seq 1 60); do
ready=$(kubectl -n "$ns" get hr "$disk_name" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)
if [ "$ready" = "True" ]; then
echo " [OK] ${ns}/hr/${disk_name} is Ready"
break
fi
if [ "$i" = "60" ]; then
echo " [WARN] ${ns}/hr/${disk_name} did not become Ready within timeout"
fi
sleep 5
done
done
# ============================================================
# STEP 7: Restore CDI webhooks
# ============================================================
# Scale cdi-operator and cdi-apiserver back up.
# cdi-apiserver will recreate webhook configurations automatically on start.
echo ""
echo "--- Step 7: Restore CDI webhooks ---"
echo " [SCALE] cdi-operator -> ${CDI_OPERATOR_REPLICAS}"
kubectl -n "$CDI_APISERVER_NS" scale deploy cdi-operator \
--replicas="$CDI_OPERATOR_REPLICAS"
echo " [SCALE] ${CDI_APISERVER_DEPLOY} -> ${CDI_APISERVER_REPLICAS}"
kubectl -n "$CDI_APISERVER_NS" scale deploy "$CDI_APISERVER_DEPLOY" \
--replicas="$CDI_APISERVER_REPLICAS"
echo " Waiting for CDI to be ready..."
kubectl -n "$CDI_APISERVER_NS" rollout status deploy cdi-operator --timeout=120s 2>/dev/null || true
kubectl -n "$CDI_APISERVER_NS" rollout status deploy "$CDI_APISERVER_DEPLOY" --timeout=120s 2>/dev/null || true
echo " --- CDI webhooks restored ---"
# ============================================================
# STEP 8: Unsuspend vm-instance HelmReleases
# ============================================================
echo ""
echo "--- Step 8: Unsuspend vm-instance HelmReleases ---"
for entry in "${INSTANCES[@]}"; do
ns="${entry%%/*}"
instance="${entry#*/}"
instance_name="${NEW_INSTANCE_PREFIX}-${instance}"
if resource_exists "$ns" "hr" "$instance_name"; then
suspended=$(kubectl -n "$ns" get hr "$instance_name" \
-o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false")
if [ "$suspended" = "true" ]; then
echo " [UNSUSPEND] ${ns}/hr/${instance_name}"
kubectl -n "$ns" patch hr "$instance_name" --type=merge -p '{"spec":{"suspend":false}}'
else
echo " [SKIP] ${ns}/hr/${instance_name} already not suspended"
fi
fi
done
echo ""
echo "=== Migration complete (${#INSTANCES[@]} instance(s)) ==="
# Stamp version
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=30 --dry-run=client -o yaml | kubectl apply -f-
@@ -1,29 +0,0 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.virtual-machine-application
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: kubevirt
dependsOn:
- cozystack.networking
- cozystack.kubevirt
- cozystack.kubevirt-cdi
libraries:
- name: cozy-lib
path: library/cozy-lib
components:
- name: virtual-machine
path: apps/virtual-machine
libraries: [cozy-lib]
- name: virtual-machine-rd
path: system/virtual-machine-rd
install:
namespace: cozy-system
releaseName: virtual-machine-rd
@@ -13,7 +13,6 @@
{{include "cozystack.platform.package.default" (list "cozystack.capi-provider-infra-kubevirt" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.bucket-application" $) }}
{{include "cozystack.platform.package" (list "cozystack.kubernetes-application" "kubevirt" $) }}
{{include "cozystack.platform.package" (list "cozystack.virtual-machine-application" "kubevirt" $) }}
{{include "cozystack.platform.package" (list "cozystack.virtualprivatecloud-application" "kubevirt" $) }}
{{include "cozystack.platform.package" (list "cozystack.vm-disk-application" "kubevirt" $) }}
{{include "cozystack.platform.package" (list "cozystack.vm-instance-application" "kubevirt" $) }}
+1 -1
View File
@@ -6,7 +6,7 @@ sourceRef:
migrations:
enabled: false
image: ghcr.io/cozystack/cozystack/platform-migrations:latest
targetVersion: 29
targetVersion: 30
# Bundle deployment configuration
bundles:
system:
@@ -1,3 +0,0 @@
apiVersion: v2
name: virtual-machine-rd
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
@@ -1,4 +0,0 @@
export NAME=virtual-machine-rd
export NAMESPACE=cozy-system
include ../../../hack/package.mk
@@ -1,37 +0,0 @@
apiVersion: cozystack.io/v1alpha1
kind: ApplicationDefinition
metadata:
name: virtual-machine
spec:
application:
kind: VirtualMachine
plural: virtualmachines
singular: virtualmachine
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""},"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":""},"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"}},"gpus":{"description":"List of GPUs to attach.","type":"array","default":[],"items":{"type":"object","required":["name"],"properties":{"name":{"description":"The name of the GPU resource to attach.","type":"string"}}}},"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",""]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"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}}},"runStrategy":{"description":"Requested running state of the VirtualMachineInstance","type":"string","default":"Always","enum":["Always","Halted","Manual","RerunOnFailure","Once"]},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}},"systemDisk":{"description":"System disk configuration.","type":"object","default":{},"required":["image","storage"],"properties":{"image":{"description":"The base image for the virtual machine.","type":"string","default":"ubuntu","enum":["ubuntu","cirros","alpine","fedora","talos"]},"storage":{"description":"The size of the disk allocated for the virtual machine.","type":"string","default":"5Gi"},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"replicated"}}}}}
release:
prefix: virtual-machine-
labels:
sharding.fluxcd.io/key: tenants
chartRef:
kind: ExternalArtifact
name: cozystack-virtual-machine-application-kubevirt-virtual-machine
namespace: cozy-system
dashboard:
category: IaaS
singular: Virtual Machine
plural: Virtual Machines
description: Virtual Machine (simple)
weight: 10
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", "systemDisk"], ["spec", "systemDisk", "image"], ["spec", "systemDisk", "storage"], ["spec", "systemDisk", "storageClass"], ["spec", "subnets"], ["spec", "gpus"], ["spec", "cpuModel"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]]
secrets:
exclude: []
include: []
services:
exclude: []
include:
- resourceNames:
- virtual-machine-{{ .name }}
@@ -1,35 +0,0 @@
{{- if .Capabilities.APIVersions.Has "strategy.backups.cozystack.io/v1alpha1" }}
apiVersion: strategy.backups.cozystack.io/v1alpha1
kind: Velero
metadata:
name: velero-backup-strategy-for-virtualmachines
spec:
template:
spec:
csiSnapshotTimeout: 10m0s
includedNamespaces:
- '{{ printf "{{ .Application.metadata.namespace }}" }}'
orLabelSelector:
- matchLabels:
apps.cozystack.io/application.group: apps.cozystack.io
apps.cozystack.io/application.kind: '{{ printf "{{ .Application.kind }}" }}'
apps.cozystack.io/application.name: '{{ printf "{{ .Application.metadata.name }}"}}'
- matchLabels:
app.kubernetes.io/instance: 'virtual-machine-{{ printf "{{ .Application.metadata.name }}"}}'
includedResources:
- helmreleases.helm.toolkit.fluxcd.io
- virtualmachines.kubevirt.io
- virtualmachineinstances.kubevirt.io
- datavolumes.cdi.kubevirt.io
- persistentvolumeclaims
- services
- configmaps
- secrets
storageLocation: '{{ printf "{{ .Parameters.backupStorageLocationName }}" }}'
volumeSnapshotLocations:
- '{{ printf "{{ .Parameters.backupStorageLocationName }}" }}'
snapshotVolumes: true
snapshotMoveData: true
ttl: 720h0m0s
itemOperationTimeout: 24h0m0s
{{- end }}
@@ -1,4 +0,0 @@
{{- range $path, $_ := .Files.Glob "cozyrds/*" }}
---
{{ $.Files.Get $path }}
{{- end }}
@@ -1 +0,0 @@
{}
@@ -8,7 +8,7 @@ spec:
singular: vminstance
plural: vminstances
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""},"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"}}}},"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"}},"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"}}}},"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",""]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"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}}},"running":{"description":"Determines if the virtual machine should be running.","type":"boolean","default":true},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}}}}
{"title":"Chart Values","type":"object","properties":{"cloudInit":{"description":"Cloud-init user data.","type":"string","default":""},"cloudInitSeed":{"description":"Seed string to generate SMBIOS UUID for the VM.","type":"string","default":""},"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":""},"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"}}}},"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"}},"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"}}}},"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",""]},"instanceType":{"description":"Virtual Machine instance type.","type":"string","default":"u1.medium"},"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}}},"runStrategy":{"description":"Requested running state of the VirtualMachineInstance","type":"string","default":"Always","enum":["Always","Halted","Manual","RerunOnFailure","Once"]},"sshKeys":{"description":"List of SSH public keys for authentication.","type":"array","default":[],"items":{"type":"string"}},"subnets":{"description":"Additional subnets","type":"array","default":[],"items":{"type":"object","properties":{"name":{"description":"Subnet name","type":"string"}}}}}}
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", "running"], ["spec", "instanceType"], ["spec", "instanceProfile"], ["spec", "disks"], ["spec", "subnets"], ["spec", "gpus"], ["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", "subnets"], ["spec", "gpus"], ["spec", "cpuModel"], ["spec", "resources"], ["spec", "sshKeys"], ["spec", "cloudInit"], ["spec", "cloudInitSeed"]]
secrets:
exclude: []
include: []