Skip to content

exec

exec runs a shell command. It is the escape hatch for anything the other resources do not cover: issuing a certificate, running an installer, installing from the AUR. A command has no state sadame can read, so to stay idempotent an exec needs a guard that says when the work is already done. Guards are evaluated during plan as well as apply; the command itself only runs during apply.

Minimal example

examples/resources/exec/minimal.rb
exec "tar -czf /var/backups/etc.tar.gz /etc", creates: "/var/backups/etc.tar.gz"

Options

Option Type Default Description
command String the resource name Command to run, when you want the name to be a short label.
creates String none Skip when this path exists.
not_if String none Skip when this command exits 0.
only_if String none Skip unless this command exits 0.
cwd String inherited Working directory for the command and its guards.
env Hash of String to String none Environment variables for the command and its guards.
action :run or :nothing :run :nothing never runs the command on its own, only when another resource notifies it.
notifies [:action, "type[name]"], optionally with :immediately, or a list of them none Actions to run on other resources when this command runs. See Notifications.
subscribes [:action, "type[name]"], optionally with :immediately, or a list of them none Actions to run on this resource when another one changes.
tags Symbol, String, or an Array of them none Tags for running part of a recipe with --tags and --skip-tags. See Tags.

The command runs when every guard you gave agrees: the creates path is missing, not_if exits non-zero, and only_if exits 0. Guards are checked in that order and checking stops at the first one that says skip. Without any guard, the command runs on every apply.

Commands and guards run with /bin/sh and standard input closed. A command that exits non-zero fails the resource: apply prints the exit status and the command's error output, marks the resource failed, and skips the resources after it.

Every command sadame runs, including yours, runs in the C.UTF-8 locale (LC_ALL=C.UTF-8, LANGUAGE unset), so programs answer in English whatever the language of the machine or of the SSH session: a guard such as not_if: "ufw status | grep -q 'Status: active'" matches on every system. Set LC_ALL inside the command if it needs another locale.

Other resources can trigger the command with notifies: [:run, "exec[NAME]"]. Combined with action: :nothing, that gives a command that runs only when something else changed, such as systemctl daemon-reload after a unit file changed; see Notifications.

Scenarios

examples/resources/exec/recipe.rb
directory "/opt/demo"

# creates: skip when the path exists.
exec "tar -czf /opt/demo/etc-backup.tar.gz -C /etc hostname hosts",
     creates: "/opt/demo/etc-backup.tar.gz"

# not_if: skip when the command succeeds.
exec "echo 'EDITOR=vim' >> /etc/environment",
     not_if: "grep -qs '^EDITOR=vim$' /etc/environment"

# only_if: run only when the command succeeds. Here: only on hosts with git,
# and only until the setting is in place.
exec "git config --system init.defaultBranch main",
     only_if: "command -v git",
     not_if: "git config --system --get init.defaultBranch"

# cwd: and env: set the working directory and environment.
exec "echo \"built by $BUILDER\" > build-info.txt",
     cwd: "/opt/demo",
     env: { "BUILDER" => "sadame" },
     creates: "/opt/demo/build-info.txt"

Plan explains why each command will run

$ sudo sadame plan examples/resources/exec
sadame 0.5.1 plan on local (Debian GNU/Linux 12 (bookworm))

+ directory[/opt/demo]
    state: absent -> present

! exec[tar -czf /opt/demo/etc-backup.tar.gz -C /etc hostname hosts]
    command: tar -czf /opt/demo/etc-backup.tar.gz -C /etc hostname hosts
    (/opt/demo/etc-backup.tar.gz does not exist)

! exec[echo 'EDITOR=vim' >> /etc/environment]
    command: echo 'EDITOR=vim' >> /etc/environment
    (not_if exited non-zero)

! exec[echo "built by $BUILDER" > build-info.txt]
    command: echo "built by $BUILDER" > build-info.txt
    (/opt/demo/build-info.txt does not exist)

Plan: 1 to create, 0 to update, 0 to remove, 3 to run, 1 unchanged.

The git config command is not listed: this machine has no git, so its only_if guard skips it.

After apply, the guards hold

$ sudo sadame apply examples/resources/exec
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

changed    directory[/opt/demo]
           state: absent -> present
changed    exec[tar -czf /opt/demo/etc-backup.tar.gz -C /etc hostname hosts]
           command: tar -czf /opt/demo/etc-backup.tar.gz -C /etc hostname hosts
           (/opt/demo/etc-backup.tar.gz does not exist)
changed    exec[echo 'EDITOR=vim' >> /etc/environment]
           command: echo 'EDITOR=vim' >> /etc/environment
           (not_if exited non-zero)
unchanged  exec[git config --system init.defaultBranch main]
changed    exec[echo "built by $BUILDER" > build-info.txt]
           command: echo "built by $BUILDER" > build-info.txt
           (/opt/demo/build-info.txt does not exist)

Summary
  changed      4  directory[/opt/demo]
                  exec[tar -czf /opt/demo/etc-backup.tar.gz -C /etc hostname hosts]
                  exec[echo 'EDITOR=vim' >> /etc/environment]
                  exec[echo "built by $BUILDER" > build-info.txt]
  unchanged    1
  failed       0
  skipped      0
  notified     0

Finished in 0.19s.

$ sudo sadame plan examples/resources/exec
sadame 0.5.1 plan on local (Debian GNU/Linux 12 (bookworm))

Plan: 0 to create, 0 to update, 0 to remove, 0 to run, 5 unchanged.

$ cat /opt/demo/build-info.txt
built by sadame

Common mistakes

  • No guard. An exec without creates, not_if or only_if runs on every apply, plan always lists it with "(no guard, runs on every apply)", and drift always reports it. Add a guard, or use action: :nothing if it should only run when notified.
  • Guards with side effects. Guards run during plan. A guard such as not_if: "mkdir /x" changes the machine when you only asked to look.
  • Interactive commands. Standard input is closed, so a command that prompts reads end-of-file and usually fails. Pass the tool's non-interactive flag (-y, --noconfirm).
  • A guard that never becomes true. If the command does not create what the guard checks for, it runs every time. Check the exact path or setting the command produces.

See also