Skip to content

Commit 54ba0b3

Browse files
authored
Merge pull request #2570 from rancher-sandbox/param-env
Add limayaml param settings to provisioning script environment
2 parents 536f375 + 2fd8ad7 commit 54ba0b3

File tree

12 files changed

+203
-9
lines changed

12 files changed

+203
-9
lines changed

examples/default.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ containerd:
237237
# playbook: playbook.yaml
238238

239239
# Probe scripts to check readiness.
240+
# The scripts run in user mode. They must start with a '#!' line.
240241
# The scripts can use the following template variables: {{.Home}}, {{.UID}}, {{.User}}, and {{.Param.Key}}
241242
# 🟢 Builtin default: null
242243
# probes:
@@ -422,7 +423,12 @@ networks:
422423
# KEY: value
423424

424425
# Defines variables used for customizing the functionality.
426+
# Key names must start with an uppercase or lowercase letter followed by
427+
# any number of letters, digits, and underscores.
428+
# Values must not contain non-printable characters except for spaces and tabs.
425429
# These variables can be referenced as {{.Param.Key}} in lima.yaml.
430+
# In provisioning scripts and probes they are also available as predefined
431+
# environment variables, prefixed with "PARAM` (so `Key` → `$PARAM_Key`).
426432
# param:
427433
# Key: value
428434

hack/test-templates.sh

+11
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ declare -A CHECKS=(
3636
["user-v2"]=""
3737
["mount-path-with-spaces"]=""
3838
["provision-ansible"]=""
39+
["param-env-variables"]=""
3940
)
4041

4142
case "$NAME" in
@@ -64,6 +65,7 @@ case "$NAME" in
6465
CHECKS["snapshot-offline"]="1"
6566
CHECKS["mount-path-with-spaces"]="1"
6667
CHECKS["provision-ansible"]="1"
68+
CHECKS["param-env-variables"]="1"
6769
;;
6870
"net-user-v2")
6971
CHECKS["port-forwards"]=""
@@ -152,6 +154,15 @@ if [[ -n ${CHECKS["provision-ansible"]} ]]; then
152154
limactl shell "$NAME" test -e /tmp/ansible
153155
fi
154156

157+
if [[ -n ${CHECKS["param-env-variables"]} ]]; then
158+
INFO 'Testing that PARAM env variables are exported to all types of provisioning scripts and probes'
159+
limactl shell "$NAME" test -e /tmp/param-boot
160+
limactl shell "$NAME" test -e /tmp/param-dependency
161+
limactl shell "$NAME" test -e /tmp/param-probe
162+
limactl shell "$NAME" test -e /tmp/param-system
163+
limactl shell "$NAME" test -e /tmp/param-user
164+
fi
165+
155166
INFO "Testing proxy settings are imported"
156167
got=$(limactl shell "$NAME" env | grep FTP_PROXY)
157168
# Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced

hack/test-templates/test-misc.yaml

+22-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# - disk
33
# - (More to come)
44
#
5-
# This template requires Lima v0.14.0 or later.
5+
# This template requires Lima v1.0.0-alpha.0 or later.
66
images:
77
# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
88
- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20220902/ubuntu-22.04-server-cloudimg-amd64.img"
@@ -26,9 +26,30 @@ mounts:
2626
- location: "/tmp/lima"
2727
writable: true
2828

29+
param:
30+
BOOT: boot
31+
DEPENDENCY: dependency
32+
PROBE: probe
33+
SYSTEM: system
34+
USER: user
35+
2936
provision:
3037
- mode: ansible
3138
playbook: ./hack/ansible-test.yaml
39+
- mode: boot
40+
script: "touch /tmp/param-$PARAM_BOOT"
41+
- mode: dependency
42+
script: "touch /tmp/param-$PARAM_DEPENDENCY"
43+
- mode: system
44+
script: "touch /tmp/param-$PARAM_SYSTEM"
45+
- mode: user
46+
script: "touch /tmp/param-$PARAM_USER"
47+
48+
probes:
49+
- mode: readiness
50+
script: |
51+
#!/bin/sh
52+
touch /tmp/param-$PARAM_PROBE
3253
3354
# in order to use this example, you must first create the disk "data". run:
3455
# $ limactl disk create data --size 10G

pkg/cidata/cidata.TEMPLATE.d/boot.sh

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ WARNING() {
1010
}
1111

1212
# shellcheck disable=SC2163
13-
while read -r line; do export "$line"; done <"${LIMA_CIDATA_MNT}"/lima.env
13+
while read -r line; do [ -n "$line" ] && export "$line"; done <"${LIMA_CIDATA_MNT}"/lima.env
14+
# shellcheck disable=SC2163
15+
while read -r line; do [ -n "$line" ] && export "$line"; done <"${LIMA_CIDATA_MNT}"/param.env
1416

1517
# shellcheck disable=SC2163
1618
while read -r line; do
@@ -61,12 +63,13 @@ if [ -d "${LIMA_CIDATA_MNT}"/provision.user ]; then
6163
if [ ! -f /sbin/openrc-run ]; then
6264
until [ -e "/run/user/${LIMA_CIDATA_UID}/systemd/private" ]; do sleep 3; done
6365
fi
66+
params=$(grep -o '^PARAM_[^=]*' "${LIMA_CIDATA_MNT}"/param.env | paste -sd ,)
6467
for f in "${LIMA_CIDATA_MNT}"/provision.user/*; do
6568
INFO "Executing $f (as user ${LIMA_CIDATA_USER})"
6669
cp "$f" "${USER_SCRIPT}"
6770
chown "${LIMA_CIDATA_USER}" "${USER_SCRIPT}"
6871
chmod 755 "${USER_SCRIPT}"
69-
if ! sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "${USER_SCRIPT}"; then
72+
if ! sudo -iu "${LIMA_CIDATA_USER}" "--preserve-env=${params}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "${USER_SCRIPT}"; then
7073
WARNING "Failed to execute $f (as user ${LIMA_CIDATA_USER})"
7174
CODE=1
7275
fi
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{range $key, $val := .Param -}}
2+
PARAM_{{ $key }}={{ $val }}
3+
{{end -}}

pkg/cidata/cidata.TEMPLATE.d/user-data

+6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ ca_certs:
8484
bootcmd:
8585
{{- range $cmd := $.BootCmds }}
8686
- |
87+
# We need to embed the params.env as a here-doc because /mnt/lima-cidata is not yet mounted
88+
while read -r line; do [ -n "$line" ] && export "$line"; done <<'EOF'
89+
{{- range $key, $val := $.Param }}
90+
PARAM_{{ $key }}={{ $val }}
91+
{{- end }}
92+
EOF
8793
{{- range $line := $cmd.Lines }}
8894
{{ $line }}
8995
{{- end }}

pkg/cidata/cidata.go

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort
140140
VirtioPort: virtioPort,
141141
Plain: *y.Plain,
142142
TimeZone: *y.TimeZone,
143+
Param: y.Param,
143144
}
144145

145146
firstUsernetIndex := limayaml.FirstUsernetIndex(y)

pkg/cidata/template.go

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type TemplateArgs struct {
7373
UDPDNSLocalPort int
7474
TCPDNSLocalPort int
7575
Env map[string]string
76+
Param map[string]string
7677
DNSAddresses []string
7778
CACerts CACerts
7879
HostHomeMountPoint string

pkg/hostagent/requirements.go

+44-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,52 @@ func (a *HostAgent) waitForRequirements(label string, requirements []requirement
4141
return errors.Join(errs...)
4242
}
4343

44+
// prefixExportParam will modify a script to be executed by ssh.ExecuteScript so that it exports
45+
// all the variables from /mnt/lima-cidata/param.env before invoking the actual interpreter.
46+
//
47+
// - The script is executed in user mode, so needs to read the file using `sudo`.
48+
//
49+
// - `sudo cat param.env | while …; do export …; done` does not work because the piping
50+
// creates a subshell, and the exported variables are not visible to the parent process.
51+
//
52+
// - The `<<<"$string"` redirection is not available on alpine-lima, where /bin/bash is
53+
// just a wrapper around busybox ash.
54+
//
55+
// A script that will start with `#!/usr/bin/env ruby` will be modified to look like this:
56+
//
57+
// while read -r line; do
58+
// [ -n "$line" ] && export "$line"
59+
// done<<EOF
60+
// $(sudo cat /mnt/lima-cidata/param.env)
61+
// EOF
62+
// /usr/bin/env ruby
63+
//
64+
// ssh.ExecuteScript will strip the `#!` prefix from the first line and invoke the rest
65+
// of the line as the command. The full script is then passed via STDIN. We use the $' '
66+
// form of shell quoting to be able to use \n as newline escapes to fit everything on a
67+
// single line:
68+
//
69+
// #!/bin/bash -c $'while … done<<EOF\n$(sudo …)\nEOF\n/usr/bin/env ruby'
70+
// #!/usr/bin/env ruby
71+
// …
72+
func prefixExportParam(script string) (string, error) {
73+
interpreter, err := ssh.ParseScriptInterpreter(script)
74+
if err != nil {
75+
return "", err
76+
}
77+
78+
// TODO we should have a symbolic constant for `/mnt/lima-cidata`
79+
exportParam := `while read -r line; do [ -n "$line" ] && export "$line"; done<<EOF\n$(sudo cat /mnt/lima-cidata/param.env)\nEOF\n`
80+
return fmt.Sprintf("#!/bin/bash -c $'%s%s'\n%s", exportParam, interpreter, script), nil
81+
}
82+
4483
func (a *HostAgent) waitForRequirement(r requirement) error {
4584
logrus.Debugf("executing script %q", r.description)
46-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, r.script, r.description)
85+
script, err := prefixExportParam(r.script)
86+
if err != nil {
87+
return err
88+
}
89+
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, script, r.description)
4790
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
4891
if err != nil {
4992
return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)

pkg/limayaml/validate.go

+21-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"regexp"
1111
"runtime"
1212
"strings"
13+
"unicode"
1314

1415
"github.com/docker/go-units"
1516
"github.com/lima-vm/lima/pkg/localpathutil"
@@ -205,11 +206,13 @@ func Validate(y *LimaYAML, warn bool) error {
205206
}
206207
}
207208
for i, p := range y.Probes {
209+
if !strings.HasPrefix(p.Script, "#!") {
210+
return fmt.Errorf("field `probe[%d].script` must start with a '#!' line", i)
211+
}
208212
switch p.Mode {
209213
case ProbeModeReadiness:
210214
default:
211-
return fmt.Errorf("field `probe[%d].mode` can only be %q",
212-
i, ProbeModeReadiness)
215+
return fmt.Errorf("field `probe[%d].mode` can only be %q", i, ProbeModeReadiness)
213216
}
214217
}
215218
for i, rule := range y.PortForwards {
@@ -315,6 +318,21 @@ func Validate(y *LimaYAML, warn bool) error {
315318
if warn {
316319
warnExperimental(y)
317320
}
321+
322+
// Validate Param settings
323+
// Names must start with a letter, followed by any number of letters, digits, or underscores
324+
validParamName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)
325+
for param, value := range y.Param {
326+
if !validParamName.MatchString(param) {
327+
return fmt.Errorf("param %q name does not match regex %q", param, validParamName.String())
328+
}
329+
for _, r := range value {
330+
if !unicode.IsPrint(r) && r != '\t' && r != ' ' {
331+
return fmt.Errorf("param %q value contains unprintable character %q", param, r)
332+
}
333+
}
334+
}
335+
318336
return nil
319337
}
320338

@@ -397,7 +415,7 @@ func validateNetwork(y *LimaYAML) error {
397415
// It should be called before the `y` parameter is passed to FillDefault() that execute template.
398416
func ValidateParamIsUsed(y *LimaYAML) error {
399417
for key := range y.Param {
400-
re, err := regexp.Compile(`{{[^}]*\.Param\.` + key + `[^}]*}}`)
418+
re, err := regexp.Compile(`{{[^}]*\.Param\.` + key + `[^}]*}}|\bPARAM_` + key + `\b`)
401419
if err != nil {
402420
return fmt.Errorf("field to compile regexp for key %q: %w", key, err)
403421
}

pkg/limayaml/validate_test.go

+82-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,84 @@ func TestValidateDefault(t *testing.T) {
3131
assert.NilError(t, err)
3232
}
3333

34+
func TestValidateProbes(t *testing.T) {
35+
images := `images: [{"location": "/"}]`
36+
validProbe := `probes: ["script": "#!foo"]`
37+
y, err := Load([]byte(validProbe+"\n"+images), "lima.yaml")
38+
assert.NilError(t, err)
39+
40+
err = Validate(y, false)
41+
assert.NilError(t, err)
42+
43+
invalidProbe := `probes: ["script": "foo"]`
44+
y, err = Load([]byte(invalidProbe+"\n"+images), "lima.yaml")
45+
assert.NilError(t, err)
46+
47+
err = Validate(y, false)
48+
assert.Error(t, err, "field `probe[0].script` must start with a '#!' line")
49+
}
50+
51+
func TestValidateParamName(t *testing.T) {
52+
images := `images: [{"location": "/"}]`
53+
validProvision := `provision: [{"script": "echo $PARAM_name $PARAM_NAME $PARAM_Name_123"}]`
54+
validParam := []string{
55+
`param: {"name": "value"}`,
56+
`param: {"NAME": "value"}`,
57+
`param: {"Name_123": "value"}`,
58+
}
59+
for _, param := range validParam {
60+
y, err := Load([]byte(param+"\n"+validProvision+"\n"+images), "lima.yaml")
61+
assert.NilError(t, err)
62+
63+
err = Validate(y, false)
64+
assert.NilError(t, err)
65+
}
66+
67+
invalidProvision := `provision: [{"script": "echo $PARAM__Name $PARAM_3Name $PARAM_Last.Name"}]`
68+
invalidParam := []string{
69+
`param: {"_Name": "value"}`,
70+
`param: {"3Name": "value"}`,
71+
`param: {"Last.Name": "value"}`,
72+
}
73+
for _, param := range invalidParam {
74+
y, err := Load([]byte(param+"\n"+invalidProvision+"\n"+images), "lima.yaml")
75+
assert.NilError(t, err)
76+
77+
err = Validate(y, false)
78+
assert.ErrorContains(t, err, "name does not match regex")
79+
}
80+
}
81+
82+
func TestValidateParamValue(t *testing.T) {
83+
images := `images: [{"location": "/"}]`
84+
provision := `provision: [{"script": "echo $PARAM_name"}]`
85+
validParam := []string{
86+
`param: {"name": ""}`,
87+
`param: {"name": "foo bar"}`,
88+
`param: {"name": "foo\tbar"}`,
89+
`param: {"name": "Symbols ½ and emoji → 👀"}`,
90+
}
91+
for _, param := range validParam {
92+
y, err := Load([]byte(param+"\n"+provision+"\n"+images), "lima.yaml")
93+
assert.NilError(t, err)
94+
95+
err = Validate(y, false)
96+
assert.NilError(t, err)
97+
}
98+
99+
invalidParam := []string{
100+
`param: {"name": "The end.\n"}`,
101+
`param: {"name": "\r"}`,
102+
}
103+
for _, param := range invalidParam {
104+
y, err := Load([]byte(param+"\n"+provision+"\n"+images), "lima.yaml")
105+
assert.NilError(t, err)
106+
107+
err = Validate(y, false)
108+
assert.ErrorContains(t, err, "value contains unprintable character")
109+
}
110+
}
111+
34112
func TestValidateParamIsUsed(t *testing.T) {
35113
paramYaml := `param:
36114
name: value`
@@ -41,7 +119,9 @@ func TestValidateParamIsUsed(t *testing.T) {
41119
`mounts: [{"location": "/tmp/{{ .Param.name }}"}]`,
42120
`mounts: [{"location": "/tmp", mountPoint: "/tmp/{{ .Param.name }}"}]`,
43121
`provision: [{"script": "echo {{ .Param.name }}"}]`,
122+
`provision: [{"script": "echo $PARAM_name"}]`,
44123
`probes: [{"script": "echo {{ .Param.name }}"}]`,
124+
`probes: [{"script": "echo $PARAM_name"}]`,
45125
`copyToHost: [{"guest": "/tmp/{{ .Param.name }}", "host": "/tmp"}]`,
46126
`copyToHost: [{"guest": "/tmp", "host": "/tmp/{{ .Param.name }}"}]`,
47127
`portForwards: [{"guestSocket": "/tmp/{{ .Param.name }}", "hostSocket": "/tmp"}]`,
@@ -53,7 +133,7 @@ func TestValidateParamIsUsed(t *testing.T) {
53133
assert.NilError(t, err)
54134
}
55135

56-
// use "{{if .Param.rootful \"true\"}}{{else}}{{end}}"" in provision, probe, copyToHost, and portForward
136+
// use "{{if eq .Param.rootful \"true\"}}{{else}}{{end}}" in provision, probe, copyToHost, and portForward
57137
rootfulYaml := `param:
58138
rootful: true`
59139
fieldsUsingIfParamRootfulTrue := []string{
@@ -64,7 +144,7 @@ func TestValidateParamIsUsed(t *testing.T) {
64144
`copyToHost: [{"guest": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}", "host": "/tmp"}]`,
65145
`copyToHost: [{"guest": "/tmp", "host": "/tmp/{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}"}]`,
66146
`portForwards: [{"guestSocket": "{{if eq .Param.rootful \"true\"}}/var/run{{else}}/run/user/{{.UID}}{{end}}/docker.sock", "hostSocket": "{{.Dir}}/sock/docker.sock"}]`,
67-
`portForwards: [{"guestSocket": "/var/run/docker.sock", "hostSocket": "{{.Dir}}/sock/docker-{{if eq .Param.rootful \"true\"}}rootfule{{else}}rootless{{end}}.sock"}]`,
147+
`portForwards: [{"guestSocket": "/var/run/docker.sock", "hostSocket": "{{.Dir}}/sock/docker-{{if eq .Param.rootful \"true\"}}rootful{{else}}rootless{{end}}.sock"}]`,
68148
}
69149
for _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {
70150
_, err = Load([]byte(fieldUsingIfParamRootfulTrue+"\n"+rootfulYaml), "paramIsUsed.yaml")

website/content/en/docs/dev/internals/_index.md

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ See [Building Ansible inventories](https://docs.ansible.com/ansible/latest/inven
157157
- `meta-data`: [Cloud-init meta-data](https://docs.cloud-init.io/en/latest/explanation/instancedata.html)
158158
- `network-config`: [Cloud-init Networking Config Version 2](https://docs.cloud-init.io/en/latest/reference/network-config-format-v2.html)
159159
- `lima.env`: The `LIMA_CIDATA_*` environment variables (see below) available during `boot.sh` processing
160+
- `param.env`: The `PARAM_*` environment variables corresponding to the `param` settings from `lima.yaml`
160161
- `lima-guestagent`: Lima guest agent binary
161162
- `nerdctl-full.tgz`: [`nerdctl-full-<VERSION>-<OS>-<ARCH>.tar.gz`](https://github.com/containerd/nerdctl/releases)
162163
- `boot.sh`: Boot script

0 commit comments

Comments
 (0)