Skip to main content

max / makenotwork

9.7 KB · 208 lines History Blame Raw
1 # Bento deploy units
2
3 `bentod` runs as a systemd **user** service on fw13 (the x86_64 build gate).
4 Unlike `sandod` (a hardened system service with a dedicated `sando` user),
5 bentod is a user service under the operator: building the apps needs the
6 operator's SSH keys (to the tailnet build hosts and the mbp ops-agent), the app
7 checkouts under `~/Code/Apps`, and the `_private` layer for signing secrets
8 (`secrets_root`). A dedicated system user can't reach those without copying keys
9 and bind-mounting home, so a user service is the right model, and it lets the
10 operator redeploy bentod with `systemctl --user restart` (no sudo).
11
12 ## Files
13
14 | File | Where it goes | Purpose |
15 |------|---------------|---------|
16 | `bentod.service` | `~/.config/systemd/user/` | The user service unit. |
17 | `bento-daemon.toml.example` | `~/.config/bento/bento-daemon.toml` | Daemon-local config (paths + listen). Absolute paths only; no shell expansion. |
18 | `bento.toml.example` | `~/.config/bento/bento.toml` | Build topology (hosts + apps). `repo` paths are tilde-expanded; `repo_by_host` overrides the path for one host. |
19 | (none, hand-written 0600) | `~/.config/bento/bento.env` | `BENTO_API_TOKEN=...` for a tailnet bind. Read by the unit's `EnvironmentFile=-`, so a loopback install can leave it absent. No example file ships: it holds a secret. |
20
21 ## Stand-up (no root except enable-linger)
22
23 ```sh
24 # 1. Build + install the binary
25 cd ~/Code/MNW/bento/daemon && cargo build --release
26 install -D -m 0755 target/release/bentod ~/.local/bin/bentod
27
28 # 2. Config + state dirs
29 mkdir -p ~/.config/bento ~/.local/state/bento/logs ~/Dist
30 install -m 0644 ~/Code/MNW/bento/deploy/bento-daemon.toml.example ~/.config/bento/bento-daemon.toml
31 install -m 0644 ~/Code/MNW/bento/deploy/bento.toml.example ~/.config/bento/bento.toml
32 # (edit the two configs: absolute home paths, real host/app rows)
33
34 # 3. Service
35 mkdir -p ~/.config/systemd/user
36 install -m 0644 ~/Code/MNW/bento/deploy/bentod.service ~/.config/systemd/user/
37 loginctl enable-linger "$USER"
38 systemctl --user daemon-reload
39 systemctl --user enable --now bentod
40
41 # 4. Verify (address must match `listen` in bento-daemon.toml, see Auth below)
42 curl -s http://127.0.0.1:8765/state | python3 -m json.tool
43 ```
44
45 ## Where artifacts end up
46
47 Each target's `collect` lands locally at `<dist_root>/<app>/<version>/<target>/`
48 and is then deposited at `<root>/<app>/<version>/<target>/` on the archive host
49 named by the `[archive]` table. Same layout both sides, so the two are one tree
50 at two addresses.
51
52 astra holds the archive: always on, on the tailnet, and already the aarch64 build
53 host and git mirror, so it is the one box every other build host can reach. That
54 is what gives "where is the AppImage for goingson 1.4.0" a single answer covering
55 the macOS and Windows targets too, rather than one per machine that built them.
56
57 The archive host needs the directory to exist and to be writable by the SSH user
58 bentod reaches it as; everything below it is created per release.
59
60 ```sh
61 ssh astra 'sudo install -d -o "$USER" -g "$USER" -m 0755 /var/lib/bento/artifacts'
62 ```
63
64 Retention prunes `dist_root` and `logs_root` on the daemon box and never touches
65 the archive: the archived copy is the one meant to outlive them. A failed deposit
66 fails that target's `collect` step, before sign and publish. Leave the `[archive]`
67 table out to skip archiving entirely.
68
69 ## Handing an artifact to Sando
70
71 Bento builds and packages; Sando decides whether a thing advances a stage. For a
72 product Sando deploys, a `[handoff.<app>]` table names where its bytes go after
73 the recipe finishes:
74
75 ```toml
76 [handoff.pom]
77 host = "sando@fw13"
78 staging_root = "/srv/sando/staging"
79 url = "http://100.103.89.95:7766"
80 sando_app = "pom"
81 token_env = "BENTO_SANDO_TOKEN"
82 ```
83
84 `token_env` names the environment variable holding sandod's bearer token; it
85 never holds the token itself and never points at a file. Set the variable in
86 bentod's `EnvironmentFile` (`~/.config/bento/bento.env`, already there for
87 `BENTO_API_TOKEN`) with the value of `SANDO_API_TOKEN` from
88 `/etc/sando/sando.env` on the Sando host. Those two have to match, and the
89 daemon says so by name when the variable is unset.
90
91 A signing key and a bearer token are not the same kind of secret: a key is a
92 file because a tool opens it by path, while a token is one opaque string held
93 for the life of the process, and a token on disk in plaintext inside `_private`
94 is a token in git. The older `token_file` key is refused rather than ignored, so
95 a config still carrying it fails at startup instead of sending no header and
96 collecting a 401.
97
98 Per target, the collect directory is rsynced to
99 `<staging_root>/<app>-<version>-<target>/` and sandod is asked to take it in.
100 Sando proves the bundle against its record before anything else happens, so a
101 transfer that dropped or corrupted a file is refused with the file named.
102
103 Two things about the transfer are load-bearing. The artifact record does **not**
104 travel with the bytes: it names the digest of the bundle, the digest covers every
105 file in the bundle, so a record copied in among the artifacts would change the
106 digest it names. It goes in the request body. And the staging directory is
107 mirrored with `--delete` rather than merged, so a retry after a partial transfer
108 holds exactly this attempt: a leftover file is an extra file, and an extra file
109 is a manifest mismatch that would get an honest bundle refused.
110
111 `host` is usually the same machine bentod runs on, and still goes through ssh:
112 sandod runs as `sando`, bentod as a user unit, and `sando@fw13` lands the bytes
113 owned by the process that has to rename them without a group or an ACL on the
114 staging directory. That means bentod's user needs an SSH key in `sando`'s
115 `authorized_keys`, and the staging directory has to exist:
116
117 ```sh
118 ssh sando@fw13 'install -d -m 0755 /srv/sando/staging'
119 ```
120
121 A failed handoff fails that target run at a `handoff` step, which is a step no
122 recipe runs: the daemon performs it after the recipe, and the failure needs a
123 column of its own rather than contradicting a green `collect`. Leave the table
124 out for every app Sando does not deploy, which is most of them.
125
126 ## Auth
127
128 On a loopback bind, build triggers are reachable only from the daemon's own
129 host, so no token is required. To operate bentod over the tailnet, bind the
130 tailnet IP in `bento-daemon.toml` and set `BENTO_API_TOKEN` via an
131 `EnvironmentFile` in the unit. bentod refuses to start on a non-loopback bind
132 without it (same posture as Sando's `SANDO_API_TOKEN`).
133
134 **fw13 runs the second form**, not the loopback default the example config
135 ships. `listen` is the tailnet address and `/build` and `/retry` require the
136 bearer token, so a `127.0.0.1` curl gets no answer at all rather than an auth
137 error. Read the address out of the config and the token out of its own file
138 rather than passing either on a command line:
139
140 ```sh
141 set -a; . ~/.config/bento/bento.env; set +a
142 curl -s -X POST http://$(grep -oP '(?<=^listen = ")[^"]+' ~/.config/bento/bento-daemon.toml)/build \
143 -H "authorization: Bearer $BENTO_API_TOKEN" -H 'content-type: application/json' \
144 -d '{"app":"makeover-webview","version":"0.5.1"}'
145 ```
146
147 `/state`, `/status.json` and the log endpoints are read-only and unauthed, so
148 watching a run needs the address but not the token.
149
150 ## Recipes
151
152 A real `/build` reads `<app>/<recipe_dir>/<platform>.rhai` from each app's
153 checkout. One per platform per app:
154
155 | App | macOS | iOS | Linux | Windows |
156 |-----|-------|-----|-------|---------|
157 | goingson | yes | yes | yes | yes (unsigned, unsupported) |
158 | audiofiles | yes | — (egui, no iOS) | yes | yes (unsigned, unsupported) |
159 | balanced_breakfast | pending¹ | pending¹ | yes | yes (unsigned, unsupported) |
160
161 ¹ BB has no Developer ID signing / notarization infra or iOS project yet, so its
162 macOS/iOS recipes are deferred until that lands. BB ships in a later wave.
163
164 A library crate (`kind = "library"` in its own `bento.toml`) has no per-platform
165 artifact, so the runner reads a single `publish.rhai` instead of the table above.
166 Every library configured in the topology carries one. That recipe asserts HEAD is
167 exactly a tag before it uploads, so a release needs `git tag -a v<version>`
168 first.
169
170 A `linux.rhai` serves both arches: `build_host()` resolves to the native host
171 the topology assigns the target (`linux/x86_64` → fw13, `linux/aarch64` → astra),
172 so there is no cross-compilation and no hard-coded host name. macOS/iOS recipes
173 run on the mbp `agent`-transport host where the Developer ID key is usable.
174
175 ### The prebuild gate
176
177 Every recipe runs a `prebuild` step between checkout and build:
178
179 ```
180 cargo clippy --workspace --all-targets <features> -- -D warnings
181 cargo test --workspace <features>
182 ```
183
184 `sh_ok` aborts the recipe on a non-zero exit, and a failed step lands in the
185 step ledger, so a red gate bars publish as well as build. It runs on the target's
186 own build host rather than once centrally: a break confined to one platform
187 (cfg-gated code, a Windows path) is then caught on the machine that would have
188 shipped it. The cost is that the suite runs once per target.
189
190 Features come from `feature_flags()`, so the gate compiles the same
191 configuration the release build does.
192
193 ### Recipe host-function vocabulary
194
195 Beyond `step`/`sh`/`sh_ok`/`log`/`collect`/`publish`/`secret`/`env` and the
196 macOS helpers (`codesign`/`notarize`/`staple`/`verify_gatekeeper`/`keychain_*`),
197 recipes can read their own context:
198
199 | Function | Returns |
200 |----------|---------|
201 | `version()` | the version being built (e.g. `"0.4.2"`) |
202 | `build_host()` | the host this target builds on (e.g. `"fw13"`) |
203 | `repo()` | the app's checkout path on this target's build host (`~`-prefixed on unix; `cd` into it, commands don't auto-cd) |
204 | `target()` / `platform()` / `arch()` | `"linux/x86_64"` / `"linux"` / `"x86_64"` |
205
206 Non-Tauri apps (audiofiles) set `version_path` in the topology to the crate
207 `Cargo.toml` carrying the version, since they have no `tauri.conf.json`.
208