Repository: lima-vm/lima

Issue:
Embedded json templates are failing jsonschema validation
### Description

The files do not match the limayaml definition:

```go
type LimaYAML struct {
        Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty" jsonschema:"nullable"`
...
        Images                []Image       `yaml:"images" json:"images"` // REQUIRED
```

```go
type BaseTemplates []LocatorWithDigest

type LocatorWithDigest struct {
        URL    string  `yaml:"url" json:"url"`
        Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
}
```

There is no alternative, for string only (no wrapper)

```yaml
minimumLimaVersion: 1.1.0

base:
- template://_images/ubuntu-24.10
- template://_default/mounts
```

```
- at '': validation failed
  - at '': missing property 'images'
  - at '/base': oneOf failed, none matched
    - at '/base': validation failed
      - at '/base/0': got string, want object
      - at '/base/1': got string, want object
    - at '/base': got array, want null 
```

And it probably doesn't need to use "nullable" either?

```yaml
base: null
```

```yaml
- url: template://_images/default
  digest: null
```

It was mostly there for legacy (empty) yaml mappings.


Discussion:
@afbjorklund: Previously there were patches to the yaml loader, to expand string values into objects.

* https://github.com/lima-vm/lima/pull/1516

But these new files need to be "preprocessed", to embed all the documents into yaml.

~~So we need to use `limayaml.Unmarshal`, instead of `yaml.Unmarshal`~~

~~We don't have any examples that use Disk, so that's why it didn't fail before.~~

Here is the code, that patches the schema.json to allow for Disk to be either:

```go
        schema := jsonschema.Reflect(&limayaml.LimaYAML{})
        // allow Disk to be either string (name) or object (struct)
        schema.Definitions["Disk"].Type = "" // was: "object"
        schema.Definitions["Disk"].OneOf = []*jsonschema.Schema{
                {Type: "string"},
                {Type: "object"},
        }
```

So it needs to be extended for LocatorWithDigest, and then the verfier should embed.

```
Schema validation errors were encountered.
  templates/ubuntu.yaml::$: 'images' is a required property
```

Also needs patching for BaseTemplates, which can be a single string instead of a list.

Solution patch:
diff --git a/Makefile b/Makefile
index 5f679955..c0b576d0 100644
--- a/Makefile
+++ b/Makefile
@@ -436,14 +436,15 @@ ifeq ($(native_compiling),true)
 	$< tmpl copy --embed-all templates/default.yaml $@
 endif
 
-schema-limayaml.json: _output/bin/limactl$(exe) default-template.yaml
+schema-limayaml.json: _output/bin/limactl$(exe) templates/default.yaml default-template.yaml
 ifeq ($(native_compiling),true)
-	$< generate-jsonschema --schemafile $@ default-template.yaml
+	# validate both the original template (with the "base" etc), and the embedded template
+	$< generate-jsonschema --schemafile $@ templates/default.yaml default-template.yaml
 endif
 
 .PHONY: check-jsonschema
-check-jsonschema: schema-limayaml.json default-template.yaml
-	check-jsonschema --schemafile schema-limayaml.json default-template.yaml
+check-jsonschema: schema-limayaml.json templates/default.yaml default-template.yaml
+	check-jsonschema --schemafile schema-limayaml.json templates/default.yaml default-template.yaml
 
 ################################################################################
 .PHONY: diagrams
diff --git a/cmd/limactl/genschema.go b/cmd/limactl/genschema.go
index 8de619fd..296db8ee 100644
--- a/cmd/limactl/genschema.go
+++ b/cmd/limactl/genschema.go
@@ -60,6 +60,18 @@ func genschemaAction(cmd *cobra.Command, args []string) error {
 		{Type: "string"},
 		{Type: "object"},
 	}
+	// allow BaseTemplates to be either string (url) or array (array)
+	schema.Definitions["BaseTemplates"].Type = "" // was: "array"
+	schema.Definitions["BaseTemplates"].OneOf = []*jsonschema.Schema{
+		{Type: "string"},
+		{Type: "array"},
+	}
+	// allow LocatorWithDigest to be either string (url) or object (struct)
+	schema.Definitions["LocatorWithDigest"].Type = "" // was: "object"
+	schema.Definitions["LocatorWithDigest"].OneOf = []*jsonschema.Schema{
+		{Type: "string"},
+		{Type: "object"},
+	}
 	properties := schema.Definitions["LimaYAML"].Properties
 	getProp(properties, "os").Enum = toAny(limayaml.OSTypes)
 	getProp(properties, "arch").Enum = toAny(limayaml.ArchTypes)
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 4082b82d..d72e5b60 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -10,13 +10,13 @@ import (
 )
 
 type LimaYAML struct {
-	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty" jsonschema:"nullable"`
+	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty"`
 	MinimumLimaVersion    *string       `yaml:"minimumLimaVersion,omitempty" json:"minimumLimaVersion,omitempty" jsonschema:"nullable"`
 	VMType                *VMType       `yaml:"vmType,omitempty" json:"vmType,omitempty" jsonschema:"nullable"`
 	VMOpts                VMOpts        `yaml:"vmOpts,omitempty" json:"vmOpts,omitempty"`
 	OS                    *OS           `yaml:"os,omitempty" json:"os,omitempty" jsonschema:"nullable"`
 	Arch                  *Arch         `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"nullable"`
-	Images                []Image       `yaml:"images" json:"images"` // REQUIRED
+	Images                []Image       `yaml:"images,omitempty" json:"images,omitempty" jsonschema:"nullable"`
 	CPUType               CPUType       `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"`
 	CPUs                  *int          `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"`
 	Memory                *string       `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
@@ -58,7 +58,7 @@ type BaseTemplates []LocatorWithDigest
 
 type LocatorWithDigest struct {
 	URL    string  `yaml:"url" json:"url"`
-	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
+	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty"` // TODO currently unused
 }
 
 type (
@@ -229,7 +229,7 @@ const (
 type Provision struct {
 	Mode                            ProvisionMode      `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
 	SkipDefaultDependencyResolution *bool              `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
-	Script                          string             `yaml:"script" json:"script"`
+	Script                          string             `yaml:"script,omitempty" json:"script,omitempty"`
 	File                            *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
 	Playbook                        string             `yaml:"playbook,omitempty" json:"playbook,omitempty"` // DEPRECATED
 	// All ProvisionData fields must be nil unless Mode is ProvisionModeData
diff --git a/pkg/limayaml/marshal.go b/pkg/limayaml/marshal.go
index 0380fa9b..70caee2f 100644
--- a/pkg/limayaml/marshal.go
+++ b/pkg/limayaml/marshal.go
@@ -45,11 +45,6 @@ func unmarshalBaseTemplates(dst *BaseTemplates, b []byte) error {
 		*dst = BaseTemplates{LocatorWithDigest{URL: s}}
 		return nil
 	}
-	var locator LocatorWithDigest
-	if err := yaml.Unmarshal(b, &locator); err == nil {
-		*dst = BaseTemplates{locator}
-		return nil
-	}
 	return yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest))
 }
 



Test patch:
diff --git a/pkg/limatmpl/abs_test.go b/pkg/limatmpl/abs_test.go
index b4f192f6..fbf7c19b 100644
--- a/pkg/limatmpl/abs_test.go
+++ b/pkg/limatmpl/abs_test.go
@@ -35,9 +35,9 @@ var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
 	{
 		"Flow style array of one base template",
 		"template://foo",
-		`base: {url: bar.yaml, digest: deadbeef}`,
+		`base: [{url: bar.yaml, digest: deadbeef}]`,
 		// not sure why the quotes around the URL were added; maybe because we don't copy the style from the source
-		`base: {url: 'template://bar.yaml', digest: deadbeef}`,
+		`base: [{url: 'template://bar.yaml', digest: deadbeef}]`,
 	},
 	{
 		"Flow style array of sequence of two base URLs",
diff --git a/pkg/limatmpl/embed_test.go b/pkg/limatmpl/embed_test.go
index a8e19291..e1db9ba2 100644
--- a/pkg/limatmpl/embed_test.go
+++ b/pkg/limatmpl/embed_test.go
@@ -374,13 +374,7 @@ provision:
 		`provision: [{script: "#! my script"}]`,
 	},
 	{
-		"ERROR base digest is not yet implemented (1)",
-		"",
-		"base: {url: base.yaml, digest: deafbad}",
-		"not yet implemented",
-	},
-	{
-		"ERROR base digest is not yet implemented (2)",
+		"ERROR base digest is not yet implemented",
 		"",
 		"base: [{url: base.yaml, digest: deafbad}]",
 		"not yet implemented",
diff --git a/pkg/limayaml/limayaml_test.go b/pkg/limayaml/limayaml_test.go
index a0320232..39c929ea 100644
--- a/pkg/limayaml/limayaml_test.go
+++ b/pkg/limayaml/limayaml_test.go
@@ -17,7 +17,7 @@ func dumpJSON(t *testing.T, d any) string {
 	return string(b)
 }
 
-const emptyYAML = "images: []\n"
+const emptyYAML = "{}\n"
 
 func TestEmptyYAML(t *testing.T) {
 	var y LimaYAML
@@ -27,7 +27,7 @@ func TestEmptyYAML(t *testing.T) {
 	assert.Equal(t, string(b), emptyYAML)
 }
 
-const defaultYAML = "images: []\n"
+const defaultYAML = "{}\n"
 
 func TestDefaultYAML(t *testing.T) {
 	bytes, err := os.ReadFile("default.yaml")
diff --git a/pkg/limayaml/marshal_test.go b/pkg/limayaml/marshal_test.go
index 29f2f72c..0f77b11f 100644
--- a/pkg/limayaml/marshal_test.go
+++ b/pkg/limayaml/marshal_test.go
@@ -28,7 +28,6 @@ func TestMarshalTilde(t *testing.T) {
 	// yaml will load ~ (or null) as null
 	// make sure that it is always quoted
 	assert.Equal(t, string(b), `---
-images: []
 mounts:
 - location: "~"
   writable: false