Skip to content

Getting started

This page takes you from installing sadame to your first plan and apply: install it, write a one-line recipe, look at the plan, apply it, and learn to read the diff sadame prints when a machine has drifted from its recipe.

Install

curl -fsSL https://raw.githubusercontent.com/enderkus/sadame/main/install.sh | sh
sadame --version

The examples on this page are in the repository; clone it to follow along:

git clone https://github.com/enderkus/sadame.git
cd sadame

The Install page has the other ways to install, including running sadame from the checkout with Ruby 3.2 or newer (bin/sadame).

Your first recipe

A recipe is a Ruby file that declares resources: packages, files, directories, services, commands and users. The smallest one has a single resource:

examples/01-hello/recipe.rb
# The smallest useful recipe: one file with fixed content.
file "/tmp/sadame-hello.txt", content: "Hello from sadame!\n"

It writes to /tmp, so it works without root.

Plan

sadame plan reads the current state of everything the recipe declares and prints what apply would change. It changes nothing.

$ sadame plan examples/01-hello
sadame 0.5.1 plan on local (Debian GNU/Linux 12 (bookworm))

+ file[/tmp/sadame-hello.txt]
    state: absent -> present
    --- /dev/null
    +++ /tmp/sadame-hello.txt (desired)
    @@ -0,0 +1 @@
    +Hello from sadame!

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

RECIPE can be a file or a directory that contains recipe.rb. Without it, sadame looks for ./recipe.rb.

Apply

sadame apply makes the same comparison, resource by resource, and changes only what differs:

$ sadame apply examples/01-hello
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

changed    file[/tmp/sadame-hello.txt]
           state: absent -> present
           --- /dev/null
           +++ /tmp/sadame-hello.txt (desired)
           @@ -0,0 +1 @@
           +Hello from sadame!

Summary
  changed      1  file[/tmp/sadame-hello.txt]
  unchanged    0
  failed       0
  skipped      0
  notified     0

Finished in 0.09s.

Run plan again and there is nothing left to do. That is what idempotent means here: once the machine matches the recipe, running it again changes nothing.

$ sadame plan examples/01-hello
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, 1 unchanged.

Read the diff

Now change the file by hand, the way a colleague or a package upgrade might:

$ echo 'Hello from someone else' > /tmp/sadame-hello.txt

$ sadame plan examples/01-hello
sadame 0.5.1 plan on local (Debian GNU/Linux 12 (bookworm))

~ file[/tmp/sadame-hello.txt]
    --- /tmp/sadame-hello.txt
    +++ /tmp/sadame-hello.txt (desired)
    @@ -1 +1 @@
    -Hello from someone else
    +Hello from sadame!

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

Each resource that would change gets a header line with a symbol and the resource's id, type[name]:

Symbol Meaning
+ create: the resource does not exist yet
~ update: it exists but some attributes differ
- remove: the recipe says state: :absent and it exists
! run: an exec command whose guards say it must run
x failed: the current state could not be read; the error follows

Below the header, each differing attribute is shown as current -> desired. File content is shown as a unified diff, the same format as diff -u and git diff: --- is the file on disk, +++ is what the recipe wants. In a terminal, removed lines are red and added lines green:

--- /tmp/sadame-hello.txt
+++ /tmp/sadame-hello.txt (desired)
@@ -1 +1 @@
-Hello from someone else
+Hello from sadame!

Colors are turned off when output is not a terminal, with --no-color, or when the NO_COLOR environment variable is set.

The last line counts resources by what would happen to them. Unchanged resources are counted but not listed.

Check for drift

sadame drift reads the same state as plan, but its exit status says whether the machine still matches the recipe: 0 when it does, 1 when it has drifted. That makes it the command for scripts, cron jobs and CI:

$ sadame drift examples/01-hello
sadame 0.5.1 drift on local (Debian GNU/Linux 12 (bookworm))

~ file[/tmp/sadame-hello.txt]
    --- /tmp/sadame-hello.txt
    +++ /tmp/sadame-hello.txt (desired)
    @@ -1 +1 @@
    -Hello from someone else
    +Hello from sadame!

Drift: 1 of 1 resource differs from the recipe.

$ echo $?
1

Plan, apply, drift explains when to use which, and Drift in CI runs the check on a schedule.

JSON output

--json prints one JSON document instead, for scripts and CI. Every field is described in the JSON output reference:

$ sadame plan examples/01-hello --json
{
  "format_version": 1,
  "sadame_version": "0.5.1",
  "command": "plan",
  "host": "local",
  "platform": {
    "name": "Debian GNU/Linux 12 (bookworm)",
    "family": "debian",
    "package_manager": "apt"
  },
  "tags": [],
  "skip_tags": [],
  "resources": [
    {
      "id": "file[/tmp/sadame-hello.txt]",
      "type": "file",
      "name": "/tmp/sadame-hello.txt",
      "status": "update",
      "changes": [
        {
          "attribute": "content",
          "sensitive": false,
          "diff": "--- current\n+++ desired\n@@ -1 +1 @@\n-Hello from someone else\n+Hello from sadame!\n"
        }
      ],
      "notifications": [],
      "error": null
    }
  ],
  "notifications": [],
  "summary": {
    "create": 0,
    "update": 1,
    "remove": 0,
    "run": 0,
    "unchanged": 0,
    "failed": 0
  },
  "drift": true,
  "failed": false,
  "exit_status": 0,
  "duration_seconds": 0.05
}

Exit status

Status plan apply drift
0 the plan was computed, with or without changes every resource and notification succeeded the machine matches the recipe
1 not used a resource or notification failed the machine differs from the recipe
2 usage or recipe error, or the state of a resource could not be read usage or recipe error usage or recipe error, or the state of a resource could not be read

Your own project

sadame init writes the files a project starts from: a recipe with one resource, vars.rb, an inventory with examples to uncomment, and a template. sadame validate checks the recipe without touching a machine:

$ sadame init infra
created infra/recipe.rb
created infra/vars.rb
created infra/hosts.rb
created infra/templates/motd.erb

Next:
  sadame validate infra        check the recipe without connecting to anything
  sudo sadame plan infra       see what it would change on this machine
  sadame plan infra -g all     on your servers, once infra/hosts.rb lists them

$ sadame validate infra
sadame 0.5.1 validate infra/recipe.rb, as on Debian GNU/Linux 12 (bookworm)

  local  ok     1 resource

validate on 1 host: 1 ok.

Add --ci github or --ci gitlab to also get a daily drift check for your CI, and set up your editor with sadame lsp to see mistakes as you type.

Common mistakes

  • Running without root. Most real recipes install packages or write under /etc. Run them with sudo sadame apply ...; plan usually needs root too, to read files only root can read.
  • Expecting apply to undo things it does not manage. Sadame only compares what the recipe declares. Deleting a resource from a recipe does not remove it from the machine; declare it with state: :absent instead.
  • Treating plan as a guarantee. plan compares each resource with the machine as it is now. During apply, earlier resources change the machine (a package creates a user, a file a service needs), so later resources are compared again right before they run.

See also