#!/usr/bin/env bash # Transactional tooling-only update. Never restarts the active application. set -Eeuo pipefail [[ ${EUID} -eq 0 ]] || { echo 'Run this installer with sudo.' >&2; exit 1; } SOURCE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" exec 9>/run/lock/eusrr-deploy.lock flock -n 9 || { echo 'A deployment is running. Retry after it finishes.' >&2; exit 3; } [[ -d /srv/eusrr-slots && ! -L /srv/eusrr-slots ]] || { echo 'Expected a real /srv/eusrr-slots directory.' >&2; exit 1; } slot_owner="$(stat -c '%u:%g' /srv/eusrr-slots)" slot_mode="$(stat -c '%a' /srv/eusrr-slots)" backup="/var/lib/eusrr-deploy/tooling-backup-$(date -u +%Y%m%dT%H%M%SZ)-$$" install -d -o root -g root -m 0700 "${backup}" "${backup}/before" "${backup}/staged" staged="${backup}/staged" installed=() complete=0 mutated=0 cleanup() { local status=$? target name recovered=1 trap - EXIT HUP INT TERM if [[ ${complete} -eq 0 && ${mutated} -eq 1 ]]; then set +e echo 'Tooling validation failed; restoring the previous files.' >&2 for target in "${installed[@]}"; do name="${target//\//_}" if [[ -f ${backup}/before/${name} ]]; then cp -a -- "${backup}/before/${name}" "${target}" || recovered=0 else rm -f -- "${target}" || recovered=0 fi done chown "${slot_owner}" /srv/eusrr-slots || recovered=0 chmod "0${slot_mode}" /srv/eusrr-slots || recovered=0 systemctl daemon-reload || recovered=0 if [[ ${recovered} -eq 1 ]]; then echo "Previous tooling restored. Review backup: ${backup}" >&2 else echo "ERROR: tooling recovery incomplete. Manual recovery required from ${backup}" >&2 fi (( status != 0 )) || status=1 fi exit "${status}" } trap cleanup EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM # Copy into a root-only directory before checking or installing anything. for file in eusrr-deploy eusrr-deploy-ssh eusrr-deploy-healthcheck logrotate.conf journald.conf gitea_known_hosts; do install -o root -g root -m 0644 "${SOURCE_DIR}/${file}" "${staged}/${file}" done for unit in backend frontend celery-worker celery-beat; do install -o root -g root -m 0644 "${SOURCE_DIR}/systemd/eusrr-${unit}@.service" \ "${staged}/eusrr-${unit}@.service" done bash -n "${staged}/eusrr-deploy" bash -n "${staged}/eusrr-deploy-ssh" python3 -m py_compile "${staged}/eusrr-deploy-healthcheck" [[ $(ssh-keygen -lf "${staged}/gitea_known_hosts" | awk '{print $2}') == \ 'SHA256:341ON+FsVZMVMsduFUZjevfxjagbFG9yPeZ0edTVSOk' ]] || { echo 'Pinned Gitea host key fingerprint does not match.' >&2; exit 1; } if ! LC_ALL=C logrotate --debug --state /dev/null "${staged}/logrotate.conf" \ >"${staged}/logrotate-validation.log" 2>&1 || grep -Eq '^error:|Ignoring .* because' "${staged}/logrotate-validation.log"; then cat "${staged}/logrotate-validation.log" >&2 exit 1 fi # Ubuntu 24.04 sudo supports anchored argument regexes. The runtime account # cannot invoke maintenance, self-test, or extra arguments through sudo. printf '%s\n' 'deploy ALL=(root) NOPASSWD: /usr/local/sbin/eusrr-deploy ^[0-9a-f]{40}$' \ >"${staged}/sudoers" visudo -cf "${staged}/sudoers" >/dev/null install_file() { local source="$1" target="$2" mode="$3" name name="${target//\//_}" [[ ! -L ${target} ]] || { echo "Refusing symlink target: ${target}" >&2; return 1; } [[ ! -e ${target} ]] || cp -a -- "${target}" "${backup}/before/${name}" installed+=("${target}") mutated=1 install -o root -g root -m "${mode}" "${source}" "${target}.new" mv -Tf -- "${target}.new" "${target}" } install -d -o root -g root -m 0755 /usr/local/libexec /etc/systemd/journald.conf.d install_file "${staged}/eusrr-deploy" /usr/local/sbin/eusrr-deploy 0755 install_file "${staged}/eusrr-deploy-ssh" /usr/local/sbin/eusrr-deploy-ssh 0755 install_file "${staged}/eusrr-deploy-healthcheck" /usr/local/libexec/eusrr-deploy-healthcheck 0755 install_file "${staged}/logrotate.conf" /etc/logrotate.d/eusrr-deploy 0644 install_file "${staged}/journald.conf" /etc/systemd/journald.conf.d/60-eusrr-retention.conf 0644 install_file "${staged}/sudoers" /etc/sudoers.d/eusrr-deploy 0440 install_file "${staged}/gitea_known_hosts" /etc/eusrr/gitea_known_hosts 0644 for unit in backend frontend celery-worker celery-beat; do install_file "${staged}/eusrr-${unit}@.service" "/etc/systemd/system/eusrr-${unit}@.service" 0644 done chown root:root /srv/eusrr-slots # A four-digit chmod retains existing setgid on GNU directory targets. chmod 00755 /srv/eusrr-slots systemctl daemon-reload /usr/local/sbin/eusrr-deploy --self-test visudo -c >/dev/null slot="$(cat /var/lib/eusrr-deploy/active-slot)" [[ ${slot} == blue || ${slot} == green ]] || { echo 'Expected a native active slot before updating deployment tooling.' >&2; exit 1; } systemctl is-active "eusrr-backend@${slot}.service" "eusrr-frontend@${slot}.service" \ "eusrr-celery-worker@${slot}.service" "eusrr-celery-beat@${slot}.service" complete=1 printf 'Deployment tooling and unit templates updated; active application was not restarted. Backup: %s\n' "${backup}" echo 'Journald limits take effect on its next restart. No logs were vacuumed or releases removed.'