#!/usr/bin/env bash set -euo pipefail [[ "$#" -eq 3 ]] || { echo "usage: setup-garbage-collection.sh SERVICE_USER CI_USER TOAD_USER" >&2 exit 2 } service_user="$1" ci_user="$2" toad_user="$3" for user in "${service_user}" "${ci_user}" "${toad_user}"; do [[ "${user}" =~ ^[a-z_][a-z0-9_-]*$ ]] || { echo "unsafe user name: ${user}" >&2 exit 2 } id -u "${user}" >/dev/null loginctl enable-linger "${user}" systemctl start "user@$(id -u "${user}").service" done service_home="$(getent passwd "${service_user}" | cut -d: -f6)" [[ "${service_home}" =~ ^/[A-Za-z0-9._/-]+$ ]] || { echo "unsafe user home" >&2 exit 2 } temporary="$(mktemp -d)" trap 'rm -rf -- "${temporary}"' EXIT # Per-owner rootless Podman garbage collection. Image prune deliberately omits # --all: only dangling images are removed, so tagged images such as the Luci # rollback image are always retained. Failures reach bugabinga-alert@. cat >"${temporary}/bugabinga-gc@.service" <<'EOF' [Unit] Description=Weekly rootless Podman garbage collection for %i OnFailure=bugabinga-alert@%n.service [Service] Type=oneshot TimeoutStartSec=30min ExecStart=/usr/sbin/runuser -u %i -- /usr/bin/bash -c 'export XDG_RUNTIME_DIR="/run/user/$(id -u)" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"; exec /usr/bin/podman container prune --force --filter until=168h' ExecStart=/usr/sbin/runuser -u %i -- /usr/bin/bash -c 'export XDG_RUNTIME_DIR="/run/user/$(id -u)" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"; exec /usr/bin/podman image prune --force --filter until=168h' EOF cat >"${temporary}/bugabinga-gc@.timer" <<'EOF' [Unit] Description=Weekly garbage collection timer for %i [Timer] OnCalendar=weekly RandomizedDelaySec=6h Persistent=true Unit=bugabinga-gc@%i.service [Install] WantedBy=timers.target EOF cat >"${temporary}/bugabinga-gc-host.service" <<'EOF' [Unit] Description=Weekly host-level log retention cleanup OnFailure=bugabinga-alert@%n.service [Service] Type=oneshot TimeoutStartSec=30min ExecStart=/usr/bin/find /data/ci/logs -type f -mtime +90 -delete ExecStart=/usr/bin/journalctl --vacuum-size=2G --vacuum-time=30d EOF cat >"${temporary}/bugabinga-gc-host.timer" <<'EOF' [Unit] Description=Weekly host-level cleanup timer [Timer] OnCalendar=weekly RandomizedDelaySec=6h Persistent=true Unit=bugabinga-gc-host.service [Install] WantedBy=timers.target EOF cat >"${temporary}/bugabinga-alert@.service" <<'EOF' [Unit] Description=Record failure alert for %i [Service] Type=oneshot ExecStart=/usr/bin/logger -p user.crit -t bugabinga-alert "unit %i failed" ExecStart=-/usr/bin/sh -c 'if command -v mailx >/dev/null 2>&1; then systemctl status %i --no-pager -n 20 | mailx -s "bugabinga: %i failed" root; fi' EOF cat >"${temporary}/bugabinga-maintenance.service" <<'EOF' [Unit] Description=Weekly maintenance freshness checks OnFailure=bugabinga-alert@%n.service [Service] Type=oneshot ExecStart=/usr/local/sbin/bugabinga-check-maintenance EOF cat >"${temporary}/bugabinga-maintenance.timer" <<'EOF' [Unit] Description=Weekly maintenance check timer [Timer] OnCalendar=weekly RandomizedDelaySec=3h Persistent=true Unit=bugabinga-maintenance.service [Install] WantedBy=timers.target EOF cat >"${temporary}/journald-retention.conf" <<'EOF' [Journal] SystemMaxUse=2G MaxRetentionSec=30day EOF cat >"${temporary}/rsyslog-logrotate" <<'EOF' /var/log/cron /var/log/maillog /var/log/messages /var/log/secure /var/log/spooler { daily rotate 7 maxsize 100M compress missingok sharedscripts postrotate /usr/bin/systemctl reload rsyslog.service >/dev/null 2>&1 || true endscript } EOF # The vendor podman-auto-update.service runs "podman image prune -f" after the # update, which fails whenever any image is still in use and masks the update # result. An empty ExecStartPost assignment clears that vendor command so the # update outcome stands alone. dropin_dir="${service_home}/.config/systemd/user/podman-auto-update.service.d" mkdir -p "${dropin_dir}" cat >"${temporary}/no-image-prune.conf" <<'EOF' [Service] ExecStartPost= EOF install -m 0644 "${temporary}/bugabinga-gc@.service" /etc/systemd/system/bugabinga-gc@.service install -m 0644 "${temporary}/bugabinga-gc@.timer" /etc/systemd/system/bugabinga-gc@.timer install -m 0644 "${temporary}/bugabinga-gc-host.service" /etc/systemd/system/bugabinga-gc-host.service install -m 0644 "${temporary}/bugabinga-gc-host.timer" /etc/systemd/system/bugabinga-gc-host.timer install -m 0644 "${temporary}/bugabinga-alert@.service" /etc/systemd/system/bugabinga-alert@.service install -m 0644 "${temporary}/bugabinga-maintenance.service" /etc/systemd/system/bugabinga-maintenance.service install -m 0644 "${temporary}/bugabinga-maintenance.timer" /etc/systemd/system/bugabinga-maintenance.timer install -d -m 0755 /etc/systemd/journald.conf.d install -m 0644 "${temporary}/journald-retention.conf" /etc/systemd/journald.conf.d/bugabinga-retention.conf install -m 0644 "${temporary}/rsyslog-logrotate" /etc/logrotate.d/rsyslog install -d -o "${service_user}" -g "${service_user}" -m 0755 "${dropin_dir}" install -o "${service_user}" -g "${service_user}" -m 0644 "${temporary}/no-image-prune.conf" "${dropin_dir}/no-image-prune.conf" systemctl disable --now bugabinga-gc.timer >/dev/null 2>&1 || true rm -f /etc/systemd/system/bugabinga-gc.service /etc/systemd/system/bugabinga-gc.timer systemctl daemon-reload systemctl restart systemd-journald.service systemctl enable --now bugabinga-gc@"${service_user}".timer bugabinga-gc@"${ci_user}".timer bugabinga-gc@"${toad_user}".timer systemctl enable --now bugabinga-gc-host.timer bugabinga-maintenance.timer # Seed a first run so freshness checks have a baseline immediately. systemctl start bugabinga-gc@"${service_user}".service bugabinga-gc@"${ci_user}".service bugabinga-gc@"${toad_user}".service bugabinga-gc-host.service systemctl start bugabinga-maintenance.service || true service_runtime="/run/user/$(id -u "${service_user}")" runuser -u "${service_user}" -- env HOME="${service_home}" USER="${service_user}" LOGNAME="${service_user}" XDG_RUNTIME_DIR="${service_runtime}" DBUS_SESSION_BUS_ADDRESS="unix:path=${service_runtime}/bus" systemctl --user daemon-reload