Add vm-default-images package (#2258)

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[vm-default-images] Added package that brings set of images that can be used clusterwide
[vm-disk] Updated source "image" for prettier dropdown selection
[vm-disk] Added new source for vm-disk called disk - to use as source vm-disk from same namespace.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Clone VM disks by specifying an existing vm-disk as the source.
* Global default image collection and chart to publish pre-provisioned
images.

* **UI**
* Forms provide selectable lists for default images and existing VM
disks.

* **Migration**
* Migration to rename existing image DataVolumes to the new
default-images naming and bumped migration version.

* **Documentation**
* VM disk docs and README updated to reflect image sourcing and cloning.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
myasnikovdaniil 2026-04-16 08:05:23 +05:00 committed by GitHub
commit 482d813d01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 613 additions and 56 deletions

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"`
}

View file

@ -70,6 +70,11 @@ func (in *ConfigSpec) DeepCopy() *ConfigSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Source) DeepCopyInto(out *Source) {
*out = *in
if in.Disk != nil {
in, out := &in.Disk, &out.Disk
*out = new(SourceDisk)
**out = **in
}
if in.Http != nil {
in, out := &in.Http, &out.Http
*out = new(SourceHTTP)
@ -97,6 +102,21 @@ func (in *Source) DeepCopy() *Source {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceDisk) DeepCopyInto(out *SourceDisk) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceDisk.
func (in *SourceDisk) DeepCopy() *SourceDisk {
if in == nil {
return nil
}
out := new(SourceDisk)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceHTTP) DeepCopyInto(out *SourceHTTP) {
*out = *in

View file

@ -63,9 +63,14 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) {
*out = make([]Disk, len(*in))
copy(*out, *in)
}
if in.Networks != nil {
in, out := &in.Networks, &out.Networks
*out = make([]Network, len(*in))
copy(*out, *in)
}
if in.Subnets != nil {
in, out := &in.Subnets, &out.Subnets
*out = make([]Subnet, len(*in))
*out = make([]Network, len(*in))
copy(*out, *in)
}
if in.Gpus != nil {
@ -121,6 +126,21 @@ func (in *GPU) DeepCopy() *GPU {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Network) DeepCopyInto(out *Network) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network.
func (in *Network) DeepCopy() *Network {
if in == nil {
return nil
}
out := new(Network)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Resources) DeepCopyInto(out *Resources) {
*out = *in
@ -138,18 +158,3 @@ func (in *Resources) DeepCopy() *Resources {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Subnet) DeepCopyInto(out *Subnet) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subnet.
func (in *Subnet) DeepCopy() *Subnet {
if in == nil {
return nil
}
out := new(Subnet)
in.DeepCopyInto(out)
return out
}

View file

@ -58,6 +58,16 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) {
*out = make([]Subnet, len(*in))
copy(*out, *in)
}
if in.Peers != nil {
in, out := &in.Peers, &out.Peers
*out = make([]Peer, len(*in))
copy(*out, *in)
}
if in.Routes != nil {
in, out := &in.Routes, &out.Routes
*out = make([]Route, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSpec.
@ -70,6 +80,36 @@ func (in *ConfigSpec) DeepCopy() *ConfigSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Peer) DeepCopyInto(out *Peer) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Peer.
func (in *Peer) DeepCopy() *Peer {
if in == nil {
return nil
}
out := new(Peer)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Route) DeepCopyInto(out *Route) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route.
func (in *Route) DeepCopy() *Route {
if in == nil {
return nil
}
out := new(Route)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Subnet) DeepCopyInto(out *Subnet) {
*out = *in

View file

@ -1,5 +1,21 @@
//go:build !ignore_autogenerated
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
@ -645,4 +661,3 @@ func (in *RestoreJobStatus) DeepCopy() *RestoreJobStatus {
in.DeepCopyInto(out)
return out
}

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"

View file

@ -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,45 @@ 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 {
if imgNameProps, ok := imgName["properties"].(map[string]any); ok {
imgNameProps["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 {
if diskNameProps, ok := diskName["properties"].(map[string]any); ok {
diskNameProps["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()

View file

@ -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{})

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` |

View file

@ -21,15 +21,18 @@ 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) }}
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:

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"
}
}

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: {}

View file

@ -0,0 +1,52 @@
#!/bin/sh
# Migration 38 --> 39
# Rename DataVolumes/PVCs in cozy-public from vm-image-<name> to vm-default-images-<name>.
#
# The vm-disk package previously looked up golden images with the prefix "vm-image-".
# The new vm-default-images package uses the prefix "vm-default-images-".
# This migration renames any existing DataVolumes so that live vm-disk resources
# continue to resolve their source PVC after upgrade.
set -euo pipefail
NAMESPACE="cozy-public"
# Skip if CDI DataVolume CRD is not installed
if ! kubectl api-resources --api-group=cdi.kubevirt.io -o name 2>/dev/null | grep -q '^datavolumes\.'; then
echo "CDI DataVolume CRD not found, skipping rename"
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=39 --dry-run=client -o yaml | kubectl apply -f-
exit 0
fi
# Find DataVolumes whose name starts with "vm-image-"
DVNAMES=$(kubectl get datavolumes.cdi.kubevirt.io -n "$NAMESPACE" \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null \
| grep '^vm-image-' || true)
if [ -z "$DVNAMES" ]; then
echo "No vm-image-* DataVolumes found in $NAMESPACE, nothing to rename"
else
for DVNAME in $DVNAMES; do
NEWNAME="vm-default-images-${DVNAME#vm-image-}"
echo "Renaming DataVolume $DVNAME -> $NEWNAME in namespace $NAMESPACE"
# Export existing DV spec, swap name, and apply as new object
kubectl get datavolumes.cdi.kubevirt.io -n "$NAMESPACE" "$DVNAME" -o json \
| jq --arg new "$NEWNAME" '
del(.metadata.resourceVersion, .metadata.uid, .metadata.creationTimestamp,
.metadata.generation, .metadata.selfLink, .metadata.managedFields,
.status)
| .metadata.name = $new
' \
| kubectl apply -f -
# Delete the old DataVolume
kubectl delete datavolumes.cdi.kubevirt.io -n "$NAMESPACE" "$DVNAME" --ignore-not-found=true
echo "Renamed $DVNAME -> $NEWNAME"
done
fi
# Stamp version
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=39 --dry-run=client -o yaml | kubectl apply -f-

View file

@ -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

View file

@ -8,6 +8,7 @@
{{- end -}}
{{include "cozystack.platform.package" (list "cozystack.kubevirt" "default" $ $kubevirtComponents) }}
{{include "cozystack.platform.package.default" (list "cozystack.kubevirt-cdi" $) }}
{{include "cozystack.platform.package.optional.default" (list "cozystack.vm-default-images" $) }}
{{include "cozystack.platform.package.optional.default" (list "cozystack.gpu-operator" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.kamaji" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.capi-operator" $) }}

View file

@ -6,7 +6,7 @@ sourceRef:
migrations:
enabled: false
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.2.1@sha256:e8fcf006a4451fc0e961455e9b27a61b7103ee49b1a81fe5e4662ffed093fad6
targetVersion: 38
targetVersion: 39
# Bundle deployment configuration
bundles:
system:

View file

@ -142,14 +142,6 @@ spec:
required:
- uri
type: object
underlyingResources:
description: |-
Holds application-specific resource metadata discovered during backup.
The payload is a self-typed JSON object carrying an inlined TypeMeta
(kind/apiVersion) so the consuming controller can dispatch on the
application kind.
type: object
x-kubernetes-preserve-unknown-fields: true
conditions:
description: Conditions represents the latest available observations
of a Backup's state.
@ -213,6 +205,14 @@ spec:
Phase is a simple, high-level summary of the backup's state.
Typical values are: Pending, Ready, Failed.
type: string
underlyingResources:
description: |-
UnderlyingResources holds application-specific resource metadata discovered
during backup (e.g., VM disks, network configuration). The payload is a
self-typed JSON object carrying an inlined TypeMeta (kind/apiVersion) so
the consuming controller can dispatch on the application kind.
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
type: object
selectableFields:

View file

@ -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

View file

@ -0,0 +1,4 @@
export NAME=vm-default-images
export NAMESPACE=cozy-system
include ../../../hack/package.mk

View file

@ -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 }}

View file

@ -0,0 +1,169 @@
##
## @section Global Golden Image Collection
##
## @param {string} storageClass - Default StorageClass for all images. If empty, uses the cluster default StorageClass.
## NOTE: The default set of images requires approximately 320Gi of storage (16 images × 20Gi each).
## Adjust the image list or per-image storage sizes to match your cluster capacity.
storageClass: "replicated"
## @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-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-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)"

View file

@ -8,7 +8,7 @@ spec:
singular: vmdisk
plural: vmdisks
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"source":{"description":"The source image location used to create a disk.","type":"object","default":{},"properties":{"http":{"description":"Download image from an HTTP source.","type":"object","required":["url"],"properties":{"url":{"description":"URL to download the image.","type":"string"}}},"image":{"description":"Use image by name.","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`).","type":"string"}}},"upload":{"description":"Upload local image.","type":"object"}}},"optical":{"description":"Defines if disk should be considered optical.","type":"boolean","default":false},"storage":{"description":"The size of the disk allocated for the virtual machine.","default":"5Gi","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},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"replicated"}}}
{"title":"Chart Values","type":"object","properties":{"source":{"description":"The source image location used to create a disk.","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","required":["url"],"properties":{"url":{"description":"URL to download the image.","type":"string"}}},"image":{"description":"Use image by name from default collection.","type":"object","required":["name"],"properties":{"name":{"description":"Name of the image to use.","type":"string"}}},"upload":{"description":"Upload local image.","type":"object"}}},"optical":{"description":"Defines if disk should be considered optical.","type":"boolean","default":false},"storage":{"description":"The size of the disk allocated for the virtual machine.","default":"5Gi","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},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":"replicated"}}}
release:
prefix: vm-disk-
labels: