Actions¶
Actions are the operations Mooncake performs. Each step in your configuration uses one action type.
Quick Navigation¶
| Action | Purpose | Jump to |
|---|---|---|
| shell | Execute commands | ↓ |
| command | Direct execution (no shell) | ↓ |
| file | Create/manage files | ↓ |
| copy | Copy files | ↓ |
| download | Download from URLs | ↓ |
| package | Manage packages | ↓ |
| unarchive | Extract archives | ↓ |
| template | Render templates | ↓ |
| service | Manage services | ↓ |
| assert | Verify state | ↓ |
| preset | Reusable workflows | ↓ |
| include | Load configs | ↓ |
| include_vars | Load variables | ↓ |
| vars | Define variables | ↓ |
📋 Complete Properties Reference → - All properties organized by type
Shell¶
Execute shell commands with full shell interpolation and scripting capabilities.
Basic Usage (Simple String)¶
Structured Shell (Advanced)¶
- name: Run with interpreter
shell:
cmd: echo "Hello"
interpreter: bash
stdin: "input data"
capture: true
Shell Properties¶
Shell commands support both simple string form and structured object form:
Simple Form:
Structured Form:
| Property | Type | Description |
|---|---|---|
shell.cmd |
string | Command to execute (required) |
shell.interpreter |
string | Shell interpreter: "bash", "sh", "pwsh", "cmd" (default: "bash" on Unix, "pwsh" on Windows) |
shell.stdin |
string | Input to pipe into command (supports templates) |
shell.capture |
boolean | Capture output (default: true). Set false for streaming-only mode |
Step-Level Properties (work with all actions):
| Property | Type | Description |
|---|---|---|
env |
object | Environment variables |
cwd |
string | Working directory |
timeout |
string | Maximum execution time (e.g., '30s', '5m') |
retries |
integer | Number of retry attempts (0-100) |
retry_delay |
string | Delay between retries (e.g., '5s') |
changed_when |
string | Expression to override changed status |
failed_when |
string | Expression to override failure status |
become_user |
string | User for sudo (when become: true) |
Plus all universal fields: name, when, become, tags, register, with_items, with_filetree
Multi-line Commands¶
With Variables¶
With Execution Control¶
- name: Robust download
shell: curl -O https://example.com/file.tar.gz
timeout: 10m
retries: 3
retry_delay: 30s
env:
HTTP_PROXY: "{{proxy_url}}"
cwd: /tmp/downloads
Structured Shell with Interpreter¶
- name: PowerShell on Windows
shell:
cmd: Get-Process | Where-Object {$_.CPU -gt 100}
interpreter: pwsh
- name: POSIX shell for compatibility
shell:
cmd: printf '%s\n' "Hello"
interpreter: sh
Shell with stdin¶
- name: Pipe data to command
shell:
cmd: python3 process_input.py
stdin: |
line1
line2
line3
- name: Use template in stdin
shell:
cmd: psql -U {{db_user}} {{db_name}}
stdin: |
SELECT * FROM users WHERE active = true;
Shell Quoting Rules¶
When to use shell vs command:
- Use
shellwhen you need: - Shell features: pipes (
|), redirects (>,<), wildcards (*) - Command substitution:
$(command)or`command` - Environment variable expansion:
$VAR -
Shell scripting:
if,for,whileloops -
Use
command(see below) when: - You have a fixed command with known arguments
- You don't need shell interpretation
- You want to avoid quoting issues
- You want better security (no shell injection)
Quoting in shell:
# Good - quotes protect spaces
- shell: echo "hello world"
# Good - single quotes prevent variable expansion
- shell: echo 'The $PATH is set'
# Template variables - use quotes if they might contain spaces
- shell: echo "User: {{username}}"
# Multiple commands
- shell: |
cd /tmp
echo "Working in $(pwd)"
ls -la
Command¶
Execute commands directly without shell interpolation. This is safer and faster when you don't need shell features.
Basic Usage¶
Command Properties¶
| Property | Type | Description |
|---|---|---|
command.argv |
array | Command and arguments as list (required) |
command.stdin |
string | Input to pipe into command (supports templates) |
command.capture |
boolean | Capture output (default: true). Set false for streaming-only mode |
Step-Level Properties (same as shell):
| Property | Type | Description |
|---|---|---|
env |
object | Environment variables |
cwd |
string | Working directory |
timeout |
string | Maximum execution time |
retries |
integer | Number of retry attempts |
retry_delay |
string | Delay between retries |
changed_when |
string | Expression to override changed status |
failed_when |
string | Expression to override failure status |
become_user |
string | User for sudo (when become: true) |
Plus all universal fields: name, when, become, tags, register, with_items, with_filetree
Command with Templates¶
- vars:
repo_url: "https://github.com/user/repo.git"
target_dir: "/opt/repo"
- name: Clone with template variables
command:
argv:
- git
- clone
- "{{repo_url}}"
- "{{target_dir}}"
Command with stdin¶
- name: Feed data to process
command:
argv: ["python3", "-c", "import sys; print(sys.stdin.read().upper())"]
stdin: "hello world"
Command vs Shell Comparison¶
# Shell - uses shell interpolation
- name: Shell with pipe
shell: ls -la | grep myfile
# Command - direct execution (no shell)
- name: Command (no pipes/wildcards)
command:
argv: ["ls", "-la", "/tmp"]
# Shell - variable expansion
- name: Shell with $HOME
shell: echo $HOME
# Command - literal arguments (no variable expansion)
- name: Command (explicit paths)
command:
argv: ["echo", "{{ansible_env.HOME}}"]
Security: Shell vs Command¶
Shell injection risk:
# UNSAFE if user_input contains "; rm -rf /"
- shell: echo "{{user_input}}"
# SAFE - no shell interpretation
- command:
argv: ["echo", "{{user_input}}"]
When to use each:
shell: Trust the input, need shell featurescommand: Don't trust input, simple command execution
File¶
Create and manage files and directories.
File Properties¶
| Property | Type | Description |
|---|---|---|
file.path |
string | File or directory path (required) |
file.state |
string | file, directory, absent, touch, link, hardlink, or perms |
file.content |
string | Content to write to file (for state: file) |
file.mode |
string | Permissions (e.g., "0644", "0755") |
file.owner |
string | File owner (username or UID) |
file.group |
string | File group (group name or GID) |
file.src |
string | Source path (required for link and hardlink states) |
file.force |
boolean | Force overwrite existing files or remove non-empty directories |
file.recurse |
boolean | Apply permissions recursively (with state: perms) |
file.backup |
boolean | Create .bak backup before overwriting |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Note: File operations do NOT support shell-specific fields (timeout, retries, env, cwd, etc.)
Create Directory¶
Create File¶
Create File with Content¶
- name: Create config
file:
path: /tmp/config.txt
state: file
mode: "0644"
content: |
key: value
debug: true
File Permissions¶
Common permission modes:
"0755"- rwxr-xr-x (directories, executables)"0644"- rw-r--r-- (regular files)"0600"- rw------- (private files)"0700"- rwx------ (private directories)
Remove File or Directory¶
- name: Remove file
file:
path: /tmp/old-file.txt
state: absent
- name: Remove directory (empty)
file:
path: /tmp/old-dir
state: absent
- name: Remove directory (recursive)
file:
path: /tmp/old-dir
state: absent
force: true
Touch File (Update Timestamp)¶
Create Symbolic Link¶
- name: Create symlink
file:
path: /usr/local/bin/myapp
src: /opt/myapp/bin/myapp
state: link
- name: Force replace existing file with symlink
file:
path: /etc/config.yml
src: /opt/configs/prod.yml
state: link
force: true
Create Hard Link¶
Change Permissions Only¶
- name: Fix permissions on existing file
file:
path: /opt/app/data
state: perms
mode: "0755"
owner: app
group: app
- name: Recursively fix directory permissions
file:
path: /var/www/html
state: perms
mode: "0644"
owner: www-data
group: www-data
recurse: true
become: true
Set Ownership¶
- name: Change file owner
file:
path: /opt/app/config.yml
state: file
owner: app
group: app
mode: "0600"
become: true
Copy¶
Copy files with checksum verification and backup support.
Copy Properties¶
| Property | Type | Description |
|---|---|---|
copy.src |
string | Source file path (required) |
copy.dest |
string | Destination file path (required) |
copy.mode |
string | Permissions (e.g., "0644", "0755") |
copy.owner |
string | File owner (username or UID) |
copy.group |
string | File group (group name or GID) |
copy.backup |
boolean | Create .bak backup before overwriting |
copy.force |
boolean | Force overwrite if destination exists |
copy.checksum |
string | Expected SHA256 or MD5 checksum |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Basic Copy¶
Copy with Backup¶
- name: Update config with backup
copy:
src: ./configs/prod.yml
dest: /etc/app/config.yml
mode: "0600"
owner: app
group: app
backup: true
become: true
Copy with Checksum Verification¶
- name: Copy binary with integrity check
copy:
src: ./downloads/app-v1.2.3
dest: /usr/local/bin/app
mode: "0755"
checksum: "sha256:a3b5c6d7e8f9..."
Unarchive¶
Extract archive files with automatic format detection and security protections.
Unarchive Properties¶
| Property | Type | Description |
|---|---|---|
unarchive.src |
string | Path to archive file (required) |
unarchive.dest |
string | Destination directory (required) |
unarchive.strip_components |
integer | Number of leading path components to strip (default: 0) |
unarchive.creates |
string | Skip extraction if this path exists (idempotency marker) |
unarchive.mode |
string | Directory permissions (e.g., "0755") |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Supported formats: .tar, .tar.gz, .tgz, .zip (auto-detected from extension)
Security: Automatically blocks path traversal attacks (../ sequences) and validates all extracted paths.
Basic Extraction¶
Extract with Path Stripping¶
Strip leading path components (like tar's --strip-components):
# Archive contains: node-v20/bin/node, node-v20/lib/...
# Result: /opt/node/bin/node, /opt/node/lib/...
- name: Extract Node.js without top-level directory
unarchive:
src: /tmp/node-v20.tar.gz
dest: /opt/node
strip_components: 1
Extract with Idempotency¶
Skip extraction if marker file already exists:
- name: Extract application
unarchive:
src: /tmp/myapp.tar.gz
dest: /opt/myapp
creates: /opt/myapp/.installed
mode: "0755"
# Run again - will skip because marker exists
Extract Multiple Archives¶
- vars:
archives:
- name: app
file: app-v1.2.3.tar.gz
- name: data
file: data.zip
- name: Extract {{item.name}}
unarchive:
src: /tmp/{{item.file}}
dest: /opt/{{item.name}}
strip_components: 1
with_items: "{{archives}}"
Extract with Become¶
- name: Extract to system directory
unarchive:
src: /tmp/archive.tar.gz
dest: /opt/myapp
mode: "0755"
become: true
Supported Archive Formats¶
- tar - Uncompressed tar archives (
.tar) - tar.gz - Gzip compressed tar archives (
.tar.gz) - tgz - Alternative gzip tar extension (
.tgz) - zip - ZIP archives (
.zip)
Format is detected automatically from the file extension (case-insensitive).
How strip_components Works¶
# Archive structure:
# project-1.0/src/main.go
# project-1.0/src/utils.go
# project-1.0/README.md
# strip_components: 0 (default)
# Result: dest/project-1.0/src/main.go
# strip_components: 1
# Result: dest/src/main.go
# strip_components: 2
# Result: dest/main.go
Files with fewer path components than strip_components are skipped.
Security Features¶
All extracted paths are validated to prevent:
- Path traversal attacks - Blocks
../sequences - Absolute paths - Prevents extracting to system paths
- Symlink escapes - Validates symlink targets stay within destination
These protections are always active and cannot be disabled.
Download¶
Download files from remote URLs with checksum verification and retry support.
Download Properties¶
| Property | Type | Description |
|---|---|---|
download.url |
string | Remote URL to download from (required) |
download.dest |
string | Destination file path (required) |
download.checksum |
string | Expected SHA256 (64 chars) or MD5 (32 chars) checksum |
download.mode |
string | File permissions (e.g., "0644", "0755") |
download.timeout |
string | Maximum download time (e.g., "30s", "5m") |
download.retries |
integer | Number of retry attempts on failure (0-100) |
download.force |
boolean | Force re-download even if destination exists |
download.backup |
boolean | Create .bak backup before overwriting |
download.headers |
object | Custom HTTP headers (Authorization, User-Agent, etc.) |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Idempotency: Downloads are skipped when:
- Destination file exists with matching checksum (when
checksumis provided) - Destination file exists and
force: false(without checksum - not recommended)
Best practice: Always use checksum for reliable idempotency and security.
Basic Download¶
- name: Download file
download:
url: "https://example.com/file.tar.gz"
dest: "/tmp/file.tar.gz"
mode: "0644"
Download with Checksum (Idempotent)¶
- name: Download Go tarball
download:
url: "https://go.dev/dl/go1.21.5.linux-amd64.tar.gz"
dest: "/tmp/go.tar.gz"
checksum: "e2bc0b3e4b64111ec117295c088bde5f00eeed1567999ff77bc859d7df70078e"
mode: "0644"
register: go_download
# Second run will skip download (idempotent)
Download with Retry and Timeout¶
- name: Download large file
download:
url: "https://releases.ubuntu.com/22.04/ubuntu.iso"
dest: "/tmp/ubuntu.iso"
timeout: "10m"
retries: 3
mode: "0644"
Authenticated Download¶
- name: Download from private API
download:
url: "https://api.example.com/files/document.pdf"
dest: "/tmp/document.pdf"
headers:
Authorization: "Bearer {{ api_token }}"
User-Agent: "Mooncake/1.0"
mode: "0644"
Download with Backup¶
- name: Update config file safely
download:
url: "https://example.com/config/app.conf"
dest: "/etc/myapp/app.conf"
backup: true
force: true
mode: "0644"
become: true
Download and Extract¶
- name: Download Node.js
download:
url: "https://nodejs.org/dist/v18.19.0/node-v18.19.0-linux-x64.tar.gz"
dest: "/tmp/node.tar.gz"
checksum: "f27e33ebe5a0c2ec8d5d6b5f5c7c2c0c1c3f7b1a2a3d4e5f6g7h8i9j0k1l2m3n"
mode: "0644"
register: node_download
- name: Extract if downloaded
unarchive:
src: "/tmp/node.tar.gz"
dest: "/opt/node"
strip_components: 1
when: node_download.changed
How Checksum Works¶
The checksum field supports both SHA256 and MD5:
# SHA256 (64 hexadecimal characters) - recommended
checksum: "e2bc0b3e4b64111ec117295c088bde5f00eeed1567999ff77bc859d7df70078e"
# MD5 (32 hexadecimal characters) - legacy support
checksum: "5d41402abc4b2a76b9719d911017c592"
How it works:
- If destination exists, calculate its checksum
- If checksums match → skip download (idempotent)
- If checksums differ → download new version
- After download, verify checksum matches expected value
Security Features¶
All downloads include these security features:
- Atomic writes - Downloads to temp file, verifies, then renames (prevents partial downloads)
- Checksum verification - Prevents man-in-the-middle attacks (when checksum provided)
- HTTPS support - Secure downloads over TLS
- Timeout protection - Prevents hanging on slow connections
Performance Tips¶
# Good - Fast idempotency check (4ms vs 40ms)
- download:
url: "https://example.com/large-file.iso"
dest: "/tmp/file.iso"
checksum: "abc123..." # Enables fast skip on second run
# Avoid - Always re-downloads without checksum verification
- download:
url: "https://example.com/file.iso"
dest: "/tmp/file.iso"
force: true # No idempotency
Package¶
Manage system packages (install, remove, update) with automatic package manager detection.
Package Properties¶
| Property | Type | Description |
|---|---|---|
package.name |
string | Single package name to manage |
package.names |
array | Multiple package names to manage |
package.state |
string | Desired state: present (default), absent, latest |
package.manager |
string | Package manager to use (auto-detected if not specified) |
package.update_cache |
boolean | Update package cache before operation |
package.upgrade |
boolean | Upgrade all installed packages (ignores name/names) |
package.extra |
array | Extra arguments to pass to package manager |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Supported Package Managers:
- Linux: apt, dnf, yum, pacman, zypper, apk
- macOS: brew, port
- Windows: choco, scoop
Auto-detection: Uses package_manager system fact or detects based on OS if not specified.
Idempotency: Checks if package is already installed before attempting installation.
Install Single Package¶
Install Multiple Packages¶
- name: Install development tools
package:
names:
- git
- curl
- vim
- wget
state: present
become: true
Auto-Detect Package Manager¶
- name: Install Python (cross-platform)
package:
name: python3
update_cache: true
become: true
# Uses apt on Debian/Ubuntu, dnf on Fedora, brew on macOS, etc.
Specify Package Manager¶
- name: Install Node.js with Homebrew
package:
name: node
manager: brew
# Explicitly use Homebrew even if other managers are available
Remove Package¶
Install Latest Version¶
Upgrade All Packages¶
- name: System-wide package upgrade
package:
upgrade: true
become: true
# Runs: apt-get upgrade, dnf upgrade, brew upgrade, etc.
With Extra Arguments¶
- name: Install with specific options
package:
name: nginx
state: present
extra:
- "--no-install-recommends"
become: true
# For apt: apt-get install -y --no-install-recommends nginx
Loop Over Packages¶
- vars:
dev_packages:
- gcc
- make
- autoconf
- pkg-config
- name: Install build tools
package:
name: "{{ item }}"
state: present
with_items: "{{ dev_packages }}"
become: true
Conditional Package Management¶
- name: Install package manager specific tools
package:
name: "{{ item.package }}"
manager: "{{ item.manager }}"
state: present
with_items:
- { manager: "apt", package: "apt-transport-https" }
- { manager: "dnf", package: "dnf-plugins-core" }
when: package_manager == item.manager
become: true
Update Cache Before Install¶
- name: Install with fresh cache
package:
name: docker-ce
state: present
update_cache: true
become: true
# Runs apt-get update or equivalent before installation
Service¶
Manage system services (systemd on Linux, launchd on macOS).
Service Properties¶
| Property | Type | Description |
|---|---|---|
service.name |
string | Service name (required) |
service.state |
string | Desired state: started, stopped, restarted, reloaded |
service.enabled |
boolean | Enable service on boot (systemd: enable/disable, launchd: bootstrap/bootout) |
service.daemon_reload |
boolean | Run systemctl daemon-reload after unit file changes (systemd only) |
service.unit |
object | Unit/plist file configuration (see below) |
service.dropin |
object | Drop-in configuration (systemd only, see below) |
Unit File Properties:
| Property | Type | Description |
|---|---|---|
unit.dest |
string | Destination path (default: /etc/systemd/system/<name>.service or ~/Library/LaunchAgents/<name>.plist) |
unit.content |
string | Inline unit/plist file content (supports templates) |
unit.src_template |
string | Path to unit/plist template file |
unit.mode |
string | File permissions (e.g., "0644") |
Drop-in Properties (systemd only):
| Property | Type | Description |
|---|---|---|
dropin.name |
string | Drop-in file name (e.g., "10-override.conf") - required |
dropin.content |
string | Inline drop-in content (supports templates) |
dropin.src_template |
string | Path to drop-in template file |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Linux (systemd) Examples¶
Start and Enable Service¶
Create Service from Template¶
- name: Deploy custom service
service:
name: myapp
unit:
src_template: templates/myapp.service.j2
dest: /etc/systemd/system/myapp.service
daemon_reload: true
state: started
enabled: true
become: true
Create Service with Inline Content¶
- name: Create simple service
service:
name: myapp
unit:
content: |
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure
[Install]
WantedBy=multi-user.target
daemon_reload: true
state: started
enabled: true
become: true
Add Drop-in Configuration¶
- name: Override service environment
service:
name: myapp
dropin:
name: "10-env.conf"
content: |
[Service]
Environment="API_KEY={{ api_key }}"
Environment="DEBUG=true"
daemon_reload: true
state: restarted
become: true
Stop and Disable Service¶
macOS (launchd) Examples¶
Create User Agent¶
- name: Start user agent
service:
name: com.example.myapp
state: started
enabled: true
unit:
content: |
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.myapp</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/myapp</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Create System Daemon (requires sudo)¶
- name: Create system daemon
service:
name: com.example.daemon
state: started
enabled: true
unit:
dest: /Library/LaunchDaemons/com.example.daemon.plist
src_template: templates/daemon.plist.j2
become: true
Create Scheduled Task¶
- name: Create backup task
service:
name: com.example.backup
enabled: true
unit:
content: |
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.backup</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/backup.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
</dict>
</plist>
Service States¶
| State | Linux (systemd) | macOS (launchd) |
|---|---|---|
started |
systemctl start |
launchctl bootstrap / kickstart |
stopped |
systemctl stop |
launchctl kill |
restarted |
systemctl restart |
launchctl kickstart -k |
reloaded |
systemctl reload |
Same as restart |
Idempotency¶
Service operations are idempotent:
- Unit/plist files: Only updated if content changed
- Service state: Checked before changing
- Enable status: Only changed if different
# First run: Creates unit, reloads daemon, starts service, enables on boot
# Second run: No changes (unit unchanged, service already running and enabled)
- name: Deploy service
service:
name: myapp
state: started
enabled: true
unit:
content: |
[Unit]
Description=My App
[Service]
ExecStart=/usr/local/bin/myapp
[Install]
WantedBy=multi-user.target
become: true
Platform Detection¶
Mooncake automatically detects the platform and uses the appropriate service manager:
- Linux: Uses systemd (
systemctl) - macOS: Uses launchd (
launchctl) - Windows: Not yet supported
Complete Examples¶
See detailed examples with real-world use cases:
- macOS Services:
examples/macos-services/- Complete launchd examples with Node.js apps, scheduled tasks, and service management patterns - Service Management README:
examples/macos-services/README.md- Comprehensive guide to macOS service management
Assert¶
Verify system state, command results, file properties, or HTTP responses. Assertions never report changed: true and fail fast if verification doesn't pass.
Use cases:
- Verify prerequisites before deployment
- Check system configuration meets requirements
- Validate API responses
- Test infrastructure state
- Ensure files have correct permissions
Assert Properties¶
Assertions require exactly one of these types:
Command Assertion:
| Property | Type | Description |
|---|---|---|
assert.command.cmd |
string | Command to execute (required) |
assert.command.exit_code |
integer | Expected exit code (default: 0) |
File Assertion:
| Property | Type | Description |
|---|---|---|
assert.file.path |
string | File path to check (required) |
assert.file.exists |
boolean | Verify file exists (true) or doesn't exist (false) |
assert.file.content |
string | Expected exact file content (supports templates) |
assert.file.contains |
string | Expected substring in file (supports templates) |
assert.file.mode |
string | Expected file permissions (e.g., "0644") |
assert.file.owner |
string | Expected file owner (UID or username) |
assert.file.group |
string | Expected file group (GID or groupname) |
HTTP Assertion:
| Property | Type | Description |
|---|---|---|
assert.http.url |
string | URL to request (required) |
assert.http.method |
string | HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS (default: GET) |
assert.http.status |
integer | Expected status code (default: 200) |
assert.http.headers |
object | Request headers (supports templates) |
assert.http.body |
string | Request body (supports templates) |
assert.http.contains |
string | Expected substring in response body |
assert.http.body_equals |
string | Expected exact response body |
assert.http.timeout |
string | Request timeout (e.g., "30s", "5m") |
Plus universal fields: name, when, tags, register, with_items, with_filetree
Command Assertions¶
Verify Command Succeeds¶
Expect Specific Exit Code¶
- name: Verify configuration is invalid
assert:
command:
cmd: validate-config broken.yml
exit_code: 1
Check Command with Template¶
- name: Verify package installed
assert:
command:
cmd: "dpkg -l | grep {{ package_name }}"
exit_code: 0
File Assertions¶
Check File Exists¶
Check File Does Not Exist¶
Verify File Content¶
- name: Check hostname configuration
assert:
file:
path: /etc/hostname
content: "{{ expected_hostname }}"
Check File Contains String¶
- name: Verify SSH config has setting
assert:
file:
path: ~/.ssh/config
contains: "ForwardAgent yes"
Verify File Permissions¶
Check File Ownership¶
HTTP Assertions¶
Check HTTP Status¶
Check Response Body Contains¶
- name: Verify API returns success
assert:
http:
url: https://api.example.com/status
status: 200
contains: '"status":"healthy"'
POST Request with Body¶
- name: Verify API accepts login
assert:
http:
url: https://api.example.com/auth
method: POST
status: 200
headers:
Content-Type: application/json
body: |
{"username": "test", "password": "{{ api_password }}"}
contains: "token"
Check Exact Response¶
- name: Verify API version
assert:
http:
url: https://api.example.com/version
status: 200
body_equals: '{"version":"2.0.0"}'
With Custom Timeout¶
- name: Check slow endpoint
assert:
http:
url: https://api.example.com/slow-operation
status: 200
timeout: 2m
Practical Examples¶
Verify Prerequisites¶
- name: Check system requirements
block:
- name: Verify Docker installed
assert:
command:
cmd: docker --version
- name: Verify Docker Compose installed
assert:
command:
cmd: docker-compose --version
- name: Verify port 80 available
assert:
command:
cmd: "! nc -z localhost 80"
exit_code: 0
- name: Check SSL certificate exists
assert:
file:
path: /etc/ssl/certs/server.crt
exists: true
Validate Deployment¶
- name: Verify deployment succeeded
block:
- name: Check application binary exists
assert:
file:
path: /usr/local/bin/myapp
exists: true
mode: "0755"
- name: Verify config has correct settings
assert:
file:
path: /etc/myapp/config.yml
contains: "production: true"
- name: Check service is running
assert:
command:
cmd: systemctl is-active myapp
exit_code: 0
- name: Verify health endpoint responds
assert:
http:
url: http://localhost:8080/health
status: 200
contains: "healthy"
Test Infrastructure¶
- name: Run infrastructure tests
block:
- name: Check database is accessible
assert:
command:
cmd: "psql -U {{ db_user }} -h {{ db_host }} -c 'SELECT 1'"
exit_code: 0
- name: Verify Redis is responding
assert:
command:
cmd: redis-cli ping
exit_code: 0
- name: Check API returns expected data
assert:
http:
url: "{{ api_base_url }}/test"
status: 200
contains: "test_passed"
With Registered Results¶
- name: Check API and capture result
assert:
http:
url: https://api.example.com/status
status: 200
register: api_check
- name: Log assertion result
shell: echo "API check passed - changed={{ api_check.changed }}"
# Output: API check passed - changed=false
Key Behaviors¶
Never Changed:
# Assertions always report changed: false
- assert:
command:
cmd: echo "test"
register: result
# result.changed is always false
Fail Fast:
# Execution stops immediately on assertion failure
- assert:
file:
path: /missing/file
exists: true
# This fails - subsequent steps won't run
- name: This won't execute
shell: echo "skipped"
Detailed Error Messages:
assertion failed (command): expected exit code 0, got exit code 1 (false)
assertion failed (file): expected file exists: true, got file exists: false (/tmp/missing)
assertion failed (http): expected HTTP 200, got HTTP 404 (https://example.com)
Template¶
Render templates with variables and logic.
Template Properties¶
| Property | Type | Description |
|---|---|---|
template.src |
string | Source template file path (required) |
template.dest |
string | Destination file path (required) |
template.vars |
object | Additional variables for rendering |
template.mode |
string | Permissions (e.g., "0644") |
Plus universal fields: name, when, become, tags, register, with_items, with_filetree
Note: Template operations do NOT support shell-specific fields (timeout, retries, env, cwd, etc.)
Basic Usage¶
With Additional Variables¶
- template:
src: ./templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: "0644"
vars:
port: 8080
ssl_enabled: true
Template Syntax (pongo2)¶
Variables:
Conditionals:
Loops:
{% for server in servers %}
upstream {{ server.name }} {
server {{ server.host }}:{{ server.port }};
}
{% endfor %}
Filters:
Include¶
Load and execute tasks from other files.
Include Properties¶
| Property | Type | Description |
|---|---|---|
include |
string | Path to YAML file with steps (required) |
Plus universal fields: name, when, tags, with_items
Note: Include does NOT support shell-specific fields or register.
Basic Usage¶
Conditional Include¶
Include Vars¶
Load variables from external files.
Include Vars Properties¶
| Property | Type | Description |
|---|---|---|
include_vars |
string | Path to YAML file with variables (required) |
Plus universal fields: name, when, tags
Note: Include vars does NOT support shell-specific fields, register, or loops.
Basic Usage¶
Dynamic Include¶
Universal Fields¶
These fields work with all action types (shell, file, template, include, etc.):
name¶
Human-readable description displayed in output:
when¶
Conditional execution - step runs only if expression evaluates to true:
tags¶
Filter execution - run only steps with specified tags:
Run with: mooncake run --config config.yml --tags test
become¶
Run with elevated privileges (sudo). Works with shell, file, and template actions:
Requires --sudo-pass flag or --raw mode for interactive sudo.
register¶
Capture command output in a variable:
- shell: whoami
register: current_user
- name: Use captured output
shell: echo "Running as {{current_user.stdout}}"
Result contains:
rc- Exit codestdout- Standard outputstderr- Standard errorchanged- Whether step made changesfailed- Whether step failed
with_items¶
Iterate over list items:
Or with variables:
with_filetree¶
Iterate over files in a directory:
Item properties:
name- File namesrc- Full source pathis_dir- Whether it's a directory
creates¶
Skip step if path exists (idempotency check):
- name: Extract application
unarchive:
src: /tmp/myapp.tar.gz
dest: /opt/myapp
creates: /opt/myapp/.installed
# Second run skips - marker file exists
Works with all actions to provide idempotency without checking actual state:
unless¶
Skip step if command succeeds (conditional idempotency):
- name: Create user
shell: useradd myuser
unless: id myuser
# Skips if user already exists (exit code 0)
The unless command is executed before the step. If it exits with code 0 (success), the step is skipped:
- name: Install package
shell: apt install nginx
unless: dpkg -l | grep nginx
become: true
# Skips if nginx is already installed
Shell-Specific Fields¶
The following fields only work with shell commands. They don't apply to file, template, or include operations.
become_user¶
Specify user when using become with shell commands (default is root):
env¶
Set environment variables for shell commands:
- name: Build with custom env
shell: make build
env:
CC: gcc-11
CFLAGS: "-O2 -Wall"
PATH: "/custom/bin:$PATH"
Values support template rendering.
cwd¶
Change working directory before executing shell command:
timeout¶
Enforce maximum execution time (duration string):
Supported units: ns, us, ms, s, m, h. Command times out with exit code 124.
retries¶
Retry failed shell commands up to N times:
Total attempts = retries + 1 (initial attempt).
retry_delay¶
Wait duration between retry attempts:
changed_when¶
Override changed status based on expression (shell commands only):
- name: Check if update needed
shell: git fetch && git status
register: git_status
changed_when: "'behind' in result.stdout"
Expression has access to result.rc, result.stdout, result.stderr.
failed_when¶
Override failure status based on expression (shell commands only):
- name: Command that may return 2
shell: ./script.sh
failed_when: "result.rc != 0 and result.rc != 2"
Useful for commands where certain non-zero exit codes are acceptable.
Shell Command Examples¶
Robust shell command with retry and timeout¶
- name: Download large file
shell: curl -O https://example.com/large-file.tar.gz
timeout: 10m
retries: 3
retry_delay: 30s
failed_when: "result.rc != 0 and result.rc != 18" # 18 = partial transfer
Conditional change detection¶
- name: Update git repository
shell: git pull
cwd: "/opt/{{project}}"
register: git_pull
changed_when: "'Already up to date' not in result.stdout"
Complex shell execution control¶
- name: Deploy with validation
shell: ./deploy.sh
cwd: "/opt/app"
env:
ENVIRONMENT: "{{env}}"
DEBUG: "{{debug_mode}}"
timeout: 15m
become: true
become_user: deployer
register: deploy_result
failed_when: "result.rc != 0 or 'ERROR' in result.stderr"
changed_when: "'deployed successfully' in result.stdout"
Preset¶
Invoke reusable, parameterized collections of steps. Presets encapsulate complex workflows into simple declarations.
📖 See Presets Guide for complete documentation and Preset Authoring for creating your own.
Basic Usage¶
Simple string form (no parameters):
With parameters:
Full object form:
- name: Deploy application
preset:
name: deploy-webapp
with:
app_name: myapp
version: v1.2.3
port: 8080
environment: production
become: true
register: deploy_result
Preset Properties¶
| Property | Type | Description |
|---|---|---|
preset |
string or object | Preset name (string) or preset invocation (object) |
preset.name |
string | Preset name (when using object form) |
preset.with |
object | Parameters to pass to preset (optional) |
Parameter Passing¶
Parameters are validated by the preset definition:
- preset: ollama
with:
state: present # string (required, enum: [present, absent])
service: true # bool (optional, default: true)
pull: [model1, model2] # array (optional, default: [])
force: false # bool (optional, default: false)
Result Registration¶
Presets return aggregate results:
- name: Install Ollama
preset: ollama
with:
state: present
register: ollama_result
- name: Check result
shell: echo "Changed = {{ ollama_result.changed }}"
Result fields:
changed:trueif any step in preset changedfailed:trueif preset execution failedrc: Exit code (0 = success)stdout: Summary message
Conditionals¶
Presets work with standard conditionals:
Tags¶
Become (Privilege Escalation)¶
Available Presets¶
Built-in presets:
- ollama: Install and manage Ollama LLM runtime
- Parameters:
state,service,pull,method,host,models_dir,force - See: examples/ollama/
Creating Custom Presets¶
Place preset files in:
./presets/(playbook directory)~/.mooncake/presets/(user directory)/usr/share/mooncake/presets/(system directory)
Example preset (presets/hello.yml):
name: hello
description: Print a greeting
version: 1.0.0
parameters:
name:
type: string
required: true
description: Name to greet
excited:
type: bool
default: false
description: Use exclamation mark
steps:
- name: Print greeting
shell: echo "Hello, {{ parameters.name }}{% if parameters.excited %}!{% endif %}"
Usage:
Key Features¶
Reusability: Write once, use everywhere Type Safety: Parameters validated before execution Idempotency: Presets inherit step idempotency Platform-aware: Can detect and adapt to OS/package managers Composable: Use with all standard step features (when, tags, register)
Limitations¶
- No Nesting: Presets cannot call other presets (architectural constraint)
- Flat Parameters: No nested parameter structures (use object type for complex data)
- Sequential Execution: Steps run in order, not parallel
Note: Preset steps support includes, loops, and conditionals - the preset definition is static, but steps can be dynamically expanded.
See Also¶
- Presets User Guide - Using presets
- Preset Authoring Guide - Creating presets
- Ollama Examples - Real-world preset usage
Print¶
Display messages to the user during execution. Useful for debugging, progress updates, and providing feedback.
Basic Usage¶
Simple string form:
Structured form:
Print Properties¶
| Property | Type | Description |
|---|---|---|
print |
string or object | Message to display (string) or print configuration (object) |
print.msg |
string | Message to display (when using object form) |
Plus universal fields: name, when, tags
Note: Print does NOT support become, register, retries, or shell-specific fields.
Multi-line Messages¶
- name: Show summary
print:
msg: |
Deployment complete!
Application: {{app_name}}
Version: {{version}}
Status: Running
With Variables¶
- vars:
app_name: MyApp
version: v1.0.0
- name: Display version info
print: "Deploying {{app_name}} version {{version}}"
Using Registered Variables¶
- name: Get current user
shell: whoami
register: current_user
- name: Show user
print: "Running as: {{current_user.stdout}}"
Conditional Printing¶
- name: Show macOS message
print: "Running on macOS"
when: os == "darwin"
- name: Show Linux message
print: "Running on Linux"
when: os == "linux"
In Loops¶
- vars:
packages: [git, vim, curl]
- name: List packages
print: "Will install: {{item}}"
with_items: "{{packages}}"
Key Behaviors¶
- Always Changed: false - Print never reports as "changed"
- No Side Effects - Only displays output, doesn't modify system
- Template Rendering - Full variable interpolation support
- Dry-Run Safe - Works identically in dry-run mode
- No Failure - Print operations cannot fail (unless template rendering fails)
Use Cases¶
Progress Updates:
- print: "Starting deployment process..."
- name: Deploy application
shell: ./deploy.sh
- print: "Deployment complete!"
Debugging:
User Feedback:
- name: Welcome message
print:
msg: |
═══════════════════════════════════════
Welcome to {{app_name}} Setup
═══════════════════════════════════════
Vars¶
Define or update variables for use in subsequent steps. Variables are available to all following steps in the same configuration.
Basic Usage¶
Simple form (inline):
Vars Properties¶
| Property | Type | Description |
|---|---|---|
vars |
object | Key-value pairs of variables to set |
Variables can be any valid YAML type:
- Strings: name: "value"
- Numbers: port: 8080
- Booleans: debug: true
- Arrays: packages: [git, vim]
- Objects: config: {host: localhost, port: 80}
Plus universal fields: name, when, tags
Note: Vars does NOT support become, register, retries, or shell-specific fields.
Complex Data Types¶
Arrays:
Objects:
Mixed:
- vars:
app_name: MyApp
environments:
- name: development
debug: true
port: 3000
- name: production
debug: false
port: 80
Template Rendering in Vars¶
Variables support template rendering:
Platform-Specific Variables¶
- name: Set package manager for Linux
vars:
pkg_manager: apt
when: os == "linux" and apt_available
- name: Set package manager for macOS
vars:
pkg_manager: brew
when: os == "darwin"
Conditional Variables¶
- name: Set debug mode for development
vars:
debug: true
log_level: debug
when: environment == "development"
- name: Set production settings
vars:
debug: false
log_level: info
when: environment == "production"
Variable Precedence¶
Variables are set in order of execution:
- vars:
port: 8080 # Initial value
- vars:
port: 3000 # Overwrites previous value
- shell: echo "Port: {{port}}" # Uses 3000
Using System Facts¶
Combine system facts with custom variables:
- vars:
install_dir: /opt/{{app_name}}
config_path: "{{install_dir}}/config"
os_family: "{{os}}"
is_macos: "{{os == 'darwin'}}"
Environment-Specific Configuration¶
- name: Load base configuration
vars:
app_name: MyApp
base_port: 8080
- name: Override for production
vars:
base_port: 80
replicas: 3
when: environment == "production"
- name: Override for development
vars:
base_port: 3000
replicas: 1
when: environment == "development"
Key Behaviors¶
- Always Changed: false - Setting vars never reports as "changed"
- Scope - Variables are available to all subsequent steps
- Overwrite - Setting a variable again overwrites the previous value
- Template Support - Values can reference other variables
- Dry-Run Safe - Works identically in dry-run mode
Use Cases¶
Application Configuration:
- vars:
app_name: web-api
version: v2.1.0
port: 8080
workers: 4
- name: Create config file
template:
src: app-config.yml.j2
dest: /etc/{{app_name}}/config.yml
Multi-Environment Setup:
- vars:
env: production
domain: example.com
ssl_enabled: true
- name: Deploy to {{env}}
shell: ./deploy.sh --env={{env}} --domain={{domain}}
Dynamic Path Construction:
- vars:
app_name: myapp
base_dir: /opt/{{app_name}}
bin_dir: "{{base_dir}}/bin"
config_dir: "{{base_dir}}/config"
log_dir: /var/log/{{app_name}}
- name: Create directories
file:
path: "{{item}}"
state: directory
with_items:
- "{{bin_dir}}"
- "{{config_dir}}"
- "{{log_dir}}"
Feature Flags:
- vars:
enable_monitoring: true
enable_caching: false
enable_debug: "{{environment == 'development'}}"
- name: Configure monitoring
shell: ./setup-monitoring.sh
when: enable_monitoring
- name: Enable debug logging
shell: ./enable-debug.sh
when: enable_debug
See Also¶
- Variables Guide - Complete variable documentation
- include_vars - Load variables from files
- System Facts - Built-in system variables
See Also¶
- Control Flow - Conditionals, loops, tags
- Variables - Variable usage and system facts
- Examples - Practical examples