Compare commits

...
Sign in to create a new pull request.

45 commits

Author SHA1 Message Date
Arsolitt
998cc77635
chore(kubernetes): merge main to resolve CRD schema conflict
Integrate the HAMi GPU virtualization addon from main while
keeping the persistent-storage diskSize and storageClass fields
introduced by this branch.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 16:54:58 +03:00
Arsolitt
633ff1a617
chore(kubernetes): merge main to resolve CRD schema conflict
Incorporate images.waitForKubeconfig support from main while
keeping the persistent-storage diskSize and storageClass fields
introduced by this branch.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 16:14:07 +03:00
Arsolitt
cd4590b426
docs(kubernetes): clarify storageClass default behavior
Specify that empty storageClass uses the management cluster default
StorageClass (annotated with is-default-class: true), not the
top-level chart storageClass value.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:01:48 +03:00
Arsolitt
3afcaa5d26
docs(kubernetes): clarify persistence scope in breaking changes note
Add explicit scope: persistence covers same-VM reboots only.
VM replacement by CAPI still provisions a fresh PVC.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:00:34 +03:00
Arsolitt
e2b10d42c8
docs(kubernetes): correct overwrite: false comment reasoning
DataVolumes are tied to VM lifecycle — PVC does not survive VM
recreation. The real reason for overwrite: false is the same-VM
reboot path: XFS on /dev/vdb already exists and cloud-init must
skip reformatting.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:00:26 +03:00
Arsolitt
7b05ce0573
fix(kubernetes): update e2e test to use renamed diskSize field
The e2e test still used the old ephemeralStorage field name,
which triggers the migration guard fail() in the Helm template.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 11:21:41 +03:00
Arsolitt
31de543e44
chore: revert unrelated backups deepcopy change
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:46 +03:00
Arsolitt
89aeb4cb83
test(kubernetes): add migration guard test for both fields present
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:46 +03:00
Arsolitt
cac38eeca1
docs(kubernetes): add inline comment explaining overwrite false behavior
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:45 +03:00
Arsolitt
a3393d4d14
fix(kubernetes): merge duplicate files blocks in KubeadmConfigTemplate
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:45 +03:00
Arsolitt
5b994b2cca
docs(kubernetes): document migration failure consequences
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:45 +03:00
Arsolitt
c5b3a93d52
fix(kubernetes): use hasKey for ephemeralStorage migration guard
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:45 +03:00
Arsolitt
a1046b5ef0
fix(kubernetes): make storageClass optional with default in schema
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:45 +03:00
Arsolitt
8a441d6232
test(kubernetes): document omitted defaults in test fixture
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:45 +03:00
Arsolitt
0fb0de7062
feat(kubernetes): add migration guard for ephemeralStorage field rename
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:44 +03:00
Arsolitt
c2eb6c0b3d
docs(kubernetes): fix storageClass default value in README parameter table
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:44 +03:00
Arsolitt
70dbb12450
fix(kubernetes): add kubebuilder default annotation to StorageClass field
The NodeGroup.StorageClass field lacked a +kubebuilder:default annotation,
causing a semantic inconsistency: the map-level default in ConfigSpec
included "storageClass": "" but CRD defaulting would not inject it at the
leaf level when a user specified a nodeGroup without storageClass.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 19:12:44 +03:00
Arsolitt
a0b18b37c5
test(kubernetes): document test schema deviation from production defaults
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:04 +03:00
Arsolitt
a451a898d6
docs(kubernetes): document ephemeralStorage to diskSize breaking change
Add a Breaking Changes section noting that ephemeralStorage has been
renamed to diskSize with no backward-compatibility fallback.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:04 +03:00
Arsolitt
fccea96730
fix(kubernetes): remove empty default from storageClass in schema
Drop the explicit `"default": ""` from storageClass so the field is
simply absent when not set, matching how other optional string fields
behave in the schema.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:04 +03:00
Arsolitt
7a215ea809
test(kubernetes): add storageClass to defaulting test schema
Include storageClass field in the test schema and default values so
the defaulting test covers the new per-nodeGroup storageClass property.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:04 +03:00
Arsolitt
5560162934
test(kubernetes): add KubeadmConfigTemplate persistent storage tests
Verify that the KubeadmConfigTemplate correctly configures XFS
filesystem, disk partitioning, mount points, bind-mounts for kubelet
and containerd, and preKubeadmCommands guards.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:04 +03:00
Arsolitt
ca12f28734
test(kubernetes): add multi-nodegroup dataVolumeTemplate test
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:04 +03:00
Arsolitt
a336f1812c
fix(kubernetes): add default value for storageClass in schema
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:03 +03:00
Arsolitt
d1dfb75581
fix(kubernetes): add explicit overwrite false to disk_setup for persistent storage safety
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:03 +03:00
Arsolitt
6403982039
test(kubernetes): add helm test for diskSize default fallback
Verify that KubevirtMachineTemplate renders 20Gi storage when diskSize
is not explicitly set, ensuring the default value propagates correctly.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:03 +03:00
Arsolitt
ad2705b81f
fix(kubernetes): rename ephemeralStorage to diskSize in defaulting tests
Align test schema and assertions with the field rename from
ephemeralStorage to diskSize introduced in the production code.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:03 +03:00
Arsolitt
c434fbb1a7
fix(kubernetes): include per-nodeGroup storageClass in values default
Add storageClass: "" to nodeGroups.md0 in values.yaml so that the
code generator picks it up and places it into keysOrder for the
dashboard field ordering.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:03 +03:00
Arsolitt
2de4a81623
refactor(kubernetes): deduplicate test fixtures in cluster_test.yaml
Extract repeated values (_cluster, _namespace, version, controlPlane,
nodeGroups base config) into a shared values/common.yaml fixture loaded
at suite level, so each test case only overrides the fields it varies.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:03 +03:00
Arsolitt
d0c13a4638
fix(kubernetes): regenerate CRD and schema files
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
f708953270
fix(kubernetes): restore storageClass in keysOrder for nodeGroups
Add the storageClass key back into the keysOrder array for
nodeGroups so the dashboard renders the field in the correct
position alongside diskSize and other per-group settings.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
ad3fbc1e55
fix(kubernetes): quote storage values and guard bind-mount commands
Quote diskSize and storageClassName Helm template outputs to prevent
bare numeric or special-char values from causing YAML parse errors.

Also ensure mount-point directories exist before bind-mounting and skip
the mount when the target is already a mountpoint, avoiding failures
on node reboot or re-provisioning.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
cf98381d48
docs(kubernetes): document automatic block size matching for worker node disks
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
b5e89c56e2
fix(kubernetes): set matchVolume blockSize on disk-kubelet for DRBD 4Kn compatibility
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
baad81333b
fix(kubernetes): regenerate deepcopy for DiskSize field rename
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
68e9529b72
fix(kubernetes): remove storageClass from keysOrder to match generator output
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:02 +03:00
Arsolitt
92b6409c5a
fix(kubernetes): add test target to Makefile for CI
The helm unit tests in tests/cluster_test.yaml were silently skipped by CI
because the Makefile had no test target. CI runs hack/helm-unit-tests.sh
which only invokes make test when the target exists.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:01 +03:00
Arsolitt
1e455a1d34
test(kubernetes): add helm unit tests for cluster template
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:01 +03:00
Arsolitt
fe6ef58b0a
fix(kubernetes): add explanatory comments for non-obvious template values
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:01 +03:00
Arsolitt
e1ab6f6274
fix(kubernetes): add storageClass to keysOrder for nodeGroups
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:01 +03:00
Arsolitt
97a2585cf9
feat(kubernetes): add per-nodeGroup storageClass for worker node disks
Allow each node group to specify its own storageClass for persistent
worker node disks. When empty, the cluster default StorageClass is used.
This enables using local storage for worker disks while keeping
replicated storage for tenant workloads.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:01 +03:00
Arsolitt
7b7686096e
fix(kubernetes): disable CDI populator for blank DataVolumes
Add cdi.kubevirt.io/storage.usePopulator: "false" annotation to
the dataVolumeTemplate to prevent CDI volume population flow.

The populator mechanism creates an intermediate "prime" PVC and
rebinds the PV, which causes ClaimMisbound errors with LINSTOR CSI.
The legacy import path writes directly to the target PVC.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:01 +03:00
Arsolitt
90e4aba4ef
fix(kubernetes): set fixed 2Gi ephemeral-storage for virt-launcher pod
With PVC-backed persistent disks, the virt-launcher pod no longer needs
ephemeral-storage sized to the full disk. Set a fixed 2Gi allocation
for QEMU runtime needs and proper scheduler behavior.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:00 +03:00
Arsolitt
0ac228036a
feat(kubernetes)!: replace emptyDisk with persistent dataVolumeTemplates for worker nodes
Replace the ephemeral emptyDisk volume with a CDI dataVolumeTemplate
backed by the tenant's storageClass. This ensures kubelet certificates,
kubeconfig, and containerd state survive VM reboots, preventing worker
nodes from falling out of the cluster.

Changes in the KubevirtMachineTemplate:
- Add dataVolumeTemplates with a blank source disk sized by diskSize
- Switch the volume reference from emptyDisk to dataVolume
- Rename disk and volume from "ephemeral" to "disk-kubelet"

Changes in the KubeadmConfigTemplate:
- Rename filesystem label from "ephemeral" to "persistent"
- Update all mount paths from /ephemeral to /persistent

BREAKING CHANGE: Worker node VMs now use persistent PVC-backed disks
instead of ephemeral emptyDisk volumes. Existing clusters will trigger
a rolling replacement of worker nodes on upgrade.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:00 +03:00
Arsolitt
dfa6fd9911
feat(kubernetes)!: rename ephemeralStorage to diskSize in node group config
Rename the ephemeralStorage field to diskSize in the NodeGroup typedef
and default values to reflect the upcoming switch from ephemeral
emptyDisk volumes to persistent dataVolumeTemplates for worker nodes.

BREAKING CHANGE: The nodeGroups.*.ephemeralStorage field has been renamed
to nodeGroups.*.diskSize. Users must update their values overrides.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 18:40:00 +03:00
12 changed files with 599 additions and 65 deletions

View file

@ -22,7 +22,7 @@ type ConfigSpec struct {
// +kubebuilder:default:="replicated"
StorageClass string `json:"storageClass"`
// Worker nodes configuration map.
// +kubebuilder:default:={"md0":{"ephemeralStorage":"20Gi","gpus":{},"instanceType":"u1.medium","maxReplicas":10,"minReplicas":0,"resources":{},"roles":{"ingress-nginx"}}}
// +kubebuilder:default:={"md0":{"diskSize":"20Gi","gpus":{},"instanceType":"u1.medium","maxReplicas":10,"minReplicas":0,"resources":{},"roles":{"ingress-nginx"},"storageClass":""}}
NodeGroups map[string]NodeGroup `json:"nodeGroups,omitempty"`
// Kubernetes major.minor version to deploy
// +kubebuilder:default:="v1.35"
@ -218,9 +218,9 @@ type MonitoringAgentsAddon struct {
}
type NodeGroup struct {
// Ephemeral storage size.
// Persistent disk size for kubelet and containerd data.
// +kubebuilder:default:="20Gi"
EphemeralStorage resource.Quantity `json:"ephemeralStorage"`
DiskSize resource.Quantity `json:"diskSize"`
// List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).
Gpus []GPU `json:"gpus,omitempty"`
// Virtual machine instance type.
@ -236,6 +236,8 @@ type NodeGroup struct {
Resources Resources `json:"resources"`
// List of node roles.
Roles []string `json:"roles,omitempty"`
// StorageClass for worker node persistent disks. When empty, uses the management cluster default StorageClass (the one annotated storageclass.kubernetes.io/is-default-class: true).
StorageClass string `json:"storageClass,omitempty"`
}
type Resources struct {

View file

@ -365,7 +365,7 @@ func (in *MonitoringAgentsAddon) DeepCopy() *MonitoringAgentsAddon {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NodeGroup) DeepCopyInto(out *NodeGroup) {
*out = *in
out.EphemeralStorage = in.EphemeralStorage.DeepCopy()
out.DiskSize = in.DiskSize.DeepCopy()
if in.Gpus != nil {
in, out := &in.Gpus, &out.Gpus
*out = make([]GPU, len(*in))

2
go.sum
View file

@ -309,6 +309,8 @@ k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc=
k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg=
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

View file

@ -58,7 +58,7 @@ spec:
host: ""
nodeGroups:
md0:
ephemeralStorage: 20Gi
diskSize: 20Gi
gpus: []
instanceType: u1.medium
maxReplicas: 10

View file

@ -63,6 +63,9 @@ Deploying it involves the following components:
- **Worker Nodes**: Virtual Machines are provisioned to serve as worker nodes using KubeVirt.
These nodes are configured to join the tenant Kubernetes cluster, enabling the deployment and management of workloads.
Worker node disks automatically detect and match the underlying volume's block size
(`blockSize.matchVolume`) to ensure compatibility with storage backends that use
non-512-byte sectors, such as LINSTOR DRBD with 4Kn drives.
- **Cluster API**: Cozystack is using the [Kubernetes Cluster API](https://cluster-api.sigs.k8s.io/) to provision the components of a cluster.
@ -80,6 +83,10 @@ See the reference for components utilized in this service:
- [github.com/kubernetes-sigs/cluster-api-provider-kubevirt](https://github.com/kubernetes-sigs/cluster-api-provider-kubevirt)
- [github.com/kubevirt/csi-driver](https://github.com/kubevirt/csi-driver)
## Breaking Changes
- **`ephemeralStorage` renamed to `diskSize`**: The `nodeGroups[name].ephemeralStorage` field has been renamed to `nodeGroups[name].diskSize` to better reflect its purpose (persistent disk for kubelet and containerd data). There is no backward-compatibility fallback; users MUST update their configurations to use `diskSize` instead of `ephemeralStorage`. If `ephemeralStorage` is still present in values, Helm template rendering will fail with an error directing you to use `diskSize`. When upgrading the CRD directly (bypassing Helm), the unrecognized field is silently dropped and kubelet storage reverts to the default 20Gi. Existing VMs will be automatically rolling-updated via CAPI when the new values are applied. State persists across same-VM reboots (virt-launcher restart, guest reboot, node failure); VM replacement by CAPI (e.g. nodeGroup field change, MachineHealthCheck remediation) provisions a fresh PVC.
## Parameters
### Common Parameters
@ -91,21 +98,22 @@ See the reference for components utilized in this service:
### Application-specific Parameters
| Name | Description | Type | Value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------- | ----------- |
| `nodeGroups` | Worker nodes configuration map. | `map[string]object` | `{...}` |
| `nodeGroups[name].minReplicas` | Minimum number of replicas. | `int` | `0` |
| `nodeGroups[name].maxReplicas` | Maximum number of replicas. | `int` | `10` |
| `nodeGroups[name].instanceType` | Virtual machine instance type. | `string` | `u1.medium` |
| `nodeGroups[name].ephemeralStorage` | Ephemeral storage size. | `quantity` | `20Gi` |
| `nodeGroups[name].roles` | List of node roles. | `[]string` | `[]` |
| `nodeGroups[name].resources` | CPU and memory resources for each worker node. | `object` | `{}` |
| `nodeGroups[name].resources.cpu` | CPU available. | `quantity` | `""` |
| `nodeGroups[name].resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `nodeGroups[name].gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
| `nodeGroups[name].gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S". | `string` | `""` |
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.35` |
| `host` | External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty. | `string` | `""` |
| Name | Description | Type | Value |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------- |
| `nodeGroups` | Worker nodes configuration map. | `map[string]object` | `{...}` |
| `nodeGroups[name].minReplicas` | Minimum number of replicas. | `int` | `0` |
| `nodeGroups[name].maxReplicas` | Maximum number of replicas. | `int` | `10` |
| `nodeGroups[name].instanceType` | Virtual machine instance type. | `string` | `u1.medium` |
| `nodeGroups[name].diskSize` | Persistent disk size for kubelet and containerd data. | `quantity` | `20Gi` |
| `nodeGroups[name].storageClass` | StorageClass for worker node persistent disks. When empty, uses the management cluster default StorageClass (the one annotated storageclass.kubernetes.io/is-default-class: true). | `string` | `""` |
| `nodeGroups[name].roles` | List of node roles. | `[]string` | `[]` |
| `nodeGroups[name].resources` | CPU and memory resources for each worker node. | `object` | `{}` |
| `nodeGroups[name].resources.cpu` | CPU available. | `quantity` | `""` |
| `nodeGroups[name].resources.memory` | Memory (RAM) available. | `quantity` | `""` |
| `nodeGroups[name].gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
| `nodeGroups[name].gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S". | `string` | `""` |
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.35` |
| `host` | External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty. | `string` | `""` |
### Cluster Addons

View file

@ -31,6 +31,25 @@ spec:
name: {{ . }}
{{- end }}
runStrategy: Always
dataVolumeTemplates:
- metadata:
name: disk-kubelet
annotations:
{{/* Populator creates intermediate PVC + PV rebind that causes ClaimMisbound with LINSTOR CSI */}}
cdi.kubevirt.io/storage.usePopulator: "false"
spec:
{{- if hasKey .group "ephemeralStorage" }}
{{- fail (printf "nodeGroup %q: ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions." .groupName) }}
{{- end }}
source:
blank: {}
storage:
resources:
requests:
storage: {{ .group.diskSize | default "20Gi" | quote }}
{{- with .group.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
template:
metadata:
annotations:
@ -70,10 +89,13 @@ spec:
disk:
bus: virtio
pciAddress: 0000:07:00.0
- name: ephemeral
- name: disk-kubelet
disk:
bus: virtio
pciAddress: 0000:08:00.0
blockSize:
matchVolume:
enabled: true
interfaces:
- name: default
bridge: {}
@ -82,15 +104,16 @@ spec:
guest: {{ .group.resources.memory }}
{{- end }}
resources:
{{- include "cozy-lib.resources.sanitize" (list (dict "ephemeral-storage" .group.ephemeralStorage) $) | nindent 14 }}
{{/* 2Gi is enough: persistent DataVolumes are not counted as pod ephemeral storage */}}
{{- include "cozy-lib.resources.sanitize" (list (dict "ephemeral-storage" "2Gi") $) | nindent 14 }}
evictionStrategy: External
volumes:
- name: system
containerDisk:
image: "{{ $.Files.Get (printf "images/ubuntu-container-disk-%s.tag" $.Values.version) | trim }}"
- name: ephemeral
emptyDisk:
capacity: {{ .group.ephemeralStorage | default "20Gi" }}
- name: disk-kubelet
dataVolume:
name: disk-kubelet
networks:
- name: default
pod: {}
@ -248,35 +271,36 @@ spec:
chmod 0755 /root/kubeadm
if /root/kubelet --version ; then mv /root/kubelet /usr/bin/kubelet ; fi
if /root/kubeadm version ; then mv /root/kubeadm /usr/bin/kubeadm ; fi
{{- $sec := lookup "v1" "Secret" $.Release.Namespace (printf "%s-patch-containerd" $.Release.Name) }}
{{- if $sec }}
{{- range $key, $_ := $sec.data }}
- path: /etc/containerd/certs.d/{{ trimSuffix ".toml" $key }}/hosts.toml
contentFrom:
secret:
name: {{ $.Release.Name }}-patch-containerd
key: {{ $key }}
permissions: "0400"
{{- end }}
{{- end }}
diskSetup:
filesystems:
- device: /dev/vdb
filesystem: xfs
label: ephemeral
label: persistent
partition: "none"
{{/* overwrite must stay false: XFS on /dev/vdb is created on first boot; subsequent reboots of the same VM must reuse the existing filesystem via LABEL=persistent */}}
overwrite: false
mounts:
- ["LABEL=ephemeral", "/ephemeral"]
- ["/ephemeral/kubelet", "/var/lib/kubelet", "none", "bind,nofail"]
- ["/ephemeral/containerd", "/var/lib/containerd", "none", "bind,nofail"]
{{- $sec := lookup "v1" "Secret" $.Release.Namespace (printf "%s-patch-containerd" $.Release.Name) }}
{{- if $sec }}
files:
{{- range $key, $_ := $sec.data }}
- path: /etc/containerd/certs.d/{{ trimSuffix ".toml" $key }}/hosts.toml
contentFrom:
secret:
name: {{ $.Release.Name }}-patch-containerd
key: {{ $key }}
permissions: "0400"
{{- end }}
{{- end }}
- ["LABEL=persistent", "/persistent"]
- ["/persistent/kubelet", "/var/lib/kubelet", "none", "bind,nofail"]
- ["/persistent/containerd", "/var/lib/containerd", "none", "bind,nofail"]
preKubeadmCommands:
- KUBELET_VERSION={{ include "kubernetes.versionMap" $}} /usr/bin/update-k8s.sh || true
- sed -i 's|root:x:|root::|' /etc/passwd
- systemctl stop containerd.service
- mkdir -p /ephemeral/kubelet /ephemeral/containerd
- mount -o bind /ephemeral/kubelet /var/lib/kubelet
- mount -o bind /ephemeral/containerd /var/lib/containerd
- mkdir -p /persistent/kubelet /persistent/containerd /var/lib/kubelet /var/lib/containerd
- mountpoint -q /var/lib/kubelet || mount -o bind /persistent/kubelet /var/lib/kubelet
- mountpoint -q /var/lib/containerd || mount -o bind /persistent/containerd /var/lib/containerd
- sudo sed -i '/\[plugins."io.containerd.grpc.v1.cri".registry\]/,/^\[/ s|^\(\s*config_path\s*=\s*\).*|\1"/etc/containerd/certs.d"|' /etc/containerd/config.toml
- systemctl start containerd.service
joinConfiguration:

View file

@ -0,0 +1,446 @@
suite: cluster.yaml template tests
templates:
- templates/cluster.yaml
values:
- values/common.yaml
# The KubevirtMachineTemplate is document index 5 when rendering with one
# nodeGroup (md0). Documents before it: Cluster(0), KamajiControlPlane(1),
# WorkloadMonitor-cp(2), KubevirtCluster(3), KubeadmConfigTemplate-md0(4).
# The named template "kubevirtmachinetemplate" is inlined inside document 5
# under spec.template.spec.
#
# Note: instanceType must be set to "" to bypass the lookup() call that
# validates VirtualMachineClusterInstancetype existence against a live cluster.
tests:
###############################################
# dataVolumeTemplates — default disk size #
###############################################
- it: falls back to 20Gi when diskSize is not set
release:
name: test-k8s
namespace: tenant-test
asserts:
- isKind:
of: KubevirtMachineTemplate
documentIndex: 5
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: "20Gi"
documentIndex: 5
- it: renders dataVolumeTemplate with default 20Gi storage
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.diskSize: 20Gi
asserts:
- isKind:
of: KubevirtMachineTemplate
documentIndex: 5
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].metadata.name
value: disk-kubelet
documentIndex: 5
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.source.blank
value: {}
documentIndex: 5
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: 20Gi
documentIndex: 5
###############################################
# dataVolumeTemplates — custom disk size #
###############################################
- it: renders dataVolumeTemplate with custom 50Gi storage
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.diskSize: 50Gi
asserts:
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: 50Gi
documentIndex: 5
###############################################
# storageClassName omitted when empty #
###############################################
- it: omits storageClassName when storageClass is empty
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.storageClass: ""
asserts:
- notExists:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.storageClassName
documentIndex: 5
###############################################
# storageClassName present when set #
###############################################
- it: sets storageClassName when storageClass is provided
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.storageClass: "my-storage"
asserts:
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.storageClassName
value: my-storage
documentIndex: 5
###############################################
# ephemeral-storage is always 2Gi #
###############################################
- it: hardcodes ephemeral-storage to 2Gi with default diskSize
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.diskSize: 20Gi
asserts:
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.template.spec.domain.resources.limits.ephemeral-storage
value: 2Gi
documentIndex: 5
- it: hardcodes ephemeral-storage to 2Gi even with large diskSize
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.diskSize: 100Gi
asserts:
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.template.spec.domain.resources.limits.ephemeral-storage
value: 2Gi
documentIndex: 5
###############################################
# disk-kubelet name consistency #
###############################################
- it: uses disk-kubelet name consistently across dataVolumeTemplates, volumes, and disks
release:
name: test-k8s
namespace: tenant-test
asserts:
# dataVolumeTemplates name
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].metadata.name
value: disk-kubelet
documentIndex: 5
# disks name
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.template.spec.domain.devices.disks[1].name
value: disk-kubelet
documentIndex: 5
# volumes name
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.template.spec.volumes[1].name
value: disk-kubelet
documentIndex: 5
# volumes dataVolume reference
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.template.spec.volumes[1].dataVolume.name
value: disk-kubelet
documentIndex: 5
###############################################
# CDI populator annotation #
###############################################
- it: includes CDI populator annotation on dataVolumeTemplate
release:
name: test-k8s
namespace: tenant-test
asserts:
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].metadata.annotations["cdi.kubevirt.io/storage.usePopulator"]
value: "false"
documentIndex: 5
###############################################
# multiple node groups — independent storage #
###############################################
# With two nodeGroups (md0, md1) the range loop emits per-group documents
# alphabetically. Documents before the loop: Cluster(0),
# KamajiControlPlane(1), WorkloadMonitor-cp(2), KubevirtCluster(3).
# Each group adds: KubeadmConfigTemplate, KubevirtMachineTemplate,
# MachineDeployment, MachineHealthCheck, WorkloadMonitor.
# md0 KubevirtMachineTemplate → index 5, md1 → index 10.
- it: renders independent dataVolumeTemplates per node group
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.minReplicas: 0
nodeGroups.md0.maxReplicas: 10
nodeGroups.md0.instanceType: ""
nodeGroups.md0.roles[0]: worker
nodeGroups.md0.resources.cpu: 2
nodeGroups.md0.resources.memory: 4Gi
nodeGroups.md0.diskSize: 20Gi
nodeGroups.md1.minReplicas: 0
nodeGroups.md1.maxReplicas: 5
nodeGroups.md1.instanceType: ""
nodeGroups.md1.roles[0]: worker
nodeGroups.md1.resources.cpu: 4
nodeGroups.md1.resources.memory: 8Gi
nodeGroups.md1.diskSize: 50Gi
nodeGroups.md1.storageClass: my-storage-class
asserts:
# --- md0: KubevirtMachineTemplate at document index 5 ---
- isKind:
of: KubevirtMachineTemplate
documentIndex: 5
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: 20Gi
documentIndex: 5
- notExists:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.storageClassName
documentIndex: 5
# --- md1: KubevirtMachineTemplate at document index 10 ---
- isKind:
of: KubevirtMachineTemplate
documentIndex: 10
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.resources.requests.storage
value: 50Gi
documentIndex: 10
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.dataVolumeTemplates[0].spec.storage.storageClassName
value: my-storage-class
documentIndex: 10
###############################################
# KubeadmConfigTemplate — persistent storage #
###############################################
# KubeadmConfigTemplate is document index 4 when rendering with one
# nodeGroup (md0).
- it: configures persistent filesystem as xfs with label persistent
release:
name: test-k8s
namespace: tenant-test
asserts:
- isKind:
of: KubeadmConfigTemplate
documentIndex: 4
- equal:
path: spec.template.spec.diskSetup.filesystems[0].label
value: persistent
documentIndex: 4
- equal:
path: spec.template.spec.diskSetup.filesystems[0].overwrite
value: false
documentIndex: 4
- equal:
path: spec.template.spec.diskSetup.filesystems[0].filesystem
value: xfs
documentIndex: 4
- it: configures persistent filesystem device and partition
release:
name: test-k8s
namespace: tenant-test
asserts:
- equal:
path: spec.template.spec.diskSetup.filesystems[0].device
value: /dev/vdb
documentIndex: 4
- equal:
path: spec.template.spec.diskSetup.filesystems[0].partition
value: "none"
documentIndex: 4
- it: mounts persistent volume and bind-mounts kubelet and containerd
release:
name: test-k8s
namespace: tenant-test
asserts:
- equal:
path: spec.template.spec.mounts[0]
value:
- LABEL=persistent
- /persistent
documentIndex: 4
- equal:
path: spec.template.spec.mounts[1]
value:
- /persistent/kubelet
- /var/lib/kubelet
- none
- bind,nofail
documentIndex: 4
- equal:
path: spec.template.spec.mounts[2]
value:
- /persistent/containerd
- /var/lib/containerd
- none
- bind,nofail
documentIndex: 4
- it: includes mountpoint guards in preKubeadmCommands
release:
name: test-k8s
namespace: tenant-test
asserts:
- contains:
path: spec.template.spec.preKubeadmCommands
content: "mountpoint -q /var/lib/kubelet || mount -o bind /persistent/kubelet /var/lib/kubelet"
documentIndex: 4
- contains:
path: spec.template.spec.preKubeadmCommands
content: "mountpoint -q /var/lib/containerd || mount -o bind /persistent/containerd /var/lib/containerd"
documentIndex: 4
- it: creates persistent directories in preKubeadmCommands
release:
name: test-k8s
namespace: tenant-test
asserts:
- contains:
path: spec.template.spec.preKubeadmCommands
content: "mkdir -p /persistent/kubelet /persistent/containerd /var/lib/kubelet /var/lib/containerd"
documentIndex: 4
###############################################
# disk-kubelet matchVolume blockSize #
###############################################
- it: sets matchVolume blockSize on disk-kubelet
release:
name: test-k8s
namespace: tenant-test
asserts:
- equal:
path: spec.template.spec.virtualMachineTemplate.spec.template.spec.domain.devices.disks[1].blockSize.matchVolume.enabled
value: true
documentIndex: 5
###############################################
# ephemeralStorage migration guard #
###############################################
- it: fails when ephemeralStorage is set instead of diskSize
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.ephemeralStorage: 50Gi
asserts:
- failedTemplate:
errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions.'
- it: fails when ephemeralStorage is set to empty string
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.ephemeralStorage: ""
asserts:
- failedTemplate:
errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions.'
- it: fails when both ephemeralStorage and diskSize are set
release:
name: test-k8s
namespace: tenant-test
set:
nodeGroups.md0.ephemeralStorage: 50Gi
nodeGroups.md0.diskSize: 50Gi
asserts:
- failedTemplate:
errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions.'
###############################################
# KubeadmConfigTemplate — files block #
###############################################
# When the containerd patch Secret does not exist (lookup returns nil),
# only the update-k8s.sh entry must appear in the files list.
- it: includes update-k8s.sh in KubeadmConfigTemplate files
release:
name: test-k8s
namespace: tenant-test
asserts:
- isKind:
of: KubeadmConfigTemplate
documentIndex: 4
- equal:
path: spec.template.spec.files[0].path
value: /usr/bin/update-k8s.sh
documentIndex: 4
- equal:
path: spec.template.spec.files[0].permissions
value: "0755"
documentIndex: 4
- matchRegex:
path: spec.template.spec.files[0].content
pattern: "KUBELET_VERSION"
documentIndex: 4
- matchRegex:
path: spec.template.spec.files[0].content
pattern: "curl -fsSL"
documentIndex: 4
- it: has exactly one file entry when no containerd patch Secret exists
release:
name: test-k8s
namespace: tenant-test
asserts:
- isKind:
of: KubeadmConfigTemplate
documentIndex: 4
- equal:
path: spec.template.spec.files
value:
- path: /usr/bin/update-k8s.sh
owner: root:root
permissions: "0755"
content: |
#!/usr/bin/env bash
set -euo pipefail
# Expected to be passed in via preKubeadmCommands
: "${KUBELET_VERSION:?KUBELET_VERSION must be set, e.g. v1.31.0}"
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) ARCH=amd64 ;;
aarch64) ARCH=arm64 ;;
esac
# Use your internal mirror here for real-world use.
BASE_URL="https://dl.k8s.io/release/${KUBELET_VERSION}/bin/linux/${ARCH}"
echo "Installing kubelet and kubeadm ${KUBELET_VERSION} for ${ARCH}..."
curl -fsSL "${BASE_URL}/kubelet" -o /root/kubelet
curl -fsSL "${BASE_URL}/kubeadm" -o /root/kubeadm
chmod 0755 /root/kubelet
chmod 0755 /root/kubeadm
if /root/kubelet --version ; then mv /root/kubelet /usr/bin/kubelet ; fi
if /root/kubeadm version ; then mv /root/kubeadm /usr/bin/kubeadm ; fi
documentIndex: 4

View file

@ -0,0 +1,34 @@
_cluster: {}
_namespace:
etcd: cozy-etcd
ingress: nginx
host: example.com
version: "v1.32"
controlPlane:
replicas: 2
apiServer:
resources: {}
resourcesPreset: "large"
controllerManager:
resources: {}
resourcesPreset: "micro"
scheduler:
resources: {}
resourcesPreset: "micro"
konnectivity:
server:
resources: {}
resourcesPreset: "micro"
# diskSize and storageClass are intentionally omitted from md0 to verify template
# fallback defaults: diskSize defaults to "20Gi", storageClass falls back to cluster default.
nodeGroups:
md0:
minReplicas: 0
maxReplicas: 10
instanceType: ""
roles:
- worker
resources:
cpu: 2
memory: 4Gi
gpus: []

View file

@ -12,7 +12,7 @@
"type": "object",
"default": {
"md0": {
"ephemeralStorage": "20Gi",
"diskSize": "20Gi",
"gpus": [],
"instanceType": "u1.medium",
"maxReplicas": 10,
@ -20,21 +20,22 @@
"resources": {},
"roles": [
"ingress-nginx"
]
],
"storageClass": ""
}
},
"additionalProperties": {
"type": "object",
"required": [
"ephemeralStorage",
"diskSize",
"instanceType",
"maxReplicas",
"minReplicas",
"resources"
],
"properties": {
"ephemeralStorage": {
"description": "Ephemeral storage size.",
"diskSize": {
"description": "Persistent disk size for kubelet and containerd data.",
"default": "20Gi",
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
"anyOf": [
@ -116,6 +117,10 @@
"items": {
"type": "string"
}
},
"storageClass": {
"description": "StorageClass for worker node persistent disks. When empty, uses the management cluster default StorageClass (the one annotated storageclass.kubernetes.io/is-default-class: true).",
"type": "string"
}
}
}

View file

@ -29,7 +29,8 @@ storageClass: replicated
## @field {int} minReplicas=0 - Minimum number of replicas.
## @field {int} maxReplicas=10 - Maximum number of replicas.
## @field {string} instanceType="u1.medium" - Virtual machine instance type.
## @field {quantity} ephemeralStorage="20Gi" - Ephemeral storage size.
## @field {quantity} diskSize="20Gi" - Persistent disk size for kubelet and containerd data.
## @field {string} [storageClass] - StorageClass for worker node persistent disks. When empty, uses the management cluster default StorageClass (the one annotated storageclass.kubernetes.io/is-default-class: true).
## @field {[]string} roles - List of node roles.
## @field {Resources} resources - CPU and memory resources for each worker node.
## @field {[]GPU} gpus - List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).
@ -40,7 +41,8 @@ nodeGroups:
minReplicas: 0
maxReplicas: 10
instanceType: "u1.medium"
ephemeralStorage: 20Gi
diskSize: 20Gi
storageClass: ""
roles:
- ingress-nginx
resources: {}

File diff suppressed because one or more lines are too long

View file

@ -36,9 +36,10 @@ var _ = Describe("defaultLikeKubernetes", func() {
Expect(ng).To(HaveKeyWithValue("minReplicas", BeNumerically("==", 3)))
Expect(ng).To(HaveKeyWithValue("instanceType", "u1.medium"))
Expect(ng).To(HaveKeyWithValue("storageClass", ""))
Expect(ng["roles"]).To(ConsistOf("ingress-nginx"))
Expect(ng).NotTo(HaveKey("ephemeralStorage"))
Expect(ng).NotTo(HaveKey("diskSize"))
Expect(ng).NotTo(HaveKey("maxReplicas"))
Expect(ng).NotTo(HaveKey("resources"))
})
@ -56,6 +57,8 @@ var _ = Describe("defaultLikeKubernetes", func() {
})
})
// buildTestSchema returns a minimal schema that exercises defaultLikeKubernetes.
// Defaults intentionally differ from values.schema.json — do not use this as a reference for production defaults.
func buildTestSchema() *apischema.Structural {
instanceType := apischema.Structural{
Generic: apischema.Generic{
@ -75,9 +78,15 @@ func buildTestSchema() *apischema.Structural {
minReplicas := apischema.Structural{
Generic: apischema.Generic{Type: "integer"},
}
ephemeralStorage := apischema.Structural{
diskSize := apischema.Structural{
Generic: apischema.Generic{Type: "string"},
}
storageClass := apischema.Structural{
Generic: apischema.Generic{
Type: "string",
Default: apischema.JSON{Object: ""},
},
}
maxReplicas := apischema.Structural{
Generic: apischema.Generic{Type: "integer"},
}
@ -89,12 +98,13 @@ func buildTestSchema() *apischema.Structural {
nodeGroupsValue := &apischema.Structural{
Generic: apischema.Generic{Type: "object"},
Properties: map[string]apischema.Structural{
"instanceType": instanceType,
"roles": roles,
"minReplicas": minReplicas,
"ephemeralStorage": ephemeralStorage,
"maxReplicas": maxReplicas,
"resources": resources,
"instanceType": instanceType,
"roles": roles,
"minReplicas": minReplicas,
"diskSize": diskSize,
"storageClass": storageClass,
"maxReplicas": maxReplicas,
"resources": resources,
},
}
@ -103,10 +113,11 @@ func buildTestSchema() *apischema.Structural {
Type: "object",
Default: apischema.JSON{Object: map[string]any{
"md0": map[string]any{
"ephemeralStorage": "20Gi",
"maxReplicas": 10,
"minReplicas": 0,
"resources": map[string]any{},
"diskSize": "20Gi",
"storageClass": "",
"maxReplicas": 10,
"minReplicas": 0,
"resources": map[string]any{},
},
}},
},