86f7dcc10b
REQ-010: modular install script (detect_os/install_binary/write_systemd_unit/register_target) REQ-011: outbound WebSocket from relay agent to SaaS REQ-012: register with tenant + target metadata REQ-013: heartbeat + exponential backoff reconnect (max 5 → alert) REQ-026 partial: SSH whitelist file + CheckCommand hook (G-004 contract lock, G-007 shadow exec.Cmd test, G-008 scope statement) G-009: unsupported-OS abort with actionable error ---ci--- phase: 4 milestone: v0.1 status: execute ---/ci---
269 lines
10 KiB
Bash
Executable File
269 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# CoreCI Chat Relay Agent — install script (Wave D Task 4, REQ-010, G-009).
|
|
#
|
|
# Modular: detect_os → install_binary → write_systemd_unit → register_target.
|
|
# Idempotent (re-run upgrades). Aborts cleanly on an unsupported OS with an
|
|
# actionable error listing the supported OSes (Edge 16).
|
|
#
|
|
# Usage:
|
|
# CORECI_TENANT_TOKEN=<jwt> CORECI_SAAS_URL=https://chat.coreci.dev \
|
|
# curl -fsSL https://chat.coreci.dev/install.sh | sh
|
|
#
|
|
# Or with the token pre-embedded by the dashboard's /dashboard/relay page:
|
|
# curl -fsSL "https://chat.coreci.dev/install.sh?t=<token>" | sh
|
|
#
|
|
# The script is designed to be readable + auditable: it never pipes an
|
|
# unverified binary into a shell. The downloaded binary's SHA256 is verified
|
|
# against a checksum file before install (R-005 pitfall guard).
|
|
set -euo pipefail
|
|
|
|
# ─── Defaults / constants ────────────────────────────────────────────────────
|
|
readonly BIN_INSTALL_PATH="/usr/local/bin/coreci-relay-agent"
|
|
readonly UNIT_PATH="/etc/systemd/system/coreci-relay-agent.service"
|
|
readonly ENV_DIR="/etc/coreci"
|
|
readonly ENV_FILE="${ENV_DIR}/relay.env"
|
|
readonly SERVICE_NAME="coreci-relay-agent"
|
|
|
|
# Released by the Gitea release for v0.0.5. Override with CORECI_RELEASE_URL for
|
|
# self-hosted deploys. The per-arch binary URL pattern is:
|
|
# ${RELEASE_URL}/coreci-relay-agent-${OS}-${ARCH}
|
|
readonly DEFAULT_RELEASE_URL="https://git.cloudinit.dev/coreci/coreci-chat/releases/download/v0.0.5"
|
|
|
|
# Agent version this script installs (must match the Go binary's AgentVersion).
|
|
readonly AGENT_VERSION="0.0.5"
|
|
|
|
# Supported OSes (REQ-010 Edge 16 / G-009). Used in the abort message.
|
|
readonly SUPPORTED_OS_MSG="Supported: Ubuntu 24.04 LTS, Debian 12+. Detected: ${ID:-unknown} ${VERSION_ID:-unknown}"
|
|
|
|
# Minimum supported VERSION_ID per distro (parsed from /etc/os-release).
|
|
declare -A MIN_VERSIONS=(
|
|
["ubuntu"]="24.04"
|
|
["debian"]="12"
|
|
)
|
|
|
|
# ─── Logging ─────────────────────────────────────────────────────────────────
|
|
log() { printf '[coreci-install] %s\n' "$*" >&2; }
|
|
ok() { printf '[coreci-install] \033[32mok\033[0m %s\n' "$*" >&2; }
|
|
warn() { printf '[coreci-install] \033[33mwarn\033[0m %s\n' "$*" >&2; }
|
|
die() { printf '[coreci-install] \033[31merror\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# ─── detect_os ───────────────────────────────────────────────────────────────
|
|
# Parses /etc/os-release (ID + VERSION_ID). Supported: ubuntu >= 24.04,
|
|
# debian >= 12. Returns 0 + sets OS_NAME/OS_VERSION if supported; returns 1 +
|
|
# prints the actionable error if not (Edge 16, G-009).
|
|
#
|
|
# Exposed for testing: the function only reads OS_NAME/OS_VERSION/ID/
|
|
# VERSION_ID globals; tests can source this script and call detect_os with a
|
|
# fake /etc/os-release by overriding the OS_RELEASE_PATH variable.
|
|
OS_RELEASE_PATH="${OS_RELEASE_PATH:-/etc/os-release}"
|
|
OS_NAME=""
|
|
OS_VERSION=""
|
|
|
|
detect_os() {
|
|
local release_file="$OS_RELEASE_PATH"
|
|
if [[ ! -f "$release_file" ]]; then
|
|
log "Unsupported OS. $SUPPORTED_OS_MSG (no /etc/os-release found)"
|
|
return 1
|
|
fi
|
|
|
|
# Parse ID and VERSION_ID (and PRETTY_NAME for the abort message).
|
|
# shellcheck disable=SC1090
|
|
ID="" VERSION_ID="" PRETTY_NAME=""
|
|
set +u
|
|
# Source the file in a subshell so its KEY=value lines populate our locals
|
|
# without polluting the caller's environment.
|
|
eval "$(grep -E '^(ID|VERSION_ID|PRETTY_NAME)=' "$release_file" || true)"
|
|
set -u
|
|
|
|
ID="${ID:-}"
|
|
VERSION_ID="${VERSION_ID:-}"
|
|
OS_NAME="$ID"
|
|
OS_VERSION="$VERSION_ID"
|
|
|
|
local min="${MIN_VERSIONS[$ID]:-}"
|
|
if [[ -z "$min" ]]; then
|
|
log "Unsupported OS. $SUPPORTED_OS_MSG"
|
|
return 1
|
|
fi
|
|
|
|
# Version compare: Debian's VERSION_ID is "12" or "12.1"; Ubuntu's is "24.04".
|
|
# We compare as floating-ish by splitting on '.' and comparing major then minor.
|
|
if ! version_ge "$VERSION_ID" "$min"; then
|
|
log "Unsupported OS. $SUPPORTED_OS_MSG"
|
|
return 1
|
|
fi
|
|
|
|
ok "detected supported OS: $ID $VERSION_ID"
|
|
return 0
|
|
}
|
|
|
|
# version_ge returns 0 if $1 >= $2, comparing dotted versions numerically
|
|
# (24.04 >= 24.04 → true; 24.10 >= 24.04 → true; 22.04 >= 24.04 → false).
|
|
version_ge() {
|
|
local a="$1" b="$2"
|
|
local IFS=.
|
|
local a_parts=($a) b_parts=($b)
|
|
local i
|
|
for i in 0 1 2 3; do
|
|
local ai="${a_parts[i]:-0}"
|
|
local bi="${b_parts[i]:-0}"
|
|
# Strip non-numeric suffixes (e.g., 12.1 → 12, 24.04 LTS → 24.04).
|
|
ai="${ai//[^0-9]/}"
|
|
bi="${bi//[^0-9]/}"
|
|
if (( 10#$ai > 10#$bi )); then return 0; fi
|
|
if (( 10#$ai < 10#$bi )); then return 1; fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# ─── install_binary ──────────────────────────────────────────────────────────
|
|
# Detects arch (uname -m → amd64/arm64), downloads the Go binary from the
|
|
# SaaS release URL, verifies the SHA256 checksum, and installs to
|
|
# /usr/local/bin/coreci-relay-agent. Idempotent (overwrites on re-run).
|
|
install_binary() {
|
|
local arch release_url bin_url checksum_url tmpdir tmp_bin tmp_sum
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
x86_64|amd64) arch="amd64" ;;
|
|
aarch64|arm64) arch="arm64" ;;
|
|
*) die "Unsupported architecture: $arch (supported: amd64, arm64)" ;;
|
|
esac
|
|
|
|
release_url="${CORECI_RELEASE_URL:-$DEFAULT_RELEASE_URL}"
|
|
bin_url="${release_url}/coreci-relay-agent-linux-${arch}"
|
|
checksum_url="${release_url}/checksums.txt"
|
|
|
|
log "downloading binary: $bin_url"
|
|
tmpdir="$(mktemp -d)"
|
|
trap 'rm -rf "$tmpdir"' RETURN
|
|
tmp_bin="$tmpdir/coreci-relay-agent"
|
|
tmp_sum="$tmpdir/checksums.txt"
|
|
|
|
if ! curl -fsSL "$bin_url" -o "$tmp_bin"; then
|
|
die "failed to download binary from $bin_url"
|
|
fi
|
|
|
|
log "downloading checksum: $checksum_url"
|
|
if ! curl -fsSL "$checksum_url" -o "$tmp_sum"; then
|
|
die "failed to download checksum from $checksum_url"
|
|
fi
|
|
|
|
# Verify: the checksum file has lines like
|
|
# <sha256> coreci-relay-agent-linux-amd64
|
|
local expected_sha actual_sha match_line
|
|
match_line="$(grep -E "coreci-relay-agent-linux-${arch}\$" "$tmp_sum" || true)"
|
|
if [[ -z "$match_line" ]]; then
|
|
die "no checksum entry for linux-${arch} in $checksum_url"
|
|
fi
|
|
expected_sha="$(awk '{print $1}' <<<"$match_line")"
|
|
actual_sha="$(sha256sum "$tmp_bin" | awk '{print $1}')"
|
|
if [[ "$expected_sha" != "$actual_sha" ]]; then
|
|
die "checksum mismatch for $bin_url: expected $expected_sha, got $actual_sha"
|
|
fi
|
|
ok "checksum verified ($expected_sha)"
|
|
|
|
install -m 0755 "$tmp_bin" "$BIN_INSTALL_PATH"
|
|
ok "installed binary to $BIN_INSTALL_PATH"
|
|
}
|
|
|
|
# ─── write_systemd_unit ──────────────────────────────────────────────────────
|
|
# Writes /etc/systemd/system/coreci-relay-agent.service and enables + starts it.
|
|
# Idempotent: overwrites + daemon-reload + restart-if-running on re-run.
|
|
write_systemd_unit() {
|
|
cat >"$UNIT_PATH" <<UNIT
|
|
[Unit]
|
|
Description=CoreCI Chat Relay Agent
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=${BIN_INSTALL_PATH}
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
Environment=CORECI_CONFIG=${ENV_FILE}
|
|
EnvironmentFile=-${ENV_FILE}
|
|
# Hardening
|
|
NoNewPrivileges=true
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
PrivateTmp=true
|
|
ReadWritePaths=${ENV_DIR}
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
|
|
ok "wrote systemd unit to $UNIT_PATH"
|
|
systemctl daemon-reload
|
|
systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || true
|
|
ok "enabled $SERVICE_NAME (will start after register_target sets the token)"
|
|
}
|
|
|
|
# ─── register_target ─────────────────────────────────────────────────────────
|
|
# Writes /etc/coreci/relay.env with CORECI_TENANT_TOKEN + CORECI_SAAS_URL.
|
|
# Idempotent: preserves an existing token if the env vars aren't set on this
|
|
# run (re-run for a binary upgrade should not wipe the registration).
|
|
register_target() {
|
|
local token saas_url
|
|
token="${CORECI_TENANT_TOKEN:-}"
|
|
saas_url="${CORECI_SAAS_URL:-}"
|
|
|
|
mkdir -p "$ENV_DIR"
|
|
chmod 0750 "$ENV_DIR"
|
|
|
|
# Preserve existing values if the env var isn't set on this run.
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
if [[ -z "$token" ]]; then
|
|
token="$(grep -E '^CORECI_TENANT_TOKEN=' "$ENV_FILE" | cut -d= -f2- || true)"
|
|
fi
|
|
if [[ -z "$saas_url" ]]; then
|
|
saas_url="$(grep -E '^CORECI_SAAS_URL=' "$ENV_FILE" | cut -d= -f2- || true)"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$token" ]]; then
|
|
die "CORECI_TENANT_TOKEN is required. Get it from the dashboard's /dashboard/relay page (the curl|bash command embeds it)."
|
|
fi
|
|
if [[ -z "$saas_url" ]]; then
|
|
die "CORECI_SAAS_URL is required (e.g. https://chat.coreci.dev)."
|
|
fi
|
|
|
|
# Write atomically (write to a temp + mv so a crash mid-write doesn't leave a
|
|
# half-written env file).
|
|
local tmp_env="${ENV_FILE}.tmp"
|
|
{
|
|
echo "CORECI_TENANT_TOKEN=${token}"
|
|
echo "CORECI_SAAS_URL=${saas_url}"
|
|
} >"$tmp_env"
|
|
chmod 0600 "$tmp_env"
|
|
mv "$tmp_env" "$ENV_FILE"
|
|
ok "wrote registration to $ENV_FILE (token preserved if already set)"
|
|
}
|
|
|
|
# ─── main ────────────────────────────────────────────────────────────────────
|
|
# Orchestrates: detect_os → install_binary → write_systemd_unit → register_target.
|
|
# On any failure, exits non-zero with an actionable error (set -e + die()).
|
|
main() {
|
|
# Root required: we install to /usr/local/bin + /etc/systemd + /etc/coreci.
|
|
if [[ $EUID -ne 0 ]]; then
|
|
die "must run as root (use sudo). The install writes to /usr/local/bin, /etc/systemd, /etc/coreci."
|
|
fi
|
|
|
|
log "CoreCI Chat Relay Agent installer v$AGENT_VERSION"
|
|
|
|
detect_os || exit 1
|
|
install_binary
|
|
write_systemd_unit
|
|
register_target
|
|
|
|
# Now that the env file exists, start (or restart) the service.
|
|
systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME"
|
|
ok "$SERVICE_NAME started. Check status: systemctl status $SERVICE_NAME"
|
|
ok "install complete. The agent will connect to the SaaS within 60s."
|
|
}
|
|
|
|
# Only run main when executed (not when sourced for testing).
|
|
if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
|
|
main "$@"
|
|
fi |