Exec Resource¶
Execute arbitrary commands with idempotency guards and security validation.
Overview¶
The Exec resource runs shell commands:
- Execute commands with idempotency guards
- Conditional execution (creates, unless, only_if)
- Environment variables and working directory
- Security validation for command injection
- Dry-run mode for safe testing
- Command change tracking
Security¶
IMPORTANT: Exec runs commands via shell. Enable security validation to prevent command injection.
Safe by Default¶
Security validation is enabled by default:
from cook import Exec
# Safe mode enabled automatically
Exec("backup", command="tar czf /backup/data.tar.gz /var/data")
Security Levels¶
| Level | Behavior |
|---|---|
strict |
Block dangerous patterns (default) |
warn |
Warn but allow dangerous patterns |
none |
No validation (dangerous) |
# Strict mode (default)
Exec("backup", command="tar czf /backup/data.tar.gz /var/data", security_level="strict")
# Warning mode
Exec("backup", command="tar czf /backup/data.tar.gz /var/data", security_level="warn")
# No validation (use with extreme caution)
Exec("backup", command="tar czf /backup/data.tar.gz /var/data", security_level="none")
Basic Usage¶
Simple Command¶
Idempotent Execution¶
Use guards to make commands idempotent:
# Run only if file doesn't exist
Exec(
"download-installer",
command="curl -o /tmp/installer.sh https://example.com/install.sh",
creates="/tmp/installer.sh"
)
Conditional Execution¶
# Run only if condition fails
Exec(
"install-composer",
command="curl -sS https://getcomposer.org/installer | php",
unless="which composer"
)
# Run only if condition succeeds
Exec(
"setup-database",
command="mysql < /tmp/schema.sql",
only_if="systemctl is-active mysql"
)
Parameters¶
name¶
Required. Resource identifier and description.
command¶
Required. Shell command to execute.
creates¶
Only run command if this file or directory does not exist.
After first run, file exists, command will not run again.
unless¶
Only run command if this guard command fails (returns non-zero exit code).
Exec(
"install-nodejs",
command="curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
unless="which node"
)
If which node succeeds (node installed), command does not run.
only_if¶
Only run command if this guard command succeeds (returns zero exit code).
Only restarts if service is currently active.
cwd¶
Working directory for command execution.
Equivalent to:
environment¶
Environment variables for command.
Exec(
"deploy",
command="./deploy.sh",
environment={
"ENVIRONMENT": "production",
"API_KEY": "secret-key"
}
)
dry_run¶
Preview mode. Shows what would be executed without running the command.
safe_mode¶
Enable security validation. Default: True.
Recommended: Keep enabled unless you have a specific reason to disable it.
# Safe mode enabled (default)
Exec("backup", command="tar czf /backup/data.tar.gz /var/data")
# Disable with extreme caution
Exec("backup", command="tar czf /backup/data.tar.gz /var/data", safe_mode=False)
allow_pipes¶
Allow pipe character (|) in commands. Default: True.
allow_redirects¶
Allow redirect characters (>, <) in commands. Default: True.
Idempotency Guards¶
Use guards to ensure commands run only when needed.
creates Guard¶
Run only if file does not exist:
Exec(
"download-file",
command="curl -o /tmp/data.json https://api.example.com/data",
creates="/tmp/data.json"
)
# First run: Downloads file
# Second run: File exists, skips download
unless Guard¶
Run only if guard command fails:
Exec(
"install-docker",
command="curl -fsSL https://get.docker.com | sh",
unless="which docker"
)
# If docker not installed: Runs installer
# If docker installed: Skips installation
only_if Guard¶
Run only if guard command succeeds:
Exec(
"backup-database",
command="pg_dump myapp > /backup/myapp.sql",
only_if="systemctl is-active postgresql"
)
# Only runs if PostgreSQL is active
Combining Guards¶
Exec(
"setup-database",
command="mysql < /tmp/schema.sql",
creates="/var/lib/mysql/myapp/users.frm",
only_if="systemctl is-active mysql"
)
# Runs only if:
# 1. MySQL is active (only_if)
# 2. Table file doesn't exist (creates)
Examples¶
System Setup Commands¶
from cook import Exec
# Update package cache
Exec("apt-update", command="apt-get update")
# Set timezone
Exec(
"set-timezone",
command="timedatectl set-timezone America/New_York",
unless="timedatectl | grep 'America/New_York'"
)
# Set hostname
Exec(
"set-hostname",
command="hostnamectl set-hostname webserver01",
unless="hostname | grep webserver01"
)
Download and Install¶
# Download installer
Exec(
"download-installer",
command="curl -fsSL https://get.docker.com -o /tmp/get-docker.sh",
creates="/tmp/get-docker.sh"
)
# Run installer
Exec(
"install-docker",
command="sh /tmp/get-docker.sh",
unless="which docker"
)
Database Operations¶
# Create database
Exec(
"create-database",
command="createdb myapp",
creates="/var/lib/postgresql/data/myapp",
only_if="systemctl is-active postgresql"
)
# Import schema
Exec(
"import-schema",
command="psql myapp < /tmp/schema.sql",
unless="psql myapp -c '\\dt' | grep users"
)
# Create database user
Exec(
"create-db-user",
command="createuser -P myapp_user",
unless="psql -c '\\du' | grep myapp_user"
)
SSL Certificates¶
from cook import Exec
import os
domain = os.getenv("DOMAIN", "example.com")
email = os.getenv("ADMIN_EMAIL", "admin@example.com")
# Obtain Let's Encrypt certificate
Exec(
"certbot-obtain",
command=f"certbot certonly --nginx -d {domain} --non-interactive --agree-tos -m {email}",
creates=f"/etc/letsencrypt/live/{domain}/fullchain.pem"
)
Application Deployment¶
from cook import Exec, File
# Application directory
File("/opt/apps/myapp", ensure="directory")
# Clone repository
Exec(
"clone-repo",
command="git clone https://github.com/user/myapp.git /opt/apps/myapp",
creates="/opt/apps/myapp/.git"
)
# Install dependencies
Exec(
"install-deps",
command="npm install --production",
cwd="/opt/apps/myapp",
unless="test -d /opt/apps/myapp/node_modules"
)
# Build application
Exec(
"build-app",
command="npm run build",
cwd="/opt/apps/myapp",
environment={"NODE_ENV": "production"}
)
Docker Operations¶
# Pull Docker image
Exec(
"pull-image",
command="docker pull nginx:latest",
unless="docker image inspect nginx:latest"
)
# Run container
Exec(
"run-container",
command="docker run -d --name web -p 80:80 nginx",
unless="docker ps | grep web"
)
# Docker Compose
Exec(
"compose-up",
command="docker-compose up -d",
cwd="/opt/apps/myapp",
creates="/opt/apps/myapp/.deployed"
)
Systemd Daemon Reload¶
from cook import File, Exec
# Create service file
service_file = File(
"/etc/systemd/system/myapp.service",
source="./myapp.service"
)
# Reload systemd after service file changes
Exec(
"systemd-reload",
command="systemctl daemon-reload"
)
Security Considerations¶
Command Injection Prevention¶
Security validation blocks dangerous patterns:
Blocked Patterns:
- Command chaining: ;, &&, ||
- Command substitution: $(...), `...`
- Variable expansion: ${...}
- Newline injection: \n, \r
Allowed with Flags:
- Pipes: | (allowed if allow_pipes=True)
- Redirects: >, < (allowed if allow_redirects=True)
Safe Commands¶
# Safe: Static paths, no user input
Exec("backup", command="tar czf /backup/data.tar.gz /var/data")
# Safe: Using creates guard
Exec(
"download",
command="curl -o /tmp/file.tar.gz https://example.com/file.tar.gz",
creates="/tmp/file.tar.gz"
)
Unsafe Commands¶
# UNSAFE: User input in command
user_file = input() # User input!
Exec("backup", command=f"tar czf /backup/{user_file}") # DANGEROUS!
# UNSAFE: Shell variable expansion
Exec("delete", command="rm -rf $TEMP_DIR") # Variable could be anything!
Environment Variables¶
Use environment parameter instead of embedding in command:
# Good: Environment variables passed separately
Exec(
"deploy",
command="./deploy.sh",
environment={"API_KEY": "secret"}
)
# Avoid: Environment variables in command
Exec("deploy", command="API_KEY=secret ./deploy.sh")
Dangerous Commands¶
Security validation warns about:
rm -rf /- Recursive delete from rootdd if=/dev/- Disk operationsmkfs.- Format filesystemchmod 777- World-writable permissionscurl ... | bash- Pipe to shelleval- Code evaluation
Dry Run Testing¶
Test commands safely with dry-run mode:
# Test without executing
Exec(
"deploy",
command="./deploy.sh",
dry_run=True
)
# Shows what would be executed, but doesn't run it
Working Directory and Environment¶
Change Working Directory¶
Equivalent to:
Set Environment Variables¶
Equivalent to:
Combined¶
Exec(
"deploy",
command="./deploy.sh",
cwd="/opt/apps/myapp",
environment={
"ENVIRONMENT": "production",
"LOG_LEVEL": "info"
}
)
Platform Compatibility¶
Exec resources work on all platforms that support shell commands:
- Linux: Uses
/bin/shor/bin/bash - macOS: Uses
/bin/shor/bin/bash - Other Unix-like systems: Uses default shell
Platform-specific commands:
# Linux-specific
Exec("update", command="apt-get update")
# macOS-specific
Exec("update", command="brew update")
Troubleshooting¶
Command Not Found¶
Ensure command is in PATH or use full path:
# May fail if not in PATH
Exec("backup", command="mybackup.sh")
# Better: Use full path
Exec("backup", command="/usr/local/bin/mybackup.sh")
Guard Command Fails¶
Check guard command independently:
# Test unless guard
which docker # Should return 0 if installed
# Test only_if guard
systemctl is-active postgresql # Should return 0 if active
Security Validation Errors¶
Review command for dangerous patterns:
# Error: Command chaining
Exec("multi", command="cmd1 && cmd2") # BLOCKED
# Fix: Use separate Exec resources
Exec("cmd1", command="cmd1")
Exec("cmd2", command="cmd2")
Environment Variables Not Working¶
Verify environment variable syntax:
# Correct
Exec(
"deploy",
command="./deploy.sh",
environment={"API_KEY": "secret"}
)
# Incorrect (command string)
Exec("deploy", command="API_KEY=secret ./deploy.sh")
Limitations¶
No Streaming Output¶
Command output is returned after execution completes. Long-running commands may appear to hang.
Shell Dependency¶
Exec requires a shell to execute commands. Some minimal containers may not have a shell.
No Interactive Commands¶
Commands requiring user input will hang. Use non-interactive flags:
# Good: Non-interactive
Exec("install", command="apt-get install -y nginx")
# Bad: Would hang waiting for input
Exec("install", command="apt-get install nginx")
API Reference¶
See cook/resources/exec.py for complete source code and implementation details.