Files
FabledSteward/docs/reference/playbook-authoring.md
T
bvandeusen b32fce1d74
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m22s
CI / publish (push) Successful in 4s
docs: Steward playbook-authoring conventions reference
A self-contained guide (docs/reference/playbook-authoring.md) to the contract
between a playbook and Steward's run UI — the `# description:` comment,
vars/vars_prompt → fill-in fields, secret naming, hosts: all targeting,
managed credentials, and idempotency. Includes a "what Steward reads" summary
table, the metadata/extensibility note (only `# description:` today; reserved
`# steward:<key>:` namespace for future keys), and an annotated example.
Meant to be fed to another session authoring Steward-friendly playbooks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:26:56 -04:00

6.7 KiB

Authoring Steward-friendly Ansible playbooks

This is the contract between a playbook and Steward's run UI. Follow it and a playbook drops into Steward with a description, fill-in variable fields, correct targeting, and credentials supplied automatically — no per-playbook wiring.

It applies to any playbook in a configured source (bundled, the writable local source, or a git source), including third-party ones.


1. Describe what it does — # description:

Steward shows a one-line description when a playbook is selected in the run form.

---
# description: Reclaim disk on Docker/Swarm nodes by pruning unused images and build cache.
- name: Docker system prune
  hosts: all
  ...
  • Format: a comment line # description: <text> anywhere in the file. First match wins. Case-insensitive on the description: key.
  • Why a comment and not a key: Ansible rejects unknown play keys (you can't add description: to a play), so a comment is the portable place. It survives ansible-playbook untouched.
  • Fallback: if there's no # description: comment, Steward uses the first play's name:. So always give your play a meaningful name: even without the comment.
  • Keep it to one readable line. Longer "how to use" notes can go in additional normal comments — Steward only reads the description: line.

2. Declare tunables in vars: — they become fill-in fields

Every scalar entry in a play's vars: block becomes an editable field in the run form, with the default shown as the input's placeholder.

  vars:
    prune_all_images: false      # → checkbox-ish text field, placeholder "default: false"
    keep_last_days: 7            # → field, placeholder "default: 7"
    registry_url: ""             # → field, placeholder "no default"
  • Blank field = use the default. Steward only sends fields the operator actually fills, so an untouched field falls through to the playbook/inventory default rather than overriding it.
  • Only scalars (string/int/float/bool) surface as fields. Lists/dicts are skipped — set those in the playbook or via inventory group/host vars.
  • Values are delivered as extra-vars (-e), which are the highest precedence in Ansible — they override the vars: defaults. (This is why the default can be empty and still be safely overridden at run time.)

vars_prompt: also works

Steward reads vars_prompt too. Use it when you want an explicit prompt or a required value:

  vars_prompt:
    - name: release_tag
      prompt: "Which release to deploy?"     # shown as the field label
      # no default → Steward marks the field REQUIRED
    - name: admin_password
      prompt: "Admin password"
      private: true                          # → masked field, never stored

3. Secrets — name them so they're masked and not persisted

A field is treated as secret (rendered masked, and its value is never written to the DB / run history) when either:

  • the variable name contains password, passwd, secret, token, api_key / apikey, private_key, or credential (case-insensitive), or
  • it's a vars_prompt entry with private: true (Ansible's default for vars_prompt is private).

So name sensitive variables accordingly (db_password, api_token, vault_secret) and they're handled safely with no extra config. Non-secret run-time vars are persisted (so scheduled runs can reuse them); secret ones are passed to the run only.

4. Target with hosts: all

Steward builds the inventory itself from the target / group the operator picks in the run form (or the single host on a host page). Your play should:

  hosts: all          # run against whatever Steward scoped to
  • Don't hardcode hostnames or rely on a checked-in inventory for Steward runs (Steward generates a fresh inventory per run).
  • Per-host connection vars (ansible_host, plus anything you set on the target in Ansible → Inventory) arrive as inventory host vars.
  • The run form's Limit / Tags map to --limit / --tags.

5. Privileges & connection — don't put credentials in the playbook

Steward supplies SSH and become for you:

  • Steady-state runs connect as the managed steward account using Steward's managed key; that account has passwordless sudo. So just use become: true where you need root.
  • First-contact provisioning uses a one-time bootstrap user/password the operator enters (never stored).
  • Never embed SSH keys, passwords, or ansible_user/ansible_ssh_pass in the playbook. Connection identity is global (Settings → Ansible) or per-target.

6. Be idempotent

Steward re-runs playbooks (updates, schedules, retries). Use modules that converge (state-based) rather than ad-hoc command:/shell: where possible, so re-runs are safe.


What Steward reads from a playbook (summary)

Source in the playbook What Steward does with it
# description: <text> comment Description shown on selection (first match)
first play name: Description fallback
vars: scalar entries Run-time fill-in fields (placeholder = default)
vars_prompt: entries Run-time fields (required if no default)
secret-looking var name / private: true Field masked + value not persisted
hosts: Expected to be all; Steward provides the inventory

Everything else (SSH user/key, become password, the inventory, steward_token etc. for the agent playbooks) is injected by Steward at run time.

Metadata convention & extensibility

Today the only comment-based metadata Steward parses is # description:. Everything else is structural (vars, vars_prompt, hosts, the play name). If we add more comment metadata later, it will use a reserved # steward:<key>: <value> namespace (e.g. # steward:category: maintenance, # steward:confirm: true for a run-confirmation gate) so it can't collide with ordinary comments. These extra keys are not implemented yet — only # description: is. Don't author playbooks that depend on # steward:* keys until this doc says they're live.

Minimal annotated example

---
# description: Restart a systemd service and confirm it came back up.
- name: Restart a service
  hosts: all
  become: true
  gather_facts: false
  vars:
    service_name: ""        # required-ish: operator fills it in the run form
  tasks:
    - name: Validate input
      ansible.builtin.assert:
        that: service_name | default('') | length > 0
        fail_msg: "Set service_name."
    - name: Restart
      ansible.builtin.systemd:
        name: "{{ service_name }}"
        state: restarted
    - name: Confirm active
      ansible.builtin.command: "systemctl is-active {{ service_name }}"
      changed_when: false