menu-admin was the odd one out: it built from the working directory with a date tag and a multi-arch manifest, deployed with `-e tag=`, and kept the Docker-era container name. The other two apps build from `git archive HEAD` and tag with the commit, so what runs on the host is exactly what git knows about; build only for the architecture emily actually runs; and use one slug for the container, its network and its data directory. The build is now their three lines: `podman build --platform linux/arm64` from `git archive HEAD`, tagged `:<commit>` and `:latest`, pushed with `podman push` rather than a manifest. `deploy.sh` pushes the commit first, then refuses a dirty tracked tree or a HEAD that is not origin/main — which is what makes the commit tag trustworthy — takes the same `--force` and `--no-push`, and ends in the same `-u tanshu ... -e image_tag=<commit>`. Its own lint/typecheck/test gate stays, but behind `--checks`, so a plain deploy behaves as theirs does. The registry credential moves into vars/default.yml and the role logs in, pulls and logs out around it, as their roles do. The image is only pulled at deploy time, so the unit keeps podman's default `Pull=missing` and a reboot never needs the registry. This is the same plaintext credential the other two repos carry. One real bug goes with it: the healthcheck gate ran before the handler that restarts a changed unit, so a run that only changed the unit checked the stale container. `meta: flush_handlers` before the gate is the fix the sibling repos already made. The container, network and data directory are all menu-<site> now. The dotted Docker-era name and the NetworkAlias it needed are gone, so `podman logs menu-<site>` and `systemctl --user status menu-<site>` are the same two commands across all three apps. The Dockerfile keeps its name for the same reason: podman reads it as readily as a Containerfile, and the siblings use that name too.
142 lines
4.9 KiB
Bash
Executable File
142 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Copies one Site's data from the Docker deployment on monoco to the rootless
|
|
# Podman deployment on emily.
|
|
#
|
|
# scripts/copy-site-data.sh hopsngrains
|
|
# scripts/copy-site-data.sh hopsngrains --live # copy while the app keeps serving
|
|
#
|
|
# Run it from a machine that can ssh to both hosts (the ssh aliases monoco and
|
|
# emily, overridable with MONOCO= and EMILY=). This is the one-off part of the
|
|
# move: the playbook deploys the container, this hands it the content.
|
|
#
|
|
# By default it stops the old container first, so the copy is of quiesced data,
|
|
# and starts it again afterwards — monoco keeps serving until DNS moves. Use
|
|
# --live to skip the stop; the app writes its manifests atomically and its images
|
|
# are immutable once written, so a live copy is usually clean, but an upload
|
|
# landing mid-copy is missed. Nothing on monoco is removed either way.
|
|
#
|
|
# Two details are load-bearing:
|
|
#
|
|
# * monoco cannot ssh to emily, so this machine is the relay — the tar streams
|
|
# through it and nothing large is written locally.
|
|
# * sudo on monoco, because /var/lib/menu.<domain> belongs to uid 1000 (the
|
|
# ubuntu account) while the ssh user there is 1001. The extraction runs as
|
|
# tanshu on emily, which is the uid keep-id maps the app onto, so the files
|
|
# land owned by the right user rather than by root or a subuid.
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
usage: copy-site-data.sh <site> [--live]
|
|
|
|
<site> inventory name from ansible/hosts: hopsngrains, greatbear, hngmohali
|
|
--live copy without stopping the old container (not quiesced)
|
|
|
|
Environment overrides:
|
|
MONOCO=host ssh target holding the Docker deployment (default: monoco)
|
|
EMILY=host ssh target holding the rootless Podman deployment (default: emily)
|
|
SRC_ROOT=dir absolute data root on monoco (default: /var/lib)
|
|
DEST_ROOT=dir absolute data root on emily (default: <emily home>/podman-data,
|
|
resolved over ssh — ~ and $HOME are not expanded for you)
|
|
USAGE
|
|
}
|
|
|
|
SITE="${1:-}"
|
|
if [[ -z "$SITE" || "$SITE" == "-h" || "$SITE" == "--help" ]]; then
|
|
usage
|
|
[[ -n "$SITE" ]] || exit 1
|
|
exit 0
|
|
fi
|
|
shift
|
|
|
|
LIVE=0
|
|
for argument in "$@"; do
|
|
case "$argument" in
|
|
--live) LIVE=1 ;;
|
|
*) echo "unknown argument: $argument" >&2; usage >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
|
|
# The repo is the source of truth for which Sites exist and what they answer.
|
|
VARS="ansible/vars/$SITE.yml"
|
|
if ! grep -qE "^$SITE[[:space:]]" ansible/hosts || [[ ! -f "$VARS" ]]; then
|
|
echo "'$SITE' is not a Site in ansible/hosts" >&2
|
|
exit 1
|
|
fi
|
|
TITLE="$(sed -n 's/^title:[[:space:]]*//p' "$VARS" | head -n 1)"
|
|
HOST="menu.$TITLE"
|
|
CONTAINER="menu-app.$HOST"
|
|
|
|
MONOCO="${MONOCO:-monoco}"
|
|
EMILY="${EMILY:-emily}"
|
|
SRC_ROOT="${SRC_ROOT:-/var/lib}"
|
|
|
|
for tool in ssh tar; do
|
|
command -v "$tool" >/dev/null 2>&1 || { echo "$tool is required" >&2; exit 1; }
|
|
done
|
|
|
|
# Resolved rather than left as a literal ~ or $HOME, so every path below is
|
|
# absolute and can be quoted safely on the remote side.
|
|
if [[ -z "${DEST_ROOT:-}" ]]; then
|
|
DEST_ROOT="$(ssh -n "$EMILY" 'printf %s "$HOME"')/podman-data"
|
|
fi
|
|
|
|
SRC="$SRC_ROOT/$HOST"
|
|
DEST="$DEST_ROOT/menu-$SITE"
|
|
|
|
echo "==> $SITE: $MONOCO:$SRC -> $EMILY:$DEST"
|
|
|
|
# Start the old container again however this exits, so a failed copy never
|
|
# leaves the live Site down.
|
|
restart_needed=0
|
|
restart_old() {
|
|
if [[ "$restart_needed" -eq 1 ]]; then
|
|
echo "==> starting $CONTAINER again on $MONOCO"
|
|
ssh -n "$MONOCO" docker start "$CONTAINER" >/dev/null || true
|
|
restart_needed=0
|
|
fi
|
|
}
|
|
trap restart_old EXIT
|
|
|
|
# -n on every monoco call: ssh would otherwise eat this script's stdin, and the
|
|
# copy below is a pipeline whose input matters.
|
|
if [[ "$LIVE" -eq 0 ]]; then
|
|
echo "==> stopping $CONTAINER on $MONOCO"
|
|
ssh -n "$MONOCO" docker stop "$CONTAINER" >/dev/null
|
|
restart_needed=1
|
|
fi
|
|
|
|
echo "==> copying"
|
|
ssh -n "$MONOCO" "sudo -n tar -C '$SRC' -cf - ." \
|
|
| ssh "$EMILY" "mkdir -p '$DEST' && tar -C '$DEST' -xf -"
|
|
|
|
restart_old
|
|
|
|
echo "==> verifying"
|
|
src_sum="$(ssh -n "$MONOCO" "sudo -n sh -c 'cd $SRC && find . -type f -exec sha256sum {} +' | sort | sha256sum" | awk '{print $1}')"
|
|
dst_sum="$(ssh -n "$EMILY" "cd '$DEST' && find . -type f -exec sha256sum {} + | sort | sha256sum" | awk '{print $1}')"
|
|
|
|
if [[ -z "$src_sum" || "$src_sum" != "$dst_sum" ]]; then
|
|
echo "the copy does not match the source: $src_sum != $dst_sum" >&2
|
|
exit 1
|
|
fi
|
|
|
|
user="$(id -un)"
|
|
strays="$(ssh -n "$EMILY" "find '$DEST' ! -user '$user'" | head -n 5)"
|
|
if [[ -n "$strays" ]]; then
|
|
echo "the copy holds files $user does not own, so keep-id will not see them:" >&2
|
|
echo "$strays" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo
|
|
echo "ok ${dst_sum:0:12} $(ssh -n "$EMILY" "du -sb '$DEST'" | awk '{print $1}') bytes, all owned by $user"
|
|
echo
|
|
echo "next:"
|
|
echo " ./deploy.sh # builds HEAD and deploys every Site"
|
|
echo " SITE=$SITE ./deploy.sh # or just this one"
|
|
echo " then check the Site on a Host: override before moving DNS to $EMILY"
|