Skip to content

Resources API

Guide to using and creating Cook resources.

Overview

Resources are the building blocks of Cook configurations. Each resource represents a piece of desired system state.

Built-in Resources

Cook provides five core resources:

Resource Purpose Example
File Manage files and directories File("/etc/motd", content="...")
Package Install system packages Package("nginx")
Repository Manage package repositories Repository("apt-update", action="update")
Service Manage system services Service("nginx", running=True)
Exec Run arbitrary commands Exec("backup", command="...")

Resource Lifecycle

All resources follow the Check-Plan-Apply pattern:

1. Check Phase

Resource inspects current system state:

from cook import File

file = File("/etc/motd", content="Welcome")
# During check phase, reads /etc/motd
# Returns: {"exists": True, "content": "Old message"}

2. Plan Phase

Cook compares actual vs desired state:

# Actual state:  {"exists": True, "content": "Old message"}
# Desired state: {"exists": True, "content": "Welcome"}

# Generated plan:
# Action: UPDATE
# Changes:
#   content: "Old message" → "Welcome"

3. Apply Phase

Resource executes changes:

# Writes "Welcome" to /etc/motd

Resource Identity

Each resource has a unique ID:

from cook import File, Package

file = File("/etc/motd", content="Welcome")
print(file.id)  # "file:/etc/motd"

pkg = Package("nginx")
print(pkg.id)  # "pkg:nginx"

ID format: {resource_type}:{name}

Resource Dependencies

Resources automatically track dependencies through references:

Reload on Change

from cook import File, Service

# Configuration file
nginx_conf = File("/etc/nginx/nginx.conf", source="./nginx.conf")

# Service reloads when config changes
Service("nginx", running=True, reload_on=[nginx_conf])

Restart on Change

from cook import File, Service

# Application binary
app_binary = File("/usr/local/bin/app", source="./app", mode=0o755)

# Service restarts when binary changes
Service("app", running=True, restart_on=[app_binary])

Execution Order

Cook automatically orders resources based on dependencies:

from cook import File, Package, Service

# Cook executes in this order:
Package("nginx")                                    # 1. Install first
config = File("/etc/nginx/nginx.conf", ...)        # 2. Then configure
Service("nginx", running=True, reload_on=[config]) # 3. Then manage service

Resource Parameters

Common Parameters

All resources support these parameters:

from cook import File

File(
    "/etc/motd",
    content="Welcome",
    # Common parameters:
    # (none currently - resource-specific)
)

Resource-Specific Parameters

Each resource type has its own parameters:

# File resource
File(
    path="/etc/motd",
    content="Welcome",      # File-specific
    mode=0o644,            # File-specific
    owner="root"           # File-specific
)

# Package resource
Package(
    name="nginx",
    version="1.18.0",      # Package-specific
    ensure="present"       # Package-specific
)

# Service resource
Service(
    name="nginx",
    running=True,          # Service-specific
    enabled=True,          # Service-specific
    reload_on=[...]        # Service-specific
)

Creating Custom Resources

Basic Custom Resource

from cook.core import Resource, Platform, Plan, Action
from cook.core.executor import get_executor
from typing import Dict, Any

class DatabaseUser(Resource):
    """Create database users."""

    def __init__(self, username: str, password: str, database: str, **options):
        super().__init__(username, **options)
        self.username = username
        self.password = password
        self.database = database

        # Auto-register with executor
        get_executor().add(self)

    def resource_type(self) -> str:
        return "dbuser"

    def check(self, platform: Platform) -> Dict[str, Any]:
        """Check if user exists."""
        cmd = f"psql -c \"SELECT 1 FROM pg_user WHERE usename='{self.username}'\""
        output, code = self._transport.run_shell(cmd)

        return {
            "exists": code == 0 and "1 row" in output
        }

    def desired_state(self) -> Dict[str, Any]:
        """User should exist."""
        return {
            "exists": True
        }

    def apply(self, plan: Plan, platform: Platform) -> None:
        """Create user."""
        if plan.action == Action.CREATE:
            cmd = f"psql -c \"CREATE USER {self.username} WITH PASSWORD '{self.password}'\""
            self._transport.run_shell(cmd)

            # Grant access
            cmd = f"psql -c \"GRANT ALL ON DATABASE {self.database} TO {self.username}\""
            self._transport.run_shell(cmd)

Using Custom Resource

# Create database user
DatabaseUser("app_user", password="secret", database="myapp")

# Execute
from cook.core.executor import get_executor
from cook.core import Platform

executor = get_executor()
platform = Platform.detect()
executor.execute(platform)

Advanced Custom Resource

With update and delete support:

class GitRepository(Resource):
    """Manage git repositories."""

    def __init__(self, path: str, url: str, branch: str = "main", **options):
        super().__init__(path, **options)
        self.path = path
        self.url = url
        self.branch = branch
        get_executor().add(self)

    def resource_type(self) -> str:
        return "git"

    def check(self, platform: Platform) -> Dict[str, Any]:
        """Check repository state."""
        if not self._transport.file_exists(f"{self.path}/.git"):
            return {"exists": False, "branch": None, "url": None}

        # Get current branch
        output, _ = self._transport.run_shell(
            f"cd {self.path} && git rev-parse --abbrev-ref HEAD"
        )
        current_branch = output.strip()

        # Get remote URL
        output, _ = self._transport.run_shell(
            f"cd {self.path} && git config --get remote.origin.url"
        )
        current_url = output.strip()

        return {
            "exists": True,
            "branch": current_branch,
            "url": current_url
        }

    def desired_state(self) -> Dict[str, Any]:
        """Desired repository state."""
        return {
            "exists": True,
            "branch": self.branch,
            "url": self.url
        }

    def apply(self, plan: Plan, platform: Platform) -> None:
        """Apply git operations."""
        if plan.action == Action.CREATE:
            # Clone repository
            self._transport.run_command(
                ["git", "clone", "-b", self.branch, self.url, self.path]
            )

        elif plan.action == Action.UPDATE:
            # Handle changes
            for change in plan.changes:
                if change.field == "branch":
                    # Checkout branch
                    self._transport.run_shell(
                        f"cd {self.path} && git checkout {change.to_value}"
                    )
                elif change.field == "url":
                    # Update remote
                    self._transport.run_shell(
                        f"cd {self.path} && git remote set-url origin {change.to_value}"
                    )

        elif plan.action == Action.DELETE:
            # Remove repository
            self._transport.run_command(["rm", "-rf", self.path])

Resource Best Practices

Idempotency

Resources should be idempotent - safe to run multiple times:

def check(self, platform: Platform) -> Dict[str, Any]:
    # Always check current state accurately
    exists = self._transport.file_exists(self.path)
    return {"exists": exists}

def apply(self, plan: Plan, platform: Platform) -> None:
    # Only make changes when needed
    if plan.action == Action.CREATE:
        # Create only if doesn't exist
        pass

Error Handling

Validate inputs and handle errors:

def __init__(self, name: str, value: str):
    # Validate inputs
    if not name:
        raise ValueError("name cannot be empty")
    if not value:
        raise ValueError("value cannot be empty")

    super().__init__(name)

def apply(self, plan: Plan, platform: Platform) -> None:
    # Check command success
    output, code = self._transport.run_command(["mycommand"])
    if code != 0:
        raise RuntimeError(f"Command failed: {output}")

Platform Detection

Use platform information for cross-platform resources:

def apply(self, plan: Plan, platform: Platform) -> None:
    if platform.system == "Linux":
        if platform.distro == "ubuntu":
            self._transport.run_command(["apt-get", "install", "..."])
        elif platform.distro == "fedora":
            self._transport.run_command(["dnf", "install", "..."])
    elif platform.system == "Darwin":
        self._transport.run_command(["brew", "install", "..."])

Transport Usage

Always use transport for system operations:

# Good: Use transport
exists = self._transport.file_exists("/path/to/file")
content = self._transport.read_file("/path/to/file")
self._transport.write_file("/path/to/file", b"content")

# Bad: Direct file operations (won't work with SSH transport)
import os
os.path.exists("/path/to/file")  # WRONG - only works locally

Resource Testing

Unit Tests

Test resources with mock transport:

from cook.transport import LocalTransport
from cook.core import Platform

class MockTransport:
    def __init__(self):
        self.files = {}
        self.commands = []

    def file_exists(self, path: str) -> bool:
        return path in self.files

    def read_file(self, path: str) -> bytes:
        return self.files.get(path, b"")

    def write_file(self, path: str, content: bytes) -> None:
        self.files[path] = content

# Test resource
def test_custom_resource():
    # Create mock transport
    transport = MockTransport()

    # Create resource
    resource = CustomResource("test", value="hello")
    resource._transport = transport

    # Check state
    platform = Platform.detect()
    state = resource.check(platform)
    assert state["exists"] == False

    # Apply changes
    plan = Plan(action=Action.CREATE, changes=[])
    resource.apply(plan, platform)

    # Verify
    assert transport.files["/tmp/test"] == b"hello"

Resource Patterns

Configuration File + Service

from cook import File, Service

# Configuration file
config = File("/etc/nginx/nginx.conf", source="./nginx.conf")

# Service that reloads on config change
Service("nginx", running=True, enabled=True, reload_on=[config])

Package + Service

from cook import Package, Service

# Install package
Package("postgresql")

# Manage service
Service("postgresql", running=True, enabled=True)

Multi-Resource Setup

from cook import File, Package, Service

# Create directory
File("/opt/apps/myapp", ensure="directory", mode=0o755)

# Install dependencies
Package("app-deps", packages=["python3", "python3-pip"])

# Application files
app_config = File("/opt/apps/myapp/config.yml", source="./config.yml")
app_binary = File("/opt/apps/myapp/app", source="./app", mode=0o755)

# Service file
service_file = File(
    "/etc/systemd/system/myapp.service",
    source="./myapp.service"
)

# Manage service
Service(
    "myapp",
    running=True,
    enabled=True,
    reload_on=[app_config],
    restart_on=[app_binary, service_file]
)

API Reference

Resource Base Class

See cook/core/resource.py for the base Resource class.

Built-in Resources

Resource Imports

# Import built-in resources
from cook import File, Package, Repository, Service, Exec

# Import core classes for custom resources
from cook.core import Resource, Platform, Plan, Action, Change
from cook.core.executor import get_executor