max / makenotwork
3 files changed,
+300 insertions,
-40 deletions
| @@ -55,7 +55,11 @@ | |||
| 55 | 55 | [[tier.node]] | |
| 56 | 56 | name = "astra" | |
| 57 | 57 | platform = "linux/aarch64" | |
| 58 | - | ssh_target = "max@astra" # tailnet name | |
| 58 | + | # Tailnet name, reached over Tailscale SSH. The connection is made by the | |
| 59 | + | # `sando` user on fw13, so astra's host key has to be in /srv/sando/.ssh/ | |
| 60 | + | # known_hosts; without it the deploy fails at `Host key verification failed` | |
| 61 | + | # before it has done anything, which is where it stood on 2026-08-15. | |
| 62 | + | ssh_target = "max@astra" | |
| 59 | 63 | release_root = "/opt/pom" | |
| 60 | 64 | service_name = "pom.service" | |
| 61 | 65 | # Readiness on top of `systemctl is-active`: pom binds its API on the tailnet | |
| @@ -75,7 +79,15 @@ | |||
| 75 | 79 | [[tier.node]] | |
| 76 | 80 | name = "hetzner" | |
| 77 | 81 | platform = "linux/x86_64" | |
| 78 | - | ssh_target = "root@alpha-west-1" # Tailscale SSH; port 2200 via the ssh config | |
| 82 | + | # Not Tailscale SSH, whatever this line used to say. sandod runs as the `sando` | |
| 83 | + | # user on fw13 and that user's ~/.ssh/config maps `alpha-west-1` to port 2200, | |
| 84 | + | # which is the box's own sshd; Tailscale SSH is 22 and never sees the | |
| 85 | + | # connection. So this needs sando's public key in root's authorized_keys there, | |
| 86 | + | # and it is not in it today: measured 2026-08-15, `Permission denied | |
| 87 | + | # (publickey)`. mnw reaches the same machine as `makenotwork@alpha-west-1`, a | |
| 88 | + | # service user with the key and a scoped systemctl grant, which is the house | |
| 89 | + | # pattern if this one wants revisiting. | |
| 90 | + | ssh_target = "root@alpha-west-1" | |
| 79 | 91 | release_root = "/opt/pom" | |
| 80 | 92 | service_name = "pom.service" | |
| 81 | 93 | health_url = "http://127.0.0.1:9100/api/health" |
| @@ -1,13 +1,18 @@ | |||
| 1 | 1 | # Deploying pom | |
| 2 | 2 | ||
| 3 | - | pom ships through **Bento**, not Sando, and not by hand. | |
| 3 | + | pom is **built by Bento and shipped by Sando**, and not by hand. Bento builds | |
| 4 | + | and packages; Sando decides whether a thing advances a stage, and performs the | |
| 5 | + | advance. Design: wiki `sando-bento-boundary`. | |
| 4 | 6 | ||
| 5 | - | Sando cannot do this job. `sando-daemon.toml` sets `build_host = "fw13"` and | |
| 6 | - | `build::run` refuses to compile anywhere else, which is the never-build-on-prod | |
| 7 | - | invariant and also makes Sando single-architecture. pom runs on astra (aarch64) | |
| 8 | - | and on the Hetzner box (x86_64), so Sando could never build half of this release | |
| 9 | - | without breaking its own rule. Bento already fans out native builds across both | |
| 10 | - | hosts, so pom is a Bento recipe. | |
| 7 | + | Neither controller could do this alone. `build::run` compiles only on the one | |
| 8 | + | host `build_host` names, which is the never-build-on-prod invariant and also | |
| 9 | + | makes Sando single-architecture; pom runs on astra (aarch64) and on the Hetzner | |
| 10 | + | box (x86_64), so Sando could never build half of a release without breaking its | |
| 11 | + | own rule. Bento fans out native builds across both machines and cannot honestly | |
| 12 | + | gate an artifact in a place it does not know about. So `pom/bento.toml` declares | |
| 13 | + | `kind = "service"` with no `[[deploy]]` tables, and the daemon's `[handoff.pom]` | |
| 14 | + | carries the collected binary to sandod, which owns install, restart, health and | |
| 15 | + | rollback from there. | |
| 11 | 16 | ||
| 12 | 17 | `deploy.sh` is gone. It cross-compiled with `cargo zigbuild` (against the | |
| 13 | 18 | native-per-architecture rule, with a cargo-zigbuild that is not installed on | |
| @@ -17,34 +22,83 @@ | |||
| 17 | 22 | ||
| 18 | 23 | ## Running a deploy | |
| 19 | 24 | ||
| 20 | - | Bump the version in `Cargo.toml`, commit, tag `v<version>`, push, then trigger | |
| 21 | - | the build through Bento for both targets. The `/deploy` skill has the procedure. | |
| 25 | + | Bump the version in `Cargo.toml`, commit, tag `pom-v<version>`, push, then | |
| 26 | + | trigger the build through Bento for both targets. The `/deploy` skill has the | |
| 27 | + | procedure. | |
| 22 | 28 | ||
| 23 | - | What the recipe does, per target (`dist/recipes/linux.rhai`): | |
| 29 | + | Bento's half, per target (`dist/recipes/linux.rhai`): | |
| 24 | 30 | ||
| 25 | 31 | 1. Pin the checkout to the release tag, and check both hosts report one commit. | |
| 26 | 32 | 2. `cargo clippy -D warnings` and `cargo test`, on that target's own build host. | |
| 27 | 33 | 3. `cargo build --release`. | |
| 28 | - | 4. Compare the binary's highest `GLIBC_` symbol against the service host's | |
| 29 | - | `ldd --version`, and assert `pom --version` matches the tag. | |
| 30 | - | 5. Stage the binary on the service host and call the privileged installer. | |
| 31 | - | 6. Poll `/api/health` until the restarted instance answers, then assert the | |
| 32 | - | running binary reports the version that was just installed. | |
| 34 | + | 4. Assert `pom --version` matches the tag. | |
| 35 | + | 5. `collect` the binary. The daemon hashes it, writes the artifact record beside | |
| 36 | + | it, stages it into `/srv/sando/pom/releases/staging` over ssh, and calls | |
| 37 | + | `POST /apps/pom/intake`. | |
| 33 | 38 | ||
| 34 | - | The two instances go one at a time, and `require_all_targets` keeps the release | |
| 35 | - | from counting as done until both are green. pom watches its own deploy, so the | |
| 36 | - | restart takes one watcher down for a moment; the other stays up and keeps | |
| 37 | - | watching. Never both at once. | |
| 39 | + | The recipe stops there. There is no `glibc_check` in it any more and no health | |
| 40 | + | poll: both are evidence about the artifact *in a place*, which a build host | |
| 41 | + | cannot honestly produce. They moved to Sando's side, where the node is known. | |
| 42 | + | ||
| 43 | + | Sando's half, one tier at a time (`sando/pom-topology.toml`): | |
| 44 | + | ||
| 45 | + | | Tier | Node | Gates | | |
| 46 | + | |------|------|-------| | |
| 47 | + | | `host` | none — the tier an intake lands on | none; Bento's verdicts ride in the artifact record | | |
| 48 | + | | `astra` | astra, aarch64 | `node_health`, `burn_in` 24h, `manual_confirm` | | |
| 49 | + | | `hetzner` | alpha-west-1, x86_64 | `node_health`, `manual_confirm` | | |
| 50 | + | ||
| 51 | + | ``` | |
| 52 | + | curl -H "Authorization: Bearer $SANDO_API_TOKEN" -X POST \ | |
| 53 | + | "$SANDO_DAEMON/apps/pom/promote/astra" | |
| 54 | + | ``` | |
| 55 | + | ||
| 56 | + | Note the `/apps/pom` prefix. The unprefixed routes address the default product, | |
| 57 | + | which is `mnw`; `POST /promote/astra` from muscle memory promotes the wrong | |
| 58 | + | thing. Read state the same way: `GET /apps/pom/status.json`. | |
| 59 | + | ||
| 60 | + | astra goes first because it is the instance that matters least if pom is down for | |
| 61 | + | a moment: it watches, it does not serve anyone. pom watches its own deploy, so | |
| 62 | + | the restart takes one watcher down; the Hetzner instance keeps watching, and | |
| 63 | + | health is read from the peer over the mesh. Never both at once. | |
| 64 | + | ||
| 65 | + | ## One-time node setup | |
| 66 | + | ||
| 67 | + | A node has to be prepared before its first promote, and the failure if it is not | |
| 68 | + | is silent rather than loud: sandod rsyncs the bundle, swaps `current`, restarts | |
| 69 | + | a unit still pointing at `/usr/local/bin/pom`, and `node_health` passes on the | |
| 70 | + | old binary. Sando reports the version shipped and nothing shipped. | |
| 71 | + | ||
| 72 | + | `bootstrap-pom-node.sh` is that preparation, idempotent, run on the node as root: | |
| 73 | + | ||
| 74 | + | ``` | |
| 75 | + | sudo DEPLOY_USER=max \ | |
| 76 | + | SANDO_PUBKEY="$(ssh fw13 'sudo cat /srv/sando/.ssh/id_ed25519.pub')" \ | |
| 77 | + | ./bootstrap-pom-node.sh | |
| 78 | + | ``` | |
| 79 | + | ||
| 80 | + | `DEPLOY_USER` is the user in that node's `ssh_target`: `max` on astra, `root` on | |
| 81 | + | the Hetzner box. It creates `/opt/pom` owned by that user, grants it | |
| 82 | + | `systemctl reload-or-restart pom.service` through one scoped sudoers line, seeds | |
| 83 | + | the currently-installed binary as the first release so the unit never points at a | |
| 84 | + | dangling symlink, and moves `ExecStart` to `/opt/pom/current/pom` through a | |
| 85 | + | drop-in. The node comes out on the version it went in on, deployable. | |
| 86 | + | ||
| 87 | + | It also has to be reachable: sandod runs as the `sando` user on fw13, so that | |
| 88 | + | user's ssh (its key, and its `known_hosts`) is what has to reach each node, not | |
| 89 | + | max's. | |
| 38 | 90 | ||
| 39 | 91 | ## What a deploy does not touch | |
| 40 | 92 | ||
| 41 | 93 | **Config.** `pom-astra.toml` and `pom-hetzner.toml` differ per instance, and the | |
| 42 | 94 | live config on prod once carried a `[targets.mnw.tests]` block this repo did not | |
| 43 | 95 | have. `deploy.sh` would have silently deleted it. Config is a separate, | |
| 44 | - | deliberate act; the installer refuses to go near it. | |
| 96 | + | deliberate act; nothing in the pipeline goes near it. | |
| 45 | 97 | ||
| 46 | 98 | **The unit file.** Same reasoning. A hardened unit that has drifted from the | |
| 47 | - | repo is a question for a human, not something a binary deploy overwrites. | |
| 99 | + | repo is a question for a human, not something a binary deploy overwrites. That | |
| 100 | + | is why the bootstrap moves `ExecStart` in a drop-in: one reversible file beside | |
| 101 | + | the unit, rather than an edit of it. | |
| 48 | 102 | ||
| 49 | 103 | ## The database path is config, not environment | |
| 50 | 104 | ||
| @@ -66,23 +120,18 @@ | |||
| 66 | 120 | sudo -u pom pom --init --config /etc/pom/pom.toml status | |
| 67 | 121 | ``` | |
| 68 | 122 | ||
| 69 | - | ## One-time host setup | |
| 123 | + | ## The retired Bento install path | |
| 70 | 124 | ||
| 71 | - | Each host that runs pom needs the installer and its scoped sudo grant: | |
| 125 | + | `install-service.sh` and `bento-deploy.sudoers` are what Bento used when it | |
| 126 | + | installed pom itself: a single script-guarded sudo grant, staging under | |
| 127 | + | `/var/tmp/bento-deploy`, atomic `install(1)` to `/usr/local/bin/pom` with the | |
| 128 | + | previous binary kept as `.prev`. Nothing in the pipeline calls them any more. | |
| 72 | 129 | ||
| 73 | - | ``` | |
| 74 | - | sudo install -d /usr/local/lib/bento | |
| 75 | - | sudo install -m 0755 install-service.sh /usr/local/lib/bento/install-service.sh | |
| 76 | - | sudo install -m 0440 bento-deploy.sudoers /etc/sudoers.d/bento-deploy | |
| 77 | - | sudo visudo -cf /etc/sudoers.d/bento-deploy | |
| 78 | - | ``` | |
| 79 | - | ||
| 80 | - | Edit the sudoers file first so the user matches how Bento reaches that host: | |
| 81 | - | `max` on astra, `root` over Tailscale SSH on the Hetzner box. The grant covers | |
| 82 | - | one script and nothing else; the script bounds its own arguments (source under | |
| 83 | - | `/var/tmp/bento-deploy`, destination under `/usr/local/bin`, a bare `*.service` | |
| 84 | - | unit), so it is a script-guarded grant rather than a broad | |
| 85 | - | `install` + `systemctl` one. | |
| 130 | + | They are still here, and still installed on both nodes, because until a Sando | |
| 131 | + | promote has actually landed on a node, `/usr/local/bin/pom` is what that node is | |
| 132 | + | running and `.prev` is its rollback. Remove both files, and the grant and | |
| 133 | + | installer on each host, in a separate pass once each node has taken a release | |
| 134 | + | through Sando. | |
| 86 | 135 | ||
| 87 | 136 | ## The test runner on astra | |
| 88 | 137 | ||
| @@ -140,8 +189,21 @@ | |||
| 140 | 189 | ||
| 141 | 190 | ## Rollback | |
| 142 | 191 | ||
| 143 | - | The installer keeps the previous binary as `<install-path>.prev`. Rolling back | |
| 144 | - | is putting it back and restarting: | |
| 192 | + | Sando owns it, per tier: | |
| 193 | + | ||
| 194 | + | ``` | |
| 195 | + | curl -H "Authorization: Bearer $SANDO_API_TOKEN" -X POST \ | |
| 196 | + | "$SANDO_DAEMON/apps/pom/rollback/astra" | |
| 197 | + | ``` | |
| 198 | + | ||
| 199 | + | That is the whole gain from the move. Release dirs are named for their content | |
| 200 | + | digest rather than their version, so every release the node has kept is a | |
| 201 | + | rollback target and the horizon is as deep as the GC allows. What pom had before | |
| 202 | + | was a single `<install-path>.prev` file: one version back, overwritten by the | |
| 203 | + | next deploy. | |
| 204 | + | ||
| 205 | + | A node that has not taken a Sando release yet is still on the old path, and | |
| 206 | + | rolling it back is putting `.prev` back by hand: | |
| 145 | 207 | ||
| 146 | 208 | ``` | |
| 147 | 209 | sudo install -m 0755 /usr/local/bin/pom.prev /usr/local/bin/pom |
| @@ -1,0 +1,186 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # Idempotent bootstrap for a pom node under Sando. Run on the node as root. | |
| 3 | + | # | |
| 4 | + | # pom used to be installed by Bento straight into /usr/local/bin/pom. Under the | |
| 5 | + | # Sando/Bento boundary Bento builds and hands off, and Sando deploys the way it | |
| 6 | + | # deploys everything else: a content-addressed bundle under <release_root>, | |
| 7 | + | # an atomic `current` symlink, and `systemctl reload-or-restart` on the unit. | |
| 8 | + | # | |
| 9 | + | # /opt/pom/ | |
| 10 | + | # releases/<digest16>/{pom,MANIFEST} | |
| 11 | + | # current -> releases/<digest16> | |
| 12 | + | # | |
| 13 | + | # Three things have to be true on the node before the first promote, and none of | |
| 14 | + | # them were: | |
| 15 | + | # | |
| 16 | + | # 1. <release_root> exists and the user Sando SSHes in as can write it. | |
| 17 | + | # sandod runs `mkdir -p <release_root>/releases/<id>` as that user, and /opt | |
| 18 | + | # is root-owned. | |
| 19 | + | # 2. That user may restart the unit: sandod runs | |
| 20 | + | # `sudo /bin/systemctl reload-or-restart pom.service`. | |
| 21 | + | # 3. The unit's ExecStart points at <release_root>/current/<bin>. This is the | |
| 22 | + | # one that fails silently rather than loudly: a promote to a node whose unit | |
| 23 | + | # still runs /usr/local/bin/pom rsyncs the bundle, swaps the symlink, | |
| 24 | + | # restarts the service onto the OLD binary, and passes node_health. Sando | |
| 25 | + | # reports the version shipped and nothing shipped. | |
| 26 | + | # | |
| 27 | + | # The ExecStart move is a drop-in, not an edit of pom.service. A deploy never | |
| 28 | + | # rewrites the unit (deploy/README.md, "What a deploy does not touch"), and the | |
| 29 | + | # same restraint applies here: the override is one file, it is legible next to | |
| 30 | + | # the unit it changes, and removing it puts the node back. | |
| 31 | + | # | |
| 32 | + | # Ordering is why this is a script rather than a runbook. Repointing ExecStart | |
| 33 | + | # before a bundle exists leaves the unit pointing at a dangling symlink until the | |
| 34 | + | # first promote, and any restart in between (a reboot, Restart=on-failure) takes | |
| 35 | + | # pom down for however long that is. So the currently-installed binary is seeded | |
| 36 | + | # as the first release and `current` points at it BEFORE the unit moves. The node | |
| 37 | + | # comes out of this on the same version it went in on, deployable. | |
| 38 | + | # | |
| 39 | + | # Usage: | |
| 40 | + | # sudo DEPLOY_USER=max SANDO_PUBKEY="$(ssh fw13 'sudo cat /srv/sando/.ssh/id_ed25519.pub')" \ | |
| 41 | + | # ./bootstrap-pom-node.sh | |
| 42 | + | # | |
| 43 | + | # Required env: | |
| 44 | + | # DEPLOY_USER — the user in the node's `ssh_target` in pom-topology.toml | |
| 45 | + | # (`max` on astra, `root` on the Hetzner box). | |
| 46 | + | # | |
| 47 | + | # Optional env: | |
| 48 | + | # SANDO_PUBKEY — sando's public key on the Sando host, appended to | |
| 49 | + | # DEPLOY_USER's authorized_keys. Omit on a host reached by | |
| 50 | + | # Tailscale SSH, which does not consult authorized_keys. | |
| 51 | + | # RELEASE_ROOT — defaults to /opt/pom. Must match the node's `release_root`. | |
| 52 | + | # BIN_NAME — defaults to pom. Must match sando-pom.toml's `bin_names[0]`. | |
| 53 | + | # SERVICE_NAME — defaults to pom.service. | |
| 54 | + | # CONFIG_PATH — defaults to /etc/pom/pom.toml. The unit's --config argument. | |
| 55 | + | # HEALTH_URL — defaults to http://127.0.0.1:9100/api/health. | |
| 56 | + | # | |
| 57 | + | # What this does NOT do, and will not: | |
| 58 | + | # | |
| 59 | + | # Config. pom-astra.toml and pom-hetzner.toml differ per instance and the live | |
| 60 | + | # copies have carried blocks this repo did not. Config is a deliberate act. | |
| 61 | + | # | |
| 62 | + | # Remove the old Bento install path. /usr/local/bin/pom, its `.prev`, the | |
| 63 | + | # /usr/local/lib/bento installer and its sudoers grant are all left in place: | |
| 64 | + | # until a promote has actually landed on this node, that binary is what the | |
| 65 | + | # rollback is. Clearing them is a separate pass after the cutover, and the | |
| 66 | + | # script prints the paths so they are not forgotten. | |
| 67 | + | ||
| 68 | + | set -euo pipefail | |
| 69 | + | ||
| 70 | + | if [[ $EUID -ne 0 ]]; then | |
| 71 | + | echo "must run as root" >&2 | |
| 72 | + | exit 1 | |
| 73 | + | fi | |
| 74 | + | if [[ -z "${DEPLOY_USER:-}" ]]; then | |
| 75 | + | echo "DEPLOY_USER env var is required (the user in this node's ssh_target)" >&2 | |
| 76 | + | exit 1 | |
| 77 | + | fi | |
| 78 | + | if ! id "$DEPLOY_USER" &>/dev/null; then | |
| 79 | + | echo "no such user: $DEPLOY_USER" >&2 | |
| 80 | + | exit 1 | |
| 81 | + | fi | |
| 82 | + | ||
| 83 | + | RELEASE_ROOT="${RELEASE_ROOT:-/opt/pom}" | |
| 84 | + | BIN_NAME="${BIN_NAME:-pom}" | |
| 85 | + | SERVICE_NAME="${SERVICE_NAME:-pom.service}" | |
| 86 | + | CONFIG_PATH="${CONFIG_PATH:-/etc/pom/pom.toml}" | |
| 87 | + | HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:9100/api/health}" | |
| 88 | + | DROPIN_DIR="/etc/systemd/system/${SERVICE_NAME}.d" | |
| 89 | + | DROPIN="${DROPIN_DIR}/20-release-root.conf" | |
| 90 | + | ||
| 91 | + | log() { echo "[bootstrap-pom] $*"; } | |
| 92 | + | ||
| 93 | + | log "1/6 release root at $RELEASE_ROOT, writable by $DEPLOY_USER" | |
| 94 | + | install -d -o "$DEPLOY_USER" -g "$(id -gn "$DEPLOY_USER")" -m 0755 \ | |
| 95 | + | "$RELEASE_ROOT" "$RELEASE_ROOT/releases" | |
| 96 | + | ||
| 97 | + | log "2/6 seeding the running binary as a release, so the unit never points at nothing" | |
| 98 | + | if [[ -L "$RELEASE_ROOT/current" ]]; then | |
| 99 | + | log " current -> $(readlink "$RELEASE_ROOT/current") (already seeded, left alone)" | |
| 100 | + | else | |
| 101 | + | # Whatever the unit runs today, resolved from its own ExecStart rather than | |
| 102 | + | # assumed to be /usr/local/bin/pom. | |
| 103 | + | running="$(systemctl show -p ExecStart --value "$SERVICE_NAME" \ | |
| 104 | + | | sed -n 's/.*path=\([^ ;]*\).*/\1/p' | head -1)" | |
| 105 | + | if [[ -z "$running" || ! -x "$running" ]]; then | |
| 106 | + | echo "cannot find the binary $SERVICE_NAME currently runs (ExecStart=$running)." >&2 | |
| 107 | + | echo "Seed $RELEASE_ROOT/current by hand, or promote before repointing the unit." >&2 | |
| 108 | + | exit 4 | |
| 109 | + | fi | |
| 110 | + | ver="$("$running" --version 2>/dev/null | awk '{print $NF}')" | |
| 111 | + | seed="$RELEASE_ROOT/releases/preexisting-${ver:-unknown}" | |
| 112 | + | install -d -o "$DEPLOY_USER" -g "$(id -gn "$DEPLOY_USER")" -m 0755 "$seed" | |
| 113 | + | install -o "$DEPLOY_USER" -g "$(id -gn "$DEPLOY_USER")" -m 0755 "$running" "$seed/$BIN_NAME" | |
| 114 | + | # No MANIFEST: this bundle came from the node, not from an intake, and a | |
| 115 | + | # hand-written one would claim an artifact record that does not exist. | |
| 116 | + | # Sando skips verification for a bundle without one and says so in the log. | |
| 117 | + | ln -sfn "releases/preexisting-${ver:-unknown}" "$RELEASE_ROOT/current.new" | |
| 118 | + | mv -Tf "$RELEASE_ROOT/current.new" "$RELEASE_ROOT/current" | |
| 119 | + | chown -h "$DEPLOY_USER:$(id -gn "$DEPLOY_USER")" "$RELEASE_ROOT/current" | |
| 120 | + | log " seeded $running (${ver:-unknown}) -> $RELEASE_ROOT/current" | |
| 121 | + | fi | |
| 122 | + | ||
| 123 | + | log "3/6 sudo grant: $DEPLOY_USER may restart $SERVICE_NAME" | |
| 124 | + | if [[ "$DEPLOY_USER" == "root" ]]; then | |
| 125 | + | log " root needs no grant; skipping (sudo must still be installed)" | |
| 126 | + | command -v sudo >/dev/null || { echo "sudo is not installed; sandod shells out to it" >&2; exit 5; } | |
| 127 | + | else | |
| 128 | + | cat > "/etc/sudoers.d/${DEPLOY_USER}-pom" <<EOF | |
| 129 | + | $DEPLOY_USER ALL=(ALL) NOPASSWD: /bin/systemctl reload-or-restart $SERVICE_NAME, /bin/systemctl restart $SERVICE_NAME, /bin/systemctl status $SERVICE_NAME | |
| 130 | + | EOF | |
| 131 | + | chmod 0440 "/etc/sudoers.d/${DEPLOY_USER}-pom" | |
| 132 | + | visudo -c -f "/etc/sudoers.d/${DEPLOY_USER}-pom" >/dev/null | |
| 133 | + | fi | |
| 134 | + | ||
| 135 | + | log "4/6 sando's key for $DEPLOY_USER" | |
| 136 | + | if [[ -n "${SANDO_PUBKEY:-}" ]]; then | |
| 137 | + | home="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)" | |
| 138 | + | install -d -o "$DEPLOY_USER" -g "$(id -gn "$DEPLOY_USER")" -m 0700 "$home/.ssh" | |
| 139 | + | if ! grep -qF "$SANDO_PUBKEY" "$home/.ssh/authorized_keys" 2>/dev/null; then | |
| 140 | + | echo "$SANDO_PUBKEY" >> "$home/.ssh/authorized_keys" | |
| 141 | + | fi | |
| 142 | + | chown "$DEPLOY_USER:$(id -gn "$DEPLOY_USER")" "$home/.ssh/authorized_keys" | |
| 143 | + | chmod 0600 "$home/.ssh/authorized_keys" | |
| 144 | + | else | |
| 145 | + | log " SANDO_PUBKEY unset; assuming Tailscale SSH reaches this node" | |
| 146 | + | fi | |
| 147 | + | ||
| 148 | + | log "5/6 ExecStart -> $RELEASE_ROOT/current/$BIN_NAME (drop-in, unit untouched)" | |
| 149 | + | install -d -m 0755 "$DROPIN_DIR" | |
| 150 | + | cat > "$DROPIN" <<EOF | |
| 151 | + | # Written by pom/deploy/bootstrap-pom-node.sh. Sando deploys pom as a | |
| 152 | + | # content-addressed bundle and swaps <release_root>/current; the unit has to | |
| 153 | + | # follow the symlink or a promote restarts the service onto the old binary and | |
| 154 | + | # still reports success. | |
| 155 | + | # | |
| 156 | + | # A drop-in rather than an edit of pom.service, so the release path is one | |
| 157 | + | # reversible file and the hardened unit stays the unit this repo ships. | |
| 158 | + | [Service] | |
| 159 | + | ExecStart= | |
| 160 | + | ExecStart=$RELEASE_ROOT/current/$BIN_NAME serve --config $CONFIG_PATH | |
| 161 | + | EOF | |
| 162 | + | systemctl daemon-reload | |
| 163 | + | ||
| 164 | + | log "6/6 restart and prove the node is serving from the new path" | |
| 165 | + | systemctl reload-or-restart "$SERVICE_NAME" | |
| 166 | + | for _ in $(seq 1 30); do | |
| 167 | + | if curl -fsS --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then | |
| 168 | + | served=1 | |
| 169 | + | break | |
| 170 | + | fi | |
| 171 | + | sleep 1 | |
| 172 | + | done | |
| 173 | + | if [[ "${served:-0}" != 1 ]]; then | |
| 174 | + | echo "$SERVICE_NAME did not answer $HEALTH_URL within 30s." >&2 | |
| 175 | + | echo "Roll back by removing $DROPIN and running: systemctl daemon-reload && systemctl restart $SERVICE_NAME" >&2 | |
| 176 | + | exit 6 | |
| 177 | + | fi | |
| 178 | + | ||
| 179 | + | echo | |
| 180 | + | log "Done. $SERVICE_NAME is running $("$RELEASE_ROOT/current/$BIN_NAME" --version 2>/dev/null)" | |
| 181 | + | log "Sando can now promote to this node: POST /apps/pom/promote/<tier>" | |
| 182 | + | echo | |
| 183 | + | log "Left in place on purpose, until a promote has landed here:" | |
| 184 | + | log " /usr/local/bin/$BIN_NAME (+ .prev) the old Bento install path, and today's rollback" | |
| 185 | + | log " /usr/local/lib/bento/install-service.sh + /etc/sudoers.d/bento-deploy" | |
| 186 | + | log "Clear both in a separate pass once this node has taken a Sando release." |