Skip to content

Commit

Permalink
Add limayaml param settings to provisioning script environment
Browse files Browse the repository at this point in the history
They will be prefixed with `PARAM_`, so `param.FOO` becomes `PARAM_FOO`.

This is useful because parameter substitution happens when a template is
instantiated, so `[ "{{.Param.ROOTFUL}}" = true ]` becomes `[ "true" = true ]`
in the cloud-init-output.log.

This mechanism also works better when the parameter contains quotes, which
would break a simplistic `FOO="{{.Param.FOO}}"`.

Signed-off-by: Jan Dubois <jan.dubois@suse.com>
  • Loading branch information
jandubois committed Sep 6, 2024
1 parent 536f375 commit b8fcab6
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 7 deletions.
6 changes: 6 additions & 0 deletions examples/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ containerd:
# playbook: playbook.yaml

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

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

Expand Down
11 changes: 11 additions & 0 deletions hack/test-templates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ declare -A CHECKS=(
["user-v2"]=""
["mount-path-with-spaces"]=""
["provision-ansible"]=""
["param-env-variables"]=""
)

case "$NAME" in
Expand Down Expand Up @@ -64,6 +65,7 @@ case "$NAME" in
CHECKS["snapshot-offline"]="1"
CHECKS["mount-path-with-spaces"]="1"
CHECKS["provision-ansible"]="1"
CHECKS["param-env-variables"]="1"
;;
"net-user-v2")
CHECKS["port-forwards"]=""
Expand Down Expand Up @@ -152,6 +154,15 @@ if [[ -n ${CHECKS["provision-ansible"]} ]]; then
limactl shell "$NAME" test -e /tmp/ansible
fi

if [[ -n ${CHECKS["param-env-variables"]} ]]; then
INFO 'Testing that PARAM env variables are exported to all types of provisioning scripts and probes'
limactl shell "$NAME" test -e /tmp/param-boot
limactl shell "$NAME" test -e /tmp/param-dependency
limactl shell "$NAME" test -e /tmp/param-probe
limactl shell "$NAME" test -e /tmp/param-system
limactl shell "$NAME" test -e /tmp/param-user
fi

INFO "Testing proxy settings are imported"
got=$(limactl shell "$NAME" env | grep FTP_PROXY)
# Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced
Expand Down
23 changes: 22 additions & 1 deletion hack/test-templates/test-misc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# - disk
# - (More to come)
#
# This template requires Lima v0.14.0 or later.
# This template requires Lima v1.0.0-alpha.0 or later.
images:
# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20220902/ubuntu-22.04-server-cloudimg-amd64.img"
Expand All @@ -26,9 +26,30 @@ mounts:
- location: "/tmp/lima"
writable: true

param:
BOOT: boot
DEPENDENCY: dependency
PROBE: probe
SYSTEM: system
USER: user

provision:
- mode: ansible
playbook: ./hack/ansible-test.yaml
- mode: boot
script: "touch /tmp/param-$PARAM_BOOT"
- mode: dependency
script: "touch /tmp/param-$PARAM_DEPENDENCY"
- mode: system
script: "touch /tmp/param-$PARAM_SYSTEM"
- mode: user
script: "touch /tmp/param-$PARAM_USER"

probes:
- mode: readiness
script: |
#!/bin/sh
touch /tmp/param-$PARAM_PROBE
# in order to use this example, you must first create the disk "data". run:
# $ limactl disk create data --size 10G
Expand Down
7 changes: 5 additions & 2 deletions pkg/cidata/cidata.TEMPLATE.d/boot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ WARNING() {
}

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

# shellcheck disable=SC2163
while read -r line; do
Expand Down Expand Up @@ -61,12 +63,13 @@ if [ -d "${LIMA_CIDATA_MNT}"/provision.user ]; then
if [ ! -f /sbin/openrc-run ]; then
until [ -e "/run/user/${LIMA_CIDATA_UID}/systemd/private" ]; do sleep 3; done
fi
params=$(grep -o '^PARAM_[^=]*' "${LIMA_CIDATA_MNT}"/param.env | paste -sd ,)
for f in "${LIMA_CIDATA_MNT}"/provision.user/*; do
INFO "Executing $f (as user ${LIMA_CIDATA_USER})"
cp "$f" "${USER_SCRIPT}"
chown "${LIMA_CIDATA_USER}" "${USER_SCRIPT}"
chmod 755 "${USER_SCRIPT}"
if ! sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "${USER_SCRIPT}"; then
if ! sudo -iu "${LIMA_CIDATA_USER}" "--preserve-env=${params}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "${USER_SCRIPT}"; then
WARNING "Failed to execute $f (as user ${LIMA_CIDATA_USER})"
CODE=1
fi
Expand Down
3 changes: 3 additions & 0 deletions pkg/cidata/cidata.TEMPLATE.d/param.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{range $key, $val := .Param -}}
PARAM_{{ $key }}={{ $val }}
{{end -}}
6 changes: 6 additions & 0 deletions pkg/cidata/cidata.TEMPLATE.d/user-data
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ ca_certs:
bootcmd:
{{- range $cmd := $.BootCmds }}
- |
# We need to embed the params.env as a here-doc because /mnt/lima-cidata is not yet mounted
while read -r line; do [ -n "$line" ] && export "$line"; done <<'EOT'
{{- range $key, $val := $.Param }}
PARAM_{{ $key }}={{ $val }}
{{- end }}
EOT
{{- range $line := $cmd.Lines }}
{{ $line }}
{{- end }}
Expand Down
1 change: 1 addition & 0 deletions pkg/cidata/cidata.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort
VirtioPort: virtioPort,
Plain: *y.Plain,
TimeZone: *y.TimeZone,
Param: y.Param,
}

firstUsernetIndex := limayaml.FirstUsernetIndex(y)
Expand Down
1 change: 1 addition & 0 deletions pkg/cidata/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type TemplateArgs struct {
UDPDNSLocalPort int
TCPDNSLocalPort int
Env map[string]string
Param map[string]string
DNSAddresses []string
CACerts CACerts
HostHomeMountPoint string
Expand Down
45 changes: 44 additions & 1 deletion pkg/hostagent/requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,52 @@ func (a *HostAgent) waitForRequirements(label string, requirements []requirement
return errors.Join(errs...)
}

// prefixExportParam will modify a script to be executed by ssh.ExecuteScript so that it exports
// all the variables from /mnt/lima-cidata/param.env before invoking the actual interpreter.
//
// * The script is executed in user mode, so needs to read the file using `sudo`.
//
// - `sudo cat param.env | while …; do export …; done` does not work because the piping
// creates a subshell, and the exported variables are not visible to the parent process.
//
// - The `<<<"$string"` redirection is not available on alpine-lima, where /bin/bash is
// just a wrapper around busybox ash.
//
// A script that will start with `#!/usr/bin/env ruby` will be modified to look like this:
//
// while read -r line; do
// [ -n "$line" ] && export "$line"
// done<<EOF
// $(sudo cat /mnt/lima-cidata/param.env)
// EOF
// /usr/bin/env ruby
//
// ssh.ExecuteScript will strip the `#!` prefix from the first line and invoke the rest
// of the line as the command. The full script is then passed via STDIN. We use the $”
// form of shell quoting to be able to use \n as newline escapes to fit everything on a
// single line:
//
// #!/bin/bash -c $'while … done<<EOF\n$(sudo …)\nEOF\n/usr/bin/env ruby'
// #!/usr/bin/env ruby
// …
func prefixExportParam(script string) (string, error) {
interpreter, err := ssh.ParseScriptInterpreter(script)
if err != nil {
return "", err
}

// TODO we should have a symbolic constant for `/mnt/lima-cidata`
exportParam := `while read -r line; do [ -n "$line" ] && export "$line"; done<<EOF\n$(sudo cat /mnt/lima-cidata/param.env)\nEOF\n`
return fmt.Sprintf("#!/bin/bash -c $'%s%s'\n%s", exportParam, interpreter, script), nil
}

func (a *HostAgent) waitForRequirement(r requirement) error {
logrus.Debugf("executing script %q", r.description)
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, r.script, r.description)
script, err := prefixExportParam(r.script)
if err != nil {
return err
}
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, script, r.description)
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
if err != nil {
return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)
Expand Down
24 changes: 21 additions & 3 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"regexp"
"runtime"
"strings"
"unicode"

"github.com/docker/go-units"
"github.com/lima-vm/lima/pkg/localpathutil"
Expand Down Expand Up @@ -205,11 +206,13 @@ func Validate(y *LimaYAML, warn bool) error {
}
}
for i, p := range y.Probes {
if !strings.HasPrefix(p.Script, "#!") {
return fmt.Errorf("field `probe[%d].mode` must start with a '#!' line", i)
}
switch p.Mode {
case ProbeModeReadiness:
default:
return fmt.Errorf("field `probe[%d].mode` can only be %q",
i, ProbeModeReadiness)
return fmt.Errorf("field `probe[%d].mode` can only be %q", i, ProbeModeReadiness)
}
}
for i, rule := range y.PortForwards {
Expand Down Expand Up @@ -315,6 +318,21 @@ func Validate(y *LimaYAML, warn bool) error {
if warn {
warnExperimental(y)
}

// Validate Param settings
// Names must start with a letter, followed by any number of letters, digits, or underscores
validParamName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)
for param, value := range y.Param {
if !validParamName.MatchString(param) {
return fmt.Errorf("param %q name does not match regex %q", param, validParamName.String())
}
for _, r := range value {
if !unicode.IsPrint(r) && r != '\t' && r != ' ' {
return fmt.Errorf("param %q value contains unprintable character %q", param, r)
}
}
}

return nil
}

Expand Down Expand Up @@ -397,7 +415,7 @@ func validateNetwork(y *LimaYAML) error {
// 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 + `[^}]*}}`)
re, err := regexp.Compile(`{{[^}]*\.Param\.` + key + `[^}]*}}|\bPARAM_` + key + `\b`)
if err != nil {
return fmt.Errorf("field to compile regexp for key %q: %w", key, err)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/limayaml/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ func TestValidateParamIsUsed(t *testing.T) {
`mounts: [{"location": "/tmp/{{ .Param.name }}"}]`,
`mounts: [{"location": "/tmp", mountPoint: "/tmp/{{ .Param.name }}"}]`,
`provision: [{"script": "echo {{ .Param.name }}"}]`,
`provision: [{"script": "echo $PARAM_name"}]`,
`probes: [{"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"}]`,
Expand Down
1 change: 1 addition & 0 deletions website/content/en/docs/dev/internals/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ See [Building Ansible inventories](https://docs.ansible.com/ansible/latest/inven
- `meta-data`: [Cloud-init meta-data](https://docs.cloud-init.io/en/latest/explanation/instancedata.html)
- `network-config`: [Cloud-init Networking Config Version 2](https://docs.cloud-init.io/en/latest/reference/network-config-format-v2.html)
- `lima.env`: The `LIMA_CIDATA_*` environment variables (see below) available during `boot.sh` processing
- `param.env`: The `PARAM_*` environment variables corresponding to the `param` settings from `lima.yaml`
- `lima-guestagent`: Lima guest agent binary
- `nerdctl-full.tgz`: [`nerdctl-full-<VERSION>-<OS>-<ARCH>.tar.gz`](https://github.com/containerd/nerdctl/releases)
- `boot.sh`: Boot script
Expand Down

0 comments on commit b8fcab6

Please sign in to comment.