Skip to content

Commit bec35e3

Browse files
[vm-default-images] Added new optional package
Signed-off-by: Myasnikov Daniil <[email protected]>
1 parent ad52992 commit bec35e3

13 files changed

Lines changed: 448 additions & 29 deletions

File tree

‎api/apps/v1alpha1/vmdisk/types.go‎

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎hack/cdi_golden_image_create.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ kubectl create -f - <<EOF
1616
apiVersion: cdi.kubevirt.io/v1beta1
1717
kind: DataVolume
1818
metadata:
19-
name: "vm-image-$name"
19+
name: "vm-default-images-$name"
2020
namespace: cozy-public
2121
annotations:
2222
cdi.kubevirt.io/storage.bind.immediate.requested: "true"

‎internal/controller/dashboard/customformsoverride.go‎

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ func (m *Manager) ensureCFOMapping(ctx context.Context, crd *cozyv1alpha1.Applic
141141
}
142142

143143
// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString
144-
// for all string fields inside spec that don't have enum
144+
// for all string fields inside spec that don't have enum.
145+
// It handles two structures:
146+
// - properties.spec.properties (most resources)
147+
// - properties.properties (VMDisk and similar resources without spec wrapper)
145148
func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
146149
if openAPISchema == "" {
147150
return map[string]any{}, nil
@@ -161,15 +164,25 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
161164
"properties": map[string]any{},
162165
}
163166

164-
// Check if there's a spec property
165-
specProp, ok := props["spec"].(map[string]any)
166-
if !ok {
167-
return map[string]any{}, nil
167+
var specProps map[string]any
168+
var hasSpec bool
169+
170+
// First try to find properties under spec
171+
if specProp, ok := props["spec"].(map[string]any); ok {
172+
specProps, hasSpec = specProp["properties"].(map[string]any)
168173
}
169174

170-
specProps, ok := specProp["properties"].(map[string]any)
171-
if !ok {
172-
return map[string]any{}, nil
175+
// If no spec wrapper, use top-level properties directly (VMDisk pattern)
176+
if !hasSpec {
177+
specProps = props
178+
// Still wrap in spec for consistency with applyListInputOverrides
179+
schemaProps := schema["properties"].(map[string]any)
180+
specSchema := map[string]any{
181+
"properties": map[string]any{},
182+
}
183+
schemaProps["spec"] = specSchema
184+
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
185+
return schema, nil
173186
}
174187

175188
// Create spec.properties structure in schema
@@ -231,10 +244,41 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma
231244
}
232245

233246
case "ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
234-
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk":
247+
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis":
248+
specProps := ensureSchemaPath(schema, "spec")
249+
specProps["storageClass"] = storageClassListInput()
250+
251+
case "VMDisk":
235252
specProps := ensureSchemaPath(schema, "spec")
236253
specProps["storageClass"] = storageClassListInput()
237254

255+
// Override source.image.name to be an API-backed dropdown listing default images
256+
if sourceObj, ok := specProps["source"].(map[string]any); ok {
257+
if imgProps, ok := sourceObj["properties"].(map[string]any); ok {
258+
if imgName, ok := imgProps["image"].(map[string]any); ok {
259+
imgName["properties"].(map[string]any)["name"] = map[string]any{
260+
"type": "listInput",
261+
"customProps": map[string]any{
262+
"valueUri": "/api/clusters/{cluster}/k8s/apis/cdi.kubevirt.io/v1beta1/namespaces/cozy-public/datavolumes",
263+
"keysToValue": []any{"metadata", "annotations", "vm-default-images.cozystack.io/name"},
264+
"keysToLabel": []any{"metadata", "annotations", "vm-default-images.cozystack.io/description"},
265+
},
266+
}
267+
}
268+
// Override source.disk.name to be an API-backed dropdown listing VMDisk resources
269+
if diskName, ok := imgProps["disk"].(map[string]any); ok {
270+
diskName["properties"].(map[string]any)["name"] = map[string]any{
271+
"type": "listInput",
272+
"customProps": map[string]any{
273+
"valueUri": "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks",
274+
"keysToValue": []any{"metadata", "name"},
275+
"keysToLabel": []any{"metadata", "name"},
276+
},
277+
}
278+
}
279+
}
280+
}
281+
238282
case "FoundationDB":
239283
storageProps := ensureSchemaPath(schema, "spec", "storage")
240284
storageProps["storageClass"] = storageClassListInput()

‎internal/controller/dashboard/customformsoverride_test.go‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,108 @@ func TestApplyListInputOverrides_StorageClassFoundationDB(t *testing.T) {
317317
assertStorageClassListInput(t, sc)
318318
}
319319

320+
func TestApplyListInputOverrides_VMDisk_SourceFields(t *testing.T) {
321+
openAPISchema := `{
322+
"properties":{
323+
"optical":{"type":"boolean"},
324+
"source":{
325+
"type":"object",
326+
"properties":{
327+
"image":{
328+
"type":"object",
329+
"properties":{
330+
"name":{"type":"string"}
331+
}
332+
},
333+
"disk":{
334+
"type":"object",
335+
"properties":{
336+
"name":{"type":"string"}
337+
}
338+
}
339+
}
340+
},
341+
"storage":{"type":"string"},
342+
"storageClass":{"type":"string"}
343+
}
344+
}`
345+
346+
schema, err := buildMultilineStringSchema(openAPISchema)
347+
if err != nil {
348+
t.Fatalf("buildMultilineStringSchema failed: %v", err)
349+
}
350+
351+
applyListInputOverrides(schema, "VMDisk", map[string]any{})
352+
353+
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
354+
355+
// Check storageClass
356+
sc, ok := specProps["storageClass"].(map[string]any)
357+
if !ok {
358+
t.Fatal("storageClass not found in spec.properties")
359+
}
360+
assertStorageClassListInput(t, sc)
361+
362+
// Check source.image.name listInput
363+
// Structure: specProps["source"]["properties"]["image"]["properties"]["name"]
364+
sourceObj, ok := specProps["source"].(map[string]any)
365+
if !ok {
366+
t.Fatal("source not found in spec.properties")
367+
}
368+
sourceObjProps, ok := sourceObj["properties"].(map[string]any)
369+
if !ok {
370+
t.Fatal("source.properties not found")
371+
}
372+
imageObj, ok := sourceObjProps["image"].(map[string]any)
373+
if !ok {
374+
t.Fatal("image not found in source.properties")
375+
}
376+
imageObjProps, ok := imageObj["properties"].(map[string]any)
377+
if !ok {
378+
t.Fatal("image.properties not found")
379+
}
380+
imgName, ok := imageObjProps["name"].(map[string]any)
381+
if !ok {
382+
t.Fatal("name not found in image.properties")
383+
}
384+
if imgName["type"] != "listInput" {
385+
t.Errorf("expected type listInput, got %v", imgName["type"])
386+
}
387+
imgNameCustomProps, ok := imgName["customProps"].(map[string]any)
388+
if !ok {
389+
t.Fatal("name.customProps not found")
390+
}
391+
expectedImageURI := "/api/clusters/{cluster}/k8s/apis/cdi.kubevirt.io/v1beta1/namespaces/cozy-public/datavolumes"
392+
if imgNameCustomProps["valueUri"] != expectedImageURI {
393+
t.Errorf("expected valueUri %s, got %v", expectedImageURI, imgNameCustomProps["valueUri"])
394+
}
395+
396+
// Check source.disk.name listInput
397+
diskObj, ok := sourceObjProps["disk"].(map[string]any)
398+
if !ok {
399+
t.Fatal("disk not found in source.properties")
400+
}
401+
diskObjProps, ok := diskObj["properties"].(map[string]any)
402+
if !ok {
403+
t.Fatal("disk.properties not found")
404+
}
405+
diskName, ok := diskObjProps["name"].(map[string]any)
406+
if !ok {
407+
t.Fatal("name not found in disk.properties")
408+
}
409+
if diskName["type"] != "listInput" {
410+
t.Errorf("expected type listInput, got %v", diskName["type"])
411+
}
412+
diskNameCustomProps, ok := diskName["customProps"].(map[string]any)
413+
if !ok {
414+
t.Fatal("disk name.customProps not found")
415+
}
416+
expectedDiskURI := "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks"
417+
if diskNameCustomProps["valueUri"] != expectedDiskURI {
418+
t.Errorf("expected valueUri %s, got %v", expectedDiskURI, diskNameCustomProps["valueUri"])
419+
}
420+
}
421+
320422
func TestApplyListInputOverrides_StorageClassKafka(t *testing.T) {
321423
schema := map[string]any{}
322424
applyListInputOverrides(schema, "Kafka", map[string]any{})

‎packages/apps/vm-disk/README.md‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ A Virtual Machine Disk
66

77
### Common parameters
88

9-
| Name | Description | Type | Value |
10-
| ------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------ |
11-
| `source` | The source image location used to create a disk. | `object` | `{}` |
12-
| `source.image` | Use image by name. | `*object` | `null` |
13-
| `source.image.name` | Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`). | `string` | `""` |
14-
| `source.upload` | Upload local image. | `*object` | `null` |
15-
| `source.http` | Download image from an HTTP source. | `*object` | `null` |
16-
| `source.http.url` | URL to download the image. | `string` | `""` |
17-
| `optical` | Defines if disk should be considered optical. | `bool` | `false` |
18-
| `storage` | The size of the disk allocated for the virtual machine. | `quantity` | `5Gi` |
19-
| `storageClass` | StorageClass used to store the data. | `string` | `replicated` |
9+
| Name | Description | Type | Value |
10+
| ------------------- | ------------------------------------------------------- | ---------- | ------------ |
11+
| `source` | The source image location used to create a disk. | `object` | `{}` |
12+
| `source.image` | Use image by name from default collection. | `*object` | `null` |
13+
| `source.image.name` | Name of the image to use. | `string` | `""` |
14+
| `source.upload` | Upload local image. | `*object` | `null` |
15+
| `source.http` | Download image from an HTTP source. | `*object` | `null` |
16+
| `source.http.url` | URL to download the image. | `string` | `""` |
17+
| `source.disk` | Clone an existing vm-disk. | `*object` | `null` |
18+
| `source.disk.name` | Name of the vm-disk to clone. | `string` | `""` |
19+
| `optical` | Defines if disk should be considered optical. | `bool` | `false` |
20+
| `storage` | The size of the disk allocated for the virtual machine. | `quantity` | `5Gi` |
21+
| `storageClass` | StorageClass used to store the data. | `string` | `replicated` |
2022

‎packages/apps/vm-disk/templates/dv.yaml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ spec:
2121
{{- end }}
2222
source:
2323
{{- if hasKey .Values.source "image" }}
24-
{{- $dv := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" "cozy-public" (printf "vm-image-%s" .Values.source.image.name) }}
24+
{{- $dv := lookup "cdi.kubevirt.io/v1beta1" "DataVolume" "cozy-public" (printf "vm-default-images-%s" .Values.source.image.name) }}
2525
pvc:
26-
name: vm-image-{{ required "A valid .Values.source.image.name entry required!" .Values.source.image.name }}
26+
name: vm-default-images-{{ required "A valid .Values.source.image.name entry required!" .Values.source.image.name }}
2727
namespace: cozy-public
2828
{{- else if hasKey .Values.source "http" }}
2929
http:
3030
url: {{ required "A valid .Values.source.http.url entry required!" .Values.source.http.url }}
3131
{{- else if hasKey .Values.source "upload" }}
3232
upload: {}
33+
{{- else if hasKey .Values.source "disk" }}
34+
pvc:
35+
name: vm-disk-{{ required "A valid .Values.source.disk.name entry required!" .Values.source.disk.name }}
36+
namespace: {{ $.Release.Namespace }}
3337
{{- end }}
3438
{{- else }}
3539
source:

‎packages/apps/vm-disk/values.schema.json‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@
77
"type": "object",
88
"default": {},
99
"properties": {
10+
"disk": {
11+
"description": "Clone an existing vm-disk.",
12+
"type": "object",
13+
"required": [
14+
"name"
15+
],
16+
"properties": {
17+
"name": {
18+
"description": "Name of the vm-disk to clone.",
19+
"type": "string"
20+
}
21+
}
22+
},
1023
"http": {
1124
"description": "Download image from an HTTP source.",
1225
"type": "object",
@@ -21,14 +34,14 @@
2134
}
2235
},
2336
"image": {
24-
"description": "Use image by name.",
37+
"description": "Use image by name from default collection.",
2538
"type": "object",
2639
"required": [
2740
"name"
2841
],
2942
"properties": {
3043
"name": {
31-
"description": "Name of the image to use (uploaded as \"golden image\" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).",
44+
"description": "Name of the image to use.",
3245
"type": "string"
3346
}
3447
}

‎packages/apps/vm-disk/values.yaml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
##
44

55
## @typedef {struct} SourceImage - Use image by name.
6-
## @field {string} name - Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).
6+
## @field {string} name - Name of the image to use.
77

88
## @typedef {struct} SourceUpload - Upload local image.
99

1010
## @typedef {struct} SourceHTTP - Download image from an HTTP source.
1111
## @field {string} url - URL to download the image.
1212

13+
## @typedef {struct} SourceDisk - Clone an existing vm-disk.
14+
## @field {string} name - Name of the vm-disk to clone.
15+
1316
## @typedef {struct} Source - The source image location used to create a disk.
14-
## @field {*SourceImage} [image] - Use image by name.
17+
## @field {*SourceImage} [image] - Use image by name from default collection.
1518
## @field {*SourceUpload} [upload] - Upload local image.
1619
## @field {*SourceHTTP} [http] - Download image from an HTTP source.
20+
## @field {*SourceDisk} [disk] - Clone an existing vm-disk.
1721

1822
## @param {Source} source - The source image location used to create a disk.
1923
source: {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
apiVersion: cozystack.io/v1alpha1
3+
kind: PackageSource
4+
metadata:
5+
name: cozystack.vm-default-images
6+
spec:
7+
sourceRef:
8+
kind: OCIRepository
9+
name: cozystack-packages
10+
namespace: cozy-system
11+
path: /
12+
variants:
13+
- name: default
14+
dependsOn:
15+
- cozystack.networking
16+
- cozystack.kubevirt-cdi
17+
components:
18+
- name: vm-default-images
19+
path: system/vm-default-images
20+
install:
21+
namespace: cozy-system
22+
releaseName: vm-default-images
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: v2
2+
name: vm-default-images
3+
description: Global Golden Image collection for virtual machines
4+
type: application
5+
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process

0 commit comments

Comments
 (0)