first commit
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
#!/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 <<EOF_CONFIG
|
||||
panel_url: "${INCUDAL_PANEL_URL}"
|
||||
agent_id: "${INCUDAL_AGENT_ID}"
|
||||
agent_secret: "${INCUDAL_AGENT_SECRET}"
|
||||
heartbeat_interval_seconds: ${HEARTBEAT_INTERVAL}
|
||||
request_timeout_seconds: ${REQUEST_TIMEOUT}
|
||||
EOF_CONFIG
|
||||
|
||||
write_file "${SERVICE_FILE}" 0644 root:root <<EOF_SERVICE
|
||||
[Unit]
|
||||
Description=Incudal Host Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${BIN_PATH} -config ${CONFIG_FILE}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User=root
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF_SERVICE
|
||||
|
||||
if [ "${DRY_RUN}" = "1" ]; then
|
||||
echo "Dry run completed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
"${BIN_PATH}" -config "${CONFIG_FILE}" -once
|
||||
systemctl enable "${SERVICE_NAME}"
|
||||
# 已安装场景下 enable --now 不会重启旧进程;restart 确保升级后立即使用最新二进制和配置。
|
||||
systemctl restart "${SERVICE_NAME}"
|
||||
systemctl status "${SERVICE_NAME}" --no-pager --lines=20
|
||||
|
||||
echo "Incudal Agent installed or upgraded and started."
|
||||
Reference in New Issue
Block a user