From f54533a24aa972335e310fbbc1efc20b5eeda1d1 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 | 21 ++++-- pkg/driver/qemu/qemu_driver.go | 51 +++++++++------ pkg/driver/vz/vm_darwin.go | 9 ++- pkg/driver/vz/vz_driver_darwin.go | 42 ++++++++---- pkg/limatmpl/embed.go | 23 +++++-- pkg/limatype/lima_yaml.go | 9 +-- pkg/limayaml/limayaml_test.go | 1 + pkg/limayaml/marshal.go | 13 ++++ pkg/limayaml/marshal_test.go | 104 ++++++++++++++++++++++++++++++ templates/docker-rootful.yaml | 2 +- templates/docker.yaml | 2 +- 11 files changed, 228 insertions(+), 49 deletions(-) diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go index 1af03ae0f82..2b4badab4ba 100644 --- a/pkg/driver/qemu/qemu.go +++ b/pkg/driver/qemu/qemu.go @@ -449,7 +449,13 @@ 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 y.VMOpts[limatype.QEMU] != nil { + 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 +466,10 @@ func resolveCPUType(y *limatype.LimaYAML) string { } } if overrideCPUType { - y.VMOpts.QEMU.CPUType = cpuType + qemuOpts.CPUType = cpuType + } + if y.VMOpts[limatype.QEMU] != nil { + y.VMOpts[limatype.QEMU] = qemuOpts } return cpuType[*y.Arch] @@ -490,8 +499,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 e468e55b384..bbf492c84d5 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -110,9 +110,15 @@ func validateConfig(ctx context.Context, 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) + if cfg.VMOpts[limatype.QEMU] != nil { + 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) + } } } @@ -146,25 +152,32 @@ func (l *LimaQemuDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, 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{} + if cfg.VMOpts == nil { + cfg.VMOpts = limatype.VMOpts{} } - - //nolint:staticcheck // Migration of top-level CPUTYPE if specified - if len(cfg.CPUType) > 0 { - logrus.Warn("The top-level `cpuType` field is deprecated and will be removed in a future release. Please migrate to `vmOpts.qemu.cpuType`.") - for arch, v := range cfg.CPUType { - if v == "" { - continue - } - if existing, ok := cfg.VMOpts.QEMU.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 + 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 + for arch, v := range cfg.CPUType { + if v == "" { + continue } - cfg.CPUType = nil + 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 + } + qemuOpts.CPUType[arch] = v + } + var opts any + if err := limayaml.Convert(qemuOpts, &opts, ""); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %+v", qemuOpts) } + cfg.VMOpts[limatype.QEMU] = opts mountTypesUnsupported := make(map[string]struct{}) for _, f := range cfg.MountTypesUnsupported { diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index e39c5493007..6029808e634 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -583,7 +583,14 @@ func attachFolderMounts(inst *limatype.Instance, vmConfig *vz.VirtualMachineConf } } - if *inst.Config.VMOpts.VZ.Rosetta.Enabled { + var vzOpts limatype.VZOpts + if inst.Config.VMOpts[limatype.VZ] != nil { + 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..a6806e2674a 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,35 @@ func (l *LimaVzDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, _ cfg.MountType = ptr.Of(limatype.VIRTIOFS) } + var vzOpts limatype.VZOpts + if cfg.VMOpts[limatype.VZ] != nil { + 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 cfg.VMOpts.VZ.Rosetta.BinFmt == nil { - cfg.VMOpts.VZ.Rosetta.BinFmt = 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) } + cfg.VMOpts[limatype.VZ] = opts return validateConfig(ctx, cfg) } diff --git a/pkg/limatmpl/embed.go b/pkg/limatmpl/embed.go index a58712247bf..578f119d1df 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,12 +180,22 @@ 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) - if tmplVersion.LessThan(baseVersion) { - const minimumQEMUVersion = "vmOpts.qemu.minimumVersion" - tmpl.copyField(minimumQEMUVersion, minimumQEMUVersion) + if tmpl.Config.VMOpts[limatype.QEMU] != nil && base.Config.VMOpts[limatype.QEMU] != nil { + 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) + } } } return nil 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..1639ea8ddb2 100644 --- a/pkg/limayaml/marshal.go +++ b/pkg/limayaml/marshal.go @@ -82,3 +82,16 @@ func Unmarshal(data []byte, y *limatype.LimaYAML, comment string) error { } return nil } + +// Convert converts from x to y, using YAML. +func Convert(x, y any, comment string) error { + 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..8783d6148d2 100644 --- a/pkg/limayaml/marshal_test.go +++ b/pkg/limayaml/marshal_test.go @@ -4,8 +4,11 @@ package limayaml import ( + "strings" "testing" + "text/template" + "github.com/goccy/go-yaml" "gotest.tools/v3/assert" "github.com/lima-vm/lima/v2/pkg/limatype" @@ -39,3 +42,104 @@ 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 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) +} + +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) +} + +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: