#!/bin/bash
# Install a freshly built binary as the running service, and undo it if the service
# does not come back healthy.
#
# Started by root, not called by CI. CI drops a binary and a request file in a
# staging directory; a systemd.path unit notices the request and runs this. Nothing
# the CI user writes becomes an argument, and CI never gains a privilege — it can
# only ask.
#
# The alternative was a sudoers rule, which does not work here anyway: the build
# instance runs with NoNewPrivileges=yes, and sudo is setuid. Unhardening the one
# unit that executes arbitrary pushed code to let it call sudo is the wrong trade.
#
# Understand what this still grants: whoever can push to the source repository
# decides which binary runs as this host's service. That is what continuous
# deployment is, and no amount of care in this script changes it. It is acceptable
# because push access to that repository is the same trust as root on this host. If
# that ever stops being true, this arrangement has to go.
#
# Install:
#   sudo install -o root -g root -m 0755 zuka-deploy /usr/local/bin/zuka-deploy
#   sudo install -o root -g root -m 0644 zuka-deploy.path zuka-deploy.service \
#       /etc/systemd/system/
#   sudo systemctl enable --now zuka-deploy.path

set -euo pipefail

readonly SERVICE="zuka"
readonly JOBS="zuka-jobs"
readonly STAGING="/var/lib/zuka-build/staging"
readonly PRIVATE="/var/lib/zuka-deploy"
readonly INSTALLED="/usr/local/bin/zuka"
readonly HEALTH="http://127.0.0.1:8790/healthz"
readonly HEALTH_TIMEOUT=90

log() { printf '[deploy] %s\n' "$*" >&2; }

# The result file is how CI learns whether this worked, so it must be written on
# every exit path — including the ones nobody thought about. Without the trap, a
# script that dies early leaves CI polling until it times out, reporting "deploy
# timed out" for what was actually a crash on line three.
result=""
finish() {
	local code=$?
	[[ -n $result ]] || result="FAIL unknown deploy failure (exit $code)"
	printf '%s\n' "$result" >"$STAGING/deploy.result.tmp"
	chmod 644 "$STAGING/deploy.result.tmp"
	mv "$STAGING/deploy.result.tmp" "$STAGING/deploy.result"
	log "$result"
}
trap finish EXIT

die() {
	result="FAIL $*"
	exit 1
}

[[ $EUID -eq 0 ]] || die "must run as root"

# Everything in the staging directory is writable by the CI user, so anything checked
# about a file there can be made untrue before it is used. Copy into a root-only
# directory and never look at the original again.
mkdir -p "$PRIVATE"
chmod 700 "$PRIVATE"
[[ -f $STAGING/zuka ]] || die "no staged binary"

readonly CANDIDATE="$PRIVATE/candidate"
cp "$STAGING/zuka" "$CANDIDATE"
chmod 755 "$CANDIDATE"

# A binary that cannot report its own version will not serve traffic either, and
# finding out here costs nothing while finding out after the restart costs an outage.
version_output="$("$CANDIDATE" version 2>/dev/null)" || die "staged binary does not run"
# `version` prints "<name> <build>".
readonly BUILD="${version_output##* }"
log "candidate $BUILD"

if [[ -x $INSTALLED ]]; then
	current="$("$INSTALLED" version 2>/dev/null || echo 'unknown unknown')"
	if [[ ${current##* } == "$BUILD" ]]; then
		result="OK $BUILD (already running)"
		exit 0
	fi
	cp "$INSTALLED" "$PRIVATE/previous"
else
	rm -f "$PRIVATE/previous"
fi

# Health alone is not proof: systemd could have restarted the old binary and reported
# perfectly good health. The build it reports has to be the one just installed.
healthy() {
	local body
	body="$(curl -fsS --max-time 3 "$HEALTH" 2>/dev/null)" || return 1
	[[ $body == *"$BUILD"* ]]
}

install_binary() {
	# Same filesystem, so the rename is atomic. systemd may restart at any moment and
	# must never find a half-written file at this path.
	cp "$1" "$INSTALLED.new"
	chmod 755 "$INSTALLED.new"
	mv "$INSTALLED.new" "$INSTALLED"
	# Both units run this binary. Restarting only the web unit would leave
	# maintenance running the old code against the new data directory.
	systemctl restart "$SERVICE" "$JOBS"
}

wait_for() {
	local waited=0
	while ((waited < HEALTH_TIMEOUT)); do
		"$1" && return 0
		sleep 2
		waited=$((waited + 2))
	done
	return 1
}

log "installing $BUILD and restarting"
install_binary "$CANDIDATE"
if wait_for healthy; then
	result="OK $BUILD"
	exit 0
fi

log "not healthy on $BUILD after ${HEALTH_TIMEOUT}s — rolling back"
[[ -f $PRIVATE/previous ]] || die "$BUILD is unhealthy and there is no previous binary; $SERVICE is DOWN"

install_binary "$PRIVATE/previous"
responds() { curl -fsS --max-time 3 "$HEALTH" >/dev/null 2>&1; }
if wait_for responds; then
	die "$BUILD did not become healthy; rolled back, the host is serving the previous build"
fi

die "$BUILD did not become healthy AND the rollback did not restore it; $SERVICE is DOWN"
