Files
pi-car-companion/scripts/pi-car-companion-update
T

189 lines
7.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME="pi-car-companion"
INSTALL_ROOT="/opt/${APP_NAME}"
REPOSITORY="${INSTALL_ROOT}/repository"
RELEASES="${INSTALL_ROOT}/releases"
CURRENT="${INSTALL_ROOT}/current"
STAGING_ROOT="${INSTALL_ROOT}/staging"
CONFIG_DIR="/etc/${APP_NAME}"
DATA_DIR="/var/lib/${APP_NAME}"
STATUS_FILE="${DATA_DIR}/update-status.json"
BRANCH_FILE="${CONFIG_DIR}/update-branch"
UPDATE_USER_FILE="${CONFIG_DIR}/update-user"
LOCK_FILE="/run/lock/${APP_NAME}-update.lock"
WORKTREE=""
RELEASE_TMP=""
FROM_REVISION=""
TO_REVISION=""
PHASE="checking for updates"
if [[ ${EUID} -ne 0 ]]; then
echo "The update helper must run as root." >&2
exit 1
fi
write_status() {
local state="$1"
local message="$2"
local update_group_id
update_group_id="$(getent group pi-companion | cut -d: -f3)"
UPDATE_STATE="${state}" \
UPDATE_MESSAGE="${message}" \
UPDATE_FROM="${FROM_REVISION}" \
UPDATE_TO="${TO_REVISION}" \
UPDATE_STATUS_FILE="${STATUS_FILE}" \
UPDATE_GROUP_ID="${update_group_id}" \
/usr/bin/node --input-type=module -e '
import { chmodSync, chownSync, renameSync, writeFileSync } from "node:fs";
const target = process.env.UPDATE_STATUS_FILE;
const temporary = `${target}.tmp`;
const value = {
state: process.env.UPDATE_STATE,
message: process.env.UPDATE_MESSAGE,
fromRevision: process.env.UPDATE_FROM || null,
toRevision: process.env.UPDATE_TO || null,
updatedAt: new Date().toISOString()
};
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o644 });
chownSync(temporary, 0, Number(process.env.UPDATE_GROUP_ID));
chmodSync(temporary, 0o644);
renameSync(temporary, target);
'
}
install_integration_files() {
local source_release="$1"
install -m 0755 "${source_release}/scripts/pi-car-companion-update" /usr/local/sbin/pi-car-companion-update
install -m 0755 "${source_release}/scripts/pi-car-companion-network-setup" /usr/local/sbin/pi-car-companion-network-setup
install -m 0644 "${source_release}/systemd/pi-car-companion.service" /etc/systemd/system/pi-car-companion.service
install -m 0644 "${source_release}/systemd/pi-car-companion-update.service" /etc/systemd/system/pi-car-companion-update.service
install -m 0644 "${source_release}/systemd/pi-car-companion-network.service" /etc/systemd/system/pi-car-companion-network.service
install -m 0644 "${source_release}/systemd/pi-car-companion-network-configure.service" /etc/systemd/system/pi-car-companion-network-configure.service
install -m 0644 "${source_release}/systemd/pi-car-companion-network-observer.service" /etc/systemd/system/pi-car-companion-network-observer.service
install -m 0440 "${source_release}/systemd/pi-car-companion.sudoers" /etc/sudoers.d/pi-car-companion
install -d -m 0755 /etc/NetworkManager/dnsmasq-shared.d
install -m 0644 "${source_release}/config/dnsmasq-shared-pi-car-companion.conf" /etc/NetworkManager/dnsmasq-shared.d/pi-car-companion.conf
}
cleanup() {
if [[ -n "${WORKTREE}" && "${WORKTREE}" == "${STAGING_ROOT}/"* && -e "${WORKTREE}" ]]; then
git -C "${REPOSITORY}" worktree remove --force "${WORKTREE}" >/dev/null 2>&1 || rm -rf -- "${WORKTREE}"
fi
if [[ -n "${RELEASE_TMP}" && "${RELEASE_TMP}" == "${RELEASES}/.new-"* && -e "${RELEASE_TMP}" ]]; then
rm -rf -- "${RELEASE_TMP}"
fi
}
on_error() {
local exit_code=$?
trap - ERR
cleanup
write_status "failed" "Update failed while ${PHASE}. The previous version is still active."
exit "${exit_code}"
}
trap on_error ERR
trap cleanup EXIT
exec 9>"${LOCK_FILE}"
if ! flock -n 9; then
write_status "failed" "Another update is already running."
exit 1
fi
if [[ ! -d "${REPOSITORY}/.git" || ! -f "${BRANCH_FILE}" || ! -f "${UPDATE_USER_FILE}" ]]; then
write_status "failed" "The deployment repository is not configured. Run install.sh again."
exit 1
fi
FROM_REVISION="$(cat "${CURRENT}/REVISION" 2>/dev/null || true)"
branch="$(tr -d '\r\n' < "${BRANCH_FILE}")"
update_user="$(tr -d '\r\n' < "${UPDATE_USER_FILE}")"
if ! id "${update_user}" >/dev/null 2>&1; then
write_status "failed" "The Git update user no longer exists. Run install.sh again."
exit 1
fi
write_status "checking" "Checking the configured Git remote for updates."
chown -R "${update_user}":"$(id -gn "${update_user}")" "${REPOSITORY}/.git"
if [[ "${update_user}" == "root" ]]; then
git -C "${REPOSITORY}" fetch --prune origin
else
sudo -H -u "${update_user}" -- git -C "${REPOSITORY}" fetch --prune origin
fi
TO_REVISION="$(git -C "${REPOSITORY}" rev-parse "origin/${branch}")"
if [[ -n "${FROM_REVISION}" && "${FROM_REVISION}" == "${TO_REVISION}" ]]; then
install_integration_files "${CURRENT}"
visudo -cf /etc/sudoers.d/pi-car-companion >/dev/null
systemctl daemon-reload
write_status "current" "Pi Car Companion is already up to date."
exit 0
fi
if [[ -n "${FROM_REVISION}" ]] && ! git -C "${REPOSITORY}" merge-base --is-ancestor "${FROM_REVISION}" "${TO_REVISION}"; then
write_status "failed" "The remote update is not a fast-forward change. Automatic update was refused."
exit 1
fi
PHASE="building the new release"
write_status "building" "Downloading dependencies and verifying the new release."
install -d -m 0755 "${STAGING_ROOT}" "${RELEASES}"
WORKTREE="${STAGING_ROOT}/worktree-${TO_REVISION}-$$"
git -C "${REPOSITORY}" worktree add --detach "${WORKTREE}" "${TO_REVISION}"
cd "${WORKTREE}"
npm ci
npm run check
printf '%s\n' "${TO_REVISION}" > REVISION
PHASE="activating the new release"
RELEASE_TMP="${RELEASES}/.new-${TO_REVISION}-$$"
install -d -m 0755 "${RELEASE_TMP}"
rsync -a --delete --exclude .git "${WORKTREE}/" "${RELEASE_TMP}/"
release_final="${RELEASES}/${TO_REVISION}"
if [[ -e "${release_final}" ]]; then
rm -rf -- "${release_final}"
fi
mv "${RELEASE_TMP}" "${release_final}"
RELEASE_TMP=""
chown -R root:root "${release_final}"
install_integration_files "${release_final}"
visudo -cf /etc/sudoers.d/pi-car-companion >/dev/null
systemctl daemon-reload
ln -sfn "releases/${TO_REVISION}" "${INSTALL_ROOT}/current.next"
mv -Tf "${INSTALL_ROOT}/current.next" "${CURRENT}"
PHASE="verifying the updated service"
write_status "building" "The update is installed. Verifying the restarted service."
healthy=false
if systemctl restart pi-car-companion.service; then
for _attempt in {1..30}; do
if curl --fail --silent --show-error http://127.0.0.1:8787/healthz >/dev/null; then
healthy=true
break
fi
sleep 1
done
fi
if [[ "${healthy}" != "true" ]]; then
if [[ -n "${FROM_REVISION}" && -d "${RELEASES}/${FROM_REVISION}" ]]; then
install_integration_files "${RELEASES}/${FROM_REVISION}"
systemctl daemon-reload
ln -sfn "releases/${FROM_REVISION}" "${INSTALL_ROOT}/current.next"
mv -Tf "${INSTALL_ROOT}/current.next" "${CURRENT}"
systemctl restart pi-car-companion.service || true
write_status "failed" "The new release failed its health check. The previous release was restored."
else
write_status "failed" "The new release failed its health check and no previous release was available."
fi
exit 1
fi
write_status "success" "Update installed and the companion service is healthy."
systemctl try-restart pi-car-companion-network.service || true
systemctl enable --now pi-car-companion-network-observer.service || true