#!/usr/bin/env bash set -euo pipefail user="${CI_USER:-ci}" default_image="localhost/bugabinga-luci:latest" image="${LUCI_IMAGE:-${default_image}}" rollback_image="localhost/bugabinga-luci:rollback" home_dir="/home/${user}" uid="$(id -u "${user}")" runtime="/run/user/${uid}" mode="${1:-start}" [[ "$#" -le 1 ]] || { echo "usage: start-luci.sh [backup|deploy]" >&2 exit 2 } [[ "${image}" == "${default_image}" ]] || { echo "LUCI_IMAGE must remain ${default_image}; the installed Quadlet selects this image" >&2 exit 2 } install -d -o "${user}" -g "${user}" -m 0700 "${runtime}" run_ci() { runuser -u "${user}" -- env \ HOME="${home_dir}" \ USER="${user}" \ LOGNAME="${user}" \ XDG_RUNTIME_DIR="${runtime}" \ DBUS_SESSION_BUS_ADDRESS="unix:path=${runtime}/bus" \ bash -lc "cd '${home_dir}' && $1" } image_exists() { run_ci "podman image exists '${1}'" } quadlet_installed() { [[ -f "${home_dir}/.config/containers/systemd/luci.container" ]] } restart_luci() { run_ci 'systemctl --user daemon-reload' run_ci 'systemctl --user restart luci.service || systemctl --user start luci.service' } container_port() { local port port="$(run_ci "podman port luci '${1}/tcp'" | head -n 1)" port="${port##*:}" [[ "${port}" =~ ^[0-9]+$ ]] || return 1 printf '%s\n' "${port}" } wait_for_health() ( local authorized_keys=/data/ci/authorized_keys health_dir='' health_key health_line='' http_port ssh_port status=1 added=false _ # shellcheck disable=SC2317,SC2329 cleanup_health_key() { local exit_status=$? if [[ "${added}" == true ]]; then ( flock -x 9 local tmp tmp="$(mktemp "${authorized_keys}.XXXXXX")" awk -v line="${health_line}" '$0 != line' "${authorized_keys}" >"${tmp}" chmod --reference="${authorized_keys}" "${tmp}" chown --reference="${authorized_keys}" "${tmp}" mv -f "${tmp}" "${authorized_keys}" ) 9>>"${authorized_keys}.lock" || echo "failed to remove temporary Luci health key" >&2 fi [[ -z "${health_dir}" ]] || rm -rf "${health_dir}" exit "${exit_status}" } trap cleanup_health_key EXIT [[ -f "${authorized_keys}" ]] || exit 1 health_dir="$(mktemp -d)" health_key="${health_dir}/key" ssh-keygen -q -t ed25519 -N '' -f "${health_key}" health_line="restrict $(<"${health_key}.pub")" ( flock -x 9 printf '%s\n' "${health_line}" >>"${authorized_keys}" ) 9>>"${authorized_keys}.lock" added=true for _ in $(seq 1 30); do if http_port="$(container_port 8080)" && ssh_port="$(container_port 2223)" && curl --fail --silent --show-error --connect-timeout 2 "http://127.0.0.1:${http_port}/llms.txt" | grep -qx '# Luci CI' && ssh -i "${health_key}" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p "${ssh_port}" root@127.0.0.1 'ci env-check' | grep -qx ok; then status=0 break fi sleep 1 done exit "${status}" ) backup_image() { if image_exists "${image}"; then run_ci "podman tag '${image}' '${rollback_image}'" echo "retained ${rollback_image}" else echo "no existing Luci image to retain" fi } rollback() { if ! image_exists "${rollback_image}"; then echo "Luci candidate failed and no rollback image exists" >&2 return 1 fi echo "Luci candidate failed; restoring ${rollback_image}" >&2 run_ci "podman tag '${rollback_image}' '${image}'" restart_luci } restore_healthy_service() { rollback || return 1 if ! wait_for_health; then echo "Luci rollback image failed health checks" >&2 return 1 fi echo "Luci rollback image is healthy" >&2 } case "${mode}" in backup) backup_image ;; deploy) if ! quadlet_installed; then echo "luci quadlet not installed yet; image retained for just apply" exit 0 fi image_exists "${image}" if ! restart_luci; then echo "Luci candidate restart failed" >&2 restore_healthy_service || true exit 1 fi if ! wait_for_health; then restore_healthy_service || true exit 1 fi echo "Luci HTTP and forced SSH are healthy" ;; start) if image_exists "${image}"; then restart_luci else echo "Luci image not loaded yet; run: just deploy luci-image" fi ;; *) echo "usage: start-luci.sh [backup|deploy]" >&2 exit 2 ;; esac