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:
commit
482d813d01
22 changed files with 613 additions and 56 deletions
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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()
|
||||
|
|
|
|||
|
|
@ -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{})
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
52
packages/core/platform/images/migrations/migrations/38
Executable file
52
packages/core/platform/images/migrations/migrations/38
Executable 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-
|
||||
22
packages/core/platform/sources/vm-default-images.yaml
Normal file
22
packages/core/platform/sources/vm-default-images.yaml
Normal 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
|
||||
|
|
@ -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" $) }}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
5
packages/system/vm-default-images/Chart.yaml
Normal file
5
packages/system/vm-default-images/Chart.yaml
Normal 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
|
||||
4
packages/system/vm-default-images/Makefile
Normal file
4
packages/system/vm-default-images/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export NAME=vm-default-images
|
||||
export NAMESPACE=cozy-system
|
||||
|
||||
include ../../../hack/package.mk
|
||||
45
packages/system/vm-default-images/templates/dv.yaml
Normal file
45
packages/system/vm-default-images/templates/dv.yaml
Normal 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 }}
|
||||
169
packages/system/vm-default-images/values.yaml
Normal file
169
packages/system/vm-default-images/values.yaml
Normal 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)"
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue