# Generated file. Do not edit. # Bundled on: 2025-11-25T11:15:48Z # Manifest: scripts/bundle.manifest # === Begin: scripts/lib/common.sh === #!/usr/bin/env bash # # Shared common utilities for Pulse shell scripts. # Provides logging, privilege escalation, command execution helpers, and cleanup management. set -o errexit set -o nounset set -o pipefail shopt -s extglob declare -gA COMMON__LOG_LEVELS=( [debug]=10 [info]=20 [warn]=30 [error]=40 ) declare -g COMMON__SCRIPT_PATH="" declare -g COMMON__SCRIPT_DIR="" declare -g COMMON__ORIGINAL_ARGS=() declare -g COMMON__LOG_LEVEL="info" declare -g COMMON__LOG_LEVEL_NUM=20 declare -g COMMON__DEBUG=false declare -g COMMON__COLOR_ENABLED=true declare -g COMMON__DRY_RUN=false declare -ag COMMON__CLEANUP_DESCRIPTIONS=() declare -ag COMMON__CLEANUP_COMMANDS=() declare -g COMMON__CLEANUP_REGISTERED=false # shellcheck disable=SC2034 declare -g COMMON__ANSI_RESET="" # shellcheck disable=SC2034 declare -g COMMON__ANSI_INFO="" # shellcheck disable=SC2034 declare -g COMMON__ANSI_WARN="" # shellcheck disable=SC2034 declare -g COMMON__ANSI_ERROR="" # shellcheck disable=SC2034 declare -g COMMON__ANSI_DEBUG="" # common::init # Initializes the common library. Call at the top of every script. # Sets script metadata, log level, color handling, traps, and stores original args. common::init() { COMMON__SCRIPT_PATH="$(common::__resolve_script_path)" COMMON__SCRIPT_DIR="$(dirname "${COMMON__SCRIPT_PATH}")" COMMON__ORIGINAL_ARGS=("$@") common::__configure_color common::__configure_log_level if [[ "${COMMON__DEBUG}" == true ]]; then common::log_debug "Debug mode enabled" fi if [[ "${COMMON__CLEANUP_REGISTERED}" == false ]]; then trap common::cleanup_run EXIT COMMON__CLEANUP_REGISTERED=true fi } # common::log_info "message" # Logs informational messages. Printed to stdout. common::log_info() { common::__log "info" "$@" } # common::log_warn "message" # Logs warning messages. Printed to stderr. common::log_warn() { common::__log "warn" "$@" } # common::log_error "message" # Logs error messages. Printed to stderr. common::log_error() { common::__log "error" "$@" } # common::log_debug "message" # Logs debug messages when log level is debug. Printed to stderr. common::log_debug() { common::__log "debug" "$@" } # common::fail "message" [--code N] # Logs an error message and exits with provided code (default 1). common::fail() { local exit_code=1 local message_parts=() while (($#)); do case "$1" in --code) shift exit_code="${1:-1}" ;; *) message_parts+=("$1") ;; esac shift || break done local message="${message_parts[*]}" common::log_error "${message}" if common::is_interactive; then echo "" if [[ -t 0 ]]; then read -p "Press Enter to exit..." elif [[ -e /dev/tty ]]; then read -p "Press Enter to exit..." < /dev/tty fi fi exit "${exit_code}" } # common::require_command cmd1 [cmd2 ...] # Verifies that required commands exist. Fails if any are missing. common::require_command() { local missing=() local cmd for cmd in "$@"; do if ! command -v "${cmd}" >/dev/null 2>&1; then missing+=("${cmd}") fi done if ((${#missing[@]} > 0)); then common::fail "Missing required command(s): ${missing[*]}" fi } # common::is_interactive # Returns success when running in an interactive TTY or PULSE_FORCE_INTERACTIVE=1. common::is_interactive() { if [[ "${PULSE_FORCE_INTERACTIVE:-0}" == "1" ]]; then return 0 fi [[ -t 0 && -t 1 ]] } # common::ensure_root [--allow-sudo] [--args "${COMMON__ORIGINAL_ARGS[@]}"] # Ensures the script is running with root privileges. Optionally re-execs with sudo. common::ensure_root() { local allow_sudo=false local reexec_args=() while (($#)); do case "$1" in --allow-sudo) allow_sudo=true ;; --args) shift reexec_args=("$@") break ;; *) common::log_warn "Unknown argument to common::ensure_root: $1" ;; esac shift || break done if [[ "${EUID}" -eq 0 ]]; then return 0 fi if [[ "${allow_sudo}" == true ]]; then if common::is_interactive; then common::log_info "Escalating privileges with sudo..." common::sudo_exec "${COMMON__SCRIPT_PATH}" "${reexec_args[@]}" exit 0 fi common::fail "Root privileges required; rerun with sudo or as root." fi common::fail "Root privileges required." } # common::sudo_exec command [args...] # Executes a command via sudo, providing user guidance on failure. common::sudo_exec() { local sudo_cmd="${PULSE_SUDO_CMD:-sudo}" if command -v "${sudo_cmd}" >/dev/null 2>&1; then exec "${sudo_cmd}" -- "$@" fi cat <<'EOF' >&2 Unable to escalate privileges automatically because sudo is unavailable. Please install sudo or rerun this script as root. EOF exit 1 } # common::run [--label desc] [--retries N] [--backoff "1 2 4"] command [args...] # Executes a command, respecting dry-run mode and retry policies. common::run() { local label="" local retries=1 local backoff=() local -a cmd=() while (($#)); do case "$1" in --label) label="$2" shift 2 continue ;; --retries) retries="$2" shift 2 continue ;; --backoff) read -r -a backoff <<<"$2" shift 2 continue ;; --) shift break ;; -*) common::log_warn "Unknown flag for common::run: $1" shift continue ;; *) break ;; esac done cmd=("$@") [[ -z "${label}" ]] && label="${cmd[*]}" if [[ "${COMMON__DRY_RUN}" == true ]]; then common::log_info "[dry-run] ${label}" return 0 fi local attempt=1 local exit_code=0 while (( attempt <= retries )); do if "${cmd[@]}"; then return 0 fi exit_code=$? if (( attempt == retries )); then common::log_error "Command failed (${exit_code}): ${label}" return "${exit_code}" fi local sleep_time="${backoff[$((attempt - 1))]:-1}" common::log_warn "Command failed (${exit_code}): ${label} — retrying in ${sleep_time}s (attempt ${attempt}/${retries})" sleep "${sleep_time}" ((attempt++)) done return "${exit_code}" } # common::run_capture [--label desc] command [args...] # Executes a command and captures stdout. Respects dry-run mode. common::run_capture() { local label="" while (($#)); do case "$1" in --label) label="$2" shift 2 continue ;; --) shift break ;; -*) common::log_warn "Unknown flag for common::run_capture: $1" shift continue ;; *) break ;; esac done local -a cmd=("$@") [[ -z "${label}" ]] && label="${cmd[*]}" if [[ "${COMMON__DRY_RUN}" == true ]]; then common::log_info "[dry-run] ${label}" echo "" return 0 fi "${cmd[@]}" } # common::temp_dir [--prefix name] # Creates a temporary directory tracked for cleanup and assigns it to . common::temp_dir() { local var_name="" local prefix="pulse-" if (($#)) && [[ $1 != --* ]]; then var_name="$1" shift fi while (($#)); do case "$1" in --prefix) prefix="$2" shift 2 continue ;; *) common::log_warn "Unknown argument to common::temp_dir: $1" shift continue ;; esac done local dir dir="$(mktemp -d -t "${prefix}XXXXXX")" if (( "${BASH_SUBSHELL:-0}" > 0 )); then common::log_warn "common::temp_dir invoked in subshell; cleanup handlers will not be registered automatically for ${dir}" else common::cleanup_push "Remove temp dir ${dir}" "rm -rf ${dir@Q}" fi if [[ -n "${var_name}" ]]; then printf -v "${var_name}" '%s' "${dir}" else printf '%s\n' "${dir}" fi } # common::cleanup_push "description" "command" # Adds a cleanup handler executed in LIFO order on exit. common::cleanup_push() { local description="${1:-}" local command="${2:-}" if [[ -z "${command}" ]]; then common::log_warn "Ignoring cleanup handler without command" return fi COMMON__CLEANUP_DESCRIPTIONS+=("${description}") COMMON__CLEANUP_COMMANDS+=("${command}") } # common::cleanup_run # Executes registered cleanup handlers. Called automatically via EXIT trap. common::cleanup_run() { local idx=$(( ${#COMMON__CLEANUP_COMMANDS[@]} - 1 )) while (( idx >= 0 )); do local command="${COMMON__CLEANUP_COMMANDS[$idx]}" local description="${COMMON__CLEANUP_DESCRIPTIONS[$idx]}" if [[ -n "${description}" ]]; then common::log_debug "Running cleanup: ${description}" fi eval "${command}" ((idx--)) done COMMON__CLEANUP_COMMANDS=() COMMON__CLEANUP_DESCRIPTIONS=() } # common::set_dry_run true|false # Enables or disables global dry-run mode. common::set_dry_run() { local flag="${1:-false}" case "${flag}" in true|1|yes) COMMON__DRY_RUN=true ;; false|0|no) COMMON__DRY_RUN=false ;; *) common::log_warn "Unknown dry-run flag: ${flag}" COMMON__DRY_RUN=true ;; esac } # common::is_dry_run # Returns success when dry-run mode is active. common::is_dry_run() { [[ "${COMMON__DRY_RUN}" == true ]] } # Internal helper: configure color output. common::__configure_color() { if [[ "${PULSE_NO_COLOR:-0}" == "1" || ! -t 1 ]]; then COMMON__COLOR_ENABLED=false fi if [[ "${COMMON__COLOR_ENABLED}" == true ]]; then COMMON__ANSI_RESET=$'\033[0m' COMMON__ANSI_INFO=$'\033[1;34m' COMMON__ANSI_WARN=$'\033[1;33m' COMMON__ANSI_ERROR=$'\033[1;31m' COMMON__ANSI_DEBUG=$'\033[1;35m' else COMMON__ANSI_RESET="" COMMON__ANSI_INFO="" COMMON__ANSI_WARN="" COMMON__ANSI_ERROR="" COMMON__ANSI_DEBUG="" fi } # Internal helper: set log level and debug flag. common::__configure_log_level() { if [[ "${PULSE_DEBUG:-0}" == "1" ]]; then COMMON__DEBUG=true COMMON__LOG_LEVEL="debug" else COMMON__LOG_LEVEL="${PULSE_LOG_LEVEL:-info}" fi COMMON__LOG_LEVEL="${COMMON__LOG_LEVEL,,}" COMMON__LOG_LEVEL_NUM="${COMMON__LOG_LEVELS[${COMMON__LOG_LEVEL}]:-20}" } # Internal helper: generic logger. common::__log() { local level="$1" shift local message="$*" local level_num="${COMMON__LOG_LEVELS[${level}]:-20}" if (( level_num < COMMON__LOG_LEVEL_NUM )); then return 0 fi local timestamp timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" local color="" case "${level}" in info) color="${COMMON__ANSI_INFO}" ;; warn) color="${COMMON__ANSI_WARN}" ;; error) color="${COMMON__ANSI_ERROR}" ;; debug) color="${COMMON__ANSI_DEBUG}" ;; esac local formatted="[$timestamp] [${level^^}] ${message}" if [[ "${level}" == "info" ]]; then printf '%s%s%s\n' "${color}" "${formatted}" "${COMMON__ANSI_RESET}" else printf '%s%s%s\n' "${color}" "${formatted}" "${COMMON__ANSI_RESET}" >&2 fi } # Internal helper: determine absolute script path. common::__resolve_script_path() { local source="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}" if [[ -z "${source}" ]]; then pwd return fi if [[ "${source}" == /* ]]; then printf '%s\n' "${source}" return fi printf '%s\n' "$(cd "$(dirname "${source}")" && pwd)/$(basename "${source}")" } # === End: scripts/lib/common.sh === # === Begin: scripts/lib/systemd.sh === #!/usr/bin/env bash # # Systemd management helpers for Pulse shell scripts. set -euo pipefail # Ensure the common library is available when sourced directly. if ! declare -F common::log_info >/dev/null 2>&1; then # shellcheck disable=SC1091 source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" fi declare -g SYSTEMD_TIMEOUT_SEC="${SYSTEMD_TIMEOUT_SEC:-20}" # systemd::safe_systemctl args... # Executes systemctl with sensible defaults and optional timeout guards. systemd::safe_systemctl() { if ! command -v systemctl >/dev/null 2>&1; then common::fail "systemctl not available on this host" fi local -a cmd systemd::__build_cmd cmd "$@" systemd::__wrap_timeout cmd local label="systemctl $*" common::run --label "${label}" -- "${cmd[@]}" } # systemd::detect_service_name [service...] # Returns the first existing service name from the provided list. systemd::detect_service_name() { local -a candidates=("$@") if ((${#candidates[@]} == 0)); then candidates=(pulse.service pulse-backend.service pulse-docker-agent.service pulse-hot-dev.service) fi local name for name in "${candidates[@]}"; do if systemd::service_exists "${name}"; then printf '%s\n' "$(systemd::__normalize_unit "${name}")" return 0 fi done return 1 } # systemd::service_exists service # Returns success if the given service unit exists. systemd::service_exists() { local unit unit="$(systemd::__normalize_unit "${1:-}")" || return 1 if command -v systemctl >/dev/null 2>&1 && ! common::is_dry_run; then local -a cmd systemd::__build_cmd cmd "list-unit-files" "${unit}" systemd::__wrap_timeout cmd local output="" if output="$("${cmd[@]}" 2>/dev/null)"; then [[ "${output}" =~ ^${unit}[[:space:]] ]] && return 0 fi fi local paths=( "/etc/systemd/system/${unit}" "/lib/systemd/system/${unit}" "/usr/lib/systemd/system/${unit}" ) local path for path in "${paths[@]}"; do if [[ -f "${path}" ]]; then return 0 fi done return 1 } # systemd::is_active service # Checks whether the given service is active. systemd::is_active() { local unit unit="$(systemd::__normalize_unit "${1:-}")" || return 1 if common::is_dry_run; then return 1 fi if ! command -v systemctl >/dev/null 2>&1; then return 1 fi local -a cmd systemd::__build_cmd cmd "is-active" "--quiet" "${unit}" systemd::__wrap_timeout cmd if "${cmd[@]}" >/dev/null 2>&1; then return 0 fi return 1 } # systemd::create_service /path/to/unit.service # Reads unit file content from stdin and writes it to the supplied path. systemd::create_service() { local target="${1:-}" local mode="${2:-0644}" if [[ -z "${target}" ]]; then common::fail "systemd::create_service requires a target path" fi local content content="$(cat)" if common::is_dry_run; then common::log_info "[dry-run] Would write systemd unit ${target}" common::log_debug "${content}" return 0 fi mkdir -p "$(dirname "${target}")" printf '%s' "${content}" > "${target}" chmod "${mode}" "${target}" common::log_info "Wrote unit file: ${target}" } # systemd::enable_and_start service # Reloads systemd, enables, and starts the given service. systemd::enable_and_start() { local unit unit="$(systemd::__normalize_unit "${1:-}")" || common::fail "Invalid systemd unit name" systemd::safe_systemctl daemon-reload systemd::safe_systemctl enable "${unit}" systemd::safe_systemctl start "${unit}" } # systemd::restart service # Safely restarts the given service. systemd::restart() { local unit unit="$(systemd::__normalize_unit "${1:-}")" || common::fail "Invalid systemd unit name" systemd::safe_systemctl restart "${unit}" } # Internal: build systemctl command array. systemd::__build_cmd() { local -n ref=$1 shift ref=("systemctl" "--no-ask-password" "--no-pager") ref+=("$@") } # Internal: wrap command with timeout if necessary. systemd::__wrap_timeout() { local -n ref=$1 if ! command -v timeout >/dev/null 2>&1; then return fi if systemd::__should_timeout; then local -a wrapped=("timeout" "${SYSTEMD_TIMEOUT_SEC}s") wrapped+=("${ref[@]}") ref=("${wrapped[@]}") fi } # Internal: determine if we are in a container environment. systemd::__should_timeout() { if [[ -f /run/systemd/container ]]; then return 0 fi if command -v systemd-detect-virt >/dev/null 2>&1; then if systemd-detect-virt --quiet --container; then return 0 fi fi return 1 } # Internal: normalize service names to include .service suffix. systemd::__normalize_unit() { local unit="${1:-}" if [[ -z "${unit}" ]]; then return 1 fi if [[ "${unit}" != *.service ]]; then unit="${unit}.service" fi printf '%s\n' "${unit}" } # === End: scripts/lib/systemd.sh === # === Begin: scripts/lib/http.sh === #!/usr/bin/env bash # # HTTP helpers for Pulse shell scripts (downloads, API calls, GitHub queries). set -euo pipefail # Ensure common library is loaded when sourced directly. if ! declare -F common::log_info >/dev/null 2>&1; then # shellcheck disable=SC1091 source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" fi declare -g HTTP_DEFAULT_RETRIES="${HTTP_DEFAULT_RETRIES:-3}" declare -g HTTP_DEFAULT_BACKOFF="${HTTP_DEFAULT_BACKOFF:-1 3 5}" # http::detect_download_tool # Emits the preferred download tool (curl/wget) or fails if neither exist. http::detect_download_tool() { if command -v curl >/dev/null 2>&1; then printf 'curl\n' return 0 fi if command -v wget >/dev/null 2>&1; then printf 'wget\n' return 0 fi return 1 } # http::download --url URL --output PATH [--insecure] [--quiet] [--header "Name: value"] # [--retries N] [--backoff "1 3 5"] # Downloads the specified URL to PATH using curl or wget. http::download() { local url="" local output="" local insecure=false local quiet=false local retries="${HTTP_DEFAULT_RETRIES}" local backoff="${HTTP_DEFAULT_BACKOFF}" local -a headers=() while (($#)); do case "$1" in --url) url="$2" shift 2 ;; --output) output="$2" shift 2 ;; --insecure) insecure=true shift ;; --quiet) quiet=true shift ;; --header) headers+=("$2") shift 2 ;; --retries) retries="$2" shift 2 ;; --backoff) backoff="$2" shift 2 ;; *) common::log_warn "Unknown flag for http::download: $1" shift ;; esac done if [[ -z "${url}" || -z "${output}" ]]; then common::fail "http::download requires --url and --output" fi if common::is_dry_run; then common::log_info "[dry-run] Download ${url} -> ${output}" return 0 fi local tool if ! tool="$(http::detect_download_tool)"; then common::fail "No download tool available (install curl or wget)" fi mkdir -p "$(dirname "${output}")" local -a cmd if [[ "${tool}" == "curl" ]]; then cmd=(curl -fL --connect-timeout 15) if [[ "${quiet}" == true ]]; then cmd+=(-sS) else cmd+=(--progress-bar) fi if [[ "${insecure}" == true ]]; then cmd+=(-k) fi local header for header in "${headers[@]}"; do cmd+=(-H "${header}") done cmd+=(-o "${output}" "${url}") else cmd=(wget --tries=3) if [[ "${quiet}" == true ]]; then cmd+=(-q) else cmd+=(--progress=bar:force) fi if [[ "${insecure}" == true ]]; then cmd+=(--no-check-certificate) fi local header for header in "${headers[@]}"; do cmd+=(--header="${header}") done cmd+=(-O "${output}" "${url}") fi local -a run_args=(--label "download ${url}") [[ -n "${retries}" ]] && run_args+=(--retries "${retries}") [[ -n "${backoff}" ]] && run_args+=(--backoff "${backoff}") common::run "${run_args[@]}" -- "${cmd[@]}" } # http::api_call --url URL [--method METHOD] [--token TOKEN] [--bearer TOKEN] # [--body DATA] [--header "Name: value"] [--insecure] # Performs an API request and prints the response body. http::api_call() { local url="" local method="GET" local token="" local bearer="" local body="" local insecure=false local -a headers=() while (($#)); do case "$1" in --url) url="$2" shift 2 ;; --method) method="$2" shift 2 ;; --token) token="$2" shift 2 ;; --bearer) bearer="$2" shift 2 ;; --body) body="$2" shift 2 ;; --header) headers+=("$2") shift 2 ;; --insecure) insecure=true shift ;; *) common::log_warn "Unknown flag for http::api_call: $1" shift ;; esac done if [[ -z "${url}" ]]; then common::fail "http::api_call requires --url" fi if common::is_dry_run; then common::log_info "[dry-run] API ${method} ${url}" return 0 fi local tool if ! tool="$(http::detect_download_tool)"; then common::fail "No HTTP client available (install curl or wget)" fi local -a cmd if [[ "${tool}" == "curl" ]]; then cmd=(curl -fsSL) [[ "${insecure}" == true ]] && cmd+=(-k) [[ -n "${method}" ]] && cmd+=(-X "${method}") if [[ -n "${body}" ]]; then cmd+=(-d "${body}") fi if [[ -n "${token}" ]]; then headers+=("X-API-Token: ${token}") fi if [[ -n "${bearer}" ]]; then headers+=("Authorization: Bearer ${bearer}") fi local header for header in "${headers[@]}"; do cmd+=(-H "${header}") done cmd+=("${url}") else cmd=(wget -qO-) [[ "${insecure}" == true ]] && cmd+=(--no-check-certificate) [[ -n "${method}" ]] && cmd+=(--method="${method}") if [[ -n "${body}" ]]; then cmd+=(--body-data="${body}") fi if [[ -n "${token}" ]]; then cmd+=(--header="X-API-Token: ${token}") fi if [[ -n "${bearer}" ]]; then cmd+=(--header="Authorization: Bearer ${bearer}") fi local header for header in "${headers[@]}"; do cmd+=(--header="${header}") done cmd+=("${url}") fi common::run_capture --label "api ${method} ${url}" -- "${cmd[@]}" } # http::get_github_latest_release owner/repo # Echoes the latest release tag for a GitHub repository. http::get_github_latest_release() { local repo="${1:-}" if [[ -z "${repo}" ]]; then common::fail "http::get_github_latest_release requires owner/repo argument" fi local response response="$(http::api_call --url "https://api.github.com/repos/${repo}/releases/latest" --header "Accept: application/vnd.github+json" 2>/dev/null || true)" if [[ -z "${response}" ]]; then return 1 fi if [[ "${response}" =~ \"tag_name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then printf '%s\n' "${BASH_REMATCH[1]}" return 0 fi if [[ "${response}" =~ \"name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then printf '%s\n' "${BASH_REMATCH[1]}" return 0 fi common::log_warn "Unable to parse GitHub release tag for ${repo}" return 1 } # http::parse_bool value # Parses truthy/falsy strings and prints canonical true/false. http::parse_bool() { local input="${1:-}" local lowered="${input,,}" case "${lowered}" in 1|true|yes|y|on) printf 'true\n' return 0 ;; 0|false|no|n|off|"") printf 'false\n' return 0 ;; esac return 1 } # === End: scripts/lib/http.sh === # === Begin: scripts/install-docker-agent-v2.sh === #!/usr/bin/env bash # # Pulse Docker Agent Installer/Uninstaller (v2) # Refactored to leverage shared script libraries. set -euo pipefail LIB_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="${LIB_ROOT}/lib" if [[ -f "${LIB_DIR}/common.sh" ]]; then # shellcheck disable=SC1090 source "${LIB_DIR}/common.sh" # shellcheck disable=SC1090 source "${LIB_DIR}/systemd.sh" # shellcheck disable=SC1090 source "${LIB_DIR}/http.sh" fi common::init "$@" log_info() { common::log_info "$1" } log_warn() { common::log_warn "$1" } log_error() { common::log_error "$1" } log_success() { common::log_info "[ OK ] $1" } trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } determine_agent_identifier() { local agent_id="" if command -v docker &> /dev/null; then agent_id=$(docker info --format '{{.ID}}' 2>/dev/null | head -n1 | tr -d '[:space:]') fi if [[ -z "$agent_id" ]] && [[ -r /etc/machine-id ]]; then agent_id=$(tr -d '[:space:]' < /etc/machine-id) fi if [[ -z "$agent_id" ]]; then agent_id=$(hostname 2>/dev/null | tr -d '[:space:]') fi printf '%s' "$agent_id" } log_header() { printf '\n== %s ==\n' "$1" } quote_shell_arg() { local value="$1" value=${value//\'/\'\\\'\'} printf "'%s'" "$value" } parse_bool() { local result if result="$(http::parse_bool "${1:-}")"; then PARSED_BOOL="$result" return 0 fi return 1 } parse_target_spec() { local spec="$1" local raw_url raw_token raw_insecure IFS='|' read -r raw_url raw_token raw_insecure <<< "$spec" raw_url=$(trim "$raw_url") raw_token=$(trim "$raw_token") raw_insecure=$(trim "$raw_insecure") if [[ -z "$raw_url" || -z "$raw_token" ]]; then echo "Error: invalid target spec \"$spec\". Expected format url|token[|insecure]." >&2 return 1 fi PARSED_TARGET_URL="${raw_url%/}" PARSED_TARGET_TOKEN="$raw_token" if [[ -n "$raw_insecure" ]]; then if ! parse_bool "$raw_insecure"; then echo "Error: invalid insecure flag \"$raw_insecure\" in target spec \"$spec\"." >&2 return 1 fi PARSED_TARGET_INSECURE="$PARSED_BOOL" else PARSED_TARGET_INSECURE="false" fi return 0 } split_targets_from_env() { local value="$1" if [[ -z "$value" ]]; then return 0 fi value="${value//$'\n'/;}" IFS=';' read -ra __env_targets <<< "$value" for entry in "${__env_targets[@]}"; do local trimmed trimmed=$(trim "$entry") if [[ -n "$trimmed" ]]; then printf '%s\n' "$trimmed" fi done } # Early runtime detection to hand off Podman installs to the container-aware script. ORIGINAL_ARGS=("$@") DETECTED_RUNTIME="${PULSE_RUNTIME:-}" if [[ -z "$DETECTED_RUNTIME" ]]; then idx=0 total_args=${#ORIGINAL_ARGS[@]} while [[ $idx -lt $total_args ]]; do arg="${ORIGINAL_ARGS[$idx]}" case "$arg" in --runtime) if (( idx + 1 < total_args )); then DETECTED_RUNTIME="${ORIGINAL_ARGS[$((idx + 1))]}" fi ((idx += 2)) continue ;; --runtime=*) DETECTED_RUNTIME="${arg#--runtime=}" ;; esac ((idx += 1)) done unset total_args fi if [[ -n "$DETECTED_RUNTIME" ]]; then runtime_lower=$(printf '%s' "$DETECTED_RUNTIME" | tr '[:upper:]' '[:lower:]') if [[ "$runtime_lower" == "podman" ]]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "${SCRIPT_DIR}/install-container-agent.sh" ]]; then exec "${SCRIPT_DIR}/install-container-agent.sh" "${ORIGINAL_ARGS[@]}" fi common::log_error "Podman runtime requested but install-container-agent.sh not found." exit 1 fi fi extract_targets_from_service() { local file="$1" [[ ! -f "$file" ]] && return local line value # Prefer explicit multi-target configuration if present line=$(grep -m1 'PULSE_TARGETS=' "$file" 2>/dev/null || true) if [[ -n "$line" ]]; then value=$(printf '%s\n' "$line" | sed -n 's/.*PULSE_TARGETS=\([^"]*\).*/\1/p') if [[ -n "$value" ]]; then IFS=';' read -ra __service_targets <<< "$value" for entry in "${__service_targets[@]}"; do entry=$(trim "$entry") if [[ -n "$entry" ]]; then printf '%s\n' "$entry" fi done fi return fi local url="" local token="" local insecure="false" line=$(grep -m1 'PULSE_URL=' "$file" 2>/dev/null || true) if [[ -n "$line" ]]; then value="${line#*PULSE_URL=}" value="${value%\"*}" url=$(trim "$value") fi line=$(grep -m1 'PULSE_TOKEN=' "$file" 2>/dev/null || true) if [[ -n "$line" ]]; then value="${line#*PULSE_TOKEN=}" value="${value%\"*}" token=$(trim "$value") fi line=$(grep -m1 'PULSE_INSECURE_SKIP_VERIFY=' "$file" 2>/dev/null || true) if [[ -n "$line" ]]; then value="${line#*PULSE_INSECURE_SKIP_VERIFY=}" value="${value%\"*}" if parse_bool "$value"; then insecure="$PARSED_BOOL" fi fi local exec_line exec_line=$(grep -m1 '^ExecStart=' "$file" 2>/dev/null || true) if [[ -n "$exec_line" ]]; then if [[ -z "$url" ]]; then if [[ "$exec_line" =~ --url[[:space:]]+\"([^\"]+)\" ]]; then url="${BASH_REMATCH[1]}" elif [[ "$exec_line" =~ --url[[:space:]]+([^[:space:]]+) ]]; then url="${BASH_REMATCH[1]}" fi fi if [[ -z "$token" ]]; then if [[ "$exec_line" =~ --token[[:space:]]+\"([^\"]+)\" ]]; then token="${BASH_REMATCH[1]}" elif [[ "$exec_line" =~ --token[[:space:]]+([^[:space:]]+) ]]; then token="${BASH_REMATCH[1]}" fi fi if [[ "$insecure" != "true" && "$exec_line" == *"--insecure"* ]]; then insecure="true" fi fi url=$(trim "$url") token=$(trim "$token") if [[ -n "$url" && -n "$token" ]]; then printf '%s|%s|%s\n' "$url" "$token" "$insecure" fi } detect_agent_path_from_service() { if [[ -n "$SERVICE_PATH" && -f "$SERVICE_PATH" ]]; then local exec_line exec_line=$(grep -m1 '^ExecStart=' "$SERVICE_PATH" 2>/dev/null || true) if [[ -n "$exec_line" ]]; then local value="${exec_line#ExecStart=}" value=$(trim "$value") if [[ -n "$value" ]]; then printf '%s' "${value%%[[:space:]]*}" return fi fi fi } detect_agent_path_from_unraid() { if [[ -n "$UNRAID_STARTUP" && -f "$UNRAID_STARTUP" ]]; then local match match=$(grep -m1 -o '/[^[:space:]]*pulse-docker-agent' "$UNRAID_STARTUP" 2>/dev/null || true) if [[ -n "$match" ]]; then printf '%s' "$match" return fi fi } detect_existing_agent_path() { local path path=$(detect_agent_path_from_service) if [[ -n "$path" ]]; then printf '%s' "$path" return fi path=$(detect_agent_path_from_unraid) if [[ -n "$path" ]]; then printf '%s' "$path" return fi if command -v pulse-docker-agent >/dev/null 2>&1; then path=$(command -v pulse-docker-agent) if [[ -n "$path" ]]; then printf '%s' "$path" return fi fi } ensure_agent_path_writable() { local file_path="$1" local dir="${file_path%/*}" if [[ -z "$dir" || "$file_path" != /* ]]; then return 1 fi if [[ ! -d "$dir" ]]; then if ! mkdir -p "$dir" 2>/dev/null; then return 1 fi fi local test_file="$dir/.pulse-agent-write-test-$$" if ! touch "$test_file" 2>/dev/null; then return 1 fi rm -f "$test_file" 2>/dev/null || true return 0 } select_agent_path_for_install() { local candidates=() declare -A seen=() local selected="" local default_attempted="false" local default_failed="false" if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then candidates+=("$AGENT_PATH_OVERRIDE") fi if [[ -n "$EXISTING_AGENT_PATH" ]]; then candidates+=("$EXISTING_AGENT_PATH") fi candidates+=("$DEFAULT_AGENT_PATH") for fallback in "${AGENT_FALLBACK_PATHS[@]}"; do candidates+=("$fallback") done for candidate in "${candidates[@]}"; do candidate=$(trim "$candidate") if [[ -z "$candidate" || "$candidate" != /* ]]; then continue fi if [[ -n "${seen[$candidate]:-}" ]]; then continue fi seen["$candidate"]=1 if [[ "$candidate" == "$DEFAULT_AGENT_PATH" ]]; then default_attempted="true" fi if ensure_agent_path_writable "$candidate"; then selected="$candidate" if [[ "$candidate" == "$DEFAULT_AGENT_PATH" ]]; then DEFAULT_AGENT_PATH_WRITABLE="true" else if [[ "$default_attempted" == "true" && "$default_failed" == "true" && "$OVERRIDE_SPECIFIED" == "false" ]]; then AGENT_PATH_NOTE="Note: Detected that $DEFAULT_AGENT_PATH is not writable. Using fallback path: $candidate" fi fi break else if [[ "$candidate" == "$DEFAULT_AGENT_PATH" ]]; then default_failed="true" DEFAULT_AGENT_PATH_WRITABLE="false" fi fi done if [[ -z "$selected" ]]; then echo "Error: Could not find a writable location for the agent binary." >&2 if [[ "$OVERRIDE_SPECIFIED" == "true" ]]; then echo "Provided agent path: $AGENT_PATH_OVERRIDE" >&2 fi exit 1 fi printf '%s' "$selected" } resolve_agent_path_for_uninstall() { if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then printf '%s' "$AGENT_PATH_OVERRIDE" return fi local existing_path existing_path=$(detect_existing_agent_path) if [[ -n "$existing_path" ]]; then printf '%s' "$existing_path" return fi printf '%s' "$DEFAULT_AGENT_PATH" } # Pulse Docker Agent Installer/Uninstaller # Install (single target): # curl -fSL http://pulse.example.com/install-docker-agent.sh -o /tmp/pulse-install-docker-agent.sh && \ # sudo bash /tmp/pulse-install-docker-agent.sh --url http://pulse.example.com --token && \ # rm -f /tmp/pulse-install-docker-agent.sh # Install (multi-target fan-out): # curl -fSL http://pulse.example.com/install-docker-agent.sh -o /tmp/pulse-install-docker-agent.sh && \ # sudo bash /tmp/pulse-install-docker-agent.sh -- \ # --target https://pulse.example.com| \ # --target https://pulse-dr.example.com| && \ # rm -f /tmp/pulse-install-docker-agent.sh # Uninstall: # curl -fSL http://pulse.example.com/install-docker-agent.sh -o /tmp/pulse-install-docker-agent.sh && \ # sudo bash /tmp/pulse-install-docker-agent.sh --uninstall && \ # rm -f /tmp/pulse-install-docker-agent.sh PULSE_URL="" DEFAULT_AGENT_PATH="/usr/local/bin/pulse-docker-agent" AGENT_FALLBACK_PATHS=( "/opt/pulse/bin/pulse-docker-agent" "/opt/bin/pulse-docker-agent" "/var/lib/pulse/bin/pulse-docker-agent" ) AGENT_PATH_OVERRIDE="${PULSE_AGENT_PATH:-}" OVERRIDE_SPECIFIED="false" if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then OVERRIDE_SPECIFIED="true" fi AGENT_PATH_NOTE="" DEFAULT_AGENT_PATH_WRITABLE="unknown" EXISTING_AGENT_PATH="" AGENT_PATH="" SERVICE_PATH="/etc/systemd/system/pulse-docker-agent.service" UNRAID_STARTUP="/boot/config/go.d/pulse-docker-agent.sh" LOG_PATH="/var/log/pulse-docker-agent.log" INTERVAL="30s" UNINSTALL=false TOKEN="${PULSE_TOKEN:-}" DOWNLOAD_ARCH="" TARGET_SPECS=() PULSE_TARGETS_ENV="${PULSE_TARGETS:-}" DEFAULT_INSECURE="$(trim "${PULSE_INSECURE_SKIP_VERIFY:-}")" PRIMARY_URL="" PRIMARY_TOKEN="" PRIMARY_INSECURE="false" JOINED_TARGETS="" ORIGINAL_ARGS=("$@") # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --url) PULSE_URL="$2" shift 2 ;; --interval) INTERVAL="$2" shift 2 ;; --uninstall) UNINSTALL=true shift ;; --token) TOKEN="$2" shift 2 ;; --target) TARGET_SPECS+=("$2") shift 2 ;; --agent-path) AGENT_PATH_OVERRIDE="$2" OVERRIDE_SPECIFIED="true" shift 2 ;; --dry-run) common::set_dry_run true shift ;; *) echo "Unknown option: $1" echo "Usage: $0 --url --token [--interval 30s]" echo " $0 --agent-path /custom/path/pulse-docker-agent" echo " $0 --uninstall" exit 1 ;; esac done # Normalize PULSE_URL - strip trailing slashes to prevent double-slash issues PULSE_URL="${PULSE_URL%/}" if ! common::is_dry_run; then common::ensure_root --allow-sudo --args "${ORIGINAL_ARGS[@]}" else common::log_debug "Skipping privilege escalation due to dry-run mode" fi AGENT_PATH_OVERRIDE=$(trim "$AGENT_PATH_OVERRIDE") if [[ -z "$AGENT_PATH_OVERRIDE" ]]; then OVERRIDE_SPECIFIED="false" fi if [[ -n "$AGENT_PATH_OVERRIDE" && "$AGENT_PATH_OVERRIDE" != /* ]]; then echo "Error: --agent-path must be an absolute path." >&2 exit 1 fi EXISTING_AGENT_PATH=$(detect_existing_agent_path) if [[ "$UNINSTALL" = true ]]; then AGENT_PATH=$(resolve_agent_path_for_uninstall) else AGENT_PATH=$(select_agent_path_for_install) fi # Handle uninstall if [ "$UNINSTALL" = true ]; then echo "===================================" echo "Pulse Docker Agent Uninstaller" echo "===================================" echo "" # Stop and disable systemd service if command -v systemctl &> /dev/null && [ -f "$SERVICE_PATH" ]; then echo "Stopping systemd service..." systemctl stop pulse-docker-agent 2>/dev/null || true systemctl disable pulse-docker-agent 2>/dev/null || true rm -f "$SERVICE_PATH" systemctl daemon-reload echo "✓ Systemd service removed" fi # Stop running agent process if pgrep -f pulse-docker-agent > /dev/null; then echo "Stopping agent process..." pkill -f pulse-docker-agent || true sleep 1 echo "✓ Agent process stopped" fi # Remove binary if [ -f "$AGENT_PATH" ]; then rm -f "$AGENT_PATH" echo "✓ Agent binary removed" fi # Remove Unraid startup script if [ -f "$UNRAID_STARTUP" ]; then rm -f "$UNRAID_STARTUP" echo "✓ Unraid startup script removed" fi # Remove Synology Upstart config if [ -f "/etc/init/pulse-docker-agent.conf" ]; then initctl stop pulse-docker-agent 2>/dev/null || true rm -f "/etc/init/pulse-docker-agent.conf" echo "✓ Synology Upstart config removed" fi # Stop and remove launchd service (macOS) if [[ "$(uname -s)" == "Darwin" ]]; then LAUNCHD_PLIST="/Library/LaunchDaemons/com.pulse.docker-agent.plist" if [[ -f "$LAUNCHD_PLIST" ]]; then echo "Stopping launchd service..." launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true rm -f "$LAUNCHD_PLIST" echo "✓ Launchd service removed" fi fi # Remove log file if [ -f "$LOG_PATH" ]; then rm -f "$LOG_PATH" echo "✓ Log file removed" fi echo "" echo "===================================" echo "✓ Uninstall complete!" echo "===================================" echo "" echo "The Pulse Docker agent has been removed from this system." echo "" exit 0 fi # Validate target configuration for install if [[ "$UNINSTALL" != true ]]; then declare -a RAW_TARGETS=() if [[ ${#TARGET_SPECS[@]} -gt 0 ]]; then RAW_TARGETS+=("${TARGET_SPECS[@]}") fi if [[ -n "$PULSE_TARGETS_ENV" ]]; then mapfile -t ENV_TARGETS < <(split_targets_from_env "$PULSE_TARGETS_ENV") if [[ ${#ENV_TARGETS[@]} -gt 0 ]]; then RAW_TARGETS+=("${ENV_TARGETS[@]}") fi fi TOKEN=$(trim "$TOKEN") PULSE_URL=$(trim "$PULSE_URL") if [[ ${#RAW_TARGETS[@]} -eq 0 ]]; then if [[ -z "$PULSE_URL" || -z "$TOKEN" ]]; then echo "Error: Provide --target / PULSE_TARGETS or legacy --url and --token values." echo "" echo "Usage:" echo " Install: $0 --target https://pulse.example.com| [--target ...] [--interval 30s]" echo " Legacy: $0 --url http://pulse.example.com --token [--interval 30s]" echo " Uninstall: $0 --uninstall" exit 1 fi if [[ -n "$DEFAULT_INSECURE" ]]; then if ! parse_bool "$DEFAULT_INSECURE"; then echo "Error: invalid PULSE_INSECURE_SKIP_VERIFY value \"$DEFAULT_INSECURE\"." >&2 exit 1 fi PRIMARY_INSECURE="$PARSED_BOOL" else PRIMARY_INSECURE="false" fi RAW_TARGETS+=("${PULSE_URL%/}|$TOKEN|$PRIMARY_INSECURE") fi if [[ -f "$SERVICE_PATH" && ${#RAW_TARGETS[@]} -eq 0 ]]; then mapfile -t EXISTING_TARGETS < <(extract_targets_from_service "$SERVICE_PATH") if [[ ${#EXISTING_TARGETS[@]} -gt 0 ]]; then RAW_TARGETS+=("${EXISTING_TARGETS[@]}") fi fi declare -A SEEN_TARGETS=() TARGETS=() for spec in "${RAW_TARGETS[@]}"; do if ! parse_target_spec "$spec"; then exit 1 fi local_normalized="${PARSED_TARGET_URL}|${PARSED_TARGET_TOKEN}|${PARSED_TARGET_INSECURE}" if [[ -z "$PRIMARY_URL" ]]; then PRIMARY_URL="$PARSED_TARGET_URL" PRIMARY_TOKEN="$PARSED_TARGET_TOKEN" PRIMARY_INSECURE="$PARSED_TARGET_INSECURE" fi if [[ -n "${SEEN_TARGETS[$local_normalized]:-}" ]]; then continue fi SEEN_TARGETS[$local_normalized]=1 TARGETS+=("$local_normalized") done if [[ ${#TARGETS[@]} -eq 0 ]]; then echo "Error: no valid Pulse targets provided." >&2 exit 1 fi JOINED_TARGETS=$(printf "%s;" "${TARGETS[@]}") JOINED_TARGETS="${JOINED_TARGETS%;}" # Backwards compatibility for older agent versions PULSE_URL="$PRIMARY_URL" TOKEN="$PRIMARY_TOKEN" fi log_header "Pulse Docker Agent Installer" if [[ "$UNINSTALL" != true ]]; then AGENT_IDENTIFIER=$(determine_agent_identifier) else AGENT_IDENTIFIER="" fi if [[ -n "$AGENT_PATH_NOTE" ]]; then log_warn "$AGENT_PATH_NOTE" fi log_info "Primary Pulse URL : $PRIMARY_URL" if [[ ${#TARGETS[@]} -gt 1 ]]; then log_info "Additional targets : $(( ${#TARGETS[@]} - 1 ))" fi log_info "Install path : $AGENT_PATH" log_info "Log directory : /var/log/pulse-docker-agent" log_info "Reporting interval: $INTERVAL" if [[ "$UNINSTALL" != true ]]; then log_info "API token : provided" if [[ -n "$AGENT_IDENTIFIER" ]]; then log_info "Docker host ID : $AGENT_IDENTIFIER" fi log_info "Targets:" for spec in "${TARGETS[@]}"; do IFS='|' read -r target_url _ target_insecure <<< "$spec" if [[ "$target_insecure" == "true" ]]; then log_info " • $target_url (skip TLS verify)" else log_info " • $target_url" fi done fi printf '\n' # Detect architecture for download if [[ "$UNINSTALL" != true ]]; then ARCH=$(uname -m) case "$ARCH" in x86_64|amd64) DOWNLOAD_ARCH="linux-amd64" ;; aarch64|arm64) DOWNLOAD_ARCH="linux-arm64" ;; armv7l|armhf|armv7) DOWNLOAD_ARCH="linux-armv7" ;; *) DOWNLOAD_ARCH="" log_warn "Unknown architecture '$ARCH'. Falling back to default agent binary." ;; esac fi # Check if Docker is installed if ! command -v docker &> /dev/null; then log_warn 'Docker not found. The agent requires Docker to be installed.' if common::is_dry_run; then log_warn 'Dry-run mode: skipping Docker enforcement.' else read -p "Continue anyway? (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi fi if [[ "$UNINSTALL" != true ]] && command -v systemctl >/dev/null 2>&1; then if systemd::service_exists "pulse-docker-agent"; then if systemd::is_active "pulse-docker-agent"; then systemd::safe_systemctl stop "pulse-docker-agent" fi fi fi # Download agent binary log_info "Downloading agent binary" DOWNLOAD_URL_BASE="$PRIMARY_URL/download/pulse-docker-agent" DOWNLOAD_URL="$DOWNLOAD_URL_BASE" if [[ -n "$DOWNLOAD_ARCH" ]]; then DOWNLOAD_URL="$DOWNLOAD_URL?arch=$DOWNLOAD_ARCH" fi download_agent_binary() { local primary_url="$1" local fallback_url="$2" local -a download_args=(--url "${primary_url}" --output "${AGENT_PATH}" --retries 3 --backoff "1 3 5") if [[ "$PRIMARY_INSECURE" == "true" ]]; then download_args+=(--insecure) fi if http::download "${download_args[@]}"; then AGENT_DOWNLOAD_SOURCE="${primary_url}" return 0 fi if ! common::is_dry_run; then rm -f "$AGENT_PATH" 2>/dev/null || true fi if [[ "${fallback_url}" != "${primary_url}" && -n "${fallback_url}" ]]; then log_info 'Falling back to server default agent binary' download_args=(--url "${fallback_url}" --output "${AGENT_PATH}" --retries 3 --backoff "1 3 5") if [[ "$PRIMARY_INSECURE" == "true" ]]; then download_args+=(--insecure) fi if http::download "${download_args[@]}"; then AGENT_DOWNLOAD_SOURCE="${fallback_url}" return 0 fi if ! common::is_dry_run; then rm -f "$AGENT_PATH" 2>/dev/null || true fi fi return 1 } unset AGENT_DOWNLOAD_SOURCE if download_agent_binary "$DOWNLOAD_URL" "$DOWNLOAD_URL_BASE"; then : else log_warn 'Failed to download agent binary' log_warn "Ensure the Pulse server is reachable at $PRIMARY_URL" if common::is_interactive; then echo "" if [[ -t 0 ]]; then read -p "Press Enter to exit..." elif [[ -e /dev/tty ]]; then read -p "Press Enter to exit..." < /dev/tty fi fi exit 1 fi fetch_checksum_header() { local url="$1" local header="" if command -v curl &> /dev/null; then local curl_args=(-fsSI "$url") if [[ "$PRIMARY_INSECURE" == "true" ]]; then curl_args=(-k "${curl_args[@]}") fi header=$(curl "${curl_args[@]}" 2>/dev/null || true) elif command -v wget &> /dev/null; then local tmp tmp=$(mktemp) if [[ "$PRIMARY_INSECURE" == "true" ]]; then wget --spider --no-check-certificate --server-response "$url" >/dev/null 2>"$tmp" || true else wget --spider --server-response "$url" >/dev/null 2>"$tmp" || true fi header=$(cat "$tmp" 2>/dev/null || true) rm -f "$tmp" fi if [[ -z "$header" ]]; then return 1 fi local checksum_line checksum_line=$(printf '%s\n' "$header" | grep -i "^ *X-Checksum-Sha256:" | head -n 1) if [[ -z "$checksum_line" ]]; then return 1 fi local value value=$(printf '%s\n' "$checksum_line" | awk -F':' '{sub(/^[[:space:]]*/,"",$2); sub(/[[:space:]]*$/,"",$2); print $2}') value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]') # Extra trim to be safe value=$(trim "$value") if [[ -z "$value" ]]; then return 1 fi FETCHED_CHECKSUM="$value" return 0 } calculate_sha256() { local file="$1" local hash="" if command -v sha256sum &> /dev/null; then hash=$(sha256sum "$file" | awk '{print $1}') elif command -v shasum &> /dev/null; then hash=$(shasum -a 256 "$file" | awk '{print $1}') fi if [[ -z "$hash" ]]; then return 1 fi CALCULATED_CHECKSUM=$(printf '%s' "$hash" | tr '[:upper:]' '[:lower:]') return 0 } verify_agent_checksum() { local url="$1" if common::is_dry_run; then log_info '[dry-run] Skipping checksum verification' return 0 fi if ! fetch_checksum_header "$url"; then log_warn 'Agent download did not include X-Checksum-Sha256 header; skipping verification' return 0 fi if ! calculate_sha256 "$AGENT_PATH"; then log_warn 'Unable to calculate sha256 checksum locally; skipping verification' return 0 fi local clean_fetched clean_fetched=$(echo "$FETCHED_CHECKSUM" | tr -d '[:space:]') local clean_calculated clean_calculated=$(echo "$CALCULATED_CHECKSUM" | tr -d '[:space:]') if [[ "$clean_fetched" != "$clean_calculated" ]]; then rm -f "$AGENT_PATH" log_error "Checksum mismatch." log_error " Expected: '$clean_fetched' (from header)" log_error " Actual: '$clean_calculated' (calculated)" return 1 fi log_success 'Checksum verified for agent binary' unset FETCHED_CHECKSUM CALCULATED_CHECKSUM return 0 } if [[ -n "${AGENT_DOWNLOAD_SOURCE:-}" ]]; then if ! verify_agent_checksum "$AGENT_DOWNLOAD_SOURCE"; then log_error 'Agent download failed checksum verification' exit 1 fi fi if ! common::is_dry_run; then chmod 0755 "$AGENT_PATH" fi log_success "Agent binary installed" allow_reenroll_if_needed() { local host_id="$1" if [[ -z "$host_id" || -z "$PRIMARY_TOKEN" || -z "$PRIMARY_URL" ]]; then return 0 fi local endpoint="$PRIMARY_URL/api/agents/docker/hosts/${host_id}/allow-reenroll" local -a api_args=(--url "${endpoint}" --method POST --token "${PRIMARY_TOKEN}") if [[ "$PRIMARY_INSECURE" == "true" ]]; then api_args+=(--insecure) fi if http::api_call "${api_args[@]}" >/dev/null 2>&1; then log_success "Cleared any previous stop block for host" else log_warn "Unable to confirm removal block clearance (continuing)" fi return 0 } allow_reenroll_if_needed "$AGENT_IDENTIFIER" # Check if systemd is available if ! command -v systemctl &> /dev/null || [ ! -d /etc/systemd/system ]; then printf '\n%s\n' '-- Systemd not detected; configuring alternative startup --' # Check if this is Unraid (has /boot/config directory) if [ -d /boot/config ]; then log_info 'Detected Unraid environment' mkdir -p /boot/config/go.d STARTUP_SCRIPT="/boot/config/go.d/pulse-docker-agent.sh" cat > "$STARTUP_SCRIPT" < /var/log/pulse-docker-agent.log 2>&1 & EOF chmod +x "$STARTUP_SCRIPT" log_success "Created startup script: $STARTUP_SCRIPT" log_info 'Starting agent' PULSE_URL="$PRIMARY_URL" PULSE_TOKEN="$PRIMARY_TOKEN" PULSE_TARGETS="$JOINED_TARGETS" PULSE_INSECURE_SKIP_VERIFY="$PRIMARY_INSECURE" $AGENT_PATH --url "$PRIMARY_URL" --interval "$INTERVAL"$NO_AUTO_UPDATE_FLAG > /var/log/pulse-docker-agent.log 2>&1 & log_header 'Installation complete' log_info 'Agent started via Unraid go.d hook' log_info 'Log file : /var/log/pulse-docker-agent.log' log_info 'Host visible in Pulse: ~30 seconds' exit 0 fi # Check if this is Synology DSM (has /usr/syno/etc/rc.sysv) if [ -d /usr/syno/etc/rc.sysv ]; then log_info 'Detected Synology DSM environment' UPSTART_CONF="/etc/init/pulse-docker-agent.conf" cat > "$UPSTART_CONF" </dev/null || true initctl start pulse-docker-agent log_header 'Installation complete' log_info 'Agent service enabled and started via Upstart' log_info 'Check status : initctl status pulse-docker-agent' log_info 'Follow logs : tail -f /var/log/upstart/pulse-docker-agent.log' log_info 'Host visible in Pulse : ~30 seconds' exit 0 fi # Check if this is macOS (Darwin) if [[ "$(uname -s)" == "Darwin" ]]; then log_info 'Detected macOS environment' LAUNCHD_PLIST="/Library/LaunchDaemons/com.pulse.docker-agent.plist" # Construct environment variables dict ENV_DICT="" if [[ -n "$PRIMARY_URL" ]]; then ENV_DICT="$ENV_DICT PULSE_URL $PRIMARY_URL" fi if [[ -n "$PRIMARY_TOKEN" ]]; then ENV_DICT="$ENV_DICT PULSE_TOKEN $PRIMARY_TOKEN" fi if [[ -n "$JOINED_TARGETS" ]]; then ENV_DICT="$ENV_DICT PULSE_TARGETS $JOINED_TARGETS" fi if [[ -n "$PRIMARY_INSECURE" ]]; then ENV_DICT="$ENV_DICT PULSE_INSECURE_SKIP_VERIFY $PRIMARY_INSECURE" fi cat > "$LAUNCHD_PLIST" < Label com.pulse.docker-agent ProgramArguments $AGENT_PATH --url $PRIMARY_URL --interval $INTERVAL --no-auto-update EnvironmentVariables $ENV_DICT RunAtLoad KeepAlive StandardOutPath /var/log/pulse-docker-agent.log StandardErrorPath /var/log/pulse-docker-agent.log EOF chmod 644 "$LAUNCHD_PLIST" log_success "Created launchd plist: $LAUNCHD_PLIST" log_info 'Starting service' launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true launchctl load -w "$LAUNCHD_PLIST" log_header 'Installation complete' log_info 'Agent service enabled and started via launchd' log_info 'Check status : sudo launchctl list com.pulse.docker-agent' log_info 'Follow logs : tail -f /var/log/pulse-docker-agent.log' log_info 'Host visible in Pulse : ~30 seconds' exit 0 fi log_info 'Manual startup environment detected' log_info "Binary location : $AGENT_PATH" log_info 'Start manually with :' printf ' PULSE_URL=%s PULSE_TOKEN= \\n' "$PRIMARY_URL" printf ' PULSE_TARGETS="%s" \\n' "https://pulse.example.com|[;https://pulse-alt.example.com|]" printf ' %s --interval %s & ' "$AGENT_PATH" "$INTERVAL" log_info 'Add the same command to your init system to start automatically.' exit 0 fi # Check if server is in development mode NO_AUTO_UPDATE_FLAG="" if http::detect_download_tool >/dev/null 2>&1; then SERVER_INFO_URL="$PRIMARY_URL/api/server/info" IS_DEV="false" SERVER_INFO="" declare -a __server_info_args=(--url "$SERVER_INFO_URL") if [[ "$PRIMARY_INSECURE" == "true" ]]; then __server_info_args+=(--insecure) fi SERVER_INFO="$(http::api_call "${__server_info_args[@]}" 2>/dev/null || true)" unset __server_info_args if [[ -n "$SERVER_INFO" ]] && echo "$SERVER_INFO" | grep -q '"isDevelopment"[[:space:]]*:[[:space:]]*true'; then IS_DEV="true" NO_AUTO_UPDATE_FLAG=" --no-auto-update" log_info 'Development server detected – auto-update disabled' fi if [[ -n "$NO_AUTO_UPDATE_FLAG" ]]; then if ! "$AGENT_PATH" --help 2>&1 | grep -q -- '--no-auto-update'; then log_warn 'Agent binary lacks --no-auto-update flag; keeping auto-update enabled' NO_AUTO_UPDATE_FLAG="" fi fi fi # Create systemd service log_header 'Configuring systemd service' SYSTEMD_ENV_TARGETS_LINE="" if [[ -n "$JOINED_TARGETS" ]]; then SYSTEMD_ENV_TARGETS_LINE="Environment=\"PULSE_TARGETS=$JOINED_TARGETS\"" fi SYSTEMD_ENV_URL_LINE="Environment=\"PULSE_URL=$PRIMARY_URL\"" SYSTEMD_ENV_TOKEN_LINE="Environment=\"PULSE_TOKEN=$PRIMARY_TOKEN\"" SYSTEMD_ENV_INSECURE_LINE="Environment=\"PULSE_INSECURE_SKIP_VERIFY=$PRIMARY_INSECURE\"" systemd::create_service "$SERVICE_PATH" <