Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixed

- The README no longer teaches the observe that cost #486: its `def` example asked `dpkg -s`, which exits 0 for a package removed without `--purge`. It now carries `apt.install`'s real question and says why. `--json`, `-v`, `--parallel` and `--limit` are documented, and the secrets note states the limit that is left rather than one ADR-0025 closed (#642).

- `htpasswd.entry` and `system.timezone` observe everything their apply sets. The first left a credentials file world-readable whenever its hash already verified; the second left `/etc/timezone` naming another zone whenever the symlink was right. Both reported `already`. Two adverse cases, each seen red on a real machine first (#635, #636).

- A def calling another instruction with too few arguments is refused instead of binding the missing one to the empty string. `file.write(path)` inside a def overwrote the file with nothing and reported `ok.done` — a file destroyed under a success verdict. A plan-level call was always checked on both bounds; only the def side was not (#633).
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,12 @@ shellf run plan.shellf --secret-file rclone_pass=./secret --secret-env db=DB_PW
```

A secret is a variable like any other (`${rclone_pass}`), but shellf **redacts
its value** (`***`) from every report, `--dry-run`, and `status`. Honest limit: the
secret still reaches the target (in the request file, `0600`, and the process
env) — root there can read it; at-rest secrecy is not yet solved.
its value** (`***`) from every report, `--dry-run`, and `status`. At rest it is kept off
persistent disk: the agent's job files live on tmpfs (`/dev/shm`), so a secret is outside
backups, snapshots and undelete — falling back to `/tmp`, owner-only and deleted after, on a
host with no writable `/dev/shm` (ADR-0025). Honest limit: the secret still reaches the target,
in that job file and in the process environment, and **root there reads both in real time**.
No on-target scheme changes that, encryption included — the key would have to travel too.

**Control flow.** `if` takes an instruction (or a captured result); the branch is
taken on its outcome. `dir.exists` is a read-only *question*, so it stays honest
Expand Down Expand Up @@ -211,7 +214,10 @@ its success:
```
def install(pkg: str) as root {
observe {
return state(installed: shell { dpkg -s "$pkg" >/dev/null 2>&1 }.exit == 0)
# `install ok installed` is the one status meaning the files are on the host
return state(installed: shell {
dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q '^install ok installed'
}.exit == 0)
}
apply {
r = shell { apt-get install -y "$pkg" }
Expand All @@ -221,6 +227,12 @@ def install(pkg: str) as root {
}
```

That is `apt.install`'s real observe, and the comment is the whole lesson: **the question an
`observe` asks has to be the one its `apply` answers**. This def first asked `dpkg -s`, which
exits 0 for a package removed without `--purge` — its config files survive, its binaries do
not — so it reported `already` on a host where the package was gone. The wrong answer was
*stable*, so re-running agreed with itself and only the machine said otherwise (#486).

A field with no same-named argument (like `installed`) must simply hold;
fields that match a parameter (`service.ensure` → `running`, `git.clone` → `url`) are
compared to it. See [ADR-0013](docs/adr/0013-observe-state-contract.md).
Expand Down Expand Up @@ -352,6 +364,13 @@ agents and wipes shellf's files from the targets.
| `--known-hosts <file>` | host-key file (default `~/.ssh/known_hosts`) |
| `--insecure` | skip host-key verification (dev only) |
| `--agent-ttl <dur>` | resident agent inactivity TTL before it self-erases (default 2h) |
| `--parallel <n>` | hosts dialled at once (default 16); `1` serialises the fan-out |
| `--limit <host\|group>` | restrict the run to a host or group (repeatable); narrows the plan, never extends it |
| `--json` | report as JSON on stdout; diagnostics stay on stderr |
| `-v` | trace the control host's decisions, and every command run on the target |

`run` takes all of them. `status` takes all but `--vars`, `--set`, `--dry-run`, `-v` and
`--agent-ttl`. `clean` takes `--inventory`, `--insecure` and `--known-hosts`.

## How it works

Expand Down
Loading