Run the Sites as rootless Podman Quadlets, and build with podman
The fleet on emily is rootless Podman with Quadlet units under ~/.config/containers/systemd and state in ~/podman-data, all behind one shared Caddy unit. This brings the menus in line instead of leaving them the last Docker deployment. The build moves with them: podman reads a Containerfile as readily as a Dockerfile, so the name now says which toolchain is real. The app is one .container and one .network file. Two details carry it. UserNS=keep-id maps the image's own node account onto the user that owns the units, so data/ and site.env stay owned by tanshu: plain tar backups, no subuids. provision-site-env.sh writes site.env with the same mapping rather than --user, which under rootless Podman would have produced a subuid-owned file the app could not read. And the Site registers its own network with Caddy by writing a drop-in beside caddy.container, so Caddy never lists its upstreams and never changes when a Site is added. The unit keeps the Docker-era container name for the scripts and muscle memory that look it up, while the Caddyfile proxies to a dotless NetworkAlias, because a container name with dots in it is a liability in DNS. The unit keeps podman's Pull=missing, so a restart or a reboot never depends on the registry being reachable, while the play pulls explicitly at deploy time. That is also where a missing `podman login` for the private registry now fails, with the instruction, rather than as a unit that will not start. copy-site-data.sh moves a Site's data across, and is not a plain rsync: /var/lib/menu.* on monoco belongs to uid 1000 while the ssh user there is 1001, so the read needs sudo; monoco cannot ssh to emily, so the workstation relays the tar; and the extraction runs as tanshu on emily, which is what makes the files land owned by the user keep-id maps the app onto. It proves the copy by comparing a SHA-256 of the whole tree against the source and checking every file's owner. Not yet done: the copies themselves and the DNS move, which need a human.
This commit is contained in:
1 parent
c412181159
commit
e68f8ab00d
23 files changed
+876
-389
No files matched your search
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/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 " cd ansible && ansible-playbook --inventory=hosts playbook.yml --limit $SITE -e tag=<tag>"
|
||||
echo " then check the Site on a Host: override before moving DNS to $EMILY"
|
||||
Reference in new issue
Block a user