Repository: lima-vm/lima
Pull request #2498 (2024-07-22 07:41:29)

limayaml: add a `param` field for defining variables used to customize scripts and other elements within `lima.yaml`.
The defined variables can be referenced within `lima.yaml` as `{{.Param.Key}}`.
The fields where this can be utilized are as follows:
- `provision[%d].script`
- `probes[%d].script`
- `copyToHost[%d].{guest,host}`
- `portForwards[%d].{guestSocket,hostSocket}`

It also changes:
- `limayaml`: add a check to verify whether the variables defined in `param` are actually used.


Following change has been merged as a separated PR: https://github.com/lima-vm/lima/pull/2501
- ~~`limactl start`: fix the issue where using `--set` would overwrite the existing instance's `lima.yaml` without validation.~~  

Following changes will be opened as a separated PR: https://github.com/lima-vm/lima/pull/2515
- ~~`docker.yaml`: add `.param.ContainerdImageStore`  
  By passing the `--set .param.ContainerdImageStore=true` option to `limactl {create,start,edit}`, the `.features."containerd-snapshotter"` option will be enabled in `docker/daemon.json` inside the VM.~~
- ~~`docker.yaml`: add `.param.Rootful`  
  By passing the `--set .param.Rootful=true` option to `limactl {create,start,edit}`, Docker inside the VM will run in rootful mode.~~
- ~~`docker-rootful.yaml`: make everything common except for setting `.param.Rootful=true` in `docker.yaml`.~~  

 
It might be better to split this into multiple parts, but for now, I’ll open it as a draft.

Thanks,

norio-nomura: I have separated the fix for `limactl start --set` into https://github.com/lima-vm/lima/pull/2501.
norio-nomura: can not reproduce panic in Unit Test on local machine
https://github.com/lima-vm/lima/actions/runs/10050557033/job/27778685157?pr=2498#step:5:2584
> panic: test timed out after 10m0s
...
FAIL	github.com/lima-vm/lima/pkg/guestagent/kubernetesservice	600.059s

norio-nomura: I also want to include the following in `lima.yaml`:
```yaml
mounts:
- location: "~/.cache/ubuntu/{{.Name}}/var/cache/apt"
  mountPoint: "/var/cache/apt"
  writable: true
- location: "~/.cache/ubuntu/{{.Name}}/var/lib/apt"
  mountPoint: "/var/lib/apt"
  writable: true
```
Should this be a separate PR?
jandubois: This PR has 2 warnings from `yamllint`:

```console
$ yamllint examples/docker.yaml
examples/docker.yaml
  58:14     warning  too few spaces before comment  (comments)

$ yamllint examples/docker-rootful.yaml
examples/docker-rootful.yaml
  58:14     warning  too few spaces before comment  (comments)
```

You can fix by adding another space before the comments, like

```diff
-- mode: user # configure docker under non-root user
+- mode: user  # configure docker under non-root user
```

Personally I don't really agree with the requirement for 2 spaces between content and comments, and would change `.yamllint` config instead:

```diff
--- .yamllint
+++ .yamllint
@@ -7,6 +7,8 @@ rules:
     indent-sequences: false
   truthy:
     allowed-values: ['true', 'false', 'on', 'off']
+  comments:
+    min-spaces-from-content: 1
   comments-indentation: disable
   document-start: disable
   line-length: disable
```

norio-nomura: updated and rebased
norio-nomura: Updated
norio-nomura: opened:
- https://github.com/lima-vm/lima/pull/2515
AkihiroSuda: @lima-vm/maintainers 
Is this safe to merge in v0.23?
afbjorklund: I am not a big fan of mixing in upper case, with the rest of the yaml that uses lower case...

Even if it means having to "translate" a param: key: value into a {{Param.Key}} so that they can each look their best 
norio-nomura: > Even if it means having to "translate" a param: key: value into a {{Param.Key}} so that they can each look their best

To reference `param: {key: value}`, use `{{.Param.key}}`. There is no functionality to convert `key` to `Key`.
jandubois: > > Even if it means having to "translate" a param: key: value into a {{Param.Key}} so that they can each look their best
> 
> To reference `param: {key: value}`, use `{{.Param.key}}`. There is no functionality to convert `key` to `Key`.

I believe there is no way to reference non-exported fields from Go templates, so `{{.Param.key}}` cannot work.

I think what @afbjorklund meant was that you have this in your `lima.yaml`:

```yaml
param:
   containerdImageStore: false
   rootful: false
```

And we would automatically capitalize the first letter when putting it into the template data struct, so you could reference it as `{{.Param.Rootful}}`.

Now you could argue that we are already transforming `param` → `.Param`, so doing the same for the keys is consistent with that.

Personally I don't like this, and I think `param` and `rootful` are semantically different entities, and don't need to be treated the same. I would prefer that the parameters are spelled consistently across the whole `lima.yaml` file and not magically change their first letter to uppercase.

I would maybe go a completely different way, and use the same conventions I would use for environment variables for params:

```yaml
param:
   CONTAINERD_IMAGE_STORE: false
   ROOTFUL: false
```

And then reference them as `{{.Param.ROOTFUL}}`. While I don't think we should enforce this, it may be something we want to use in the bundled templates.

What do other people think?

Assuming we are not implementing @afbjorklund's suggestion to auto-capitalize param names (I would vote against it), we should catch them in validation.
norio-nomura: > I believe there is no way to reference non-exported fields from Go templates, so {{.Param.key}} cannot work.


`{{.Param.key}}` works because `Param` is a [`map[string]string`](https://github.com/lima-vm/lima/pull/2498/files#diff-7cd9c1627051261f447947eaa346dd2480920529599fee8d2db3bd35f960239bR37).

In my local tests, #2515 actually works when I change it to:
```yaml
param:
  ContainerdImageStore: true
  rootful: true
```
and use `{{.Param.rootful}}` in combination.

> Now you could argue that we are already transforming `param` → `.Param`, so doing the same for the keys is consistent with that.

The reason it is `.Param` for the reference is not because `text/template` is converting it automatically, but because it is being passed to `text/template` as `Param` in [this line of the code](https://github.com/lima-vm/lima/pull/2498/files#diff-1e17bbd436fd831ebcaac290c75166c21aefc990f1be6125ddcea9832a1dd0b0R764).
jandubois: > `{{.Param.key}}` works because `Param` is a [`map[string]string`]

Thanks, didn't think of that. In that case I'm even more against automatically capitalizing the first letter of params.

> The reason it is `.Param` for the reference is not because `text/template` is converting it automatically, but because it is being passed to `text/template` as `Param`

I understand. And the same way we could capitalize the first letter of `k` before we assign the values in the `param` list with `param[k] = v`.

I just think we shouldn't.

It is also possible that I misunderstand what @afbjorklund is asking for.

Just to be clear: I think the code should be left the way it is right now.
jandubois: > @lima-vm/maintainers Is this safe to merge in v0.23?

I think it is, and would like to see it included!

Patch:
diff --git a/examples/default.yaml b/examples/default.yaml
index 36d04ac9..92ac2ec9 100644
--- a/examples/default.yaml
+++ b/examples/default.yaml
@@ -193,7 +193,7 @@ containerd:
 
 # Provisioning scripts need to be idempotent because they might be called
 # multiple times, e.g. when the host VM is being restarted.
-# The scripts can use the following template variables: {{.Home}}, {{.UID}}, and {{.User}}
+# The scripts can use the following template variables: {{.Home}}, {{.UID}}, {{.User}}, and {{.Param.Key}}
 # 🟢 Builtin default: null
 # provision:
 # # `system` is executed with root privileges
@@ -235,6 +235,7 @@ containerd:
 #   playbook: playbook.yaml
 
 # Probe scripts to check readiness.
+# The scripts can use the following template variables: {{.Home}}, {{.UID}}, {{.User}}, and {{.Param.Key}}
 # 🟢 Builtin default: null
 # probes:
 # # Only `readiness` probes are supported right now.
@@ -377,8 +378,8 @@ networks:
 # - guestSocket: "/run/user/{{.UID}}/my.sock"
 #   hostSocket: mysocket
 # # default: reverse: false
-# # "guestSocket" can include these template variables: {{.Home}}, {{.UID}}, and {{.User}}.
-# # "hostSocket" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, and {{.User}}.
+# # "guestSocket" can include these template variables: {{.Home}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
+# # "hostSocket" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
 # # "reverse" can only be used for unix sockets right now, not for tcp sockets.
 # # Put sockets into "{{.Dir}}/sock" to avoid collision with Lima internal sockets!
 # # Sockets can also be forwarded to ports and vice versa, but not to/from a range of ports.
@@ -397,8 +398,8 @@ networks:
 # - guest: "/etc/myconfig.cfg"
 #   host: "{{.Dir}}/copied-from-guest/myconfig"
 # # deleteOnStop: false
-# # "guest" can include these template variables: {{.Home}}, {{.UID}}, and {{.User}}.
-# # "host" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, and {{.User}}.
+# # "guest" can include these template variables: {{.Home}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
+# # "host" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.
 # # "deleteOnStop" will delete the file from the host when the instance is stopped.
 
 # Message. Information to be shown to the user, given as a Go template for the instance.
@@ -418,6 +419,11 @@ networks:
 # env:
 #   KEY: value
 
+# Defines variables used for customizing the functionality.
+# These variables can be referenced as {{.Param.Key}} in lima.yaml.
+# param:
+#   Key: value
+
 # Lima will override the proxy environment variables with values from the current process
 # environment (the environment in effect when you run `limactl start`). It will automatically
 # replace the strings "localhost" and "127.0.0.1" with the host gateway address from inside
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 52a85551..0f09c6ce 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -155,13 +155,13 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
 	// Block ports 22 and sshLocalPort on all IPs
 	for _, port := range []int{sshGuestPort, sshLocalPort} {
 		rule := limayaml.PortForward{GuestIP: net.IPv4zero, GuestPort: port, Ignore: true}
-		limayaml.FillPortForwardDefaults(&rule, inst.Dir)
+		limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Param)
 		rules = append(rules, rule)
 	}
 	rules = append(rules, y.PortForwards...)
 	// Default forwards for all non-privileged ports from "127.0.0.1" and "::1"
 	rule := limayaml.PortForward{}
-	limayaml.FillPortForwardDefaults(&rule, inst.Dir)
+	limayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Param)
 	rules = append(rules, rule)
 
 	limaDriver := driverutil.CreateTargetDriverInstance(&driver.BaseDriver{
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 13815bb6..f69a320f 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -394,7 +394,7 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 		if provision.Mode == ProvisionModeDependency && provision.SkipDefaultDependencyResolution == nil {
 			provision.SkipDefaultDependencyResolution = ptr.Of(false)
 		}
-		if out, err := executeGuestTemplate(provision.Script, instDir); err == nil {
+		if out, err := executeGuestTemplate(provision.Script, instDir, y.Param); err == nil {
 			provision.Script = out.String()
 		} else {
 			logrus.WithError(err).Warnf("Couldn't process provisioning script %q as a template", provision.Script)
@@ -460,17 +460,22 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 		if probe.Description == "" {
 			probe.Description = fmt.Sprintf("user probe %d/%d", i+1, len(y.Probes))
 		}
+		if out, err := executeGuestTemplate(probe.Script, instDir, y.Param); err == nil {
+			probe.Script = out.String()
+		} else {
+			logrus.WithError(err).Warnf("Couldn't process probing script %q as a template", probe.Script)
+		}
 	}
 
 	y.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...)
 	for i := range y.PortForwards {
-		FillPortForwardDefaults(&y.PortForwards[i], instDir)
+		FillPortForwardDefaults(&y.PortForwards[i], instDir, y.Param)
 		// After defaults processing the singular HostPort and GuestPort values should not be used again.
 	}
 
 	y.CopyToHost = append(append(o.CopyToHost, y.CopyToHost...), d.CopyToHost...)
 	for i := range y.CopyToHost {
-		FillCopyToHostDefaults(&y.CopyToHost[i], instDir)
+		FillCopyToHostDefaults(&y.CopyToHost[i], instDir, y.Param)
 	}
 
 	if y.HostResolver.Enabled == nil {
@@ -669,6 +674,18 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 	}
 	y.Env = env
 
+	param := make(map[string]string)
+	for k, v := range d.Param {
+		param[k] = v
+	}
+	for k, v := range y.Param {
+		param[k] = v
+	}
+	for k, v := range o.Param {
+		param[k] = v
+	}
+	y.Param = param
+
 	if y.CACertificates.RemoveDefaults == nil {
 		y.CACertificates.RemoveDefaults = d.CACertificates.RemoveDefaults
 	}
@@ -735,15 +752,16 @@ func fixUpForPlainMode(y *LimaYAML) {
 	y.TimeZone = ptr.Of("")
 }
 
-func executeGuestTemplate(format, instDir string) (bytes.Buffer, error) {
+func executeGuestTemplate(format, instDir string, param map[string]string) (bytes.Buffer, error) {
 	tmpl, err := template.New("").Parse(format)
 	if err == nil {
 		user, _ := osutil.LimaUser(false)
-		data := map[string]string{
-			"Home": fmt.Sprintf("/home/%s.linux", user.Username),
-			"Name": filepath.Base(instDir),
-			"UID":  user.Uid,
-			"User": user.Username,
+		data := map[string]interface{}{
+			"Home":  fmt.Sprintf("/home/%s.linux", user.Username),
+			"Name":  filepath.Base(instDir),
+			"UID":   user.Uid,
+			"User":  user.Username,
+			"Param": param,
 		}
 		var out bytes.Buffer
 		if err := tmpl.Execute(&out, data); err == nil {
@@ -753,18 +771,19 @@ func executeGuestTemplate(format, instDir string) (bytes.Buffer, error) {
 	return bytes.Buffer{}, err
 }
 
-func executeHostTemplate(format, instDir string) (bytes.Buffer, error) {
+func executeHostTemplate(format, instDir string, param map[string]string) (bytes.Buffer, error) {
 	tmpl, err := template.New("").Parse(format)
 	if err == nil {
 		user, _ := osutil.LimaUser(false)
 		home, _ := os.UserHomeDir()
 		limaHome, _ := dirnames.LimaDir()
-		data := map[string]string{
-			"Dir":  instDir,
-			"Home": home,
-			"Name": filepath.Base(instDir),
-			"UID":  user.Uid,
-			"User": user.Username,
+		data := map[string]interface{}{
+			"Dir":   instDir,
+			"Home":  home,
+			"Name":  filepath.Base(instDir),
+			"UID":   user.Uid,
+			"User":  user.Username,
+			"Param": param,
 
 			"Instance": filepath.Base(instDir), // DEPRECATED, use `{{.Name}}`
 			"LimaHome": limaHome,               // DEPRECATED, use `{{.Dir}}` instead of `{{.LimaHome}}/{{.Instance}}`
@@ -777,7 +796,7 @@ func executeHostTemplate(format, instDir string) (bytes.Buffer, error) {
 	return bytes.Buffer{}, err
 }
 
-func FillPortForwardDefaults(rule *PortForward, instDir string) {
+func FillPortForwardDefaults(rule *PortForward, instDir string, param map[string]string) {
 	if rule.Proto == "" {
 		rule.Proto = TCP
 	}
@@ -809,14 +828,14 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
 		}
 	}
 	if rule.GuestSocket != "" {
-		if out, err := executeGuestTemplate(rule.GuestSocket, instDir); err == nil {
+		if out, err := executeGuestTemplate(rule.GuestSocket, instDir, param); err == nil {
 			rule.GuestSocket = out.String()
 		} else {
 			logrus.WithError(err).Warnf("Couldn't process guestSocket %q as a template", rule.GuestSocket)
 		}
 	}
 	if rule.HostSocket != "" {
-		if out, err := executeHostTemplate(rule.HostSocket, instDir); err == nil {
+		if out, err := executeHostTemplate(rule.HostSocket, instDir, param); err == nil {
 			rule.HostSocket = out.String()
 		} else {
 			logrus.WithError(err).Warnf("Couldn't process hostSocket %q as a template", rule.HostSocket)
@@ -827,16 +846,16 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
 	}
 }
 
-func FillCopyToHostDefaults(rule *CopyToHost, instDir string) {
+func FillCopyToHostDefaults(rule *CopyToHost, instDir string, param map[string]string) {
 	if rule.GuestFile != "" {
-		if out, err := executeGuestTemplate(rule.GuestFile, instDir); err == nil {
+		if out, err := executeGuestTemplate(rule.GuestFile, instDir, param); err == nil {
 			rule.GuestFile = out.String()
 		} else {
 			logrus.WithError(err).Warnf("Couldn't process guest %q as a template", rule.GuestFile)
 		}
 	}
 	if rule.HostFile != "" {
-		if out, err := executeHostTemplate(rule.HostFile, instDir); err == nil {
+		if out, err := executeHostTemplate(rule.HostFile, instDir, param); err == nil {
 			rule.HostFile = out.String()
 		} else {
 			logrus.WithError(err).Warnf("Couldn't process host %q as a template", rule.HostFile)
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index a6761b6c..4b3bc1cd 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -34,6 +34,7 @@ type LimaYAML struct {
 	Networks           []Network     `yaml:"networks,omitempty" json:"networks,omitempty"`
 	// `network` was deprecated in Lima v0.7.0, removed in Lima v0.14.0. Use `networks` instead.
 	Env          map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
+	Param        map[string]string `yaml:"param,omitempty" json:"param,omitempty"`
 	DNS          []net.IP          `yaml:"dns,omitempty" json:"dns,omitempty"`
 	HostResolver HostResolver      `yaml:"hostResolver,omitempty" json:"hostResolver,omitempty"`
 	// `useHostResolver` was deprecated in Lima v0.8.1, removed in Lima v0.14.0. Use `hostResolver.enabled` instead.
diff --git a/pkg/limayaml/load.go b/pkg/limayaml/load.go
index f9c7e3c4..20670eb6 100644
--- a/pkg/limayaml/load.go
+++ b/pkg/limayaml/load.go
@@ -86,6 +86,11 @@ func Load(b []byte, filePath string) (*LimaYAML, error) {
 		return nil, err
 	}
 
+	// It should be called before the `y` parameter is passed to FillDefault() that execute template.
+	if err := ValidateParamIsUsed(&y); err != nil {
+		return nil, err
+	}
+
 	FillDefault(&y, &d, &o, filePath)
 	return &y, nil
 }
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 6e398a59..9210e86c 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"regexp"
 	"runtime"
 	"strings"
 
@@ -252,7 +253,7 @@ func Validate(y *LimaYAML, warn bool) error {
 		}
 		if rule.GuestSocket != "" {
 			if !path.IsAbs(rule.GuestSocket) {
-				return fmt.Errorf("field `%s.guestSocket` must be an absolute path", field)
+				return fmt.Errorf("field `%s.guestSocket` must be an absolute path, but is %q", field, rule.GuestSocket)
 			}
 			if rule.HostSocket == "" && rule.HostPortRange[1]-rule.HostPortRange[0] > 0 {
 				return fmt.Errorf("field `%s.guestSocket` can only be mapped to a single port or socket. not a range", field)
@@ -287,7 +288,7 @@ func Validate(y *LimaYAML, warn bool) error {
 		field := fmt.Sprintf("CopyToHost[%d]", i)
 		if rule.GuestFile != "" {
 			if !path.IsAbs(rule.GuestFile) {
-				return fmt.Errorf("field `%s.guest` must be an absolute path", field)
+				return fmt.Errorf("field `%s.guest` must be an absolute path, but is %q", field, rule.GuestFile)
 			}
 		}
 		if rule.HostFile != "" {
@@ -385,6 +386,46 @@ func validateNetwork(y *LimaYAML) error {
 	return nil
 }
 
+// ValidateParamIsUsed checks if the keys in the `param` field are used in any script, probe, copyToHost, or portForward.
+// It should be called before the `y` parameter is passed to FillDefault() that execute template.
+func ValidateParamIsUsed(y *LimaYAML) error {
+	for key := range y.Param {
+		re, err := regexp.Compile(`{{[^}]*\.Param\.` + key + `[^}]*}}`)
+		if err != nil {
+			return fmt.Errorf("field to compile regexp for key %q: %w", key, err)
+		}
+		keyIsUsed := false
+		for _, p := range y.Provision {
+			if re.MatchString(p.Script) {
+				keyIsUsed = true
+				break
+			}
+		}
+		for _, p := range y.Probes {
+			if re.MatchString(p.Script) {
+				keyIsUsed = true
+				break
+			}
+		}
+		for _, p := range y.CopyToHost {
+			if re.MatchString(p.GuestFile) || re.MatchString(p.HostFile) {
+				keyIsUsed = true
+				break
+			}
+		}
+		for _, p := range y.PortForwards {
+			if re.MatchString(p.GuestSocket) || re.MatchString(p.HostSocket) {
+				keyIsUsed = true
+				break
+			}
+		}
+		if !keyIsUsed {
+			return fmt.Errorf("field `param` key %q is not used in any provision, probe, copyToHost, or portForward", key)
+		}
+	}
+	return nil
+}
+
 func validatePort(field string, port int) error {
 	switch {
 	case port < 0:
diff --git a/pkg/store/instance.go b/pkg/store/instance.go
index 6edfc435..1a88a8c6 100644
--- a/pkg/store/instance.go
+++ b/pkg/store/instance.go
@@ -59,6 +59,7 @@ type Instance struct {
 	SSHAddress      string             `json:"sshAddress,omitempty"`
 	Protected       bool               `json:"protected"`
 	LimaVersion     string             `json:"limaVersion"`
+	Param           map[string]string  `json:"param,omitempty"`
 }
 
 func (inst *Instance) LoadYAML() (*limayaml.LimaYAML, error) {
@@ -180,6 +181,7 @@ func Inspect(instName string) (*Instance, error) {
 	} else if !errors.Is(err, os.ErrNotExist) {
 		inst.Errors = append(inst.Errors, err)
 	}
+	inst.Param = y.Param
 	return inst, nil
 }
 
diff --git a/pkg/vz/vz_driver_darwin.go b/pkg/vz/vz_driver_darwin.go
index 8b0353a0..15bba341 100644
--- a/pkg/vz/vz_driver_darwin.go
+++ b/pkg/vz/vz_driver_darwin.go
@@ -42,6 +42,7 @@ var knownYamlProperties = []string{
 	"MountInotify",
 	"Networks",
 	"OS",
+	"Param",
 	"Plain",
 	"PortForwards",
 	"Probes",
diff --git a/pkg/wsl2/wsl_driver_windows.go b/pkg/wsl2/wsl_driver_windows.go
index 49b9c55a..6f285e48 100644
--- a/pkg/wsl2/wsl_driver_windows.go
+++ b/pkg/wsl2/wsl_driver_windows.go
@@ -29,6 +29,7 @@ var knownYamlProperties = []string{
 	"Message",
 	"Mounts",
 	"MountType",
+	"Param",
 	"Plain",
 	"PortForwards",
 	"Probes",


Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index ecca9e36..c09fcdbd 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -129,10 +129,10 @@ func TestFillDefault(t *testing.T) {
 		},
 		MountType: ptr.Of(NINEP),
 		Provision: []Provision{
-			{Script: "#!/bin/true"},
+			{Script: "#!/bin/true # {{.Param.ONE}}"},
 		},
 		Probes: []Probe{
-			{Script: "#!/bin/false"},
+			{Script: "#!/bin/false # {{.Param.ONE}}"},
 		},
 		Networks: []Network{
 			{Lima: "shared"},
@@ -145,19 +145,22 @@ func TestFillDefault(t *testing.T) {
 			{GuestPort: 80},
 			{GuestPort: 8080, HostPort: 8888},
 			{
-				GuestSocket: "{{.Home}} | {{.UID}} | {{.User}}",
-				HostSocket:  "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}}",
+				GuestSocket: "{{.Home}} | {{.UID}} | {{.User}} | {{.Param.ONE}}",
+				HostSocket:  "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}} | {{.Param.ONE}}",
 			},
 		},
 		CopyToHost: []CopyToHost{
 			{
-				GuestFile: "{{.Home}} | {{.UID}} | {{.User}}",
-				HostFile:  "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}}",
+				GuestFile: "{{.Home}} | {{.UID}} | {{.User}} | {{.Param.ONE}}",
+				HostFile:  "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}} | {{.Param.ONE}}",
 			},
 		},
 		Env: map[string]string{
 			"ONE": "Eins",
 		},
+		Param: map[string]string{
+			"ONE": "Eins",
+		},
 		CACertificates: CACertificates{
 			Files: []string{"ca.crt"},
 			Certs: []string{
@@ -212,10 +215,12 @@ func TestFillDefault(t *testing.T) {
 
 	expect.Provision = y.Provision
 	expect.Provision[0].Mode = ProvisionModeSystem
+	expect.Provision[0].Script = "#!/bin/true # Eins"
 
 	expect.Probes = y.Probes
 	expect.Probes[0].Mode = ProbeModeReadiness
 	expect.Probes[0].Description = "user probe 1/1"
+	expect.Probes[0].Script = "#!/bin/true # Eins"
 
 	expect.Networks = y.Networks
 	expect.Networks[0].MACAddress = MACAddress(fmt.Sprintf("%s#%d", filePath, 0))
@@ -243,14 +248,16 @@ func TestFillDefault(t *testing.T) {
 	expect.PortForwards[2].HostPort = 8888
 	expect.PortForwards[2].HostPortRange = [2]int{8888, 8888}
 
-	expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s", guestHome, user.Uid, user.Username)
-	expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username)
+	expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s | %s", guestHome, user.Uid, user.Username, y.Param["ONE"])
+	expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username, y.Param["ONE"])
 
-	expect.CopyToHost[0].GuestFile = fmt.Sprintf("%s | %s | %s", guestHome, user.Uid, user.Username)
-	expect.CopyToHost[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username)
+	expect.CopyToHost[0].GuestFile = fmt.Sprintf("%s | %s | %s | %s", guestHome, user.Uid, user.Username, y.Param["ONE"])
+	expect.CopyToHost[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username, y.Param["ONE"])
 
 	expect.Env = y.Env
 
+	expect.Param = y.Param
+
 	expect.CACertificates = CACertificates{
 		RemoveDefaults: ptr.Of(false),
 		Files:          []string{"ca.crt"},
@@ -380,6 +387,10 @@ func TestFillDefault(t *testing.T) {
 			"ONE": "one",
 			"TWO": "two",
 		},
+		Param: map[string]string{
+			"ONE": "one",
+			"TWO": "two",
+		},
 		CACertificates: CACertificates{
 			RemoveDefaults: ptr.Of(true),
 			Certs: []string{
@@ -459,6 +470,8 @@ func TestFillDefault(t *testing.T) {
 	// "TWO" does not exist in filledDefaults.Env, so is set from d.Env
 	expect.Env["TWO"] = d.Env["TWO"]
 
+	expect.Param["TWO"] = d.Param["TWO"]
+
 	FillDefault(&y, &d, &LimaYAML{}, filePath)
 	assert.DeepEqual(t, &y, &expect, opts...)
 
@@ -584,6 +597,10 @@ func TestFillDefault(t *testing.T) {
 			"TWO":   "deux",
 			"THREE": "trois",
 		},
+		Param: map[string]string{
+			"TWO":   "deux",
+			"THREE": "trois",
+		},
 		CACertificates: CACertificates{
 			RemoveDefaults: ptr.Of(true),
 		},
@@ -632,6 +649,8 @@ func TestFillDefault(t *testing.T) {
 	// ONE remains from filledDefaults.Env; the rest are set from o
 	expect.Env["ONE"] = y.Env["ONE"]
 
+	expect.Param["ONE"] = y.Param["ONE"]
+
 	expect.CACertificates.RemoveDefaults = ptr.Of(true)
 	expect.CACertificates.Files = []string{"ca.crt"}
 	expect.CACertificates.Certs = []string{
diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go
index 751952cc..9be1b3a6 100644
--- a/pkg/limayaml/validate_test.go
+++ b/pkg/limayaml/validate_test.go
@@ -30,3 +30,50 @@ func TestValidateDefault(t *testing.T) {
 	err = Validate(y, true)
 	assert.NilError(t, err)
 }
+
+func TestValidateParamIsUsed(t *testing.T) {
+	paramYaml := `param:
+  name: value`
+	_, err := Load([]byte(paramYaml), "paramIsNotUsed.yaml")
+	assert.Error(t, err, "field `param` key \"name\" is not used in any provision, probe, copyToHost, or portForward")
+
+	fieldsUsingParam := []string{
+		`provision: [{"script": "echo {{ .Param.name }}"}]`,
+		`probes: [{"script": "echo {{ .Param.name }}"}]`,
+		`copyToHost: [{"guest": "/tmp/{{ .Param.name }}", "host": "/tmp"}]`,
+		`copyToHost: [{"guest": "/tmp", "host": "/tmp/{{ .Param.name }}"}]`,
+		`portForwards: [{"guestSocket": "/tmp/{{ .Param.name }}", "hostSocket": "/tmp"}]`,
+		`portForwards: [{"guestSocket": "/tmp", "hostSocket": "/tmp/{{ .Param.name }}"}]`,
+	}
+	for _, fieldUsingParam := range fieldsUsingParam {
+		_, err = Load([]byte(fieldUsingParam+"\n"+paramYaml), "paramIsUsed.yaml")
+		//
+		assert.NilError(t, err)
+	}
+
+	// use "{{if .Param.rootful \"true\"}}{{else}}{{end}}"" in provision, probe, copyToHost, and portForward
+	rootfulYaml := `param:
+  rootful: true`
+	fieldsUsingIfParamRootfulTrue := []string{
+		`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"}]`,
+		`copyToHost: [{"guest": "/tmp", "host": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
+		`portForwards: [{"guestSocket": "{{if eq .Param.rootful \"true\"}}/var/run{{else}}/run/user/{{.UID}}{{end}}/docker.sock", "hostSocket": "{{.Dir}}/sock/docker.sock"}]`,
+		`portForwards: [{"guestSocket": "/var/run/docker.sock", "hostSocket": "{{.Dir}}/sock/docker-{{if eq .Param.rootful \"true\"}}rootfule{{else}}rootless{{end}}.sock"}]`,
+	}
+	for _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {
+		_, err = Load([]byte(fieldUsingIfParamRootfulTrue+"\n"+rootfulYaml), "paramIsUsed.yaml")
+		//
+		assert.NilError(t, err)
+	}
+
+	// use rootFul instead of rootful
+	rootFulYaml := `param:
+  rootFul: true`
+	for _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {
+		_, err = Load([]byte(fieldUsingIfParamRootfulTrue+"\n"+rootFulYaml), "paramIsUsed.yaml")
+		//
+		assert.Error(t, err, "field `param` key \"rootFul\" is not used in any provision, probe, copyToHost, or portForward")
+	}
+}