mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 11:38:09 +09:00
Picks up serverkit-agent 219e75e, which points the usage and --help examples at
/api/v1/servers/install.{sh,ps1} instead of the domain root. Produced by
scripts/sync-agent-installers.sh, not hand-edited — scripts/test/check-installer-drift.sh
fails on any divergence and confirms these match the canonical copies again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
490 lines
15 KiB
Bash
490 lines
15 KiB
Bash
#!/bin/bash
|
||
#
|
||
# ServerKit Agent Installation Script
|
||
#
|
||
# ---------------------------------------------------------------------------
|
||
# CANONICAL SOURCE: jhd3197/serverkit-agent -> install.sh
|
||
#
|
||
# This file is VENDORED into the panel repo at ServerKit/scripts/install.sh,
|
||
# which is the copy the panel serves at GET /api/v1/servers/install.sh (it has
|
||
# to exist on the panel's own disk, so the panel cannot simply link to it).
|
||
#
|
||
# Edit it in the agent repo, then re-vendor with ServerKit's
|
||
# scripts/sync-agent-installers.sh. Do not patch one copy only: they silently
|
||
# drifted once already, and the panel spent that entire time serving an
|
||
# installer that downloaded from a tag scheme that no longer existed
|
||
# (ServerKit issue #101). ServerKit's nightly CI now fails on any drift.
|
||
# ---------------------------------------------------------------------------
|
||
#
|
||
# Usage (the panel serves this at /api/v1/servers/install.sh, not at the domain
|
||
# root -- the root of a ServerKit domain is the panel UI):
|
||
# curl -fsSL https://your-serverkit.com/api/v1/servers/install.sh \
|
||
# -o /tmp/serverkit-agent-install.sh \
|
||
# && sudo bash /tmp/serverkit-agent-install.sh --token "TOKEN" --server "URL"
|
||
#
|
||
# Deliberately not `curl … | sudo bash`: a pipeline reports bash's exit status,
|
||
# not curl's, so a failed download exits 0 having installed nothing (issue #101).
|
||
#
|
||
# Options:
|
||
# --token, -t Registration token (required)
|
||
# --server, -s ServerKit server URL (required)
|
||
# --name, -n Display name for this server (optional)
|
||
# --version, -v Specific agent version to install (optional, defaults to latest)
|
||
# --help, -h Show this help message
|
||
#
|
||
|
||
set -e
|
||
|
||
# Palette (truecolor violet gradient)
|
||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
|
||
NC=$'\033[0m'; BOLD=$'\033[1m'
|
||
fg() { printf '\033[38;2;%s;%s;%sm' "$1" "$2" "$3"; }
|
||
else
|
||
NC=''; BOLD=''
|
||
fg() { :; }
|
||
fi
|
||
|
||
V1="$(fg 196 181 253)"; V2="$(fg 167 139 250)"; V3="$(fg 139 92 246)"
|
||
V4="$(fg 124 58 237)"; V5="$(fg 109 40 217)"
|
||
WHITE="$(fg 237 233 254)"; MUTED="$(fg 165 160 190)"; FAINT="$(fg 113 108 140)"
|
||
GREEN="$(fg 52 211 153)"; RED="$(fg 248 113 113)"; YELLOW="$(fg 250 204 21)"
|
||
CYAN="$(fg 103 232 249)"
|
||
|
||
# Configuration
|
||
INSTALL_DIR="/usr/local/bin"
|
||
CONFIG_DIR="/etc/serverkit-agent"
|
||
LOG_DIR="/var/log/serverkit-agent"
|
||
SERVICE_USER="serverkit-agent"
|
||
# The agent publishes its own releases from its own repo, tagged plain `vX.Y.Z`.
|
||
# It used to live in the panel monorepo under `agent-v*` tags, and this script
|
||
# was never updated when it moved -- so every download 404'd against a tag scheme
|
||
# that no longer exists anywhere (ServerKit issue #101).
|
||
GITHUB_REPO="jhd3197/serverkit-agent"
|
||
AGENT_BINARY="serverkit-agent"
|
||
|
||
# Arguments
|
||
TOKEN=""
|
||
SERVER_URL=""
|
||
SERVER_NAME=""
|
||
VERSION="latest"
|
||
|
||
# Version injected by the panel when it serves this script (the panel already
|
||
# resolves the latest agent release for its update endpoints). Empty means
|
||
# discover the version via the GitHub API in get_latest_version().
|
||
SERVERKIT_AGENT_VERSION=""
|
||
|
||
print_banner() {
|
||
echo
|
||
printf " ${V1}${BOLD}╔═╗┌─┐┌─┐┌┬┐┌─┐┬┌─┐${NC}\n"
|
||
printf " ${V2}${BOLD}╚═╗│ ││ │ ├┤ │├┤ ${NC} ${WHITE}${BOLD}ServerKit Agent${NC}\n"
|
||
printf " ${V3}${BOLD}╚═╝└─┘└─┘ ┴ └─┘┴└──┘${NC} ${FAINT}v%s${NC}\n" "$VERSION"
|
||
echo
|
||
}
|
||
|
||
log_info() {
|
||
echo -e " ${CYAN}›${NC} $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e " ${GREEN}●${NC} $1"
|
||
}
|
||
|
||
log_warn() {
|
||
echo -e " ${YELLOW}▲${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e " ${RED}■${NC} $1"
|
||
exit 1
|
||
}
|
||
|
||
show_help() {
|
||
echo "ServerKit Agent Installation Script"
|
||
echo ""
|
||
echo "Usage: install.sh [OPTIONS]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --token, -t Registration token (required)"
|
||
echo " --server, -s ServerKit server URL (required)"
|
||
echo " --name, -n Display name for this server (optional)"
|
||
echo " --version, -v Specific agent version (optional, defaults to latest)"
|
||
echo " --help, -h Show this help message"
|
||
echo ""
|
||
echo "Example:"
|
||
echo " curl -fsSL https://your-serverkit.com/api/v1/servers/install.sh \\"
|
||
echo " -o /tmp/serverkit-agent-install.sh \\"
|
||
echo " && sudo bash /tmp/serverkit-agent-install.sh \\"
|
||
echo " --token 'sk_reg_xxx' \\"
|
||
echo " --server 'https://your-serverkit.com'"
|
||
exit 0
|
||
}
|
||
|
||
parse_args() {
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--token|-t)
|
||
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
|
||
TOKEN="$2"
|
||
shift 2
|
||
;;
|
||
--server|-s)
|
||
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
|
||
SERVER_URL="$2"
|
||
shift 2
|
||
;;
|
||
--name|-n)
|
||
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
|
||
SERVER_NAME="$2"
|
||
shift 2
|
||
;;
|
||
--version|-v)
|
||
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
|
||
VERSION="$2"
|
||
shift 2
|
||
;;
|
||
--help|-h)
|
||
show_help
|
||
;;
|
||
*)
|
||
log_error "Unknown option: $1"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Validate required arguments
|
||
if [[ -z "$TOKEN" ]]; then
|
||
log_error "Registration token is required. Use --token or -t"
|
||
fi
|
||
|
||
if [[ -z "$SERVER_URL" ]]; then
|
||
log_error "Server URL is required. Use --server or -s"
|
||
fi
|
||
}
|
||
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
log_error "This script must be run as root (use sudo)"
|
||
fi
|
||
}
|
||
|
||
detect_platform() {
|
||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||
ARCH=$(uname -m)
|
||
|
||
case "$ARCH" in
|
||
x86_64|amd64)
|
||
ARCH="amd64"
|
||
;;
|
||
aarch64|arm64)
|
||
ARCH="arm64"
|
||
;;
|
||
*)
|
||
log_error "Unsupported architecture: $ARCH"
|
||
;;
|
||
esac
|
||
|
||
if [[ "$OS" != "linux" ]]; then
|
||
log_error "This script only supports Linux. For Windows, use install.ps1"
|
||
fi
|
||
|
||
log_info "Detected platform: ${OS}-${ARCH}"
|
||
}
|
||
|
||
check_dependencies() {
|
||
log_info "Checking dependencies..."
|
||
|
||
# Check for required tools
|
||
for cmd in curl tar; do
|
||
if ! command -v "$cmd" &> /dev/null; then
|
||
log_error "$cmd is required but not installed"
|
||
fi
|
||
done
|
||
|
||
# Check for systemd
|
||
if ! command -v systemctl &> /dev/null; then
|
||
log_warn "systemd not found. You will need to start the agent manually."
|
||
fi
|
||
}
|
||
|
||
get_latest_version() {
|
||
if [[ "$VERSION" != "latest" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
# The panel injects the version it already resolved when serving this
|
||
# script — use it and keep GitHub out of the enrollment path entirely.
|
||
if [[ -n "$SERVERKIT_AGENT_VERSION" ]]; then
|
||
VERSION="$SERVERKIT_AGENT_VERSION"
|
||
log_info "Using panel-resolved version: v${VERSION}"
|
||
return 0
|
||
fi
|
||
|
||
log_info "Fetching latest version..."
|
||
|
||
# Every release in the agent repo is an agent release, so the first vX.Y.Z
|
||
# tag is the one we want. The paging kept from the monorepo era costs one
|
||
# extra request at most and covers a repo with many pre-release tags.
|
||
#
|
||
# The `[0-9]` is load-bearing: the repo carries a malformed release literally
|
||
# tagged "v", which would otherwise parse to an empty VERSION and send the
|
||
# download at .../releases/download/v/serverkit-agent--linux-amd64.tar.gz.
|
||
local page releases
|
||
VERSION=""
|
||
for page in 1 2 3; do
|
||
releases=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=100&page=${page}") || break
|
||
VERSION=$(printf '%s\n' "$releases" | grep -o '"tag_name": *"v[0-9][^"]*"' | head -1 | sed -e 's/.*"v//' -e 's/"$//') || true
|
||
[[ -n "$VERSION" ]] && break
|
||
# A page with no tags at all means the release list is exhausted.
|
||
printf '%s' "$releases" | grep -q '"tag_name"' || break
|
||
done
|
||
|
||
if [[ -z "$VERSION" ]]; then
|
||
log_error "Failed to fetch latest version"
|
||
fi
|
||
log_info "Latest version: v${VERSION}"
|
||
}
|
||
|
||
# Verify the downloaded archive against the release's checksums.txt.
|
||
# Best-effort when checksums.txt is unavailable (older releases); a present-
|
||
# but-mismatching checksum is a hard failure.
|
||
verify_checksum() {
|
||
local tmp_dir="$1" asset_name="$2"
|
||
local checksums_url="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/checksums.txt"
|
||
|
||
if ! command -v sha256sum &> /dev/null; then
|
||
log_warn "sha256sum not available — skipping checksum verification"
|
||
return 0
|
||
fi
|
||
|
||
if ! curl -fsSL "$checksums_url" -o "${tmp_dir}/checksums.txt"; then
|
||
log_warn "Could not download checksums.txt — skipping verification"
|
||
return 0
|
||
fi
|
||
|
||
if ! grep -q "$asset_name" "${tmp_dir}/checksums.txt"; then
|
||
log_warn "checksums.txt has no entry for ${asset_name} — skipping verification"
|
||
return 0
|
||
fi
|
||
|
||
if ! (cd "$tmp_dir" && sha256sum -c <(grep "$asset_name" checksums.txt) >/dev/null 2>&1); then
|
||
rm -rf "$tmp_dir"
|
||
log_error "Checksum verification FAILED for ${asset_name} — aborting install"
|
||
fi
|
||
|
||
log_success "Checksum verified"
|
||
}
|
||
|
||
download_agent() {
|
||
log_info "Downloading ServerKit Agent v${VERSION}..."
|
||
|
||
ASSET_NAME="serverkit-agent-${VERSION}-linux-${ARCH}.tar.gz"
|
||
DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/${ASSET_NAME}"
|
||
TMP_DIR=$(mktemp -d)
|
||
ARCHIVE="${TMP_DIR}/${ASSET_NAME}"
|
||
|
||
if ! curl -fsSL "$DOWNLOAD_URL" -o "$ARCHIVE"; then
|
||
rm -rf "$TMP_DIR"
|
||
log_error "Failed to download agent from: $DOWNLOAD_URL"
|
||
fi
|
||
|
||
verify_checksum "$TMP_DIR" "$ASSET_NAME"
|
||
|
||
# Extract binary
|
||
log_info "Extracting agent..."
|
||
tar -xzf "$ARCHIVE" -C "$TMP_DIR"
|
||
|
||
# Install binary
|
||
mv "${TMP_DIR}/serverkit-agent-linux-${ARCH}" "${INSTALL_DIR}/${AGENT_BINARY}"
|
||
chmod +x "${INSTALL_DIR}/${AGENT_BINARY}"
|
||
|
||
# Cleanup
|
||
rm -rf "$TMP_DIR"
|
||
|
||
log_success "Agent installed to ${INSTALL_DIR}/${AGENT_BINARY}"
|
||
}
|
||
|
||
create_user() {
|
||
if id "$SERVICE_USER" &>/dev/null; then
|
||
log_info "User $SERVICE_USER already exists"
|
||
else
|
||
log_info "Creating service user: $SERVICE_USER"
|
||
if command -v useradd &> /dev/null; then
|
||
useradd -r -s /bin/false -d /nonexistent "$SERVICE_USER"
|
||
elif command -v adduser &> /dev/null; then
|
||
# Alpine/busybox ships adduser instead of useradd
|
||
adduser -S -D -H -s /bin/false "$SERVICE_USER"
|
||
else
|
||
log_error "Cannot create user: neither useradd nor adduser found"
|
||
fi
|
||
fi
|
||
|
||
# Add to docker group if it exists
|
||
if getent group docker > /dev/null 2>&1; then
|
||
if command -v usermod &> /dev/null; then
|
||
usermod -aG docker "$SERVICE_USER"
|
||
else
|
||
# busybox equivalent of usermod -aG
|
||
addgroup "$SERVICE_USER" docker
|
||
fi
|
||
log_info "Added $SERVICE_USER to docker group"
|
||
fi
|
||
}
|
||
|
||
create_config_dir() {
|
||
log_info "Creating configuration and log directories..."
|
||
mkdir -p "$CONFIG_DIR" "$LOG_DIR"
|
||
chown "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR"
|
||
chmod 750 "$CONFIG_DIR" "$LOG_DIR"
|
||
}
|
||
|
||
register_agent() {
|
||
log_info "Registering agent with ServerKit..."
|
||
|
||
REGISTER_CMD=(
|
||
"${INSTALL_DIR}/${AGENT_BINARY}"
|
||
--config "${CONFIG_DIR}/config.yaml"
|
||
register
|
||
--token "${TOKEN}"
|
||
--server "${SERVER_URL}"
|
||
)
|
||
|
||
if [[ -n "$SERVER_NAME" ]]; then
|
||
REGISTER_CMD+=(--name "${SERVER_NAME}")
|
||
fi
|
||
|
||
if ! "${REGISTER_CMD[@]}"; then
|
||
log_error "Agent registration failed"
|
||
fi
|
||
|
||
# Fix permissions on config files
|
||
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR"
|
||
[[ -f "${CONFIG_DIR}/config.yaml" ]] && chmod 600 "${CONFIG_DIR}/config.yaml"
|
||
[[ -f "${CONFIG_DIR}/agent.key" ]] && chmod 600 "${CONFIG_DIR}/agent.key"
|
||
|
||
log_success "Agent registered successfully"
|
||
}
|
||
|
||
install_systemd_service() {
|
||
if ! command -v systemctl &> /dev/null; then
|
||
log_warn "Skipping systemd service installation (systemd not found)"
|
||
return
|
||
fi
|
||
|
||
log_info "Installing systemd service..."
|
||
|
||
cat > /etc/systemd/system/serverkit-agent.service << EOF
|
||
[Unit]
|
||
Description=ServerKit Agent
|
||
Documentation=https://github.com/${GITHUB_REPO}
|
||
After=network-online.target docker.service
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=${SERVICE_USER}
|
||
Group=${SERVICE_USER}
|
||
ExecStart=${INSTALL_DIR}/${AGENT_BINARY} --config ${CONFIG_DIR}/config.yaml start
|
||
Restart=always
|
||
RestartSec=10
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
SyslogIdentifier=serverkit-agent
|
||
|
||
# Security hardening
|
||
NoNewPrivileges=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ReadWritePaths=${CONFIG_DIR} ${LOG_DIR}
|
||
PrivateTmp=true
|
||
ProtectKernelLogs=yes
|
||
ProtectKernelModules=yes
|
||
ProtectKernelTunables=yes
|
||
ProtectControlGroups=yes
|
||
ProtectClock=yes
|
||
ProtectHostname=yes
|
||
RestrictRealtime=yes
|
||
RestrictSUIDSGID=yes
|
||
LockPersonality=yes
|
||
MemoryMax=512M
|
||
LimitNOFILE=65535
|
||
|
||
# Environment
|
||
Environment=HOME=/nonexistent
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable serverkit-agent
|
||
|
||
log_success "Systemd service installed"
|
||
}
|
||
|
||
start_service() {
|
||
if ! command -v systemctl &> /dev/null; then
|
||
log_warn "Cannot auto-start without systemd"
|
||
echo ""
|
||
echo "To start the agent manually:"
|
||
echo " ${INSTALL_DIR}/${AGENT_BINARY} --config ${CONFIG_DIR}/config.yaml start"
|
||
return
|
||
fi
|
||
|
||
log_info "Starting ServerKit Agent..."
|
||
systemctl start serverkit-agent
|
||
|
||
sleep 2
|
||
|
||
if systemctl is-active --quiet serverkit-agent; then
|
||
log_success "Agent is running"
|
||
else
|
||
log_error "Failed to start agent. Check logs with: journalctl -u serverkit-agent"
|
||
fi
|
||
}
|
||
|
||
print_success() {
|
||
echo ""
|
||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ Installation completed successfully! ║${NC}"
|
||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo "Agent Status:"
|
||
echo " Binary: ${INSTALL_DIR}/${AGENT_BINARY}"
|
||
echo " Config: ${CONFIG_DIR}/config.yaml"
|
||
echo " Service: serverkit-agent"
|
||
echo ""
|
||
echo "Useful commands:"
|
||
echo " Check status: systemctl status serverkit-agent"
|
||
echo " View logs: journalctl -u serverkit-agent -f"
|
||
echo " Restart agent: systemctl restart serverkit-agent"
|
||
echo " Stop agent: systemctl stop serverkit-agent"
|
||
echo ""
|
||
}
|
||
|
||
# Main execution
|
||
main() {
|
||
print_banner
|
||
parse_args "$@"
|
||
check_root
|
||
detect_platform
|
||
check_dependencies
|
||
get_latest_version
|
||
download_agent
|
||
create_user
|
||
create_config_dir
|
||
register_agent
|
||
install_systemd_service
|
||
start_service
|
||
print_success
|
||
}
|
||
|
||
# Sourcing this file (e.g. from scripts/test/test_agent_install.sh) defines
|
||
# every function above for unit testing without running an install. Piped
|
||
# execution (curl | bash) has no BASH_SOURCE[0], so only a real `source`
|
||
# takes the early return.
|
||
if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
|
||
return 0
|
||
fi
|
||
|
||
main "$@"
|