Repository: lima-vm/lima
Pull request #2588 (2024-09-04 01:38:25)

limayaml: support template expansion in `.mounts[].location` and `.mounts[].mountPoint`
This PR is based on #2587. I will mark it as ready for review once #2587 is merged.


Patch:
diff --git a/examples/default.yaml b/examples/default.yaml
index 4df63956..8c31da87 100644
--- a/examples/default.yaml
+++ b/examples/default.yaml
@@ -50,6 +50,8 @@ memory: null
 disk: null
 
 # Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest
+# "location" can use these template variables: {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
+# "mountPoint" can use these template variables: {{.Home}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}
 # 🟢 Builtin default: null (Mount nothing)
 # 🔵 This file: Mount the home as read-only, /tmp/lima as writable
 mounts:
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 8ac8c26c..d3cc7ef8 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -577,6 +577,16 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 	mounts := make([]Mount, 0, len(d.Mounts)+len(y.Mounts)+len(o.Mounts))
 	location := make(map[string]int)
 	for _, mount := range append(append(d.Mounts, y.Mounts...), o.Mounts...) {
+		if out, err := executeHostTemplate(mount.Location, instDir, y.Param); err == nil {
+			mount.Location = out.String()
+		} else {
+			logrus.WithError(err).Warnf("Couldn't process mount location %q as a template", mount.Location)
+		}
+		if out, err := executeGuestTemplate(mount.MountPoint, instDir, y.Param); err == nil {
+			mount.MountPoint = out.String()
+		} else {
+			logrus.WithError(err).Warnf("Couldn't process mount point %q as a template", mount.MountPoint)
+		}
 		if i, ok := location[mount.Location]; ok {
 			if mount.SSHFS.Cache != nil {
 				mounts[i].SSHFS.Cache = mount.SSHFS.Cache
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 8eb719ed..0cfee5e7 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -426,6 +426,16 @@ func ValidateParamIsUsed(y *LimaYAML) error {
 				break
 			}
 		}
+		for _, p := range y.Mounts {
+			if re.MatchString(p.Location) {
+				keyIsUsed = true
+				break
+			}
+			if re.MatchString(p.MountPoint) {
+				keyIsUsed = true
+				break
+			}
+		}
 		if !keyIsUsed {
 			return fmt.Errorf("field `param` key %q is not used in any provision, probe, copyToHost, or portForward", key)
 		}


Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index 8f49816a..16fff194 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -131,6 +131,7 @@ func TestFillDefault(t *testing.T) {
 		},
 		Mounts: []Mount{
 			{Location: "/tmp"},
+			{Location: "{{.Dir}}/{{.Param.ONE}}", MountPoint: "/mnt/{{.Param.ONE}}"},
 		},
 		MountType: ptr.Of(NINEP),
 		Provision: []Provision{
@@ -214,6 +215,17 @@ func TestFillDefault(t *testing.T) {
 	expect.Mounts[0].NineP.Cache = ptr.Of(Default9pCacheForRO)
 	expect.Mounts[0].Virtiofs.QueueSize = nil
 	// Only missing Mounts field is Writable, and the default value is also the null value: false
+	expect.Mounts[1].Location = fmt.Sprintf("%s/%s", instDir, y.Param["ONE"])
+	expect.Mounts[1].MountPoint = fmt.Sprintf("/mnt/%s", y.Param["ONE"])
+	expect.Mounts[1].Writable = ptr.Of(false)
+	expect.Mounts[1].SSHFS.Cache = ptr.Of(true)
+	expect.Mounts[1].SSHFS.FollowSymlinks = ptr.Of(false)
+	expect.Mounts[1].SSHFS.SFTPDriver = ptr.Of("")
+	expect.Mounts[1].NineP.SecurityModel = ptr.Of(Default9pSecurityModel)
+	expect.Mounts[1].NineP.ProtocolVersion = ptr.Of(Default9pProtocolVersion)
+	expect.Mounts[1].NineP.Msize = ptr.Of(Default9pMsize)
+	expect.Mounts[1].NineP.Cache = ptr.Of(Default9pCacheForRO)
+	expect.Mounts[1].Virtiofs.QueueSize = nil
 
 	expect.MountType = ptr.Of(NINEP)
 
diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go
index 9be1b3a6..347ee7df 100644
--- a/pkg/limayaml/validate_test.go
+++ b/pkg/limayaml/validate_test.go
@@ -38,6 +38,8 @@ func TestValidateParamIsUsed(t *testing.T) {
 	assert.Error(t, err, "field `param` key \"name\" is not used in any provision, probe, copyToHost, or portForward")
 
 	fieldsUsingParam := []string{
+		`mounts: [{"location": "/tmp/{{ .Param.name }}"}]`,
+		`mounts: [{"location": "/tmp", mountPoint: "/tmp/{{ .Param.name }}"}]`,
 		`provision: [{"script": "echo {{ .Param.name }}"}]`,
 		`probes: [{"script": "echo {{ .Param.name }}"}]`,
 		`copyToHost: [{"guest": "/tmp/{{ .Param.name }}", "host": "/tmp"}]`,
@@ -55,6 +57,8 @@ func TestValidateParamIsUsed(t *testing.T) {
 	rootfulYaml := `param:
   rootful: true`
 	fieldsUsingIfParamRootfulTrue := []string{
+		`mounts: [{"location": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}", "mountPoint": "/tmp"}]`,
+		`mounts: [{"location": "/tmp", "mountPoint": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
 		`provision: [{"script": "echo {{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
 		`probes: [{"script": "echo {{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
 		`copyToHost: [{"guest": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}", "host": "/tmp"}]`,