[vm-default-images] Added new optional package

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
Myasnikov Daniil committed 2026-04-14 19:59:19 +05:00
1 parent ad52992301
commit bec35e3aad
13 files changed
+448 -29

No files matched your search

+9 -2
View File
@@ -32,21 +32,28 @@ type ConfigSpec struct {
}
type Source struct {
// Clone an existing vm-disk.
Disk *SourceDisk `json:"disk,omitempty"`
// Download image from an HTTP source.
Http *SourceHTTP `json:"http,omitempty"`
// Use image by name.
// Use image by name from default collection.
Image *SourceImage `json:"image,omitempty"`
// Upload local image.
Upload *SourceUpload `json:"upload,omitempty"`
}
type SourceDisk struct {
// Name of the vm-disk to clone.
Name string `json:"name"`
}
type SourceHTTP struct {
// URL to download the image.
Url string `json:"url"`
}
type SourceImage struct {
// Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).
// Name of the image to use.
Name string `json:"name"`
}
+1 -1
View File
@@ -16,7 +16,7 @@ kubectl create -f - <<EOF
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
name: "vm-image-$name"
name: "vm-default-images-$name"
namespace: cozy-public
annotations:
cdi.kubevirt.io/storage.bind.immediate.requested: "true"
@@ -141,7 +141,10 @@ func (m *Manager) ensureCFOMapping(ctx context.Context, crd *cozyv1alpha1.Applic
}
// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString
// for all string fields inside spec that don't have enum
// for all string fields inside spec that don't have enum.
// It handles two structures:
// - properties.spec.properties (most resources)
// - properties.properties (VMDisk and similar resources without spec wrapper)
func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
if openAPISchema == "" {
return map[string]any{}, nil
@@ -161,15 +164,25 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
"properties": map[string]any{},
}
// Check if there's a spec property
specProp, ok := props["spec"].(map[string]any)
if !ok {
return map[string]any{}, nil
var specProps map[string]any
var hasSpec bool
// First try to find properties under spec
if specProp, ok := props["spec"].(map[string]any); ok {
specProps, hasSpec = specProp["properties"].(map[string]any)
}
specProps, ok := specProp["properties"].(map[string]any)
if !ok {
return map[string]any{}, nil
// If no spec wrapper, use top-level properties directly (VMDisk pattern)
if !hasSpec {
specProps = props
// Still wrap in spec for consistency with applyListInputOverrides
schemaProps := schema["properties"].(map[string]any)
specSchema := map[string]any{
"properties": map[string]any{},
}
schemaProps["spec"] = specSchema
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
return schema, nil
}
// Create spec.properties structure in schema
@@ -231,10 +244,41 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma
}
case "ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk":
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis":
specProps := ensureSchemaPath(schema, "spec")
specProps["storageClass"] = storageClassListInput()
case "VMDisk":
specProps := ensureSchemaPath(schema, "spec")
specProps["storageClass"] = storageClassListInput()
// Override source.image.name to be an API-backed dropdown listing default images
if sourceObj, ok := specProps["source"].(map[string]any); ok {
if imgProps, ok := sourceObj["properties"].(map[string]any); ok {
if imgName, ok := imgProps["image"].(map[string]any); ok {
imgName["properties"].(map[string]any)["name"] = map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/cdi.kubevirt.io/v1beta1/namespaces/cozy-public/datavolumes",
"keysToValue": []any{"metadata", "annotations", "vm-default-images.cozystack.io/name"},
"keysToLabel": []any{"metadata", "annotations", "vm-default-images.cozystack.io/description"},
},
}
}
// Override source.disk.name to be an API-backed dropdown listing VMDisk resources
if diskName, ok := imgProps["disk"].(map[string]any); ok {
diskName["properties"].(map[string]any)["name"] = map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
}
}
}
case "FoundationDB":
storageProps := ensureSchemaPath(schema, "spec", "storage")
storageProps["storageClass"] = storageClassListInput()
@@ -317,6 +317,108 @@ func TestApplyListInputOverrides_StorageClassFoundationDB(t *testing.T) {
assertStorageClassListInput(t, sc)
}
func TestApplyListInputOverrides_VMDisk_SourceFields(t *testing.T) {
openAPISchema := `{
"properties":{
"optical":{"type":"boolean"},
"source":{
"type":"object",
"properties":{
"image":{
"type":"object",
"properties":{
"name":{"type":"string"}
}
},
"disk":{
"type":"object",
"properties":{
"name":{"type":"string"}
}
}
}
},
"storage":{"type":"string"},
"storageClass":{"type":"string"}
}
}`
schema, err := buildMultilineStringSchema(openAPISchema)
if err != nil {
t.Fatalf("buildMultilineStringSchema failed: %v", err)
}
applyListInputOverrides(schema, "VMDisk", map[string]any{})
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
// Check storageClass
sc, ok := specProps["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.properties")
}
assertStorageClassListInput(t, sc)
// Check source.image.name listInput
// Structure: specProps["source"]["properties"]["image"]["properties"]["name"]
sourceObj, ok := specProps["source"].(map[string]any)
if !ok {
t.Fatal("source not found in spec.properties")
}
sourceObjProps, ok := sourceObj["properties"].(map[string]any)
if !ok {
t.Fatal("source.properties not found")
}
imageObj, ok := sourceObjProps["image"].(map[string]any)
if !ok {
t.Fatal("image not found in source.properties")
}
imageObjProps, ok := imageObj["properties"].(map[string]any)
if !ok {
t.Fatal("image.properties not found")
}
imgName, ok := imageObjProps["name"].(map[string]any)
if !ok {
t.Fatal("name not found in image.properties")
}
if imgName["type"] != "listInput" {
t.Errorf("expected type listInput, got %v", imgName["type"])
}
imgNameCustomProps, ok := imgName["customProps"].(map[string]any)
if !ok {
t.Fatal("name.customProps not found")
}
expectedImageURI := "/api/clusters/{cluster}/k8s/apis/cdi.kubevirt.io/v1beta1/namespaces/cozy-public/datavolumes"
if imgNameCustomProps["valueUri"] != expectedImageURI {
t.Errorf("expected valueUri %s, got %v", expectedImageURI, imgNameCustomProps["valueUri"])
}
// Check source.disk.name listInput
diskObj, ok := sourceObjProps["disk"].(map[string]any)
if !ok {
t.Fatal("disk not found in source.properties")
}
diskObjProps, ok := diskObj["properties"].(map[string]any)
if !ok {
t.Fatal("disk.properties not found")
}
diskName, ok := diskObjProps["name"].(map[string]any)
if !ok {
t.Fatal("name not found in disk.properties")
}
if diskName["type"] != "listInput" {
t.Errorf("expected type listInput, got %v", diskName["type"])
}
diskNameCustomProps, ok := diskName["customProps"].(map[string]any)
if !ok {
t.Fatal("disk name.customProps not found")
}
expectedDiskURI := "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks"
if diskNameCustomProps["valueUri"] != expectedDiskURI {
t.Errorf("expected valueUri %s, got %v", expectedDiskURI, diskNameCustomProps["valueUri"])
}
}
func TestApplyListInputOverrides_StorageClassKafka(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "Kafka", map[string]any{})
+13 -11
View File
@@ -6,15 +6,17 @@ A Virtual Machine Disk
### Common parameters
| Name | Description | Type | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------ |
| `source` | The source image location used to create a disk. | `object` | `{}` |
| `source.image` | Use image by name. | `*object` | `null` |
| `source.image.name` | Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`). | `string` | `""` |
| `source.upload` | Upload local image. | `*object` | `null` |
| `source.http` | Download image from an HTTP source. | `*object` | `null` |
| `source.http.url` | URL to download the image. | `string` | `""` |
| `optical` | Defines if disk should be considered optical. | `bool` | `false` |
| `storage` | The size of the disk allocated for the virtual machine. | `quantity` | `5Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `replicated` |
| Name | Description | Type | Value |
| ------------------- | ------------------------------------------------------- | ---------- | ------------ |
| `source` | The source image location used to create a disk. | `object` | `{}` |
| `source.image` | Use image by name from default collection. | `*object` | `null` |
| `source.image.name` | Name of the image to use. | `string` | `""` |
| `source.upload` | Upload local image. | `*object` | `null` |
| `source.http` | Download image from an HTTP source. | `*object` | `null` |
| `source.http.url` | URL to download the image. | `string` | `""` |
| `source.disk` | Clone an existing vm-disk. | `*object` | `null` |
| `source.disk.name` | Name of the vm-disk to clone. | `string` | `""` |
| `optical` | Defines if disk should be considered optical. | `bool` | `false` |
| `storage` | The size of the disk allocated for the virtual machine. | `quantity` | `5Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `replicated` |
+6 -2
View File
@@ -21,15 +21,19 @@ spec:
{{- end }}
source:
{{- if hasKey .Values.source "image" }}
{{- $dv := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" "cozy-public" (printf "vm-image-%s" .Values.source.image.name) }}
{{- $dv := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" "cozy-public" (printf "vm-default-images-%s" .Values.source.image.name) }}
pvc:
name: vm-image-{{ required "A valid .Values.source.image.name entry required!" .Values.source.image.name }}
name: vm-default-images-{{ required "A valid .Values.source.image.name entry required!" .Values.source.image.name }}
namespace: cozy-public
{{- else if hasKey .Values.source "http" }}
http:
url: {{ required "A valid .Values.source.http.url entry required!" .Values.source.http.url }}
{{- else if hasKey .Values.source "upload" }}
upload: {}
{{- else if hasKey .Values.source "disk" }}
pvc:
name: vm-disk-{{ required "A valid .Values.source.disk.name entry required!" .Values.source.disk.name }}
namespace: {{ $.Release.Namespace }}
{{- end }}
{{- else }}
source:
+15 -2
View File
@@ -7,6 +7,19 @@
"type": "object",
"default": {},
"properties": {
"disk": {
"description": "Clone an existing vm-disk.",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "Name of the vm-disk to clone.",
"type": "string"
}
}
},
"http": {
"description": "Download image from an HTTP source.",
"type": "object",
@@ -21,14 +34,14 @@
}
},
"image": {
"description": "Use image by name.",
"description": "Use image by name from default collection.",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "Name of the image to use (uploaded as \"golden image\" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).",
"description": "Name of the image to use.",
"type": "string"
}
}
+6 -2
View File
@@ -3,17 +3,21 @@
##
## @typedef {struct} SourceImage - Use image by name.
## @field {string} name - Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).
## @field {string} name - Name of the image to use.
## @typedef {struct} SourceUpload - Upload local image.
## @typedef {struct} SourceHTTP - Download image from an HTTP source.
## @field {string} url - URL to download the image.
## @typedef {struct} SourceDisk - Clone an existing vm-disk.
## @field {string} name - Name of the vm-disk to clone.
## @typedef {struct} Source - The source image location used to create a disk.
## @field {*SourceImage} [image] - Use image by name.
## @field {*SourceImage} [image] - Use image by name from default collection.
## @field {*SourceUpload} [upload] - Upload local image.
## @field {*SourceHTTP} [http] - Download image from an HTTP source.
## @field {*SourceDisk} [disk] - Clone an existing vm-disk.
## @param {Source} source - The source image location used to create a disk.
source: {}
@@ -0,0 +1,22 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.vm-default-images
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.networking
- cozystack.kubevirt-cdi
components:
- name: vm-default-images
path: system/vm-default-images
install:
namespace: cozy-system
releaseName: vm-default-images
@@ -0,0 +1,5 @@
apiVersion: v2
name: vm-default-images
description: Global Golden Image collection for virtual machines
type: application
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
@@ -0,0 +1,4 @@
export NAME=vm-default-images
export NAMESPACE=cozy-system
include ../../../hack/package.mk
@@ -0,0 +1,45 @@
{{- range .Values.images }}
---
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
annotations:
vm-default-images.cozystack.io/name: {{ .name | quote }}
{{- with .os }}
{{- with .family }}
vm-default-images.cozystack.io/os-family: {{ . | quote }}
{{- end }}
{{- with .name }}
vm-default-images.cozystack.io/os-name: {{ . | quote }}
{{- end }}
{{- with .version }}
vm-default-images.cozystack.io/os-version: {{ . | quote }}
{{- end }}
{{- end }}
{{- with .architecture }}
vm-default-images.cozystack.io/architecture: {{ . | quote }}
{{- end }}
{{- with .description }}
vm-default-images.cozystack.io/description: {{ . | quote }}
{{- end }}
cdi.kubevirt.io/storage.bind.immediate.requested: "true"
cdi.kubevirt.io/storage.usePopulator: "true"
labels:
app.kubernetes.io/managed-by: cozystack
name: vm-default-images-{{ required "A valid .name entry required for each image!" .name }}
namespace: cozy-public
spec:
contentType: kubevirt
source:
http:
url: {{ required (printf "A valid .url entry required for image '%s'!" .name) .url | quote }}
storage:
resources:
requests:
storage: {{ default "5Gi" .storage }}
{{- if .storageClass }}
storageClassName: {{ .storageClass }}
{{- else if $.Values.storageClass }}
storageClassName: {{ $.Values.storageClass }}
{{- end }}
{{- end }}
@@ -0,0 +1,167 @@
##
## @section Global Golden Image Collection
##
## @param {string} storageClass - Default StorageClass for all images. If empty, uses the cluster default StorageClass.
storageClass: ""
## @typedef {struct} ImageOS - Operating system metadata for a Golden Image.
## @field {string} [family] - OS family (e.g. "Linux", "Windows").
## @field {string} [name] - OS distribution name (e.g. "Ubuntu", "Fedora").
## @field {string} [version] - OS version (e.g. "24.04", "40").
## @typedef {struct} GlobalImage - A global Golden Image entry.
## @field {string} name - Unique image name used to reference this image in vm-disk (e.g. "ubuntu").
## @field {string} url - HTTP(S) URL to download the image from.
## @field {quantity} storage - Storage size to allocate for this image.
## @field {string} storageClass - StorageClass used to store the image (overrides global default).
## @field {ImageOS} [os] - Operating system metadata.
## @field {string} [architecture] - CPU architecture (e.g. "amd64", "arm64").
## @field {string} [description] - Human-readable description.
## @param {[]GlobalImage} images - List of global Golden Images to provision in the cozy-public namespace.
images:
- name: ubuntu-22.04
url: https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img
storage: 20Gi
os:
family: Linux
name: Ubuntu
version: "22.04"
architecture: amd64
description: "Ubuntu 22.04 LTS (Jammy Jellyfish) cloud image"
- name: ubuntu-24.04
url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
storage: 20Gi
os:
family: Linux
name: Ubuntu
version: "24.04"
architecture: amd64
description: "Ubuntu 24.04 LTS (Noble Numbat) cloud image"
- name: rocky-8
url: https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud-Base.latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: Rocky Linux
version: "8"
architecture: amd64
description: "Rocky Linux 8 GenericCloud image"
- name: rocky-9
url: https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: Rocky Linux
version: "9"
architecture: amd64
description: "Rocky Linux 9 GenericCloud image"
- name: rocky-10
url: https://dl.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: Rocky Linux
version: "10"
architecture: amd64
description: "Rocky Linux 10 GenericCloud image"
- name: almalinux-8
url: https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: AlmaLinux
version: "8"
architecture: amd64
description: "AlmaLinux 8 GenericCloud image"
- name: almalinux-9
url: https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: AlmaLinux
version: "9"
architecture: amd64
description: "AlmaLinux 9 GenericCloud image"
- name: almalinux-10
url: https://repo.almalinux.org/almalinux/10/cloud/x86_64/images/AlmaLinux-10-GenericCloud-latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: AlmaLinux
version: "10"
architecture: amd64
description: "AlmaLinux 10 GenericCloud image"
- name: debian-12
url: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
storage: 20Gi
os:
family: Linux
name: Debian
version: "12"
architecture: amd64
description: "Debian 12 (Bookworm) generic cloud image"
- name: debian-13
url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2
storage: 20Gi
os:
family: Linux
name: Debian
version: "13"
architecture: amd64
description: "Debian 13 (Trixie) generic cloud image"
- name: centos-stream-9
url: https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-9-latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: CentOS
version: "9"
architecture: amd64
description: "CentOS Stream 9 GenericCloud image"
- name: centos-stream-10
url: https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-10-latest.x86_64.qcow2
storage: 20Gi
os:
family: Linux
name: CentOS
version: "10"
architecture: amd64
description: "CentOS Stream 10 GenericCloud image"
- name: opensuse-leap-15.6
url: https://download.opensuse.org/repositories/Cloud:/Images:/Leap_15.6/images/openSUSE-Leap-15.6.x86_64-NoCloud.qcow2
storage: 20Gi
os:
family: Linux
name: openSUSE
version: "15.6"
architecture: amd64
description: "openSUSE Leap 15.6 NoCloud image"
- name: opensuse-leap-16.0
url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0:/Images/images/Leap-16.0-Minimal-VM.x86_64-Cloud.qcow2
storage: 20Gi
os:
family: Linux
name: openSUSE
version: "16.0"
architecture: amd64
description: "openSUSE Leap 16.0 Minimal VM cloud image"
- name: ubuntu-20.04
url: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img
storage: 20Gi
os:
family: Linux
name: Ubuntu
version: "20.04"
architecture: amd64
description: "Ubuntu 20.04 LTS (Focal Fossa) cloud image"
- name: alpine-3.21
url: https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/cloud/nocloud_alpine-3.21.6-x86_64-bios-cloudinit-r0.qcow2
storage: 20Gi
os:
family: Linux
name: Alpine
version: "3.21"
architecture: amd64
description: "Alpine Linux 3.21 cloud image (cloud-init)"