207 lines
7.8 KiB
Bash
Executable File
207 lines
7.8 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"
|
|
CHECK_ONLY=false
|
|
|
|
if [[ "${1:-}" == "--check-only" ]]; then
|
|
CHECK_ONLY=true
|
|
elif [[ $# -gt 0 ]]; then
|
|
echo "Usage: pi-car-companion-update [--check-only]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
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 0755 "${source_release}/scripts/pi-car-companion-power" /usr/local/sbin/pi-car-companion-power
|
|
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-update-check.service" /etc/systemd/system/pi-car-companion-update-check.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
|
|
if [[ "${CHECK_ONLY}" != "true" ]]; then
|
|
install_integration_files "${CURRENT}"
|
|
visudo -cf /etc/sudoers.d/pi-car-companion >/dev/null
|
|
systemctl daemon-reload
|
|
fi
|
|
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
|
|
|
|
if [[ "${CHECK_ONLY}" == "true" ]]; then
|
|
write_status "available" "A newer Pi Car Companion release is available to install."
|
|
exit 0
|
|
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 pi-car-companion-network-observer.service || true
|
|
systemctl restart pi-car-companion-network-observer.service || true
|