From b32fce1d7412aff78ba7ba175bc7420e345f0f87 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 17 Jun 2026 11:26:56 -0400 Subject: [PATCH] docs: Steward playbook-authoring conventions reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::` 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) --- docs/reference/playbook-authoring.md | 169 +++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/reference/playbook-authoring.md diff --git a/docs/reference/playbook-authoring.md b/docs/reference/playbook-authoring.md new file mode 100644 index 0000000..02a6bf9 --- /dev/null +++ b/docs/reference/playbook-authoring.md @@ -0,0 +1,169 @@ +# 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. + +```yaml +--- +# 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: ` 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. + +```yaml + 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: + +```yaml + 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: + +```yaml + 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: ` 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:: `** 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 + +```yaml +--- +# 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 +```