johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
#!/bin/sh
# Moves the services named onto what docker-compose.yml beside this script
# now says, one at a time, with no gap in between:
#
# /etc/knotless/rollout.sh pages serve
#
# deploy.sh copies it to the host and runs it there. For each service that
# changed, a second container starts beside the running one, under the
# same names on the same networks; once its healthcheck passes, the old
# one is stopped, which finishes the requests it has (SIGTERM, then
# stop_grace_period), and removed. Until then both answer: the proxy and
# the services beside them reach a service by its name, which docker's DNS
# gives as every running container's address, and a dial to one not yet
# listening, or no longer, is refused and goes to the other.
#
# A new container that is not healthy within KNOTLESS_ROLLOUT_TIMEOUT
# seconds (180) is removed, the old one is left serving, and this fails.
#
# serve is rolled only if the new build keeps the index (`knotless-serve
# --check-index`): both have it open while they overlap, and a build that
# changes its tables drops and rebuilds it under the old one. Then serve
# is stopped and started instead, down until the new one listens. A new
# build that cannot answer the check is not started at all.
set -eu
cd "$(dirname "$0")"
TIMEOUT="${KNOTLESS_ROLLOUT_TIMEOUT:-180}"
# Where each line said is kept too, with its time, for a freeze to be
# lined up against a deploy: KNOTLESS_DEPLOY_LOG, beside deploy.sh's.
LOG="${KNOTLESS_DEPLOY_LOG:-/var/log/knotless-deploy.log}"
# Says "$*" with the time, here and in $LOG.
say() {
line="$(date -u +%Y-%m-%dT%H:%M:%SZ) → $*"
echo "$line"
echo "$line" >>"$LOG" 2>/dev/null || true
}
# The service's containers, running or not, oldest first.
containers() {
docker compose ps -aq "$1" | xargs -r docker inspect -f '{{.Created}} {{.Id}}' | sort | cut -d' ' -f2
}
# Whether container $1 is what the compose file now makes of service $2.
current() {
[ "$(docker inspect -f '{{.Image}}' "$1")" = "$(docker image inspect -f '{{.Id}}' "$(docker inspect -f '{{.Config.Image}}' "$1")")" ] &&
[ "$(docker inspect -f '{{index .Config.Labels "com.docker.compose.config-hash"}}' "$1")" = "$(docker compose config --hash "$2" | cut -d' ' -f2)" ] &&
[ "$(docker inspect -f '{{.State.Running}}' "$1")" = true ]
}
# Waits for container $1 to pass its healthcheck, or to be running if it
# has none; fails if it does not within TIMEOUT.
healthy() {
waited=0
WAITED=0
while [ "$waited" -lt "$TIMEOUT" ]; do
case "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$1")" in
healthy | running) WAITED=$waited && return 0 ;;
esac
sleep 1
waited=$((waited + 1))
done
return 1
}
roll() {
svc=$1
old=$(containers "$svc")
if [ -z "$old" ]; then
say "$svc: starting"
docker compose up -d --no-deps "$svc"
healthy "$(containers "$svc")" || { say "$svc never turned healthy" >&2; exit 1; }
return
fi
if [ "$(echo "$old" | wc -l)" -eq 1 ] && current "$old" "$svc"; then
say "$svc: unchanged"
return
fi
if [ "$svc" = serve ]; then
check=$(docker compose run --rm --no-deps -T -e RUST_LOG=off serve knotless-serve --check-index /data/index.db 2>&1 || true)
case "$check" in
*"keeps it"*) ;;
*"would rebuild it"*)
say "serve: $check"
say "serve: so the old one stops first: down until the new one listens"
docker compose up -d --no-deps --force-recreate serve
healthy "$(containers serve)" || { say "serve never turned healthy" >&2; exit 1; }
return
;;
*)
say "serve: the new build could not check the index; keeping the old one" >&2
echo "$check" >&2
exit 1
;;
esac
fi
say "$svc: starting a new one beside $(echo "$old" | cut -c1-12 | tr '\n' ' ')"
docker compose up -d --no-deps --no-recreate --scale "$svc=$(($(echo "$old" | wc -l) + 1))" "$svc"
new=$(containers "$svc" | grep -vxF "$old")
say "$svc: started $(echo "$new" | cut -c1-12)"
if ! healthy "$new"; then
say "$svc: the new one never turned healthy; keeping the old one" >&2
docker logs --tail 50 "$new" >&2 || true
docker rm -f "$new" >/dev/null
exit 1
fi
say "$svc: healthy after ${WAITED}s; stopping the old one"
# shellcheck disable=SC2086
docker stop $old >/dev/null
# shellcheck disable=SC2086
docker rm $old >/dev/null
say "$svc: the old one stopped"
}
for svc in "$@"; do
roll "$svc"
done