| 1 |
# Sando topology config. |
| 2 |
# |
| 3 |
# Tiers run in declaration order. Each tier lists the gates that must pass to |
| 4 |
# unlock promotion *to* the next tier, the nodes it ships to, and the canary |
| 5 |
# policy for shipping within the tier. |
| 6 |
# |
| 7 |
# Day-one wiring: host (fw13, local) -> A (testnot.work) -> B (prod-1). C is |
| 8 |
# declared but not provisioned; adding the second prod node later is a config |
| 9 |
# edit (set provisioned = true, fill in [[tier.node]]). |
| 10 |
# |
| 11 |
# The first tier is "host" — it refers to whatever machine sandod runs on |
| 12 |
# (currently fw13). Renamed from the legacy "mm" name in Session 1 of |
| 13 |
# the sando bundle redesign. |
| 14 |
|
| 15 |
[repo] |
| 16 |
bare_path = "/srv/sando/mnw.git" |
| 17 |
branch = "main" |
| 18 |
# Canonical remote sandod fetches the deploy branch from before worktree-ing a |
| 19 |
# sha (pull-based /rebuild). Authorized via the `sando-deploy` read-only |
| 20 |
# service account on the makenot.work git server (fetch-only collaborator). |
| 21 |
# Leave unset for push-based hosts (commits arrive via the bare-repo hook). |
| 22 |
upstream = "git@ssh.makenot.work:max/makenotwork.git" |
| 23 |
|
| 24 |
# ---- auxiliary repos ---- |
| 25 |
# Extra repos fetched and checked out beside the per-sha worktree so a cross-repo |
| 26 |
# path dependency resolves at build time. The mnw-cli companion (built from the |
| 27 |
# MNW worktree) carries `synckit-client = { path = "../../synckit/synckit-client" }` |
| 28 |
# after synckit moved to its own repo; from <workdir>/<sha>/mnw-cli that resolves |
| 29 |
# to <workdir>/synckit, so synckit must be checked out there. checkout_dir is a |
| 30 |
# path under the workdir, nesting allowed; the checkout is shared across shas and |
| 31 |
# refreshed to `branch` HEAD each build. See the maintainer wiki, sando-overview. |
| 32 |
[[aux_repo]] |
| 33 |
name = "synckit" |
| 34 |
bare_path = "/srv/sando/synckit.git" |
| 35 |
upstream = "git@ssh.makenot.work:max/synckit.git" |
| 36 |
branch = "main" |
| 37 |
checkout_dir = "synckit" |
| 38 |
|
| 39 |
# The server and multithreaded both carry |
| 40 |
# `docengine = { path = "../../Libraries/docengine" }` after docengine left |
| 41 |
# MNW/shared on 2026-07-30. From <workdir>/<sha>/server that resolves to |
| 42 |
# <workdir>/Libraries/docengine, hence the nested checkout_dir. |
| 43 |
[[aux_repo]] |
| 44 |
name = "docengine" |
| 45 |
bare_path = "/srv/sando/docengine.git" |
| 46 |
upstream = "git@ssh.makenot.work:max/docengine.git" |
| 47 |
branch = "main" |
| 48 |
checkout_dir = "Libraries/docengine" |
| 49 |
|
| 50 |
# Prod-backup clones used by migration_dry_run on the Sando host, one per |
| 51 |
# database that has a [[migration_check]] in the daemon config. For localhost dev |
| 52 |
# a source can be a file:// path to a fixture dump. In prod we pull from |
| 53 |
# alpha-west-1 via a scoped `backup-puller` rrsync user, whose forced command is |
| 54 |
# `rrsync -ro /var/lib/mnw/backups` — so every path here is relative to that |
| 55 |
# directory, and the per-DB subdirs are what `server/deploy/backup-db.sh` writes. |
| 56 |
[[backup]] |
| 57 |
name = "server" |
| 58 |
source = "ssh://backup-puller@alpha-west-1:2200/makenotwork/latest.sql.gz" |
| 59 |
local_path = "/srv/sando/backups/latest.sql.gz" |
| 60 |
|
| 61 |
# multithreaded has its own database and applies its own migrations at boot |
| 62 |
# (multithreaded/src/main.rs, `sqlx::migrate!()`), so it needs its own dump to |
| 63 |
# dry-run against. Restoring the server's would prove nothing about it. |
| 64 |
[[backup]] |
| 65 |
name = "multithreaded" |
| 66 |
source = "ssh://backup-puller@alpha-west-1:2200/multithreaded/latest.sql.gz" |
| 67 |
local_path = "/srv/sando/backups/multithreaded-latest.sql.gz" |
| 68 |
|
| 69 |
# ---- host: fw13 local pre-staging gate ---- |
| 70 |
[[tier]] |
| 71 |
name = "host" |
| 72 |
provisioned = true |
| 73 |
canary = "sequential" |
| 74 |
# code_smoke runs FIRST: it boots the freshly-built binary against a throwaway |
| 75 |
# empty DB it migrates from scratch + seeds (the example catalog), then probes |
| 76 |
# /health. Fast + infra-light (no prod-dump restore), so a green here proves the |
| 77 |
# code is sound and isolates a later cargo_test / migration_dry_run red as an |
| 78 |
# environment problem rather than a code one. |
| 79 |
# |
| 80 |
# hardening_test is cargo_test's blind spot: cargo_test builds with |
| 81 |
# --features fast-tests, which relaxes the auth/sandbox rate limits and argon2 |
| 82 |
# cost AND #[ignore]s the whole rate-limiting suite, so the gate never touched |
| 83 |
# the auth hardening it exists to protect. hardening_test re-runs that suite |
| 84 |
# with no features, single-threaded, against production constants. It pays for |
| 85 |
# a second compile of the server's test binary; that is the cost of the |
| 86 |
# coverage. |
| 87 |
# |
| 88 |
# The lint + supply-chain gates. `-D warnings` used to be enforced in exactly |
| 89 |
# one place (server/deploy/run-ci.sh, which died with the astra pipeline) and |
| 90 |
# `cargo fmt --check` nowhere at all. fmt runs first: it needs no compilation, |
| 91 |
# so a formatting red comes back in seconds instead of after a full build. |
| 92 |
# cargo_audit and cargo_deny only run in crates carrying a triaged |
| 93 |
# .cargo/audit.toml / deny.toml -- four crates in this repo fail cargo audit |
| 94 |
# purely for lack of a reviewed advisory posture, and a permanently red gate |
| 95 |
# teaches everyone to ignore it. |
| 96 |
gates = [ |
| 97 |
{ kind = "fmt" }, |
| 98 |
{ kind = "code_smoke" }, |
| 99 |
{ kind = "cargo_test" }, |
| 100 |
{ kind = "hardening_test" }, |
| 101 |
{ kind = "clippy" }, |
| 102 |
{ kind = "cargo_audit" }, |
| 103 |
{ kind = "cargo_deny" }, |
| 104 |
{ kind = "migration_dry_run" }, |
| 105 |
{ kind = "boot_smoke" }, |
| 106 |
] |
| 107 |
# Host is the daemon's own machine (fw13); no remote node row. |
| 108 |
|
| 109 |
# ---- A: testnot.work staging ---- |
| 110 |
[[tier]] |
| 111 |
name = "a" |
| 112 |
provisioned = true |
| 113 |
canary = "sequential" |
| 114 |
# node_health (post-deploy) probes the deployed node over its executor — the |
| 115 |
# gate boot_smoke used to stand in for, but boot_smoke runs on the build host and |
| 116 |
# proves nothing about testnot-1. boot_smoke stays a host build-time gate only. |
| 117 |
# |
| 118 |
# A tier's gates guard promotion *out* of it, so THIS list is what stands between |
| 119 |
# testnot and production. manual_confirm lives here for that reason. It used to |
| 120 |
# sit only on tier b, where it guards b -> c — and c is not provisioned, so it |
| 121 |
# gated nothing: a prod ship was cleared by node_health + burn_in alone, and |
| 122 |
# hotfix: true skips burn_in, leaving one gate on a production deploy. |
| 123 |
# |
| 124 |
# hotfix does NOT skip manual_confirm (only burn_in), so every ship to prod-1 |
| 125 |
# now needs an explicit POST /confirm/a naming the version. That is the point: |
| 126 |
# there was no human sign-off anywhere on the path to production. |
| 127 |
gates = [ |
| 128 |
{ kind = "node_health" }, |
| 129 |
{ kind = "burn_in", hours = 48 }, |
| 130 |
{ kind = "manual_confirm" }, |
| 131 |
] |
| 132 |
[[tier.node]] |
| 133 |
name = "testnot-1" |
| 134 |
ssh_target = "deploy@testnot" # tailnet name; never the public IP / testnot.work hostname |
| 135 |
release_root = "/opt/mnw" |
| 136 |
service_name = "makenotwork.service" |
| 137 |
# What this box IS, checked against it before anything is pushed (2026-08-25). |
| 138 |
# Note it is NOT the same base as prod-1 below: staging is two Ubuntu releases |
| 139 |
# and four glibc point versions ahead, so tier A does not rehearse tier B on the |
| 140 |
# one axis this field exists for. That is a real gap, tracked separately; the |
| 141 |
# declaration states it rather than hiding it. |
| 142 |
base_image = "ubuntu/26.04" |
| 143 |
libc = "2.43" |
| 144 |
# Pre-swap config-drift guard: source this node's env and run the freshly-rsynced |
| 145 |
# binary in MNW_CHECK_CONFIG=1 mode BEFORE the symlink swap. A required var |
| 146 |
# missing here (how prod crash-looped on CDN_BASE_URL, postmortem 2026-07-09 #2) |
| 147 |
# fails the promote with the running service intact. The node runs 0.10.14, which |
| 148 |
# supports the mode; validated end-to-end 2026-07-10. |
| 149 |
config_check_env_file = "/etc/mnw/makenotwork.env" |
| 150 |
# Readiness probe on top of `systemctl is-active`. Without this, node_health |
| 151 |
# proves only that systemd thinks the unit is running — which a crash-looping |
| 152 |
# binary can satisfy between restarts. The node binds HOST=127.0.0.1 PORT=8080 |
| 153 |
# (/etc/mnw/makenotwork.env); verified serving 200 on 2026-07-21. |
| 154 |
health_url = "http://127.0.0.1:8080/health" |
| 155 |
# Companion: multithreaded. Re-enabled 2026-08-06, after the read-only demo was |
| 156 |
# provisioned on this box (GoingsOn multithreaded 34dc3ff6). It was removed in |
| 157 |
# 2fccc2d0 because testnot-1 had no mt at all, and a companion block for a node |
| 158 |
# that cannot install it fails the promote AFTER the symlink swap — the expensive |
| 159 |
# half of the failure. That is no longer the case; what is on the box now: |
| 160 |
# - postgres role + database `multithreaded`, peer auth over the unix socket |
| 161 |
# (DATABASE_URL=postgres:///multithreaded, matching prod) |
| 162 |
# - system user `multithreaded`, /opt/multithreaded, .env, and the unit from |
| 163 |
# multithreaded/deploy/multithreaded.service, enabled and running on |
| 164 |
# 127.0.0.1:3400, seeded via `--seed` |
| 165 |
# - /usr/local/lib/mnw/install-companion.sh + /etc/sudoers.d/mnw-companion |
| 166 |
# |
| 167 |
# The install path was exercised end to end before this line was uncommented, as |
| 168 |
# `deploy`, through the wrapper's own guards — a staged binary under |
| 169 |
# */releases/*/companions/* installed to /opt/multithreaded and restarted the |
| 170 |
# unit, exit 0. It is not inferred from the grant being present. |
| 171 |
# |
| 172 |
# mt is on testnot FIRST, unlike mnw-cli, because this is the tier where the 48h |
| 173 |
# burn-in runs: a forum that fails to boot on the new binary should fail here, |
| 174 |
# not on prod. See multithreaded/deploy/README.md. |
| 175 |
[[tier.node.companion]] |
| 176 |
name = "multithreaded" |
| 177 |
install_path = "/opt/multithreaded/multithreaded" |
| 178 |
service_name = "multithreaded.service" |
| 179 |
|
| 180 |
# ---- B: prod-1 ---- |
| 181 |
[[tier]] |
| 182 |
name = "b" |
| 183 |
provisioned = true |
| 184 |
canary = "sequential" |
| 185 |
# Guards b -> c. Kept for the day prod-2 is provisioned; it gates nothing today |
| 186 |
# (c has provisioned = false). The sign-off that matters for shipping to THIS |
| 187 |
# tier lives on tier a. |
| 188 |
gates = [ |
| 189 |
{ kind = "node_health" }, |
| 190 |
{ kind = "manual_confirm" }, |
| 191 |
] |
| 192 |
[[tier.node]] |
| 193 |
name = "prod-1" |
| 194 |
# Tailnet name; port 2200 supplied via /srv/sando/.ssh/config Host block. |
| 195 |
# Service user is "makenotwork" (pre-existing on prod), not "deploy" — chose |
| 196 |
# not to chown 885M of backups + redo postgres peer auth for a cosmetic rename. |
| 197 |
ssh_target = "makenotwork@alpha-west-1" |
| 198 |
release_root = "/opt/mnw" |
| 199 |
service_name = "makenotwork.service" |
| 200 |
# Measured 2026-08-25. The margin here is zero: makenotwork, mnw-cli and pom all |
| 201 |
# carry a GLIBC_2.39 floor and this box has exactly 2.39, so one point release of |
| 202 |
# drift on the build host breaks it. Declaring the number is what lets that be |
| 203 |
# compared before a build rather than by the pre-swap `ldd` guard afterwards. |
| 204 |
base_image = "ubuntu/24.04" |
| 205 |
libc = "2.39" |
| 206 |
# Pre-swap config-drift guard (see testnot-1 above). This node is the one the |
| 207 |
# 0.10.14 deploy crash-looped on a missing CDN_BASE_URL — the exact miss this |
| 208 |
# gate closes. First real exercise is the next prod promote. |
| 209 |
config_check_env_file = "/etc/mnw/makenotwork.env" |
| 210 |
# Readiness probe (see testnot-1). The unit binds HOST=0.0.0.0 PORT=3000, so the |
| 211 |
# probe dials it on loopback; verified serving 200 on 2026-07-21. This is the |
| 212 |
# node whose crash-loop `systemctl is-active` alone would have missed. |
| 213 |
health_url = "http://127.0.0.1:3000/health" |
| 214 |
# Companion: install the mnw-cli built in this same promote (see [[companion]] in |
| 215 |
# sando-daemon.toml) after the server is up, and restart its unit. Closes the |
| 216 |
# drift that broke git hosting during the 0.10.14 deploy — mnw-cli now ships from |
| 217 |
# the same sha as the server. Needs the node-side wrapper + sudoers grant |
| 218 |
# (deploy/install-companion.sh, deploy/mnw-companion.sudoers). |
| 219 |
[[tier.node.companion]] |
| 220 |
name = "mnw-cli" |
| 221 |
install_path = "/opt/mnw-cli/mnw-cli" |
| 222 |
service_name = "mnw-cli.service" |
| 223 |
# Companion: multithreaded, same sha as the server, same host. This is what |
| 224 |
# retires multithreaded/deploy/deploy-hetzner.sh (cross-compiled on macOS, scp'd |
| 225 |
# to root@, 41 commits behind when found). mt now rides the whole ladder — native |
| 226 |
# build on fw13, the migration_dry_run gate against its own dump, the test and |
| 227 |
# lint gates, and the server's burn-in on testnot — instead of a laptop and an |
| 228 |
# scp. Design: wiki sando-mt-pom-pipelines. |
| 229 |
# |
| 230 |
# prod-1 is the only node that installs it, because it is the only node that runs |
| 231 |
# mt (see the note on testnot-1 above). It already has the wrapper and the |
| 232 |
# sudoers grant, from mnw-cli. |
| 233 |
[[tier.node.companion]] |
| 234 |
name = "multithreaded" |
| 235 |
install_path = "/opt/multithreaded/multithreaded" |
| 236 |
service_name = "multithreaded.service" |
| 237 |
|
| 238 |
# ---- C: prod-2 (declared, not yet provisioned) ---- |
| 239 |
[[tier]] |
| 240 |
name = "c" |
| 241 |
provisioned = false |
| 242 |
canary = "sequential" |
| 243 |
gates = [ |
| 244 |
{ kind = "node_health" }, |
| 245 |
] |
| 246 |
# [[tier.node]] entries to be added when the second prod node ships. |
| 247 |
|