Files
menu/scripts/copy-site-data.sh
tanshu 537a4f1e46 Match the deploy system to hops and greatbear
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.
2026-10-16 11:10:59 +05:30

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"