Skip to main content

max / makenotwork

4.3 KB · 106 lines History Blame Raw
1 #!/usr/bin/env bash
2 # Install a staged service binary and restart its unit. Runs as ROOT, invoked by
3 # Bento's deploy step over the host's executor as:
4 #
5 # sudo /usr/local/lib/bento/install-service.sh <src> <dst> <service>
6 #
7 # where <src> is the binary Bento staged under /var/tmp/bento-deploy/<app>/,
8 # <dst> is the unit's ExecStart path (/usr/local/bin/pom), and <service> is the
9 # systemd unit to restart (pom.service).
10 #
11 # This wrapper exists so the deploy user's sudo grant is ONE auditable script
12 # rather than a broad install/systemctl grant on a box running production. Same
13 # shape, and the same reasoning, as sando/deploy/install-companion.sh.
14 #
15 # Install (one-time per host, as root):
16 # sudo install -d /usr/local/lib/bento
17 # sudo install -m 0755 install-service.sh /usr/local/lib/bento/install-service.sh
18 # # then add the scoped sudoers line -- see bento-deploy.sudoers
19 #
20 # What this deliberately does NOT touch:
21 #
22 # Config. pom-astra.toml and pom-hetzner.toml differ per instance, and the
23 # live config on prod once carried a [targets.mnw.tests] block the repo did
24 # not have. The old deploy.sh copied config over the top with no diff and no
25 # backup, which is how that block would have been silently deleted. A deploy
26 # ships the binary; config is a separate, deliberate act.
27 #
28 # The unit file. Same reasoning: a hardened unit that drifted from the repo is
29 # a question for a human, not something a binary deploy overwrites.
30 #
31 # The install is atomic (install(1) writes a temp then renames), so a running
32 # service never execs a half-written file. The previous binary is kept next to
33 # the new one as <dst>.prev, which is the whole rollback: put it back and
34 # restart. The unit is restarted only after a successful install, so a failed
35 # install leaves the service on the binary it already had.
36 set -euo pipefail
37
38 if [[ $# -ne 3 ]]; then
39 echo "usage: install-service.sh <src-binary> <dst-path> <service>" >&2
40 exit 2
41 fi
42
43 STAGING_ROOT=/var/tmp/bento-deploy
44
45 SRC="$1"
46 DST="$2"
47 SERVICE="$3"
48
49 # Guard rails. These bound what a caller -- already behind the single sudoers
50 # grant -- can install and restart. Being the ONLY control on a NOPASSWD grant,
51 # they have to actually hold, so normalise BEFORE testing any prefix: a
52 # glob-only test against the raw argument is something `..` walks straight out
53 # of. `/var/tmp/bento-deploy/../../../etc/shadow` matches a naive src pattern,
54 # and `/usr/local/bin/../../../etc/systemd/system/x` a naive dst one -- i.e.
55 # `install -m 0755` as root to anywhere on the filesystem. `realpath -m`
56 # resolves `..` and symlinks without requiring the path to exist (the dst does
57 # not on a first install). The roots are resolved too, so the comparison still
58 # holds if either is a symlink.
59 SRC="$(realpath -m -- "$SRC")"
60 DST="$(realpath -m -- "$DST")"
61 STAGING_ROOT="$(realpath -m -- "$STAGING_ROOT")"
62 BIN_ROOT="$(realpath -m -- /usr/local/bin)"
63
64 case "$SRC" in
65 "$STAGING_ROOT"/*) : ;;
66 *) echo "install-service: refusing src outside $STAGING_ROOT: $SRC" >&2; exit 3 ;;
67 esac
68 case "$DST" in
69 "$BIN_ROOT"/*) : ;;
70 *) echo "install-service: refusing dst outside $BIN_ROOT: $DST" >&2; exit 3 ;;
71 esac
72 case "$SERVICE" in
73 *[/[:space:]]*|"") echo "install-service: bad service name: $SERVICE" >&2; exit 3 ;;
74 *.service) : ;;
75 *) echo "install-service: service must end in .service: $SERVICE" >&2; exit 3 ;;
76 esac
77
78 if [[ ! -f "$SRC" ]]; then
79 echo "install-service: no binary at $SRC" >&2
80 exit 4
81 fi
82
83 # Refuse a binary that cannot run here. Bento checks this too, before it stages
84 # anything, but that check reads the build host's objdump output; this one is
85 # the target actually trying to load it. A unit restarted onto a binary it
86 # cannot exec is down until someone notices.
87 if ! "$SRC" --version >/dev/null 2>&1; then
88 echo "install-service: staged binary at $SRC will not run on this host" >&2
89 "$SRC" --version || true
90 exit 5
91 fi
92
93 if [[ -f "$DST" ]]; then
94 echo "install-service: keeping the current binary as $DST.prev"
95 cp -p -- "$DST" "$DST.prev"
96 fi
97
98 # Installs the normalised paths, not the raw arguments, so there is no gap
99 # between what was checked and what is written.
100 echo "install-service: installing $SRC -> $DST"
101 install -m 0755 -- "$SRC" "$DST"
102
103 echo "install-service: restarting $SERVICE"
104 systemctl restart "$SERVICE"
105 echo "install-service: done ($SERVICE live on $(basename -- "$DST"))"
106