refactor Adopt single-account deploy model

Run the whole pipeline as the git user, which owns the bare repo and the entire /srv/hito tree. The hook builds and deploys directly with no privilege hop; the only sudo right is 'systemctl restart hito'. Initialize a git-owned opam root and run the service as User=git. Remove the deploy-delegation program and its sudoers rule.

Commit
852b4740244882c1e4ccaed750add319677a9446
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
deploy/post-receive
index bee34e2d..f8d902f6 100755..100755
@@ -1,12 +1,15 @@
1 1 #!/usr/bin/env bash
2 2 #
3 Removed: # hito post-receive hook. Runs on the VPS after every push to the bare repo.
3 Added: # hito post-receive hook. Runs as the git user after every push to the bare
4 Added: # repo. The git user owns the bare repo and the whole /srv/hito tree, so this
5 Added: # hook builds and publishes directly, with no privilege hop. The only
6 Added: # privileged action is restarting the service, through one scoped sudoers rule.
4 7 #
5 8 # For a push to master: check out the pushed revision into a fresh timestamped
6 9 # release, build and test it against the shared opam switch, flip
7 10 # /srv/hito/current, restart the service, and confirm it answers HTTP. Any
8 Removed: # build or test failure leaves the live release untouched and fails the push.
9 Removed: # A failed health check rolls current back to the previous release and restarts.
11 Added: # build or test failure leaves the live release untouched. A failed health
12 Added: # check rolls current back to the previous release and restarts.
10 13 #
11 14 # Style: Google Shell Style Guide.
12 15
@@ -22,7 +25,7 @@
22 25 readonly HEALTH_TRIES=10
23 26 readonly HEALTH_DELAY=1
24 27
25 Removed: export OPAMROOT="/home/mpeter/.opam"
28 Added: export OPAMROOT="/srv/git/.opam"
26 29 export OPAMSWITCH="${SHARED}"
27 30
28 31 err() {
deploy/setup.sh
index 6ceb29b4..d76f93c4 100755..100755
@@ -2,27 +2,32 @@
2 2 #
3 3 # hito deployment setup. One root script, three subcommands.
4 4 #
5 Removed: # sudo bash setup.sh install One-time host setup: shared switch, hook,
6 Removed: # scoped sudoers, and the systemd unit pointed at
7 Removed: # current/. Idempotent. Creates no release; the
8 Removed: # first push (or 'deploy') does.
5 Added: # sudo bash setup.sh install One-time host setup: take git ownership of
6 Added: # /srv/hito, build the shared switch as git,
7 Added: # install the hook, the scoped sudoers rule, and
8 Added: # the systemd unit (User=git) pointed at current/.
9 Added: # Idempotent. Creates no release; the first push
10 Added: # (or 'deploy') does.
9 11 # sudo bash setup.sh deploy Build master as a fresh release on the shared
10 12 # switch and flip current. Health-checks and
11 13 # rolls back on failure. Use to go live without a
12 14 # push, or right after 'install'.
13 15 # sudo bash setup.sh reset Wipe /srv/hito to the clean layout, preserving
14 Removed: # the database and the shared switch, then
15 Removed: # 'deploy'. Destructive: all else is removed.
16 Added: # only the database, then rebuild the switch as
17 Added: # git and 'deploy'. Destructive: all else,
18 Added: # including the old switch, is removed.
16 19 #
17 Removed: # The post-receive hook beside this script is the per-push equivalent of
18 Removed: # 'deploy'; 'install' copies it into the bare repo.
20 Added: # Single-account model: the git user owns the bare repo and the whole
21 Added: # /srv/hito tree, runs the service, and runs the deploy. The only privileged
22 Added: # action is 'systemctl restart hito', through one scoped sudoers rule. The
23 Added: # post-receive hook beside this script is the per-push equivalent of 'deploy'.
19 24 #
20 25 # Style: Google Shell Style Guide.
21 26
22 27 set -euo pipefail
23 28
24 Removed: readonly DEPLOY_USER="mpeter"
25 Removed: readonly DEPLOY_GROUP="git"
29 Added: readonly SVC_USER="git"
30 Added: readonly SVC_GROUP="git"
26 31 readonly SRV="/srv/hito"
27 32 readonly BARE="/srv/git/hito.git"
28 33 readonly UNIT="/etc/systemd/system/hito.service"
@@ -33,7 +38,7 @@
33 38 readonly SWITCH="${SHARED}/_opam"
34 39 readonly OCAML_COMPILER="ocaml-base-compiler.5.5.0"
35 40 readonly HEALTH_URL="http://127.0.0.1:8080/login"
36 Removed: readonly OPAMROOT_DIR="/home/${DEPLOY_USER}/.opam"
41 Added: readonly OPAMROOT_DIR="/srv/git/.opam"
37 42 HERE="$(cd "$(dirname "$0")" && pwd)"
38 43 readonly HERE
39 44
@@ -44,9 +49,10 @@
44 49 fi
45 50 }
46 51
47 Removed: # Run a command as the deploy user with the opam root exported.
48 Removed: as_deploy() {
49 Removed: sudo -u "${DEPLOY_USER}" -H env "OPAMROOT=${OPAMROOT_DIR}" "$@"
52 Added: # Run a command as the service user with the git-owned opam root exported.
53 Added: # git's login shell is git-shell, so invoke bash explicitly rather than -H login.
54 Added: as_svc() {
55 Added: sudo -u "${SVC_USER}" env "OPAMROOT=${OPAMROOT_DIR}" "$@"
50 56 }
51 57
52 58 ensure_switch() {
@@ -54,36 +60,50 @@
54 60 echo " shared switch present"
55 61 return 0
56 62 fi
57 Removed: echo " creating shared switch (${OCAML_COMPILER})"
58 Removed: as_deploy opam switch create "${SHARED}" "${OCAML_COMPILER}" --no-install -y
63 Added: # A fresh, git-owned opam root must be initialized before a switch can be
64 Added: # created. --bare creates the root and default repository without a switch.
65 Added: # --disable-sandboxing: this host has no bubblewrap, so opam's sandbox would
66 Added: # fail; the build already runs as an unprivileged, filesystem-scoped user.
67 Added: if [[ ! -d "${OPAMROOT_DIR}/repo/default" ]]; then
68 Added: echo " initializing git-owned opam root at ${OPAMROOT_DIR}"
69 Added: as_svc opam init --bare --disable-sandboxing --no-setup -y
70 Added: fi
71 Added: echo " creating shared switch (${OCAML_COMPILER}) as ${SVC_USER}"
72 Added: as_svc opam switch create "${SHARED}" "${OCAML_COMPILER}" --no-install -y
59 73 }
60 74
61 75 install_hook() {
62 Removed: install -o "${DEPLOY_USER}" -g "${DEPLOY_GROUP}" -m 0755 \
76 Added: install -o "${SVC_USER}" -g "${SVC_GROUP}" -m 0755 \
63 77 "${HERE}/post-receive" "${BARE}/hooks/post-receive"
64 78 echo " installed ${BARE}/hooks/post-receive"
65 79 }
66 80
67 81 install_sudoers() {
82 Added: # The one privileged action in the whole pipeline: git may restart the service.
68 83 cat >"${SUDOERS}" <<SUDO
69 Removed: ${DEPLOY_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart hito
84 Added: ${SVC_USER} ALL=(root) NOPASSWD: /usr/bin/systemctl restart hito
70 85 SUDO
71 86 chmod 0440 "${SUDOERS}"
72 87 visudo -c -f "${SUDOERS}" >/dev/null && echo " sudoers valid"
88 Added: # Remove obsolete rules from the earlier multi-account design, if present.
89 Added: rm -f /etc/sudoers.d/hito-git-deploy
90 Added: rm -f /usr/local/libexec/hito-deploy
73 91 }
74 92
75 Removed: point_unit_at_current() {
93 Added: point_unit() {
76 94 cp -a "${UNIT}" "${UNIT}.bak.$(date +%s)"
77 95 sed -i \
78 96 -e 's#^ExecStart=.*#ExecStart=/srv/hito/current/_build/default/bin/main.exe#' \
79 97 -e 's#^WorkingDirectory=.*#WorkingDirectory=/srv/hito/current#' \
80 98 -e 's#^Environment=HITO_DB=.*#Environment=HITO_DB=sqlite3:/srv/hito/shared/hito.sqlite#' \
99 Added: -e "s#^User=.*#User=${SVC_USER}#" \
100 Added: -e "s#^Group=.*#Group=${SVC_GROUP}#" \
81 101 "${UNIT}"
82 102 if ! grep -q '^EnvironmentFile=/srv/hito/shared/deploy.env$' "${UNIT}"; then
83 103 sed -i '/^Environment=HITO_DB=/a EnvironmentFile=/srv/hito/shared/deploy.env' "${UNIT}"
84 104 fi
85 105 systemctl daemon-reload
86 Removed: echo " unit points at current/ (backup kept alongside)"
106 Added: echo " unit runs as ${SVC_USER} and points at current/ (backup kept alongside)"
87 107 }
88 108
89 109 health_ok() {
@@ -100,6 +120,7 @@
100 120 # Build master in a staging dir OUTSIDE /srv/hito, so dune's project-root search
101 121 # never sees current or a sibling release, then publish it, flip current,
102 122 # restart, health-check, and roll back on failure. Records deploy metadata.
123 Added: # Runs entirely as the service user; root only orchestrates.
103 124 build_and_flip() {
104 125 local start="${SECONDS}"
105 126 local ts rel stage sha subject build_secs prev
@@ -107,13 +128,12 @@
107 128 rel="${RELEASES}/${ts}"
108 129 prev="$(readlink -f "${SRV}/current" 2>/dev/null || true)"
109 130 sha="$(git --git-dir="${BARE}" rev-parse "refs/heads/master")"
110 Removed: stage="$(as_deploy mktemp -d /tmp/hito-stage.XXXXXX)"
131 Added: stage="$(as_svc mktemp -d /tmp/hito-stage.XXXXXX)"
111 132
112 Removed: echo " building master ${sha:0:12}"
113 Removed: git --git-dir="${BARE}" archive "${sha}" | tar -x -C "${stage}"
114 Removed: chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${stage}"
115 Removed: as_deploy bash -lc "
133 Added: echo " building master ${sha:0:12} as ${SVC_USER}"
134 Added: as_svc bash -c "
116 135 set -euo pipefail
136 Added: git --git-dir='${BARE}' archive '${sha}' | tar -x -C '${stage}'
117 137 cd '${stage}'
118 138 eval \"\$(opam env --switch='${SHARED}' --set-switch)\"
119 139 opam install . --deps-only --with-test -y 2>&1 | tee deploy.log
@@ -124,6 +144,7 @@
124 144 build_secs=$((SECONDS - start))
125 145
126 146 mkdir -p "${RELEASES}"
147 Added: chown "${SVC_USER}:${SVC_GROUP}" "${RELEASES}"
127 148 mv "${stage}" "${rel}"
128 149 ln -sfn "${DB}" "${rel}/hito.sqlite"
129 150
@@ -144,11 +165,11 @@
144 165 >>"${SHARED}/deploy-history.tsv"
145 166 printf 'HITO_VERSION=%s\n' "${sha:0:12}" >"${SHARED}/deploy.env"
146 167
147 Removed: chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${rel}"
148 Removed: chown "${DEPLOY_USER}:${DEPLOY_GROUP}" "${SHARED}/deploy-history.tsv"
168 Added: chown -R "${SVC_USER}:${SVC_GROUP}" "${rel}"
169 Added: chown "${SVC_USER}:${SVC_GROUP}" "${SHARED}/deploy-history.tsv" "${SHARED}/deploy.env"
149 170 ln -sfn "${rel}" "${SRV}/current.tmp"
150 171 mv -T "${SRV}/current.tmp" "${SRV}/current"
151 Removed: chown -h "${DEPLOY_USER}:${DEPLOY_GROUP}" "${SRV}/current"
172 Added: chown -h "${SVC_USER}:${SVC_GROUP}" "${SRV}/current"
152 173 echo " current -> ${rel} (built in ${build_secs}s)"
153 174
154 175 systemctl restart hito
@@ -178,11 +199,15 @@
178 199 mv "${SRV}/hito.sqlite" "${DB}"
179 200 echo " adopted existing hito.sqlite -> ${DB}"
180 201 fi
181 Removed: chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${RELEASES}" "${SHARED}"
202 Added: echo "== take ${SVC_USER} ownership of ${SRV} =="
203 Added: chown -R "${SVC_USER}:${SVC_GROUP}" "${SRV}"
204 Added: echo "== git-owned opam root =="
205 Added: mkdir -p "${OPAMROOT_DIR}"
206 Added: chown "${SVC_USER}:${SVC_GROUP}" "${OPAMROOT_DIR}"
182 207 echo "== shared opam switch =="; ensure_switch
183 208 echo "== post-receive hook =="; install_hook
184 209 echo "== scoped sudoers =="; install_sudoers
185 Removed: echo "== systemd unit =="; point_unit_at_current
210 Added: echo "== systemd unit =="; point_unit
186 211 echo
187 212 echo "Done. Run 'sudo bash setup.sh deploy' to build the first release,"
188 213 echo "or push master to trigger the hook."
@@ -203,23 +228,21 @@
203 228 fi
204 229 local stash
205 230 echo "== stop service =="; systemctl stop hito || true
206 Removed: echo "== stash DB and switch =="
231 Added: echo "== stash the database =="
207 232 stash="$(mktemp -d /root/hito-reset-stash.XXXXXX)"
208 233 cp -a "${DB}" "${stash}/hito.sqlite" # safety copy, never removed by us
209 234 mv "${DB}" "${stash}/hito.sqlite.live"
210 Removed: if [[ -d "${SWITCH}" ]]; then
211 Removed: mv "${SWITCH}" "${stash}/_opam"
212 Removed: fi
213 235 echo " DB backup at ${stash}/hito.sqlite"
214 236 echo "== wipe /srv/hito contents =="
237 Added: # The old switch is intentionally not preserved: it was built under a
238 Added: # different opam root and owner. reset rebuilds it fresh as ${SVC_USER}.
215 239 find "${SRV}" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
216 Removed: echo "== rebuild skeleton =="
240 Added: echo "== rebuild skeleton, ${SVC_USER}-owned =="
217 241 mkdir -p "${SHARED}" "${RELEASES}"
218 242 mv "${stash}/hito.sqlite.live" "${DB}"
219 Removed: if [[ -d "${stash}/_opam" ]]; then
220 Removed: mv "${stash}/_opam" "${SWITCH}"
221 Removed: fi
222 Removed: chown -R "${DEPLOY_USER}:${DEPLOY_GROUP}" "${SHARED}" "${RELEASES}"
243 Added: chown -R "${SVC_USER}:${SVC_GROUP}" "${SRV}"
244 Added: mkdir -p "${OPAMROOT_DIR}"
245 Added: chown "${SVC_USER}:${SVC_GROUP}" "${OPAMROOT_DIR}"
223 246 ensure_switch
224 247 echo "== deploy master =="; build_and_flip
225 248 echo
@@ -228,6 +251,10 @@
228 251
229 252 main() {
230 253 require_root
254 Added: # Run from a directory that no subcommand deletes. reset wipes /srv/hito, so a
255 Added: # caller sitting in /srv/hito/... would otherwise leave child processes with a
256 Added: # nonexistent working directory.
257 Added: cd /
231 258 case "${1:-}" in
232 259 install) cmd_install ;;
233 260 deploy) cmd_deploy ;;