#!/usr/bin/env bash set -euo pipefail SERVICE_NAME="incudal-agent" CONFIG_DIR="${INCUDAL_CONFIG_DIR:-/etc/incudal-agent}" CONFIG_FILE="${INCUDAL_CONFIG_FILE:-${CONFIG_DIR}/config.yaml}" INSTALL_DIR="${INCUDAL_INSTALL_DIR:-/usr/local/bin}" BIN_PATH="${INCUDAL_AGENT_BIN:-${INSTALL_DIR}/incudal-agent}" SERVICE_FILE="${INCUDAL_SERVICE_FILE:-/etc/systemd/system/${SERVICE_NAME}.service}" HEARTBEAT_INTERVAL="${INCUDAL_HEARTBEAT_INTERVAL_SECONDS:-30}" REQUEST_TIMEOUT="${INCUDAL_REQUEST_TIMEOUT_SECONDS:-10}" DRY_RUN="${INCUDAL_AGENT_DRY_RUN:-0}" INSTALL_CONFIG_PATH="" fail() { echo "error: $*" >&2 exit 1 } need_env() { local name="$1" if [ -z "${!name:-}" ]; then fail "${name} is required" fi } detect_os() { case "$(uname -s)" in Linux) echo "linux" ;; *) fail "unsupported OS: $(uname -s)" ;; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; *) fail "unsupported architecture: $(uname -m)" ;; esac } run() { if [ "${DRY_RUN}" = "1" ]; then printf '+' printf ' %q' "$@" printf '\n' return 0 fi "$@" } write_file() { local path="$1" local mode="$2" local owner="$3" local tmp if [ "${DRY_RUN}" = "1" ]; then echo "+ write ${path} mode=${mode} owner=${owner}" sed 's/^/| /' return 0 fi tmp="$(mktemp)" cat > "${tmp}" install -d -m 0755 "$(dirname "${path}")" install -m "${mode}" -o "${owner%%:*}" -g "${owner##*:}" "${tmp}" "${path}" rm -f "${tmp}" } download_binary_once() { local binary_url="$1" local target="$2" local expected_sha256="${3:-}" local binary_path="${binary_url%%\?*}" binary_path="${binary_path%%#*}" if [ "${DRY_RUN}" = "1" ]; then echo "+ download ${binary_url} -> ${target}" if [ -n "${expected_sha256}" ]; then echo "+ verify sha256 ${expected_sha256} ${target}.download" fi return 0 fi install -d -m 0755 "$(dirname "${target}")" if [[ "${binary_url}" == file://* ]]; then install -m 0644 "${binary_url#file://}" "${target}.download" || return 1 else command -v curl >/dev/null 2>&1 || fail "curl is required" curl -fsSL "${binary_url}" -o "${target}.download" || { rm -f "${target}.download" return 1 } fi if [ -n "${expected_sha256}" ]; then verify_sha256 "${target}.download" "${expected_sha256}" || { rm -f "${target}.download" return 1 } fi if [[ "${binary_path}" == *.gz ]]; then command -v gzip >/dev/null 2>&1 || { rm -f "${target}.download" return 1 } gzip -dc "${target}.download" > "${target}.tmp" || { rm -f "${target}.download" "${target}.tmp" return 1 } rm -f "${target}.download" else mv "${target}.download" "${target}.tmp" fi install -m 0755 "${target}.tmp" "${target}" rm -f "${target}.tmp" } download_binary() { local binary_url="$1" local target="$2" local fallback_url="${3:-}" local expected_sha256="${4:-}" if download_binary_once "${binary_url}" "${target}" "${expected_sha256}"; then return 0 fi if [ -n "${fallback_url}" ]; then echo "warning: failed to download ${binary_url}, fallback to ${fallback_url}" >&2 download_binary_once "${fallback_url}" "${target}" return $? fi return 1 } append_query_param() { local url="$1" local key="$2" local value="$3" if [[ "${url}" == *\?* ]]; then echo "${url}&${key}=${value}" else echo "${url}?${key}=${value}" fi } sha256_file() { local path="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "${path}" | awk '{print $1}' return 0 fi if command -v shasum >/dev/null 2>&1; then shasum -a 256 "${path}" | awk '{print $1}' return 0 fi fail "sha256sum or shasum is required" } verify_sha256() { local path="$1" local expected="$2" local actual actual="$(sha256_file "${path}")" if [ "${actual}" != "${expected}" ]; then echo "warning: sha256 mismatch for ${path}: expected=${expected} actual=${actual}" >&2 return 1 fi } download_manifest() { local manifest_url="$1" local target="$2" if [ "${DRY_RUN}" = "1" ]; then echo "+ download ${manifest_url} -> ${target}" return 0 fi command -v curl >/dev/null 2>&1 || fail "curl is required" curl -fsSL "${manifest_url}" -o "${target}" } manifest_value() { local manifest_path="$1" local platform="$2" local key="$3" awk -v platform="\"${platform}\"" -v key="\"${key}\"" ' $0 ~ platform { in_platform=1; next } in_platform && $0 ~ /^[[:space:]]*}/ { exit } in_platform && $0 ~ key { line=$0 sub(/^[^:]*:[[:space:]]*/, "", line) sub(/[,\r]*$/, "", line) gsub(/^"|"$/, "", line) print line exit } ' "${manifest_path}" } validate_binary_name() { local name="$1" local os="$2" local arch="$3" case "${name}" in "incudal-agent-${os}-${arch}"|"incudal-agent-${os}-${arch}.gz") return 0 ;; *) fail "manifest binary name is invalid for ${os}-${arch}: ${name}" ;; esac } install_binary_atomically() { local source="$1" local target="$2" local next="${target}.new" if [ "${DRY_RUN}" = "1" ]; then echo "+ install ${source} -> ${target} (atomic replace)" return 0 fi install -d -m 0755 "$(dirname "${target}")" install -m 0755 "${source}" "${next}" mv -f "${next}" "${target}" } fetch_agent_install_config() { local install_token="${INCUDAL_AGENT_INSTALL_TOKEN:-}" if [ -z "${install_token}" ]; then return 0 fi local config_url="${PANEL_BASE_URL}/api/agent/install-config/${install_token}" if [ "${DRY_RUN}" = "1" ]; then echo "+ fetch ${config_url}" INCUDAL_AGENT_ID="${INCUDAL_AGENT_ID:-agt_from_install_token}" INCUDAL_AGENT_SECRET="${INCUDAL_AGENT_SECRET:-ias_from_install_token}" return 0 fi command -v curl >/dev/null 2>&1 || fail "curl is required" INSTALL_CONFIG_PATH="$(mktemp)" curl -fsSL "${config_url}" -o "${INSTALL_CONFIG_PATH}" # shellcheck disable=SC1090 . "${INSTALL_CONFIG_PATH}" rm -f "${INSTALL_CONFIG_PATH}" INSTALL_CONFIG_PATH="" } need_env INCUDAL_PANEL_URL PANEL_BASE_URL="${INCUDAL_PANEL_URL%/}" OS="$(detect_os)" ARCH="$(detect_arch)" DEFAULT_BINARY_BASE_URL="${PANEL_BASE_URL}/api/agent/binary" BINARY_BASE_URL="${INCUDAL_AGENT_BINARY_BASE_URL:-${DEFAULT_BINARY_BASE_URL}}" BINARY_NAME="incudal-agent-${OS}-${ARCH}" BINARY_URL="${INCUDAL_AGENT_BINARY_URL:-}" BINARY_EXPECTED_SHA256="${INCUDAL_AGENT_BINARY_SHA256:-}" BINARY_FALLBACK_URL="" MANIFEST_PATH="" MANIFEST_URL="${INCUDAL_AGENT_MANIFEST_URL:-${PANEL_BASE_URL}/api/agent/manifest.json}" STAGED_BIN="${BIN_PATH}.download.$$" cleanup() { rm -f "${STAGED_BIN}" "${STAGED_BIN}.download" "${STAGED_BIN}.tmp" "${BIN_PATH}.new" if [ -n "${INSTALL_CONFIG_PATH:-}" ]; then rm -f "${INSTALL_CONFIG_PATH}" fi if [ -n "${MANIFEST_PATH:-}" ]; then rm -f "${MANIFEST_PATH}" fi } trap cleanup EXIT fetch_agent_install_config need_env INCUDAL_AGENT_ID need_env INCUDAL_AGENT_SECRET if [ -z "${INCUDAL_AGENT_BINARY_URL:-}" ]; then # Cloudflare 等边缘缓存可能缓存旧二进制;默认面板下载强制按安装批次换 URL。 BINARY_CACHE_BUSTER="${INCUDAL_AGENT_BINARY_CACHE_BUSTER:-$(date +%s)}" MANIFEST_URL="$(append_query_param "${MANIFEST_URL}" "v" "${BINARY_CACHE_BUSTER}")" MANIFEST_PATH="${BIN_PATH}.manifest.$$" download_manifest "${MANIFEST_URL}" "${MANIFEST_PATH}" if [ "${DRY_RUN}" != "1" ]; then BINARY_NAME="$(manifest_value "${MANIFEST_PATH}" "${OS}-${ARCH}" "name")" BINARY_EXPECTED_SHA256="$(manifest_value "${MANIFEST_PATH}" "${OS}-${ARCH}" "sha256")" if [ -z "${BINARY_NAME}" ] || [ -z "${BINARY_EXPECTED_SHA256}" ]; then fail "agent manifest does not contain ${OS}-${ARCH} binary metadata" fi validate_binary_name "${BINARY_NAME}" "${OS}" "${ARCH}" else BINARY_NAME="${BINARY_NAME}.gz" fi BINARY_URL="${BINARY_BASE_URL}/${BINARY_NAME}" BINARY_URL="$(append_query_param "${BINARY_URL}" "v" "${BINARY_CACHE_BUSTER}")" elif [ -z "${BINARY_EXPECTED_SHA256}" ]; then echo "warning: INCUDAL_AGENT_BINARY_URL is set without INCUDAL_AGENT_BINARY_SHA256, skip binary checksum verification" >&2 fi echo "Installing Incudal Agent" echo " panel: ${INCUDAL_PANEL_URL}" echo " agent: ${INCUDAL_AGENT_ID}" echo " binary: ${BINARY_URL}" if [ -n "${BINARY_EXPECTED_SHA256}" ]; then echo " sha256: ${BINARY_EXPECTED_SHA256}" fi if [ "${DRY_RUN}" != "1" ]; then install -d -m 0755 "$(dirname "${STAGED_BIN}")" fi # 先下载到临时路径,再原子替换,避免覆盖正在运行的二进制时报 Text file busy。 download_binary "${BINARY_URL}" "${STAGED_BIN}" "${BINARY_FALLBACK_URL}" "${BINARY_EXPECTED_SHA256}" install_binary_atomically "${STAGED_BIN}" "${BIN_PATH}" write_file "${CONFIG_FILE}" 0600 root:root <