42f7840c26
Extend the playbook metadata convention with a namespaced `# steward:<key>:` comment block: - steward:category — free-text grouping label, shown as a badge in the browse list and on the run form. - steward:confirm — true/yes/1/on marks a playbook destructive; the run form then requires a confirmation tick (required checkbox in the shared vars fragment) before it can launch. sources.discover_playbook_meta() parses description + category + confirm (first match per key; `# description:` still primary, `# steward:description:` alias). discover_playbook_description() now delegates to it. The browse list reads per-playbook meta to show category badges + descriptions; the run-form and playbook-vars fragments render the badge + confirm gate. Bundled playbooks tagged: docker_prune → category maintenance + confirm true; provision/install/update → category host-agent. Docs: docs/reference/playbook-authoring.md updated (keys now implemented) and a quick reference added next to the code at steward/ansible/PLAYBOOK_CONVENTIONS.md. Tests added for category/confirm/alias parsing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
192 lines
7.6 KiB
Markdown
192 lines
7.6 KiB
Markdown
# 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: <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.
|
|
|
|
```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: <text>` comment | Description shown on selection (first match) |
|
|
| first play `name:` | Description fallback |
|
|
| `# steward:category: <text>` | Grouping badge in the browse list + run form |
|
|
| `# steward:confirm: true` | Requires a confirmation tick before the run launches |
|
|
| `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 comments
|
|
|
|
Steward reads metadata from magic comments (Ansible rejects unknown play keys,
|
|
so comments are the portable place). Two forms:
|
|
|
|
- `# description: <text>` — the description (see §1).
|
|
- `# steward:<key>: <value>` — the namespaced metadata block. First match per
|
|
key wins; keys are case-insensitive.
|
|
|
|
**Implemented `# steward:` keys:**
|
|
|
|
| Key | Example | Effect |
|
|
|---|---|---|
|
|
| `category` | `# steward:category: maintenance` | Free-text grouping label. Shown as a badge in the browse list and on the run form. Purely organizational. |
|
|
| `confirm` | `# steward:confirm: true` | Marks the playbook as significant/destructive. The run form then requires an explicit confirmation tick before it can be launched. Use for data-loss-capable plays (prune with volumes, resets, etc.). Truthy values: `true`/`yes`/`1`/`on`. |
|
|
| `description` | `# steward:description: ...` | Alias for `# description:` (the unprefixed form takes precedence if both exist). |
|
|
|
|
```yaml
|
|
---
|
|
# description: Reclaim disk on Docker/Swarm nodes by pruning unused data.
|
|
# steward:category: maintenance
|
|
# steward:confirm: true
|
|
- name: Docker system prune
|
|
hosts: all
|
|
...
|
|
```
|
|
|
|
Other `# steward:<key>:` keys are simply ignored today — the namespace is
|
|
reserved, so it's safe to add ones Steward doesn't yet understand without
|
|
breaking anything, but only `category`, `confirm`, and `description` do something.
|
|
|
|
## 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
|
|
```
|