Repository: lima-vm/lima

Issue:
pkg/qemu: split qemu-img functions to imgutil
https://github.com/lima-vm/lima/blob/1f787fb94457ddc15fc44ba1d0928ed57469c305/pkg/qemu/qemu.go#L130-L154

These functions above should be moved from `pkg/qemu` to `pkg/qemu/imgutil`, so that `cmd/limactl` does not need to depend on `pkg/qemu`

https://github.com/lima-vm/lima/blob/1f787fb94457ddc15fc44ba1d0928ed57469c305/cmd/limactl/disk.go#L117



Discussion:

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/disk.go b/cmd/limactl/disk.go
index f5d17136..44e09bdb 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -16,7 +16,7 @@ import (
 	"github.com/docker/go-units"
 	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/lima/pkg/nativeimgutil"
-	"github.com/lima-vm/lima/pkg/qemu"
+	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
@@ -114,7 +114,7 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 	if format == "raw" {
 		err = nativeimgutil.CreateRawDataDisk(diskDir, int(diskSize))
 	} else {
-		err = qemu.CreateDataDisk(diskDir, format, int(diskSize))
+		err = imgutil.CreateDataDisk(diskDir, format, int(diskSize))
 	}
 	if err != nil {
 		rerr := os.RemoveAll(diskDir)
@@ -410,7 +410,7 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 	if disk.Format == "raw" {
 		err = nativeimgutil.ResizeRawDataDisk(disk.Dir, int(diskSize))
 	} else {
-		err = qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
+		err = imgutil.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
 	}
 	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
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))
 }
 
diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go
index 2fffbfd1..688c5787 100644
--- a/pkg/qemu/imgutil/imgutil.go
+++ b/pkg/qemu/imgutil/imgutil.go
@@ -6,9 +6,15 @@ package imgutil
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"io/fs"
+	"os"
 	"os/exec"
+	"path/filepath"
+	"strconv"
 
+	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
 )
 
@@ -22,6 +28,32 @@ type InfoFormatSpecific struct {
 	Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
 }
 
+func CreateDataDisk(dir, format string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		// datadisk already exists
+		return err
+	}
+
+	args := []string{"create", "-f", format, dataDisk, strconv.Itoa(size)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
+func ResizeDataDisk(dir, format string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+
+	args := []string{"resize", "-f", format, dataDisk, strconv.Itoa(size)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
 func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
 	if sp.Type != "qcow2" {
 		return nil
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 4211d2e6..b6ede13d 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -127,32 +127,6 @@ func EnsureDisk(ctx context.Context, cfg Config) error {
 	return nil
 }
 
-func CreateDataDisk(dir, format string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		// datadisk already exists
-		return err
-	}
-
-	args := []string{"create", "-f", format, dataDisk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
-func ResizeDataDisk(dir, format string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-
-	args := []string{"resize", "-f", format, dataDisk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
 func newQmpClient(cfg Config) (*qmp.SocketMonitor, error) {
 	qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)
 	qmpClient, err := qmp.NewSocketMonitor("unix", qmpSock, 5*time.Second)



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