Add arr upgrade webhook listener and setup — closes propagation window without manual arr config
This commit is contained in:
@@ -256,6 +256,14 @@
|
|||||||
# HOST1_DISCORD_WEBHOOK / HOST2_DISCORD_WEBHOOK
|
# HOST1_DISCORD_WEBHOOK / HOST2_DISCORD_WEBHOOK
|
||||||
# Allows different webhooks per server, or only one server notifying.
|
# Allows different webhooks per server, or only one server notifying.
|
||||||
|
|
||||||
|
# Upgrade webhook — standalone PHP listener that receives OnUpgrade events from
|
||||||
|
# Sonarr/Radarr/Lidarr and immediately pushes the upgraded file to all mesh nodes.
|
||||||
|
# Bypasses Unraid nginx auth — the secret in the URL is the only gate.
|
||||||
|
# Run Tools/webhook_setup.sh once to register the connection in each arr.
|
||||||
|
# WEBHOOK_PORT 0 disables the listener.
|
||||||
|
WEBHOOK_PORT=7821
|
||||||
|
WEBHOOK_SECRET="cdbfde3c13468b7ae1cc1adc466b0e3db28592dec9d99c33689702ce0431e073" # auto-generated on first start if empty
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── GIT / REPO ────────────────────────────────────────────────────────────────────────────────
|
# ── GIT / REPO ────────────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -294,6 +302,7 @@
|
|||||||
"System_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
"System_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
||||||
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
||||||
"Tools/claude_startup.sh" # persist Claude data + binary to appdata; re-symlink on boot
|
"Tools/claude_startup.sh" # persist Claude data + binary to appdata; re-symlink on boot
|
||||||
|
"Media/start_webhook_listener.sh" # arr upgrade webhook listener — continuous
|
||||||
"Fallback/fallback.sh" # mutual failover — continuous
|
"Fallback/fallback.sh" # mutual failover — continuous
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,14 @@
|
|||||||
# HOST1_DISCORD_WEBHOOK / HOST2_DISCORD_WEBHOOK
|
# HOST1_DISCORD_WEBHOOK / HOST2_DISCORD_WEBHOOK
|
||||||
# Allows different webhooks per server, or only one server notifying.
|
# Allows different webhooks per server, or only one server notifying.
|
||||||
|
|
||||||
|
# Upgrade webhook — standalone PHP listener that receives OnUpgrade events from
|
||||||
|
# Sonarr/Radarr/Lidarr and immediately pushes the upgraded file to all mesh nodes.
|
||||||
|
# Bypasses Unraid nginx auth — the secret in the URL is the only gate.
|
||||||
|
# Run Tools/webhook_setup.sh once to register the connection in each arr.
|
||||||
|
# WEBHOOK_PORT 0 disables the listener.
|
||||||
|
WEBHOOK_PORT=7821
|
||||||
|
WEBHOOK_SECRET="" # auto-generated on first start if empty
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── GIT / REPO ────────────────────────────────────────────────────────────────────────────────
|
# ── GIT / REPO ────────────────────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
@@ -289,6 +297,7 @@
|
|||||||
"unRAID_Essentials/php_fpm_max_children.sh" # WebGUI performance tuning
|
"unRAID_Essentials/php_fpm_max_children.sh" # WebGUI performance tuning
|
||||||
"unRAID_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
"unRAID_Essentials/inotify_tuning.sh" # bump inotify limits — containers miss events if exhausted
|
||||||
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
"Docker_Essentials/docker_network_connect.sh" # ensure networks exist + connect containers
|
||||||
|
"Media/start_webhook_listener.sh" # arr upgrade webhook listener — continuous
|
||||||
"Fallback/fallback.sh" # mutual failover — continuous
|
"Fallback/fallback.sh" # mutual failover — continuous
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================================
|
||||||
|
# ======================= Upgrade Webhook Listener (continuous) ================================
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# PURPOSE
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Starts a standalone PHP HTTP server that receives Sonarr/Radarr/Lidarr
|
||||||
|
# OnUpgrade webhooks and dispatches upgrade_webhook_handler.sh.
|
||||||
|
#
|
||||||
|
# Runs outside Unraid nginx — no session auth required. The shared secret in
|
||||||
|
# the webhook URL is the only gate. Arrs on this host call:
|
||||||
|
#
|
||||||
|
# http://<HOST_LAN_IP>:<WEBHOOK_PORT>/webhook?key=<WEBHOOK_SECRET>
|
||||||
|
#
|
||||||
|
# Runs as a continuous script started by array_started.sh. Execs php -S which
|
||||||
|
# replaces this process — the PID stays the same for array_started.sh's check.
|
||||||
|
#
|
||||||
|
# If WEBHOOK_SECRET is empty in master.conf: generates and saves one, then starts.
|
||||||
|
# If WEBHOOK_PORT is 0: exits cleanly (disables the listener).
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ECOSYSTEM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
source "$ECOSYSTEM_ROOT/load_config.sh"
|
||||||
|
|
||||||
|
[[ "${WEBHOOK_PORT:-0}" -eq 0 ]] && {
|
||||||
|
echo "[webhook] WEBHOOK_PORT=0 — listener disabled"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Auto-generate secret if not yet set ─────────────────────────────────────
|
||||||
|
if [[ -z "${WEBHOOK_SECRET:-}" ]]; then
|
||||||
|
GENERATED=$(openssl rand -hex 32)
|
||||||
|
MASTER_CONF="$ECOSYSTEM_ROOT/Configurations/master.conf"
|
||||||
|
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$GENERATED\"/" "$MASTER_CONF"
|
||||||
|
WEBHOOK_SECRET="$GENERATED"
|
||||||
|
echo "[webhook] Generated WEBHOOK_SECRET — run Tools/webhook_setup.sh to register in arrs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /var/log/varaverk
|
||||||
|
|
||||||
|
exec node "$ECOSYSTEM_ROOT/Media/webhook_listener.js" \
|
||||||
|
"$WEBHOOK_PORT" "$WEBHOOK_SECRET" "$ECOSYSTEM_ROOT" \
|
||||||
|
>> /var/log/varaverk/upgrade_webhook.log 2>&1
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Standalone arr upgrade webhook listener.
|
||||||
|
// Started by start_webhook_listener.sh via `node webhook_listener.js`.
|
||||||
|
// Lives outside Unraid nginx — no session auth. Secret in URL is the only gate.
|
||||||
|
//
|
||||||
|
// Usage: node webhook_listener.js <port> <secret> <scripts_dir>
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const [,, port, secret, scriptsDir] = process.argv;
|
||||||
|
|
||||||
|
if (!port || !secret || !scriptsDir) {
|
||||||
|
process.stderr.write('Usage: webhook_listener.js <port> <secret> <scripts_dir>\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_FILE = '/var/log/varaverk/upgrade_webhook.log';
|
||||||
|
const HANDLER = path.join(scriptsDir, 'Media', 'upgrade_webhook_handler.sh');
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
const ts = new Date().toTimeString().slice(0, 8);
|
||||||
|
const line = `[${ts}] ${msg}\n`;
|
||||||
|
fs.appendFile(LOG_FILE, line, () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(res, code, obj) {
|
||||||
|
const body = JSON.stringify(obj);
|
||||||
|
res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
||||||
|
res.end(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, `http://localhost`);
|
||||||
|
const key = url.searchParams.get('key');
|
||||||
|
|
||||||
|
if (key !== secret) {
|
||||||
|
send(res, 403, { ok: false, error: 'Forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
send(res, 405, { ok: false, error: 'POST only' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => { body += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
let payload;
|
||||||
|
try { payload = JSON.parse(body); }
|
||||||
|
catch (_) { send(res, 400, { ok: false, error: 'Invalid JSON' }); return; }
|
||||||
|
|
||||||
|
const event = payload.eventType ?? '';
|
||||||
|
const isUpgrade = payload.isUpgrade ?? false;
|
||||||
|
|
||||||
|
if (event === 'Test') {
|
||||||
|
send(res, 200, { ok: true, message: 'Webhook connected — upgrade propagation active' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event !== 'Download' || !isUpgrade) {
|
||||||
|
send(res, 200, { ok: true, skipped: event });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let arrType, itemPath;
|
||||||
|
if (payload.series?.path) {
|
||||||
|
arrType = 'sonarr';
|
||||||
|
itemPath = payload.series.path;
|
||||||
|
} else if (payload.movie?.folderPath) {
|
||||||
|
arrType = 'radarr';
|
||||||
|
itemPath = payload.movie.folderPath;
|
||||||
|
} else if (payload.artist?.path) {
|
||||||
|
arrType = 'lidarr';
|
||||||
|
itemPath = payload.artist.path;
|
||||||
|
} else {
|
||||||
|
send(res, 400, { ok: false, error: 'Unrecognised payload structure' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemPath || !itemPath.startsWith('/') || itemPath.includes('..') || /[\x00\n\r]/.test(itemPath)) {
|
||||||
|
send(res, 400, { ok: false, error: 'Unsafe path' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Upgrade: ${arrType} — ${path.basename(itemPath)}`);
|
||||||
|
send(res, 200, { ok: true, arr: arrType, path: itemPath });
|
||||||
|
|
||||||
|
const cmd = `bash ${JSON.stringify(HANDLER)} ${JSON.stringify(arrType)} ${JSON.stringify(itemPath)}`;
|
||||||
|
const out = fs.createWriteStream(LOG_FILE, { flags: 'a' });
|
||||||
|
exec(cmd, { stdio: ['ignore', out, out] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(parseInt(port, 10), '0.0.0.0', () => {
|
||||||
|
log(`Webhook listener started on port ${port}`);
|
||||||
|
process.stdout.write(`[webhook] Listening on port ${port}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('error', err => {
|
||||||
|
process.stderr.write(`[webhook] Server error: ${err.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Executable
+172
@@ -0,0 +1,172 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================================
|
||||||
|
# ========================= Upgrade Webhook Setup =============================================
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# PURPOSE
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Registers the Varaverk upgrade webhook in Sonarr, Radarr, and Lidarr via
|
||||||
|
# their notification APIs. Idempotent — skips any arr that already has it.
|
||||||
|
#
|
||||||
|
# Run once after first install, or any time you add a new arr or host.
|
||||||
|
# The listener (start_webhook_listener.sh) must be running before arrs will
|
||||||
|
# actually deliver events, but this script can register the connection first.
|
||||||
|
#
|
||||||
|
# ── WHAT IT DOES ─────────────────────────────────────────────────────────────
|
||||||
|
# 1. Generates WEBHOOK_SECRET in master.conf if empty
|
||||||
|
# 2. Registers webhook in each local arr (Sonarr / Radarr / Lidarr)
|
||||||
|
# 3. SSHes to remote host and runs itself there (unless --local-only)
|
||||||
|
#
|
||||||
|
# ── USAGE ────────────────────────────────────────────────────────────────────
|
||||||
|
# webhook_setup.sh — configure local + remote
|
||||||
|
# webhook_setup.sh --local-only — local arrs only (used internally for SSH)
|
||||||
|
# webhook_setup.sh --dry-run — show what would be registered
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
LOCAL_ONLY=false
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--local-only) LOCAL_ONLY=true ;;
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ECOSYSTEM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
MASTER_CONF="$ECOSYSTEM_ROOT/Configurations/master.conf"
|
||||||
|
|
||||||
|
source "$ECOSYSTEM_ROOT/load_config.sh"
|
||||||
|
detect_hosts
|
||||||
|
|
||||||
|
WEBHOOK_NAME="Varaverk Upgrade"
|
||||||
|
|
||||||
|
# ── Resolve or generate the secret ──────────────────────────────────────────
|
||||||
|
# OVERRIDE_SECRET env var is set when called recursively via SSH from the
|
||||||
|
# primary host, so both ends use the same secret.
|
||||||
|
if [[ -n "${OVERRIDE_SECRET:-}" ]]; then
|
||||||
|
if [[ -z "${WEBHOOK_SECRET:-}" ]]; then
|
||||||
|
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$OVERRIDE_SECRET\"/" "$MASTER_CONF"
|
||||||
|
fi
|
||||||
|
WEBHOOK_SECRET="$OVERRIDE_SECRET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${WEBHOOK_SECRET:-}" ]]; then
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
WEBHOOK_SECRET="<would-generate>"
|
||||||
|
else
|
||||||
|
GENERATED=$(openssl rand -hex 32)
|
||||||
|
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$GENERATED\"/" "$MASTER_CONF"
|
||||||
|
WEBHOOK_SECRET="$GENERATED"
|
||||||
|
echo "Generated WEBHOOK_SECRET — saved to master.conf"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
WEBHOOK_URL="http://${LOCAL_IP}:${WEBHOOK_PORT}/webhook?key=${WEBHOOK_SECRET}"
|
||||||
|
|
||||||
|
# ── Helper: register webhook in one arr ─────────────────────────────────────
|
||||||
|
_register() {
|
||||||
|
local label="$1" base_url="$2" api_key="$3" api_ver="$4"
|
||||||
|
local import_field="$5" # onDownload (Sonarr/Radarr) or onReleaseImport (Lidarr)
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo " [$label] Would register → $WEBHOOK_URL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if already registered by name
|
||||||
|
local existing
|
||||||
|
existing=$(curl -sf --max-time 5 \
|
||||||
|
-H "X-Api-Key: $api_key" \
|
||||||
|
"$base_url/api/$api_ver/notification" 2>/dev/null) || {
|
||||||
|
echo " [$label] Cannot reach arr — skipping"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if echo "$existing" | grep -q "\"name\":[[:space:]]*\"${WEBHOOK_NAME}\""; then
|
||||||
|
echo " [$label] Already registered — skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local payload
|
||||||
|
payload=$(printf '{
|
||||||
|
"name": "%s",
|
||||||
|
"implementation": "Webhook",
|
||||||
|
"configContract": "WebhookSettings",
|
||||||
|
"onGrab": false,
|
||||||
|
"%s": true,
|
||||||
|
"onUpgrade": true,
|
||||||
|
"onRename": false,
|
||||||
|
"onHealthIssue": false,
|
||||||
|
"includeHealthWarnings": false,
|
||||||
|
"onApplicationUpdate": false,
|
||||||
|
"tags": [],
|
||||||
|
"fields": [
|
||||||
|
{"name": "url", "value": "%s"},
|
||||||
|
{"name": "method", "value": 1},
|
||||||
|
{"name": "username", "value": ""},
|
||||||
|
{"name": "password", "value": ""}
|
||||||
|
]
|
||||||
|
}' "$WEBHOOK_NAME" "$import_field" "$WEBHOOK_URL")
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -sf --max-time 5 -o /dev/null -w '%{http_code}' \
|
||||||
|
-X POST \
|
||||||
|
-H "X-Api-Key: $api_key" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "$payload" \
|
||||||
|
"$base_url/api/$api_ver/notification" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "$http_code" == "201" || "$http_code" == "200" ]]; then
|
||||||
|
echo " [$label] Registered ✅ → $WEBHOOK_URL"
|
||||||
|
else
|
||||||
|
echo " [$label] Failed (HTTP ${http_code:-timeout})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Local arrs ───────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "━━━ Upgrade Webhook Setup — $MY_ID ($LOCAL_SERVER_NAME) ━━━"
|
||||||
|
echo " LAN IP : $LOCAL_IP"
|
||||||
|
echo " Port : $WEBHOOK_PORT"
|
||||||
|
echo " URL : $WEBHOOK_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[[ -n "${SONARR_URL:-}" && -n "${SONARR_API_KEY:-}" ]] && \
|
||||||
|
_register "Sonarr" "$SONARR_URL" "$SONARR_API_KEY" "v3" "onDownload"
|
||||||
|
|
||||||
|
[[ -n "${RADARR_URL:-}" && -n "${RADARR_API_KEY:-}" ]] && \
|
||||||
|
_register "Radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3" "onDownload"
|
||||||
|
|
||||||
|
[[ -n "${LIDARR_URL:-}" && -n "${LIDARR_API_KEY:-}" ]] && \
|
||||||
|
_register "Lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "onReleaseImport"
|
||||||
|
|
||||||
|
# ── Remote host ───────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$LOCAL_ONLY" == false && -n "${REMOTE_ID:-}" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "━━━ Configuring remote: $REMOTE_SERVER_NAME ━━━"
|
||||||
|
|
||||||
|
remote_ip=$(resolve_tailscale_ip "$REMOTE_SERVER_NAME") || {
|
||||||
|
echo " Cannot resolve Tailscale IP for $REMOTE_SERVER_NAME — skipping"
|
||||||
|
echo " Run manually on that host: Tools/webhook_setup.sh --local-only"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_flags="--local-only"
|
||||||
|
[[ "$DRY_RUN" == true ]] && remote_flags="$remote_flags --dry-run"
|
||||||
|
|
||||||
|
ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
|
||||||
|
root@"$remote_ip" \
|
||||||
|
"OVERRIDE_SECRET='$WEBHOOK_SECRET' bash /boot/config/plugins/varaverk/Tools/webhook_setup.sh $remote_flags" \
|
||||||
|
|| echo " Remote setup failed — check SSH and that Varaverk is installed on $REMOTE_SERVER_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done."
|
||||||
Reference in New Issue
Block a user