#!/usr/bin/env bash # Pulse Container Runtime Agent installer set -euo pipefail # ----------------------------------------------------------------------------- # Logging helpers (borrowed from legacy installer for consistent UX) # ----------------------------------------------------------------------------- log_info() { printf '[INFO] %s\n' "$1" } log_warn() { printf '[WARN] %s\n' "$1" >&2 } log_error() { printf '[ERROR] %s\n' "$1" >&2 } log_success() { printf '[ OK ] %s\n' "$1" } log_header() { printf '\n== %s ==\n' "$1" } # ----------------------------------------------------------------------------- # Utility helpers # ----------------------------------------------------------------------------- trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } to_lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' } parse_bool() { local value value="$(to_lower "${1:-}")" case "$value" in 1|true|yes|y|on) PARSED_BOOL="true" return 0 ;; 0|false|no|n|off|"") PARSED_BOOL="false" return 0 ;; esac return 1 } # ----------------------------------------------------------------------------- # Target parsing helpers (reused from legacy installer) # ----------------------------------------------------------------------------- declare -a TARGET_SPECS=() declare -a TARGETS=() declare -A SEEN_TARGETS 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 log_error "Invalid target spec \"$spec\". Expected format url|token[|insecure]." return 1 fi PARSED_TARGET_URL="${raw_url%/}" PARSED_TARGET_TOKEN="$raw_token" if [[ -n "$raw_insecure" ]]; then if parse_bool "$raw_insecure"; then PARSED_TARGET_INSECURE="$PARSED_BOOL" else log_error "Invalid insecure flag \"$raw_insecure\" in target spec \"$spec\"." return 1 fi 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_entry trimmed_entry=$(trim "$entry") if [[ -n "$trimmed_entry" ]]; then TARGET_SPECS+=("$trimmed_entry") fi done } # ----------------------------------------------------------------------------- # Default paths + globals # ----------------------------------------------------------------------------- DEFAULT_ROOTFUL_AGENT="/usr/local/bin/pulse-docker-agent" DEFAULT_ROOTLESS_AGENT="$HOME/.local/bin/pulse-docker-agent" ROOTFUL_ENV_FILE="/etc/pulse/pulse-docker-agent.env" ROOTFUL_SERVICE="/etc/systemd/system/pulse-docker-agent.service" ROOTFUL_LOG="/var/log/pulse-docker-agent.log" ROOTLESS_ENV_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/pulse/pulse-docker-agent.env" ROOTLESS_SERVICE="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/pulse-docker-agent.service" ROOTLESS_LOG="${XDG_STATE_HOME:-$HOME/.local/state}/pulse-docker-agent/pulse-docker-agent.log" INTERVAL="30s" UNINSTALL="false" PURGE="false" RUNTIME="podman" RUNTIME_MODE="auto" ROOTLESS="auto" CONTAINER_SOCKET_INPUT="" CONTAINER_SOCKET_PATH="" CONTAINER_SOCKET_URI="" PULSE_URL="" TOKEN="" PRIMARY_URL="" PRIMARY_TOKEN="" PRIMARY_INSECURE="false" JOINED_TARGETS="" INSTALLER_URL_HINT="" NO_AUTO_UPDATE_FLAG="" DOWNLOAD_ARCH="" AGENT_PATH_OVERRIDE="" AGENT_PATH="" KUBE_INCLUDE_ALL_PODS="false" KUBE_INCLUDE_ALL_DEPLOYMENTS="false" PULSE_TARGETS_ENV="${PULSE_TARGETS:-}" PULSE_RUNTIME_ENV="$(trim "${PULSE_RUNTIME:-}")" PULSE_RUNTIME_SOCKET_ENV="$(trim "${PULSE_CONTAINER_SOCKET:-${CONTAINER_HOST:-}}")" PULSE_ROOTLESS_ENV="$(trim "${PULSE_RUNTIME_ROOTLESS:-}")" ORIGINAL_ARGS=("$@") # ----------------------------------------------------------------------------- # Argument parsing # ----------------------------------------------------------------------------- usage() { cat <<'EOF' Pulse Container Agent Installer Usage: install-container-agent.sh [options] Options: --url Primary Pulse server URL. --token API token for the primary server (legacy mode). --target Fan-out target spec (url|token[|insecure]); repeatable. --interval Reporting interval (default 30s). --runtime Container runtime (podman|docker|auto). Default podman. --container-socket Container runtime socket path or unix:// URI. --rootless Force rootless install (user service). --system Force system-wide install (requires root). --agent-path Override binary installation path. --kube-include-all-pods Include all non-succeeded pods. --kube-include-all-deployments Include all deployments. --uninstall Remove existing installation. --purge Remove config files when uninstalling. --help Show this help message. EOF } if [[ $# -eq 0 ]]; then : # allow interactive prompts when desired fi while [[ $# -gt 0 ]]; do case "$1" in --help|-h) usage exit 0 ;; --url) PULSE_URL="$2" shift 2 ;; --token) TOKEN="$2" shift 2 ;; --target) TARGET_SPECS+=("$2") shift 2 ;; --interval) INTERVAL="$2" shift 2 ;; --runtime) RUNTIME="$2" shift 2 ;; --runtime=*) RUNTIME="${1#--runtime=}" shift ;; --container-socket|--socket) CONTAINER_SOCKET_INPUT="$2" shift 2 ;; --container-socket=*|--socket=*) CONTAINER_SOCKET_INPUT="${1#*=}" shift ;; --rootless) ROOTLESS="true" shift ;; --system) ROOTLESS="false" shift ;; --agent-path) AGENT_PATH_OVERRIDE="$2" shift 2 ;; --agent-path=*) AGENT_PATH_OVERRIDE="${1#*=}" shift ;; --kube-include-all-pods) KUBE_INCLUDE_ALL_PODS="true" shift ;; --kube-include-all-deployments) KUBE_INCLUDE_ALL_DEPLOYMENTS="true" shift ;; --uninstall) UNINSTALL="true" shift ;; --purge) PURGE="true" shift ;; *) log_error "Unknown option: $1" usage exit 1 ;; esac done # ----------------------------------------------------------------------------- # Normalise runtime + mode # ----------------------------------------------------------------------------- if [[ -n "$PULSE_RUNTIME_ENV" ]]; then RUNTIME="$PULSE_RUNTIME_ENV" fi RUNTIME="$(to_lower "$(trim "$RUNTIME")")" if [[ -z "$RUNTIME" || "$RUNTIME" == "auto" ]]; then # Prefer docker if explicitly available (compat with legacy usage), # otherwise fall back to podman. if command -v docker >/dev/null 2>&1; then RUNTIME="docker" elif command -v podman >/dev/null 2>&1; then RUNTIME="podman" else RUNTIME="podman" fi fi if [[ "$RUNTIME" == "docker" ]]; then # Chain to legacy installer for Docker runtime. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "${SCRIPT_DIR}/install-docker-agent.sh" ]]; then exec "${SCRIPT_DIR}/install-docker-agent.sh" "${ORIGINAL_ARGS[@]}" fi log_error "Docker runtime requested but install-docker-agent.sh not found." exit 1 fi if [[ "$RUNTIME" != "podman" ]]; then log_error "Unsupported runtime \"$RUNTIME\". Supported: podman, docker." exit 1 fi # Determine rootless/system preference if [[ -n "$PULSE_ROOTLESS_ENV" ]]; then if parse_bool "$PULSE_ROOTLESS_ENV"; then ROOTLESS="$PARSED_BOOL" fi fi if [[ "$ROOTLESS" == "auto" ]]; then if [[ "$EUID" -ne 0 ]]; then ROOTLESS="true" else ROOTLESS="false" fi else ROOTLESS="$(to_lower "$ROOTLESS")" case "$ROOTLESS" in true|false) ;; *) if parse_bool "$ROOTLESS"; then ROOTLESS="$PARSED_BOOL" else ROOTLESS="false" fi ;; esac fi # Runtime socket if [[ -n "$PULSE_RUNTIME_SOCKET_ENV" && -z "$CONTAINER_SOCKET_INPUT" ]]; then CONTAINER_SOCKET_INPUT="$PULSE_RUNTIME_SOCKET_ENV" fi normalize_socket() { local input="$1" if [[ "$input" == unix://* ]]; then CONTAINER_SOCKET_URI="$input" CONTAINER_SOCKET_PATH="${input#unix://}" else CONTAINER_SOCKET_PATH="$input" CONTAINER_SOCKET_URI="unix://$CONTAINER_SOCKET_PATH" fi if [[ "$CONTAINER_SOCKET_PATH" != /* ]]; then log_error "Socket path must be absolute: $CONTAINER_SOCKET_PATH" exit 1 fi } if [[ -n "$CONTAINER_SOCKET_INPUT" ]]; then normalize_socket "$CONTAINER_SOCKET_INPUT" else if [[ "$ROOTLESS" == "true" ]]; then CONTAINER_SOCKET_PATH="/run/user/$UID/podman/podman.sock" else CONTAINER_SOCKET_PATH="/run/podman/podman.sock" fi CONTAINER_SOCKET_URI="unix://$CONTAINER_SOCKET_PATH" fi # ----------------------------------------------------------------------------- # Common validation helpers # ----------------------------------------------------------------------------- ensure_command() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then log_error "Required command '$cmd' not found in PATH." exit 1 fi } ensure_podman_socket() { if [[ ! -S "$CONTAINER_SOCKET_PATH" ]]; then if [[ "$ROOTLESS" == "true" ]]; then log_info "Podman socket not detected at $CONTAINER_SOCKET_PATH" log_info "Attempting to enable podman.socket for the current user" if command -v systemctl >/dev/null 2>&1; then systemctl --user enable --now podman.socket >/dev/null 2>&1 || true fi else log_info "Podman socket not detected at $CONTAINER_SOCKET_PATH" log_info "Attempting to enable system podman.socket" if command -v systemctl >/dev/null 2>&1; then systemctl enable --now podman.socket >/dev/null 2>&1 || true fi fi fi if [[ ! -S "$CONTAINER_SOCKET_PATH" ]]; then log_warn "Podman API socket not found at $CONTAINER_SOCKET_PATH." log_warn "Start it manually with 'podman system service --time=0' or enable podman.socket." fi } # ----------------------------------------------------------------------------- # Target + token normalisation # ----------------------------------------------------------------------------- PULSE_URL="$(trim "$PULSE_URL")" PULSE_URL="${PULSE_URL%/}" TOKEN="$(trim "$TOKEN")" split_targets_from_env "$PULSE_TARGETS_ENV" if [[ -n "$PULSE_URL" && -n "$TOKEN" ]]; then TARGET_SPECS+=("${PULSE_URL}|${TOKEN}") fi if [[ -n "$PULSE_URL" && -z "$TOKEN" && ${#TARGET_SPECS[@]} -eq 0 ]]; then log_error "--token or at least one --target is required when --url is provided." exit 1 fi if [[ ${#TARGET_SPECS[@]} -eq 0 ]]; then log_error "No Pulse targets specified. Use --target or --url + --token." exit 1 fi for spec in "${TARGET_SPECS[@]}"; do if ! parse_target_spec "$spec"; then exit 1 fi local_normalized="${PARSED_TARGET_URL}|${PARSED_TARGET_TOKEN}|${PARSED_TARGET_INSECURE}" if [[ -n "${SEEN_TARGETS[$local_normalized]+x}" ]]; then continue fi SEEN_TARGETS[$local_normalized]=1 TARGETS+=("$local_normalized") done if [[ ${#TARGETS[@]} -eq 0 ]]; then log_error "No valid Pulse targets after normalisation." exit 1 fi PRIMARY_URL="$(printf '%s' "${TARGETS[0]}" | awk -F'|' '{print $1}')" PRIMARY_TOKEN="$(printf '%s' "${TARGETS[0]}" | awk -F'|' '{print $2}')" PRIMARY_INSECURE="$(printf '%s' "${TARGETS[0]}" | awk -F'|' '{print $3}')" JOINED_TARGETS=$(printf "%s;" "${TARGETS[@]}") JOINED_TARGETS="${JOINED_TARGETS%;}" # ----------------------------------------------------------------------------- # Download helpers (shared for both modes) # ----------------------------------------------------------------------------- determine_arch() { local arch 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" ;; armv6l) DOWNLOAD_ARCH="linux-armv6" ;; i386|i686) DOWNLOAD_ARCH="linux-386" ;; *) DOWNLOAD_ARCH="" ;; esac } download_agent() { ensure_command uname determine_arch if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then log_error "Neither curl nor wget found in PATH." exit 1 fi local download_url="$PRIMARY_URL/download/pulse-docker-agent" if [[ -n "$DOWNLOAD_ARCH" ]]; then download_url="$download_url?arch=$DOWNLOAD_ARCH" fi log_info "Fetching agent binary from $download_url" local tmp tmp=$(mktemp "${AGENT_PATH}.download.XXXXXX") local fetched="false" if command -v wget >/dev/null 2>&1; then local wget_args=(-q -O "$tmp" "$download_url") if [[ "$PRIMARY_INSECURE" == "true" ]]; then wget_args=(--no-check-certificate "${wget_args[@]}") fi if wget "${wget_args[@]}"; then fetched="true" else rm -f "$tmp" fi fi if [[ "$fetched" != "true" ]]; then if command -v curl >/dev/null 2>&1; then local curl_args=(-fL --progress-bar -o "$tmp" "$download_url") if [[ "$PRIMARY_INSECURE" == "true" ]]; then curl_args=(-k "${curl_args[@]}") fi if curl "${curl_args[@]}"; then fetched="true" else rm -f "$tmp" fi fi fi if [[ "$fetched" != "true" ]]; then log_error "Failed to download agent binary." exit 1 fi # Checksum verification local checksum_url="${download_url}.sha256" local expected_checksum="" # Try to fetch checksum if command -v curl >/dev/null 2>&1; then local curl_args=(-fsSL "$checksum_url") if [[ "$PRIMARY_INSECURE" == "true" ]]; then curl_args=(-k "${curl_args[@]}") fi expected_checksum=$(curl "${curl_args[@]}" 2>/dev/null || true) elif command -v wget >/dev/null 2>&1; then local wget_args=(-qO- "$checksum_url") if [[ "$PRIMARY_INSECURE" == "true" ]]; then wget_args=(--no-check-certificate "${wget_args[@]}") fi expected_checksum=$(wget "${wget_args[@]}" 2>/dev/null || true) fi if [[ -n "$expected_checksum" ]]; then log_info "Verifying checksum..." local actual_checksum="" if command -v sha256sum >/dev/null 2>&1; then actual_checksum=$(sha256sum "$tmp" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then actual_checksum=$(shasum -a 256 "$tmp" | awk '{print $1}') fi if [[ -n "$actual_checksum" ]]; then # Normalize checksums local clean_expected clean_expected=$(echo "$expected_checksum" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') local clean_actual clean_actual=$(echo "$actual_checksum" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') if [[ "$clean_expected" != "$clean_actual" ]]; then rm -f "$tmp" log_error "Checksum mismatch." log_error " Expected: '$clean_expected'" log_error " Actual: '$clean_actual'" exit 1 fi log_success "Checksum verified" else log_warn "Unable to calculate local checksum (sha256sum/shasum not found). Skipping verification." fi else log_info "No checksum file found at $checksum_url. Skipping verification." fi mv "$tmp" "$AGENT_PATH" chmod 0755 "$AGENT_PATH" log_success "Agent installed at $AGENT_PATH" } # ----------------------------------------------------------------------------- # Rootful (system) installation helpers # ----------------------------------------------------------------------------- SERVICE_USER="pulse-docker" SERVICE_GROUP="pulse-docker" SERVICE_HOME="/var/lib/pulse-docker-agent" SERVICE_USER_CREATED="false" SERVICE_USER_ACTUAL="$SERVICE_USER" SERVICE_GROUP_ACTUAL="$SERVICE_GROUP" SYSTEMD_SUPPLEMENTARY_GROUPS_LINE="" ensure_service_user() { if id -u "$SERVICE_USER" >/dev/null 2>&1; then SERVICE_USER_ACTUAL="$SERVICE_USER" if getent group "$SERVICE_GROUP" >/dev/null 2>&1; then SERVICE_GROUP_ACTUAL="$SERVICE_GROUP" else SERVICE_GROUP_ACTUAL="$(id -gn "$SERVICE_USER")" fi return fi if ! command -v useradd >/dev/null 2>&1; then log_warn "useradd not available; running service as root" SERVICE_USER_ACTUAL="root" SERVICE_GROUP_ACTUAL="root" return fi if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then groupadd --system "$SERVICE_GROUP" >/dev/null 2>&1 || true fi useradd --system --home "$SERVICE_HOME" --shell /usr/sbin/nologin --gid "$SERVICE_GROUP" "$SERVICE_USER" >/dev/null 2>&1 || true SERVICE_USER_ACTUAL="$SERVICE_USER" if getent group "$SERVICE_GROUP" >/dev/null 2>&1; then SERVICE_GROUP_ACTUAL="$SERVICE_GROUP" else SERVICE_GROUP_ACTUAL="$(id -gn "$SERVICE_USER")" fi SERVICE_USER_CREATED="true" } ensure_service_home() { if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then return fi if [[ ! -d "$SERVICE_HOME" ]]; then mkdir -p "$SERVICE_HOME" fi chown "$SERVICE_USER_ACTUAL":"$SERVICE_GROUP_ACTUAL" "$SERVICE_HOME" >/dev/null 2>&1 || true chmod 750 "$SERVICE_HOME" >/dev/null 2>&1 || true } ensure_podman_group_membership() { SYSTEMD_SUPPLEMENTARY_GROUPS_LINE="" if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then return fi local group="podman" if ! getent group "$group" >/dev/null 2>&1; then log_info "Creating '$group' group for socket access" groupadd --system "$group" >/dev/null 2>&1 || { log_warn "Failed to create group '$group'; ensure $SERVICE_USER_ACTUAL can access $CONTAINER_SOCKET_PATH" return } # Change socket ownership to the new group if it exists if [[ -S "$CONTAINER_SOCKET_PATH" ]]; then chgrp "$group" "$CONTAINER_SOCKET_PATH" >/dev/null 2>&1 || log_warn "Failed to change socket group; adjust permissions manually" fi fi if ! id -nG "$SERVICE_USER_ACTUAL" | tr ' ' '\n' | grep -Fxq "$group"; then if command -v usermod >/dev/null 2>&1; then usermod -a -G "$group" "$SERVICE_USER_ACTUAL" >/dev/null 2>&1 || log_warn "Failed to add $SERVICE_USER_ACTUAL to $group; adjust socket permissions manually." else log_warn "Unable to modify group memberships; ensure $SERVICE_USER_ACTUAL can access $CONTAINER_SOCKET_PATH." fi fi if id -nG "$SERVICE_USER_ACTUAL" | tr ' ' '\n' | grep -Fxq "$group"; then SYSTEMD_SUPPLEMENTARY_GROUPS_LINE="SupplementaryGroups=$group" else log_warn "Service user $SERVICE_USER_ACTUAL is not in $group; socket access may fail." fi } write_rootful_env() { mkdir -p "$(dirname "$ROOTFUL_ENV_FILE")" local tmp tmp=$(mktemp "${ROOTFUL_ENV_FILE}.XXXXXX") chmod 600 "$tmp" { printf 'PULSE_URL=%q\n' "$PRIMARY_URL" printf 'PULSE_TOKEN=%q\n' "$PRIMARY_TOKEN" printf 'PULSE_TARGETS=%q\n' "$JOINED_TARGETS" printf 'PULSE_INTERVAL=%q\n' "$INTERVAL" printf 'PULSE_RUNTIME=podman\n' printf 'CONTAINER_HOST=%q\n' "$CONTAINER_SOCKET_URI" printf 'DOCKER_HOST=%q\n' "$CONTAINER_SOCKET_URI" if [[ "$PRIMARY_INSECURE" == "true" ]]; then printf 'PULSE_INSECURE_SKIP_VERIFY=true\n' fi printf 'PULSE_KUBE_INCLUDE_ALL_PODS=%q\n' "$KUBE_INCLUDE_ALL_PODS" printf 'PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS=%q\n' "$KUBE_INCLUDE_ALL_DEPLOYMENTS" } > "$tmp" mv "$tmp" "$ROOTFUL_ENV_FILE" chmod 600 "$ROOTFUL_ENV_FILE" log_success "Environment file written to $ROOTFUL_ENV_FILE" } install_rootful() { if [[ "$EUID" -ne 0 ]]; then if command -v sudo >/dev/null 2>&1; then exec sudo -- "$0" "${ORIGINAL_ARGS[@]}" --system fi log_error "Root privileges required for system installation." exit 1 fi ensure_command podman ensure_podman_socket if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then AGENT_PATH=$(trim "$AGENT_PATH_OVERRIDE") if [[ ! "$AGENT_PATH" =~ ^/ ]]; then log_error "--agent-path must be absolute." exit 1 fi else AGENT_PATH="$DEFAULT_ROOTFUL_AGENT" fi mkdir -p "$(dirname "$AGENT_PATH")" download_agent ensure_service_user ensure_service_home ensure_podman_group_membership write_rootful_env mkdir -p "$(dirname "$ROOTFUL_LOG")" if [[ ! -f "$ROOTFUL_LOG" ]]; then touch "$ROOTFUL_LOG" fi chown "$SERVICE_USER_ACTUAL":"$SERVICE_GROUP_ACTUAL" "$ROOTFUL_LOG" >/dev/null 2>&1 || true cat > "$ROOTFUL_SERVICE" </dev/null 2>&1; then exec sudo -- "$0" "${ORIGINAL_ARGS[@]}" --system --uninstall fi log_error "Root privileges required to uninstall system service." exit 1 fi if command -v systemctl >/dev/null 2>&1; then systemctl stop pulse-docker-agent 2>/dev/null || true systemctl disable pulse-docker-agent 2>/dev/null || true fi rm -f "$ROOTFUL_SERVICE" rm -f "$ROOTFUL_ENV_FILE" if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then AGENT_PATH="$AGENT_PATH_OVERRIDE" else AGENT_PATH="$DEFAULT_ROOTFUL_AGENT" fi rm -f "$AGENT_PATH" systemctl daemon-reload 2>/dev/null || true log_success "System installation removed" } # ----------------------------------------------------------------------------- # Rootless helpers # ----------------------------------------------------------------------------- rootless_env_dir="$(dirname "$ROOTLESS_ENV_FILE")" rootless_service_dir="$(dirname "$ROOTLESS_SERVICE")" rootless_log_dir="$(dirname "$ROOTLESS_LOG")" write_rootless_env() { mkdir -p "$rootless_env_dir" local tmp tmp=$(mktemp "${ROOTLESS_ENV_FILE}.XXXXXX") chmod 600 "$tmp" { printf 'PULSE_URL=%q\n' "$PRIMARY_URL" printf 'PULSE_TOKEN=%q\n' "$PRIMARY_TOKEN" printf 'PULSE_TARGETS=%q\n' "$JOINED_TARGETS" printf 'PULSE_INTERVAL=%q\n' "$INTERVAL" printf 'PULSE_RUNTIME=podman\n' printf 'CONTAINER_HOST=%q\n' "$CONTAINER_SOCKET_URI" printf 'DOCKER_HOST=%q\n' "$CONTAINER_SOCKET_URI" if [[ "$PRIMARY_INSECURE" == "true" ]]; then printf 'PULSE_INSECURE_SKIP_VERIFY=true\n' fi printf 'PULSE_KUBE_INCLUDE_ALL_PODS=%q\n' "$KUBE_INCLUDE_ALL_PODS" printf 'PULSE_KUBE_INCLUDE_ALL_DEPLOYMENTS=%q\n' "$KUBE_INCLUDE_ALL_DEPLOYMENTS" } > "$tmp" mv "$tmp" "$ROOTLESS_ENV_FILE" chmod 600 "$ROOTLESS_ENV_FILE" log_success "Environment file written to $ROOTLESS_ENV_FILE" } install_rootless() { ensure_command podman ensure_podman_socket if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then AGENT_PATH=$(trim "$AGENT_PATH_OVERRIDE") if [[ ! "$AGENT_PATH" =~ ^/ ]]; then log_error "--agent-path must be absolute." exit 1 fi else AGENT_PATH="$DEFAULT_ROOTLESS_AGENT" fi mkdir -p "$(dirname "$AGENT_PATH")" download_agent write_rootless_env mkdir -p "$rootless_log_dir" touch "$ROOTLESS_LOG" mkdir -p "$rootless_service_dir" cat > "$ROOTLESS_SERVICE" </dev/null 2>&1; then systemctl --user daemon-reload systemctl --user enable --now podman.socket >/dev/null 2>&1 || true systemctl --user enable --now pulse-docker-agent else log_warn "systemctl not available; start the agent manually:" log_info " env \$(grep -v '^#' $ROOTLESS_ENV_FILE | xargs) $AGENT_PATH --runtime podman --interval $INTERVAL" return fi if command -v loginctl >/dev/null 2>&1; then if ! loginctl show-user "$USER" | grep -q 'Linger=yes'; then log_info "Enable lingering to keep the service alive after logout:" log_info " sudo loginctl enable-linger $USER" fi fi log_header "Rootless installation complete" log_info "Agent binary : $AGENT_PATH" log_info "Runtime socket : $CONTAINER_SOCKET_PATH" log_info "Service status : systemctl --user status pulse-docker-agent" log_info "Logs : journalctl --user -u pulse-docker-agent -f" } uninstall_rootless() { if command -v systemctl >/dev/null 2>&1; then systemctl --user stop pulse-docker-agent 2>/dev/null || true systemctl --user disable pulse-docker-agent 2>/dev/null || true fi rm -f "$ROOTLESS_SERVICE" rm -f "$ROOTLESS_ENV_FILE" if [[ -n "$AGENT_PATH_OVERRIDE" ]]; then AGENT_PATH=$(trim "$AGENT_PATH_OVERRIDE") else AGENT_PATH="$DEFAULT_ROOTLESS_AGENT" fi rm -f "$AGENT_PATH" log_success "Rootless installation removed" } # ----------------------------------------------------------------------------- # Entry point # ----------------------------------------------------------------------------- if [[ "$UNINSTALL" == "true" ]]; then if [[ "$ROOTLESS" == "true" ]]; then uninstall_rootless else uninstall_rootful fi if [[ "$PURGE" == "true" ]]; then rm -f "$ROOTFUL_LOG" "$ROOTLESS_LOG" 2>/dev/null || true fi exit 0 fi log_header "Pulse Podman Agent Installer" log_info "Primary Pulse URL : $PRIMARY_URL" log_info "Runtime : Podman ($([[ "$ROOTLESS" == "true" ]] && printf 'rootless' || printf 'system'))" log_info "Runtime socket : $CONTAINER_SOCKET_PATH" log_info "Reporting interval : $INTERVAL" if [[ "$ROOTLESS" == "true" ]]; then install_rootless else install_rootful fi