From cd003dbaf338210d2e05000c465d1c3375ebbba4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 04:26:08 +0000 Subject: [PATCH] Add Gatus auto-sync from Caddyfile, promote ensure_yq to lib/common.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds one Gatus endpoint per Caddy site block automatically, tagged group: caddy-sync — the sync only ever adds/removes entries in that exact group, so anything added by hand (the default external checks, a custom endpoint) is never touched regardless of what the Caddyfile looks like. Offered at install time (syncs once immediately) and, if systemd is available, scheduled via a timer every 15 minutes so a site added or removed later gets picked up without re-running the installer — matches the "schedule that checks the Caddyfile" shape asked for. Domain extraction tracks actual brace depth (reusing the same approach as remove_service's Caddy block removal) rather than a naive line-by-line scan, so it correctly skips the global options block and parenthesized snippet definitions like (authelia) without needing to special-case them by name. Verified end-to-end against a real Caddyfile/config.yaml fixture with the actual mikefarah/yq binary: initial sync adds the right entries and leaves the default "external" group alone, a second run with no Caddyfile changes is a true no-op (0 added, 0 removed), and changing the Caddyfile (removing one site, adding another) correctly adds the new endpoint and removes only the stale one. Also fixes a real gap surfaced while building this: ensure_yq (used by both gatus.sh now and onlyoffice.sh already) checked `command -v yq` alone, which a box can satisfy with a completely different, incompatible yq — confirmed live in this environment, Debian/Ubuntu's own `yq` apt package is kislyuk/yq (a Python jq-wrapper) which silently errors on mikefarah/yq's `e '.path' file` syntax every caller here depends on. Now checks the version string actually identifies as mikefarah's before trusting it, installing to /usr/local/bin (which precedes /usr/bin on Ubuntu's default PATH) if not. Promoted ensure_yq itself from onlyoffice.sh (its only previous user) to lib/common.sh now that gatus.sh needs the same thing, so both share one implementation. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn --- lib/common.sh | 27 ++++++++ services/gatus.sh | 154 +++++++++++++++++++++++++++++++++++++++++ services/onlyoffice.sh | 17 ++--- 3 files changed, 185 insertions(+), 13 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 01e5687..421701e 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -244,6 +244,33 @@ ensure_caddy_network() { && log_info "Created Docker network ${_net} (needed by Caddy-fronted services)" } +# Installs yq v4 (Go binary release, not the Python click-based yq some +# distros package under the same name) if not already present. Shared by +# any service that needs to patch YAML config robustly instead of +# hand-rolling sed/awk text surgery — originally lived only in +# services/onlyoffice.sh (FileBrowser config patching); promoted here once +# services/gatus.sh needed the same thing (endpoint sync from Caddyfile), +# so both use one implementation instead of two copies drifting apart. +ensure_yq() { + # Not just `command -v yq` — confirmed live, a box can already have + # `yq` on PATH that's actually kislyuk/yq (the Python jq-wrapper apt + # packages under the same name on Debian/Ubuntu), which silently + # errors on mikefarah's `e '.path' file` syntax every caller here + # uses ("argument files: can't open '.path'"). Check the version + # string actually identifies as mikefarah's before trusting it. + yq --version 2>/dev/null | grep -q mikefarah && return 0 + log_info "Installing yq..." + local _arch; _arch=$(uname -m) + local _binary="yq_linux_amd64" + [[ "$_arch" == "aarch64" || "$_arch" == "arm64" ]] && _binary="yq_linux_arm64" + # /usr/local/bin precedes /usr/bin on Ubuntu's default PATH, so this + # correctly shadows a wrong /usr/bin/yq without needing to touch or + # remove it (something else on the box may depend on the real one). + curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/${_binary}" \ + -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq \ + && log_success "yq installed" || log_warning "yq install failed" +} + # Enables UFW if it isn't already active. Call this AFTER the caller has # already added its own `ufw allow` rules for whatever it needs — this only # flips UFW from inactive to active, it doesn't add rules for the calling diff --git a/services/gatus.sh b/services/gatus.sh index ca12035..16aab0c 100644 --- a/services/gatus.sh +++ b/services/gatus.sh @@ -207,6 +207,135 @@ fi register_service gatus utilities "Status & uptime monitoring page (Gatus)" 8086 +# ── Caddyfile -> Gatus endpoint sync ──────────────────────────────────────── +# Every auto-added endpoint is tagged group: "caddy-sync" — the sync script +# only ever adds or removes entries in that exact group, so anything you add +# by hand (the default external checks, a custom endpoint, different group +# name) is never touched regardless of what the Caddyfile looks like. +_gatus_write_sync_script() { + local gatus_dir="$1" caddy_file="$2" + cat > "$gatus_dir/sync-from-caddy.sh" << 'SYNC_SCRIPT' +#!/bin/bash +# Auto-generated by services/gatus.sh — do not edit by hand, re-run the +# installer to regenerate. Syncs Gatus endpoints with the current +# Caddyfile: adds a group: caddy-sync endpoint for every site block found, +# removes caddy-sync endpoints for sites no longer there. Never touches +# any other endpoint. +set -uo pipefail + +CADDYFILE="$1" +GATUS_CONFIG="$2" + +# Not just `command -v yq` — some distros package a completely different, +# incompatible yq (kislyuk's Python jq-wrapper) under the same name; check +# it actually identifies as mikefarah's before trusting its syntax. +yq --version 2>/dev/null | grep -q mikefarah || { echo "yq unavailable or incompatible — skipping sync"; exit 0; } +[ -f "$CADDYFILE" ] || { echo "No Caddyfile at $CADDYFILE — skipping sync"; exit 0; } +[ -f "$GATUS_CONFIG" ] || { echo "No Gatus config at $GATUS_CONFIG — skipping sync"; exit 0; } + +# Site block headers at brace-depth 0, skipping the bare global-options +# block (no address before its "{") and parenthesized snippet definitions +# like (authelia) — same brace-tracking approach as the Caddy site-block +# removal in lib/common.sh (remove_service), for the same reason: a naive +# "next blank line" scan is exactly the class of bug that already bit this +# repo once on a live Caddy/Samba config. +CURRENT_DOMAINS="$(awk ' + BEGIN { depth = 0 } + { + line = $0 + opens = gsub(/\{/, "{", line) + closes = gsub(/\}/, "}", line) + if (depth == 0 && opens > 0) { + header = $0 + sub(/[[:space:]]*\{.*$/, "", header) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", header) + if (header != "" && header !~ /^\(/) print header + depth += opens - closes + next + } + if (depth > 0) { depth += opens - closes; next } + } +' "$CADDYFILE")" + +EXISTING_SYNCED="$(yq e '.endpoints[] | select(.group == "caddy-sync") | .name' "$GATUS_CONFIG" 2>/dev/null)" + +ADDED=0 +REMOVED=0 + +while IFS= read -r domain; do + [ -z "$domain" ] && continue + # Domains only — a defensive filter, not strictly needed since these + # come from our own Caddyfile, but cheap insurance against ever + # building a yq expression out of anything unexpected. + [[ "$domain" =~ ^[a-zA-Z0-9.-]+$ ]] || continue + if ! grep -qxF "$domain" <<< "$EXISTING_SYNCED"; then + yq e -i ".endpoints += [{\"name\": \"${domain}\", \"group\": \"caddy-sync\", \"url\": \"https://${domain}\", \"interval\": \"5m\", \"conditions\": [\"[STATUS] < 400\"]}]" "$GATUS_CONFIG" \ + && ADDED=$((ADDED + 1)) + fi +done <<< "$CURRENT_DOMAINS" + +while IFS= read -r name; do + [ -z "$name" ] && continue + if ! grep -qxF "$name" <<< "$CURRENT_DOMAINS"; then + yq e -i "del(.endpoints[] | select(.group == \"caddy-sync\" and .name == \"${name}\"))" "$GATUS_CONFIG" \ + && REMOVED=$((REMOVED + 1)) + fi +done <<< "$EXISTING_SYNCED" + +echo "Gatus sync: added $ADDED, removed $REMOVED endpoint(s) from $(basename "$CADDYFILE")" +SYNC_SCRIPT + chmod 700 "$gatus_dir/sync-from-caddy.sh" + chown "$ACTUAL_USER:$ACTUAL_USER" "$gatus_dir/sync-from-caddy.sh" 2>/dev/null || true +} + +_gatus_setup_caddy_sync() { + local gatus_dir="$1" + local caddy_file="$DOCKER_DIR/caddy/Caddyfile" + + [ -f "$caddy_file" ] || return 0 + + echo "" + local ENABLE_SYNC="" + prompt_yn "Automatically keep Gatus endpoints in sync with the Caddyfile (syncs now, then every 15 minutes)? (y/n):" "y" ENABLE_SYNC + [[ "$ENABLE_SYNC" =~ ^[Yy]$ ]] || return 0 + + declare -F ensure_yq >/dev/null 2>&1 && ensure_yq + command -v yq >/dev/null 2>&1 || { log_warning "yq unavailable — skipping Caddy sync."; return 0; } + + _gatus_write_sync_script "$gatus_dir" "$caddy_file" + "$gatus_dir/sync-from-caddy.sh" "$caddy_file" "$gatus_dir/gatus_config/config.yaml" + + if command -v systemctl >/dev/null 2>&1; then + cat > /etc/systemd/system/gatus-caddy-sync.service << UNIT +[Unit] +Description=Sync Gatus endpoints from Caddyfile + +[Service] +Type=oneshot +ExecStart=${gatus_dir}/sync-from-caddy.sh ${caddy_file} ${gatus_dir}/gatus_config/config.yaml +UNIT + cat > /etc/systemd/system/gatus-caddy-sync.timer << 'UNIT' +[Unit] +Description=Periodic Gatus/Caddyfile sync + +[Timer] +OnBootSec=5min +OnUnitActiveSec=15min + +[Install] +WantedBy=timers.target +UNIT + systemctl daemon-reload + if systemctl enable --now gatus-caddy-sync.timer >/dev/null 2>&1; then + log_success "Sync scheduled every 15 minutes (systemd timer: gatus-caddy-sync)" + else + log_warning "Couldn't enable the sync timer — re-run $gatus_dir/sync-from-caddy.sh manually after Caddyfile changes." + fi + else + log_warning "No systemd — synced once, but won't auto-repeat. Re-run $gatus_dir/sync-from-caddy.sh manually after Caddyfile changes." + fi +} + install_gatus() { require_docker || return 1 log_info "Installing Gatus..." @@ -218,6 +347,7 @@ install_gatus() { echo "[DRY-RUN] Would deploy twinproduction/gatus:latest" echo "[DRY-RUN] Port 8086 published (auto-scanned for a free host port)," echo "[DRY-RUN] config at gatus_config/config.yaml" + echo "[DRY-RUN] Would offer to auto-sync endpoints from the Caddyfile (adds/removes group: caddy-sync entries, every 15min via a systemd timer)" return 0 fi @@ -359,6 +489,28 @@ Key concepts: Full docs: https://github.com/TwiN/gatus +## Caddyfile sync + +If you enabled it during install, \`sync-from-caddy.sh\` runs every 15 +minutes (systemd timer: \`gatus-caddy-sync\`) and keeps one endpoint per +Caddy site block in sync automatically — added when a new site appears, +removed when one goes away. Every auto-added endpoint is tagged +\`group: caddy-sync\`; the sync only ever touches entries in that group, +so anything you add or edit by hand (a different group, the default +external checks) is left alone no matter what. + +Run it by hand any time instead of waiting for the timer: +\`\`\`bash +$GATUS_DIR/sync-from-caddy.sh $DOCKER_DIR/caddy/Caddyfile $GATUS_DIR/gatus_config/config.yaml +\`\`\` + +Check/manage the timer: +\`\`\`bash +systemctl status gatus-caddy-sync.timer +systemctl list-timers gatus-caddy-sync.timer +sudo systemctl disable --now gatus-caddy-sync.timer # turn it off +\`\`\` + ## Manage \`\`\`bash cd $GATUS_DIR @@ -377,6 +529,8 @@ MD || log_warning "Failed to start — check: docker compose logs" fi + _gatus_setup_caddy_sync "$GATUS_DIR" + echo " Access at: http://localhost:${WEB_PORT}" echo " Config: $GATUS_DIR/gatus_config/config.yaml (hot-reloaded)" echo "" diff --git a/services/onlyoffice.sh b/services/onlyoffice.sh index 187ef0a..bed75bd 100644 --- a/services/onlyoffice.sh +++ b/services/onlyoffice.sh @@ -191,17 +191,8 @@ fi register_service onlyoffice utilities "Self-hosted OnlyOffice Document Server (Nextcloud/FileBrowser)" 8082 -# ── Helper: install yq v4 if absent ────────────────────────────────────────── -_ensure_yq() { - command -v yq &>/dev/null && return 0 - log_info "Installing yq (required for FileBrowser config patching)..." - local _arch; _arch=$(uname -m) - local _binary="yq_linux_amd64" - [[ "$_arch" == "aarch64" || "$_arch" == "arm64" ]] && _binary="yq_linux_arm64" - curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/${_binary}" \ - -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq \ - && log_success "yq installed" || log_warning "yq install failed — FileBrowser wiring skipped" -} +# yq install helper moved to lib/common.sh (ensure_yq) — shared with +# services/gatus.sh now that it needs the same thing. # ── Helper: wire OnlyOffice into Nextcloud (idempotent) ─────────────────────── _wire_nextcloud() { @@ -228,7 +219,7 @@ _wire_nextcloud() { _wire_filebrowser() { local _fbq_config="$DOCKER_DIR/filebrowser/config.yaml" [[ -f "$_fbq_config" ]] || { log_info "FileBrowser config not found — skipping"; return 0; } - command -v yq &>/dev/null || { log_info "yq not found — skipping FileBrowser wiring"; return 0; } + yq --version 2>/dev/null | grep -q mikefarah || { log_info "yq unavailable or incompatible — skipping FileBrowser wiring"; return 0; } log_info "Wiring OnlyOffice into FileBrowser Quantum..." yq e '.officeServer = "http://onlyoffice:80/"' -i "$_fbq_config" \ && log_success "FileBrowser officeServer set" || log_warning "Could not patch FileBrowser config" @@ -237,7 +228,7 @@ _wire_filebrowser() { install_onlyoffice() { require_docker || return 1 - _ensure_yq + declare -F ensure_yq >/dev/null 2>&1 && ensure_yq log_info "Installing OnlyOffice Document Server..." local DIR="$DOCKER_DIR/onlyoffice"