[OCaml] High Intensity Training Online
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.
Changed files
deploy/post-receive
@@ -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
@@ -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 ;;