From f50549912401561d5961d2f0d8f34cb4d83fc2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 23 Aug 2025 20:34:46 +0200 Subject: [PATCH] Convert limayaml.VMType to an abstract map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The format of the VMOpts is known only to the driver, everyone else will see a basic map[string]any or something similar to it. Converting from the abstract format to the actual format is done using YAML, just like it was before when the format was known. Signed-off-by: Anders F Björklund --- pkg/driver/qemu/qemu.go | 20 ++++- pkg/driver/qemu/qemu_driver.go | 31 +++++-- pkg/driver/vz/vm_darwin.go | 7 +- pkg/driver/vz/vz_driver_darwin.go | 43 ++++++--- pkg/limatmpl/embed.go | 15 +++- pkg/limatype/lima_yaml.go | 9 +- pkg/limayaml/limayaml_test.go | 1 + pkg/limayaml/marshal.go | 17 ++++ pkg/limayaml/marshal_test.go | 143 ++++++++++++++++++++++++++++++ templates/docker-rootful.yaml | 2 +- templates/docker.yaml | 2 +- 11 files changed, 256 insertions(+), 34 deletions(-) diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go index c3d3b6e31d0..529b37471c8 100644 --- a/pkg/driver/qemu/qemu.go +++ b/pkg/driver/qemu/qemu.go @@ -449,7 +449,11 @@ func defaultCPUType() limatype.CPUType { func resolveCPUType(y *limatype.LimaYAML) string { cpuType := defaultCPUType() var overrideCPUType bool - for k, v := range y.VMOpts.QEMU.CPUType { + var qemuOpts limatype.QEMUOpts + if err := limayaml.Convert(y.VMOpts[limatype.QEMU], &qemuOpts, "vmOpts.qemu"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", y.VMOpts[limatype.QEMU]) + } + for k, v := range qemuOpts.CPUType { if !slices.Contains(limatype.ArchTypes, *y.Arch) { logrus.Warnf("field `vmOpts.qemu.cpuType` uses unsupported arch %q", k) continue @@ -460,7 +464,11 @@ func resolveCPUType(y *limatype.LimaYAML) string { } } if overrideCPUType { - y.VMOpts.QEMU.CPUType = cpuType + qemuOpts.CPUType = cpuType + if y.VMOpts == nil { + y.VMOpts = limatype.VMOpts{} + } + y.VMOpts[limatype.QEMU] = qemuOpts } return cpuType[*y.Arch] @@ -490,8 +498,12 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er if version.LessThan(softMin) { logrus.Warnf("QEMU %v is too old, %v or later is recommended", version, softMin) } - if y.VMOpts.QEMU.MinimumVersion != nil && version.LessThan(*semver.New(*y.VMOpts.QEMU.MinimumVersion)) { - logrus.Fatalf("QEMU %v is too old, template requires %q or later", version, *y.VMOpts.QEMU.MinimumVersion) + var qemuOpts limatype.QEMUOpts + if err := limayaml.Convert(y.VMOpts[limatype.QEMU], &qemuOpts, "vmOpts.qemu"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", y.VMOpts[limatype.QEMU]) + } + if qemuOpts.MinimumVersion != nil && version.LessThan(*semver.New(*qemuOpts.MinimumVersion)) { + logrus.Fatalf("QEMU %v is too old, template requires %q or later", version, *qemuOpts.MinimumVersion) } } diff --git a/pkg/driver/qemu/qemu_driver.go b/pkg/driver/qemu/qemu_driver.go index 78f77140d2a..339bca2c9b2 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -104,9 +104,13 @@ func validateConfig(cfg *limatype.LimaYAML) error { } } - if cfg.VMOpts.QEMU.MinimumVersion != nil { - if _, err := semver.NewVersion(*cfg.VMOpts.QEMU.MinimumVersion); err != nil { - return fmt.Errorf("field `vmOpts.qemu.minimumVersion` must be a semvar value, got %q: %w", *cfg.VMOpts.QEMU.MinimumVersion, err) + var qemuOpts limatype.QEMUOpts + if err := limayaml.Convert(cfg.VMOpts[limatype.QEMU], &qemuOpts, "vmOpts.qemu"); err != nil { + return err + } + if qemuOpts.MinimumVersion != nil { + if _, err := semver.NewVersion(*qemuOpts.MinimumVersion); err != nil { + return fmt.Errorf("field `vmOpts.qemu.minimumVersion` must be a semvar value, got %q: %w", *qemuOpts.MinimumVersion, err) } } @@ -154,8 +158,12 @@ func (l *LimaQemuDriver) FillConfig(_ context.Context, cfg *limatype.LimaYAML, f cfg.Video.VNC.Display = ptr.Of("127.0.0.1:0,to=9") } - if cfg.VMOpts.QEMU.CPUType == nil { - cfg.VMOpts.QEMU.CPUType = limatype.CPUType{} + var qemuOpts limatype.QEMUOpts + if err := limayaml.Convert(cfg.VMOpts[limatype.QEMU], &qemuOpts, "vmOpts.qemu"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", cfg.VMOpts[limatype.QEMU]) + } + if qemuOpts.CPUType == nil { + qemuOpts.CPUType = limatype.CPUType{} } //nolint:staticcheck // Migration of top-level CPUTYPE if specified @@ -165,13 +173,22 @@ func (l *LimaQemuDriver) FillConfig(_ context.Context, cfg *limatype.LimaYAML, f if v == "" { continue } - if existing, ok := cfg.VMOpts.QEMU.CPUType[arch]; ok && existing != "" && existing != v { + if existing, ok := qemuOpts.CPUType[arch]; ok && existing != "" && existing != v { logrus.Warnf("Conflicting cpuType for arch %q: top-level=%q, vmOpts.qemu=%q; using vmOpts.qemu value", arch, v, existing) continue } - cfg.VMOpts.QEMU.CPUType[arch] = v + qemuOpts.CPUType[arch] = v } cfg.CPUType = nil + + var opts any + if err := limayaml.Convert(qemuOpts, &opts, ""); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %+v", qemuOpts) + } + if cfg.VMOpts == nil { + cfg.VMOpts = limatype.VMOpts{} + } + cfg.VMOpts[limatype.QEMU] = opts } mountTypesUnsupported := make(map[string]struct{}) diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index 8d1a0577d43..ec45e39832f 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -602,7 +602,12 @@ func attachFolderMounts(inst *limatype.Instance, vmConfig *vz.VirtualMachineConf } } - if *inst.Config.VMOpts.VZ.Rosetta.Enabled { + var vzOpts limatype.VZOpts + if err := limayaml.Convert(inst.Config.VMOpts[limatype.VZ], &vzOpts, "vmOpts.vz"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", inst.Config.VMOpts[limatype.VZ]) + } + + if vzOpts.Rosetta.Enabled != nil && *vzOpts.Rosetta.Enabled { logrus.Info("Setting up Rosetta share") directorySharingDeviceConfig, err := createRosettaDirectoryShareConfiguration() if err != nil { diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index ec901b73227..927e9b90d26 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -108,13 +108,20 @@ func (l *LimaVzDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriv } } + var vzOpts limatype.VZOpts + if l.Instance.Config.VMOpts[limatype.VZ] != nil { + if err := limayaml.Convert(l.Instance.Config.VMOpts[limatype.VZ], &vzOpts, "vmOpts.vz"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", l.Instance.Config.VMOpts[limatype.VZ]) + } + } + if runtime.GOOS == "darwin" && limayaml.IsNativeArch(limatype.AARCH64) { - if l.Instance.Config.VMOpts.VZ.Rosetta.Enabled != nil { - l.rosettaEnabled = *l.Instance.Config.VMOpts.VZ.Rosetta.Enabled + if vzOpts.Rosetta.Enabled != nil { + l.rosettaEnabled = *vzOpts.Rosetta.Enabled } } - if l.Instance.Config.VMOpts.VZ.Rosetta.BinFmt != nil { - l.rosettaBinFmt = *l.Instance.Config.VMOpts.VZ.Rosetta.BinFmt + if vzOpts.Rosetta.BinFmt != nil { + l.rosettaBinFmt = *vzOpts.Rosetta.BinFmt } return &driver.ConfiguredDriver{ @@ -131,22 +138,36 @@ func (l *LimaVzDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, _ cfg.MountType = ptr.Of(limatype.VIRTIOFS) } + var vzOpts limatype.VZOpts + if err := limayaml.Convert(cfg.VMOpts[limatype.VZ], &vzOpts, "vmOpts.vz"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", cfg.VMOpts[limatype.VZ]) + } + //nolint:staticcheck // Migration of top-level Rosetta if specified - if (cfg.VMOpts.VZ.Rosetta.Enabled == nil && cfg.VMOpts.VZ.Rosetta.BinFmt == nil) && (!isEmpty(cfg.Rosetta)) { + if (vzOpts.Rosetta.Enabled == nil && vzOpts.Rosetta.BinFmt == nil) && (!isEmpty(cfg.Rosetta)) { logrus.Debug("Migrating top-level Rosetta configuration to vmOpts.vz.rosetta") - cfg.VMOpts.VZ.Rosetta = cfg.Rosetta + vzOpts.Rosetta = cfg.Rosetta } //nolint:staticcheck // Warning about both top-level and vmOpts.vz.Rosetta being set - if (cfg.VMOpts.VZ.Rosetta.Enabled != nil && cfg.VMOpts.VZ.Rosetta.BinFmt != nil) && (!isEmpty(cfg.Rosetta)) { + if (vzOpts.Rosetta.Enabled != nil && vzOpts.Rosetta.BinFmt != nil) && (!isEmpty(cfg.Rosetta)) { logrus.Warn("Both top-level 'rosetta' and 'vmOpts.vz.rosetta' are configured. Using vmOpts.vz.rosetta. Top-level 'rosetta' is deprecated.") } - if cfg.VMOpts.VZ.Rosetta.Enabled == nil { - cfg.VMOpts.VZ.Rosetta.Enabled = ptr.Of(false) + if vzOpts.Rosetta.Enabled == nil { + vzOpts.Rosetta.Enabled = ptr.Of(false) + } + if vzOpts.Rosetta.BinFmt == nil { + vzOpts.Rosetta.BinFmt = ptr.Of(false) + } + + var opts any + if err := limayaml.Convert(vzOpts, &opts, ""); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %+v", vzOpts) } - if cfg.VMOpts.VZ.Rosetta.BinFmt == nil { - cfg.VMOpts.VZ.Rosetta.BinFmt = ptr.Of(false) + if cfg.VMOpts == nil { + cfg.VMOpts = limatype.VMOpts{} } + cfg.VMOpts[limatype.VZ] = opts return validateConfig(ctx, cfg) } diff --git a/pkg/limatmpl/embed.go b/pkg/limatmpl/embed.go index a505af1d634..c42f81deda0 100644 --- a/pkg/limatmpl/embed.go +++ b/pkg/limatmpl/embed.go @@ -21,6 +21,7 @@ import ( "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/version/versionutil" "github.com/lima-vm/lima/v2/pkg/yqutil" ) @@ -179,9 +180,17 @@ func (tmpl *Template) mergeBase(base *Template) error { tmpl.copyField(minimumLimaVersion, minimumLimaVersion) } } - if tmpl.Config.VMOpts.QEMU.MinimumVersion != nil && base.Config.VMOpts.QEMU.MinimumVersion != nil { - tmplVersion := *semver.New(*tmpl.Config.VMOpts.QEMU.MinimumVersion) - baseVersion := *semver.New(*base.Config.VMOpts.QEMU.MinimumVersion) + var tmplOpts limatype.QEMUOpts + if err := limayaml.Convert(tmpl.Config.VMOpts[limatype.QEMU], &tmplOpts, "vmOpts.qemu"); err != nil { + return err + } + var baseOpts limatype.QEMUOpts + if err := limayaml.Convert(base.Config.VMOpts[limatype.QEMU], &baseOpts, "vmOpts.qemu"); err != nil { + return err + } + if tmplOpts.MinimumVersion != nil && baseOpts.MinimumVersion != nil { + tmplVersion := *semver.New(*tmplOpts.MinimumVersion) + baseVersion := *semver.New(*baseOpts.MinimumVersion) if tmplVersion.LessThan(baseVersion) { const minimumQEMUVersion = "vmOpts.qemu.minimumVersion" tmpl.copyField(minimumQEMUVersion, minimumQEMUVersion) diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index 367f5bc9887..57470c10350 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -20,7 +20,7 @@ type LimaYAML struct { OS *OS `yaml:"os,omitempty" json:"os,omitempty" jsonschema:"nullable"` Arch *Arch `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"nullable"` Images []Image `yaml:"images,omitempty" json:"images,omitempty" jsonschema:"nullable"` - // Deprecated: Use VMOpts.Qemu.CPUType instead. + // Deprecated: Use vmOpts.qemu.cpuType instead. 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 @@ -51,7 +51,7 @@ type LimaYAML struct { // `useHostResolver` was deprecated in Lima v0.8.1, removed in Lima v0.14.0. Use `hostResolver.enabled` instead. PropagateProxyEnv *bool `yaml:"propagateProxyEnv,omitempty" json:"propagateProxyEnv,omitempty" jsonschema:"nullable"` CACertificates CACertificates `yaml:"caCerts,omitempty" json:"caCerts,omitempty"` - // Deprecated: Use VMOpts.VZ.Rosetta instead. + // Deprecated: Use vmOpts.vz.rosetta instead. Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` Plain *bool `yaml:"plain,omitempty" json:"plain,omitempty" jsonschema:"nullable"` TimeZone *string `yaml:"timezone,omitempty" json:"timezone,omitempty" jsonschema:"nullable"` @@ -110,10 +110,7 @@ type User struct { UID *uint32 `yaml:"uid,omitempty" json:"uid,omitempty" jsonschema:"nullable"` } -type VMOpts struct { - QEMU QEMUOpts `yaml:"qemu,omitempty" json:"qemu,omitempty"` - VZ VZOpts `yaml:"vz,omitempty" json:"vz,omitempty"` -} +type VMOpts map[VMType]any type QEMUOpts struct { MinimumVersion *string `yaml:"minimumVersion,omitempty" json:"minimumVersion,omitempty" jsonschema:"nullable"` diff --git a/pkg/limayaml/limayaml_test.go b/pkg/limayaml/limayaml_test.go index 97fd8bae7a6..64e70b6f260 100644 --- a/pkg/limayaml/limayaml_test.go +++ b/pkg/limayaml/limayaml_test.go @@ -51,6 +51,7 @@ func TestDefaultYAML(t *testing.T) { y.Images = nil // remove default images y.Mounts = nil // remove default mounts y.Base = nil // remove default base templates + y.VMOpts = nil // remove default vmopts mapping y.MinimumLimaVersion = nil // remove minimum Lima version y.MountTypesUnsupported = nil // remove default workaround for kernel 6.9-6.11 t.Log(dumpJSON(t, y)) diff --git a/pkg/limayaml/marshal.go b/pkg/limayaml/marshal.go index a43821f3e13..71d6d4d7ff1 100644 --- a/pkg/limayaml/marshal.go +++ b/pkg/limayaml/marshal.go @@ -82,3 +82,20 @@ func Unmarshal(data []byte, y *limatype.LimaYAML, comment string) error { } return nil } + +// Convert converts from x to y, using YAML. +// If x is nil, then y is left unmodified. +func Convert(x, y any, comment string) error { + if x == nil { + return nil + } + b, err := yaml.Marshal(x) + if err != nil { + return err + } + err = yaml.Unmarshal(b, y) + if err != nil { + return fmt.Errorf("failed to unmarshal YAML (%s): %w", comment, err) + } + return nil +} diff --git a/pkg/limayaml/marshal_test.go b/pkg/limayaml/marshal_test.go index 4ad8f3ab03e..f70d6960794 100644 --- a/pkg/limayaml/marshal_test.go +++ b/pkg/limayaml/marshal_test.go @@ -4,14 +4,23 @@ package limayaml import ( + "strings" "testing" + "text/template" + "github.com/goccy/go-yaml" "gotest.tools/v3/assert" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/ptr" ) +func dumpYAML(t *testing.T, d any) string { + b, err := yaml.Marshal(d) + assert.NilError(t, err) + return string(b) +} + func TestMarshalEmpty(t *testing.T) { _, err := Marshal(&limatype.LimaYAML{}, false) assert.NilError(t, err) @@ -39,3 +48,137 @@ mounts: ... `) } + +type Opts struct { + Foo int + Bar string +} + +var ( + opts = Opts{Foo: 1, Bar: "two"} + text = `{"foo":1,"bar":"two"}` + code any +) + +func TestConvert(t *testing.T) { + err := yaml.Unmarshal([]byte(text), &code) + assert.NilError(t, err) + o := opts + var a any + err = Convert(o, &a, "") + assert.NilError(t, err) + assert.DeepEqual(t, a, code) + err = Convert(a, &o, "") + assert.NilError(t, err) + assert.Equal(t, o, opts) +} + +func TestVMOpts(t *testing.T) { + text := ` +vmType: null +` + var y limatype.LimaYAML + err := Unmarshal([]byte(text), &y, "lima.yaml") + assert.NilError(t, err) + var o limatype.VMOpts + err = Convert(y.VMOpts, &o, "vmOpts") + assert.NilError(t, err) + t.Log(dumpYAML(t, o)) +} + +func TestQEMUOpts(t *testing.T) { + text := ` +vmType: "qemu" +vmOpts: + qemu: + minimumVersion: null + cpuType: +` + var y limatype.LimaYAML + err := Unmarshal([]byte(text), &y, "lima.yaml") + assert.NilError(t, err) + var o limatype.QEMUOpts + err = Convert(y.VMOpts[limatype.QEMU], &o, "vmOpts.qemu") + assert.NilError(t, err) + t.Log(dumpYAML(t, o)) +} + +func TestVZOpts(t *testing.T) { + text := ` +vmType: "vz" +vmOpts: + vz: + rosetta: + enabled: null + binfmt: null +` + var y limatype.LimaYAML + err := Unmarshal([]byte(text), &y, "lima.yaml") + assert.NilError(t, err) + var o limatype.VZOpts + err = Convert(y.VMOpts[limatype.VZ], &o, "vmOpts.vz") + assert.NilError(t, err) + t.Log(dumpYAML(t, o)) +} + +func TestVMOptsNull(t *testing.T) { + text := ` +vmOpts: null +` + var y limatype.LimaYAML + err := Unmarshal([]byte(text), &y, "lima.yaml") + assert.NilError(t, err) + var o limatype.VMOpts + err = Convert(y.VMOpts, &o, "vmOpts") + assert.NilError(t, err) + var oq limatype.QEMUOpts + err = Convert(y.VMOpts[limatype.QEMU], &oq, "vmOpts.qemu") + assert.NilError(t, err) + var ov limatype.VZOpts + err = Convert(y.VMOpts[limatype.VZ], &ov, "vmOpts.vz") + assert.NilError(t, err) +} + +type FormatData struct { + limatype.Instance `yaml:",inline"` +} + +func TestVZOptsRosettaMessage(t *testing.T) { + text := ` +vmType: "vz" +vmOpts: + vz: + rosetta: + enabled: true + binfmt: false + +message: | + {{- if .Instance.Config.VMOpts.vz.rosetta.enabled}} + Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon. + {{- end}} +` + want := `vmType: vz +vmOpts: + vz: + rosetta: + binfmt: false + enabled: true +message: | + + Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon. +` + var y limatype.LimaYAML + err := Unmarshal([]byte(text), &y, "lima.yaml") + assert.NilError(t, err) + tmpl, err := template.New("format").Parse(y.Message) + assert.NilError(t, err) + inst := limatype.Instance{Config: &y} + var message strings.Builder + data := FormatData{Instance: inst} + err = tmpl.Execute(&message, data) + assert.NilError(t, err) + y.Message = message.String() + b, err := Marshal(&y, false) + assert.NilError(t, err) + assert.Equal(t, string(b), want) +} diff --git a/templates/docker-rootful.yaml b/templates/docker-rootful.yaml index 53477203efc..1b35f3d9de0 100644 --- a/templates/docker-rootful.yaml +++ b/templates/docker-rootful.yaml @@ -69,7 +69,7 @@ message: | docker context use lima-{{.Name}} docker run hello-world ------ - {{- if .Instance.Config.VMOpts.VZ.Rosetta.Enabled}} + {{- if .Instance.Config.VMOpts.vz.rosetta.enabled}} Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon. You can use Rosetta AOT Caching with the CDI spec: - To run a container, add `--device=lima-vm.io/rosetta=cached` to your `docker run` command: diff --git a/templates/docker.yaml b/templates/docker.yaml index 4d5c0e7a0e2..37ba5644c14 100644 --- a/templates/docker.yaml +++ b/templates/docker.yaml @@ -76,7 +76,7 @@ message: | docker context use lima-{{.Name}} docker run hello-world ------ - {{- if .Instance.Config.VMOpts.VZ.Rosetta.Enabled}} + {{- if .Instance.Config.VMOpts.vz.rosetta.enabled}} Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon. You can use Rosetta AOT Caching with the CDI spec: - To run a container, add `--device=lima-vm.io/rosetta=cached` to your `docker run` command: