#!/bin/bash # ============================================================================ # Webhook Relay Server # Installs and configures a webhook relay server using adnanh/webhook # ============================================================================ # Author : Phil Connor # Contact : contact@mylinux.work # License : MIT # Version : 1.0.0 # ============================================================================ set -euo pipefail # ── Defaults ───────────────────────────────────────────────────────────────── WEBHOOK_VERSION="${WEBHOOK_VERSION:-2.8.2}" WEBHOOK_PORT="${WEBHOOK_PORT:-9000}" WEBHOOK_USER="webhook" WEBHOOK_BIN="/usr/local/bin/webhook" WEBHOOK_CONF_DIR="/etc/webhook" WEBHOOK_HOOKS_FILE="${WEBHOOK_CONF_DIR}/hooks.json" WEBHOOK_HANDLER_DIR="${WEBHOOK_CONF_DIR}/handlers" WEBHOOK_LOG_DIR="/var/log/webhook" WEBHOOK_SECRET="${WEBHOOK_SECRET:-}" PROM_TEXTFILE_DIR="/var/lib/node_exporter/textfile" PROM_FILE="${PROM_TEXTFILE_DIR}/webhook.prom" SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME MODE="install" # ── Colors ─────────────────────────────────────────────────────────────────── if [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" CYAN="" BOLD="" RESET="" fi # ── Logging ────────────────────────────────────────────────────────────────── info() { echo -e "${GREEN}[INFO]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } die() { err "$*"; exit 1; } section() { echo "" echo -e " ${BOLD}${CYAN}── $1 ──${RESET}" echo "" } field() { printf " ${BOLD}%-24s${RESET} %s\n" "$1" "$2" } # ── Usage ──────────────────────────────────────────────────────────────────── show_help() { cat </dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then info "Installing missing dependencies: ${missing[*]}" if [[ "$OS_FAMILY" == "debian" ]]; then apt-get update -qq apt-get install -y -qq "${missing[@]}" else dnf install -y -q "${missing[@]}" fi fi } # ── Install webhook binary ────────────────────────────────────────────────── install_binary() { section "Installing webhook binary" if [[ -f "$WEBHOOK_BIN" ]]; then local current_ver current_ver=$("$WEBHOOK_BIN" -version 2>&1 || echo "unknown") info "Existing binary found: $current_ver" fi local download_url="https://github.com/adnanh/webhook/releases/download/${WEBHOOK_VERSION}/webhook-linux-${WEBHOOK_ARCH}.tar.gz" local tmp_dir tmp_dir="$(mktemp -d)" info "Downloading webhook ${WEBHOOK_VERSION} (${WEBHOOK_ARCH})..." if ! curl -fsSL "$download_url" -o "${tmp_dir}/webhook.tar.gz"; then rm -rf "$tmp_dir" die "Failed to download webhook from $download_url" fi tar -xzf "${tmp_dir}/webhook.tar.gz" -C "$tmp_dir" local extracted extracted=$(find "$tmp_dir" -name webhook -type f | head -1) if [[ -z "$extracted" ]]; then rm -rf "$tmp_dir" die "webhook binary not found in archive" fi install -m 0755 "$extracted" "$WEBHOOK_BIN" rm -rf "$tmp_dir" info "Installed: $WEBHOOK_BIN" field "Version" "$WEBHOOK_VERSION" field "Architecture" "$WEBHOOK_ARCH" } # ── Create user ────────────────────────────────────────────────────────────── create_user() { if id "$WEBHOOK_USER" &>/dev/null; then info "User $WEBHOOK_USER already exists" return fi useradd --system --no-create-home --shell /usr/sbin/nologin "$WEBHOOK_USER" info "Created system user: $WEBHOOK_USER" } # ── Directory structure ────────────────────────────────────────────────────── create_directories() { section "Creating directory structure" mkdir -p "$WEBHOOK_CONF_DIR" "$WEBHOOK_HANDLER_DIR" "$WEBHOOK_LOG_DIR" "$PROM_TEXTFILE_DIR" chown "$WEBHOOK_USER":"$WEBHOOK_USER" "$WEBHOOK_LOG_DIR" chown "$WEBHOOK_USER":"$WEBHOOK_USER" "$PROM_TEXTFILE_DIR" field "Config" "$WEBHOOK_CONF_DIR" field "Handlers" "$WEBHOOK_HANDLER_DIR" field "Logs" "$WEBHOOK_LOG_DIR" field "Metrics" "$PROM_TEXTFILE_DIR" } # ── hooks.json ─────────────────────────────────────────────────────────────── create_hooks_config() { section "Creating hooks.json" local secret="${WEBHOOK_SECRET:-CHANGE_ME}" if [[ -f "$WEBHOOK_HOOKS_FILE" ]]; then warn "hooks.json already exists — backing up to hooks.json.bak" cp "$WEBHOOK_HOOKS_FILE" "${WEBHOOK_HOOKS_FILE}.bak" fi cat > "$WEBHOOK_HOOKS_FILE" < "${WEBHOOK_HANDLER_DIR}/metrics.sh" <<'METRICS' #!/bin/bash # Prometheus textfile collector helper — sourced by handler scripts PROM_FILE="${PROM_FILE:-/var/lib/node_exporter/webhook.prom}" _WEBHOOK_START=$(date +%s%N) write_metrics() { local hook_id="$1" local status="$2" local end_time end_time=$(date +%s%N) local duration duration=$(echo "scale=3; ($end_time - $_WEBHOOK_START) / 1000000000" | bc 2>/dev/null || echo "0") local tmp_file="${PROM_FILE}.$$" local existing="" [[ -f "$PROM_FILE" ]] && existing=$(cat "$PROM_FILE") { echo "$existing" echo "# HELP webhook_receive_total Total webhook events received" echo "# TYPE webhook_receive_total counter" echo "webhook_receive_total{hook=\"${hook_id}\"} 1" if [[ "$status" == "success" ]]; then echo "# HELP webhook_success_total Successful handler executions" echo "# TYPE webhook_success_total counter" echo "webhook_success_total{hook=\"${hook_id}\"} 1" echo "# HELP webhook_last_success_timestamp Unix timestamp of last success" echo "# TYPE webhook_last_success_timestamp gauge" echo "webhook_last_success_timestamp{hook=\"${hook_id}\"} $(date +%s)" else echo "# HELP webhook_failure_total Failed handler executions" echo "# TYPE webhook_failure_total counter" echo "webhook_failure_total{hook=\"${hook_id}\"} 1" fi echo "# HELP webhook_handler_duration_seconds Handler execution time" echo "# TYPE webhook_handler_duration_seconds gauge" echo "webhook_handler_duration_seconds{hook=\"${hook_id}\"} ${duration}" } > "$tmp_file" mv "$tmp_file" "$PROM_FILE" } METRICS # ── Mirror sync handler ── cat > "${WEBHOOK_HANDLER_DIR}/mirror-sync.sh" <<'HANDLER' #!/bin/bash # Mirror sync handler — fetches from origin and pushes to mirror remote set -euo pipefail source "$(dirname "$0")/metrics.sh" REPO_NAME="${1:-}" MIRROR_BASE="/srv/git/mirrors" HOOK_ID="mirror-sync" if [[ -z "$REPO_NAME" ]]; then echo "No repository name received" >&2 write_metrics "$HOOK_ID" "failure" exit 1 fi MIRROR_DIR="${MIRROR_BASE}/${REPO_NAME}.git" if [[ ! -d "$MIRROR_DIR" ]]; then echo "Mirror not found: $MIRROR_DIR" >&2 write_metrics "$HOOK_ID" "failure" exit 1 fi cd "$MIRROR_DIR" git fetch --prune origin git push --mirror mirror write_metrics "$HOOK_ID" "success" echo "Mirror sync complete: $REPO_NAME" HANDLER # ── Deploy handler ── cat > "${WEBHOOK_HANDLER_DIR}/deploy.sh" <<'HANDLER' #!/bin/bash # Deploy handler — pulls latest code and restarts the application service set -euo pipefail source "$(dirname "$0")/metrics.sh" REPO_NAME="${1:-}" REF="${2:-}" DEPLOY_BASE="/srv/apps" HOOK_ID="deploy" if [[ -z "$REPO_NAME" ]]; then echo "No repository name received" >&2 write_metrics "$HOOK_ID" "failure" exit 1 fi APP_NAME="${REPO_NAME##*/}" DEPLOY_DIR="${DEPLOY_BASE}/${APP_NAME}" if [[ ! -d "$DEPLOY_DIR" ]]; then echo "Deploy directory not found: $DEPLOY_DIR" >&2 write_metrics "$HOOK_ID" "failure" exit 1 fi cd "$DEPLOY_DIR" git pull origin main if systemctl is-active --quiet "$APP_NAME"; then systemctl restart "$APP_NAME" echo "Restarted service: $APP_NAME" fi write_metrics "$HOOK_ID" "success" echo "Deploy complete: $APP_NAME (ref: $REF)" HANDLER # ── Notification relay handler ── cat > "${WEBHOOK_HANDLER_DIR}/notify.sh" <<'HANDLER' #!/bin/bash # Notification relay — posts push events to Slack and/or Discord set -euo pipefail source "$(dirname "$0")/metrics.sh" REPO_NAME="${1:-}" SENDER="${2:-unknown}" COMMIT_MSG="${3:-no message}" HOOK_ID="notify" SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}" if [[ -z "$SLACK_WEBHOOK_URL" && -z "$DISCORD_WEBHOOK_URL" ]]; then echo "No SLACK_WEBHOOK_URL or DISCORD_WEBHOOK_URL configured" >&2 write_metrics "$HOOK_ID" "failure" exit 1 fi PAYLOAD="{\"text\":\"[${REPO_NAME}] ${SENDER}: ${COMMIT_MSG}\"}" if [[ -n "$SLACK_WEBHOOK_URL" ]]; then curl -sf -X POST -H 'Content-Type: application/json' -d "$PAYLOAD" "$SLACK_WEBHOOK_URL" fi if [[ -n "$DISCORD_WEBHOOK_URL" ]]; then curl -sf -X POST -H 'Content-Type: application/json' -d "$PAYLOAD" "$DISCORD_WEBHOOK_URL" fi write_metrics "$HOOK_ID" "success" echo "Notification sent: $REPO_NAME ($SENDER)" HANDLER chmod 750 "${WEBHOOK_HANDLER_DIR}"/*.sh chown root:"$WEBHOOK_USER" "${WEBHOOK_HANDLER_DIR}"/*.sh info "Created handler: mirror-sync.sh" info "Created handler: deploy.sh" info "Created handler: notify.sh" info "Created helper: metrics.sh" } # ── Systemd service ────────────────────────────────────────────────────────── create_systemd_service() { section "Creating systemd service" cat > /etc/systemd/system/webhook.service < /etc/logrotate.d/webhook </dev/null || true endscript } LOGROTATE info "Created: /etc/logrotate.d/webhook" } # ── Status ─────────────────────────────────────────────────────────────────── show_status() { section "Webhook Relay Server — Status" if [[ -f "$WEBHOOK_BIN" ]]; then local ver ver=$("$WEBHOOK_BIN" -version 2>&1 || echo "unknown") field "Binary" "$WEBHOOK_BIN" field "Version" "$ver" else field "Binary" "not installed" fi if systemctl is-active --quiet webhook.service 2>/dev/null; then field "Service" "active (running)" elif systemctl is-enabled --quiet webhook.service 2>/dev/null; then field "Service" "enabled (not running)" else field "Service" "not installed" fi if [[ -f "$WEBHOOK_HOOKS_FILE" ]]; then local hook_count hook_count=$(jq 'length' "$WEBHOOK_HOOKS_FILE" 2>/dev/null || echo "?") field "Hooks config" "${WEBHOOK_HOOKS_FILE} (${hook_count} hooks)" else field "Hooks config" "not found" fi if [[ -d "$WEBHOOK_HANDLER_DIR" ]]; then local handler_count handler_count=$(find "$WEBHOOK_HANDLER_DIR" -name '*.sh' -not -name 'metrics.sh' | wc -l) field "Handlers" "${handler_count} scripts in ${WEBHOOK_HANDLER_DIR}" else field "Handlers" "not found" fi field "Log directory" "$WEBHOOK_LOG_DIR" field "Metrics file" "$PROM_FILE" echo "" if systemctl is-active --quiet webhook.service 2>/dev/null; then systemctl status webhook.service --no-pager -l 2>/dev/null | head -15 fi } # ── Uninstall ──────────────────────────────────────────────────────────────── do_uninstall() { check_root section "Uninstalling Webhook Relay Server" if systemctl is-active --quiet webhook.service 2>/dev/null; then systemctl stop webhook.service info "Stopped webhook.service" fi if systemctl is-enabled --quiet webhook.service 2>/dev/null; then systemctl disable webhook.service fi local items=( /etc/systemd/system/webhook.service /etc/logrotate.d/webhook "$WEBHOOK_BIN" ) for item in "${items[@]}"; do if [[ -e "$item" ]]; then rm -f "$item" info "Removed: $item" fi done if [[ -d "$WEBHOOK_CONF_DIR" ]]; then rm -rf "$WEBHOOK_CONF_DIR" info "Removed: $WEBHOOK_CONF_DIR" fi if [[ -d "$WEBHOOK_LOG_DIR" ]]; then rm -rf "$WEBHOOK_LOG_DIR" info "Removed: $WEBHOOK_LOG_DIR" fi if [[ -f "$PROM_FILE" ]]; then rm -f "$PROM_FILE" info "Removed: $PROM_FILE" fi if id "$WEBHOOK_USER" &>/dev/null; then userdel "$WEBHOOK_USER" 2>/dev/null || true info "Removed user: $WEBHOOK_USER" fi systemctl daemon-reload info "Uninstall complete" } # ── Summary ────────────────────────────────────────────────────────────────── print_summary() { section "Installation Summary" field "Binary" "$WEBHOOK_BIN ($WEBHOOK_VERSION)" field "Config" "$WEBHOOK_HOOKS_FILE" field "Handlers" "$WEBHOOK_HANDLER_DIR/" field "Service" "webhook.service (port $WEBHOOK_PORT)" field "Logs" "$WEBHOOK_LOG_DIR/webhook.log" field "Metrics" "$PROM_FILE" field "User" "$WEBHOOK_USER" echo "" info "Next steps:" echo " 1. Edit ${WEBHOOK_HOOKS_FILE} — set your webhook secret" echo " 2. Configure handler scripts in ${WEBHOOK_HANDLER_DIR}/" echo " 3. Start the service: systemctl start webhook" echo " 4. Test: curl -X POST http://localhost:${WEBHOOK_PORT}/hooks/mirror-sync" echo "" } # ── Main ───────────────────────────────────────────────────────────────────── main() { case "$MODE" in status) show_status ;; uninstall) do_uninstall ;; install) check_root detect_os detect_arch check_dependencies install_binary create_user create_directories create_hooks_config create_handlers create_systemd_service create_logrotate print_summary ;; esac } main