#!/usr/bin/env bash set -euo pipefail INSTALLER_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="${ENV_FILE:-$INSTALLER_ROOT/.env}" ENV_EXAMPLE="$INSTALLER_ROOT/.env.example" COMPOSE_FILE="$INSTALLER_ROOT/compose/docker-compose.yml" MANIFEST_FILE="${MANIFEST_FILE:-$INSTALLER_ROOT/PACKAGE-MANIFEST.env}" log() { printf '[ours-rp-installer] %s\n' "$*" } warn() { printf '[ours-rp-installer][WARN] %s\n' "$*" >&2 } die() { printf '[ours-rp-installer][ERROR] %s\n' "$*" >&2 exit 1 } require_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1" } normalize_arch() { case "$1" in amd64|x86_64|linux/amd64) printf 'amd64\n' ;; arm64|aarch64|linux/arm64) printf 'arm64\n' ;; *) return 1 ;; esac } platform_for_arch() { printf 'linux/%s\n' "$1" } env_flag_enabled() { case "${1:-0}" in 1|true|TRUE|yes|YES|on|ON) return 0 ;; *) return 1 ;; esac } detect_host_arch() { local raw_arch raw_arch="$(uname -m)" normalize_arch "$raw_arch" || die "unsupported host architecture: $raw_arch" } load_manifest() { MANIFEST_PACKAGE_ARCH_RAW="" MANIFEST_PACKAGE_PLATFORM_RAW="" if [[ -f "$MANIFEST_FILE" ]]; then set -a # shellcheck disable=SC1090 source "$MANIFEST_FILE" set +a MANIFEST_PACKAGE_ARCH_RAW="${PACKAGE_ARCH:-${package_arch:-}}" MANIFEST_PACKAGE_PLATFORM_RAW="${PACKAGE_PLATFORM:-${package_platform:-}}" fi } effective_package_arch() { if [[ -n "${PACKAGE_ARCH:-}" ]]; then normalize_arch "$PACKAGE_ARCH" || die "unsupported PACKAGE_ARCH=${PACKAGE_ARCH}" return 0 fi if [[ -n "${PACKAGE_PLATFORM:-}" ]]; then normalize_arch "$PACKAGE_PLATFORM" || die "unsupported PACKAGE_PLATFORM=${PACKAGE_PLATFORM}" return 0 fi if [[ -n "${RPKI_PLATFORM:-}" ]]; then normalize_arch "$RPKI_PLATFORM" || die "unsupported RPKI_PLATFORM=${RPKI_PLATFORM}" return 0 fi if [[ -n "${MONITOR_PLATFORM:-}" ]]; then normalize_arch "$MONITOR_PLATFORM" || die "unsupported MONITOR_PLATFORM=${MONITOR_PLATFORM}" return 0 fi die "unable to determine package architecture from manifest or env" } arch_mode() { local host_arch package_arch host_arch="$(detect_host_arch)" package_arch="$(effective_package_arch)" if [[ "$host_arch" == "$package_arch" ]]; then printf 'native\n' return 0 fi if env_flag_enabled "${ALLOW_CROSS_ARCH:-0}"; then printf 'cross-arch-enabled\n' return 0 fi printf 'mismatch-blocked\n' } assert_arch_compatibility() { local host_arch package_arch package_platform mode host_arch="$(detect_host_arch)" package_arch="$(effective_package_arch)" package_platform="${PACKAGE_PLATFORM:-${RPKI_PLATFORM:-$(platform_for_arch "$package_arch")}}" mode="$(arch_mode)" case "$mode" in native) return 0 ;; cross-arch-enabled) log "cross-arch execution explicitly enabled: host_arch=$host_arch package_arch=$package_arch package_platform=$package_platform" return 0 ;; mismatch-blocked) die "package architecture mismatch: host_arch=$host_arch package_arch=$package_arch package_platform=$package_platform. Set ALLOW_CROSS_ARCH=1 in $ENV_FILE to allow explicit QEMU/binfmt emulation." ;; *) die "unknown arch compatibility mode: $mode" ;; esac } load_env() { load_manifest if [[ ! -f "$ENV_FILE" ]]; then [[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE" cp "$ENV_EXAMPLE" "$ENV_FILE" log "created .env from .env.example" fi set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a if [[ -n "${MANIFEST_PACKAGE_ARCH_RAW:-}" ]]; then manifest_package_arch="$(normalize_arch "$MANIFEST_PACKAGE_ARCH_RAW")" || die "unsupported manifest package arch: $MANIFEST_PACKAGE_ARCH_RAW" if [[ -n "${PACKAGE_ARCH:-}" ]]; then env_package_arch="$(normalize_arch "$PACKAGE_ARCH")" || die "unsupported env package arch: $PACKAGE_ARCH" [[ "$env_package_arch" == "$manifest_package_arch" ]] || die "env PACKAGE_ARCH=$env_package_arch mismatches manifest PACKAGE_ARCH=$manifest_package_arch" fi PACKAGE_ARCH="$manifest_package_arch" else if [[ -z "${PACKAGE_ARCH:-}" ]]; then PACKAGE_ARCH="$(effective_package_arch)" else PACKAGE_ARCH="$(normalize_arch "$PACKAGE_ARCH")" fi fi if [[ -n "${MANIFEST_PACKAGE_PLATFORM_RAW:-}" ]]; then manifest_package_platform="$MANIFEST_PACKAGE_PLATFORM_RAW" if [[ -n "${PACKAGE_PLATFORM:-}" && "$PACKAGE_PLATFORM" != "$manifest_package_platform" ]]; then die "env PACKAGE_PLATFORM=$PACKAGE_PLATFORM mismatches manifest PACKAGE_PLATFORM=$manifest_package_platform" fi PACKAGE_PLATFORM="$manifest_package_platform" else PACKAGE_PLATFORM="${PACKAGE_PLATFORM:-$(platform_for_arch "$PACKAGE_ARCH")}" fi HOST_DATA_DIR="${HOST_DATA_DIR:-/var/lib/ours-rp-${PACKAGE_ARCH}-installer}" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ours-rp-${PACKAGE_ARCH}-installer}" RPKI_IMAGE="${RPKI_IMAGE:-ours-rp-runtime-${PACKAGE_ARCH}:dev}" RPKI_PLATFORM="${RPKI_PLATFORM:-$PACKAGE_PLATFORM}" MONITOR_PLATFORM="${MONITOR_PLATFORM:-$PACKAGE_PLATFORM}" PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}" GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}" METRICS_INSTANCE="${METRICS_INSTANCE:-${PACKAGE_ARCH}-installer}" ALLOW_CROSS_ARCH="${ALLOW_CROSS_ARCH:-0}" FIRST_RUN_WAIT_TIMEOUT_SECS="${FIRST_RUN_WAIT_TIMEOUT_SECS:-7200}" } compose_cmd() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT_NAME" "$@" } create_data_dirs() { load_env mkdir -p \ "$HOST_DATA_DIR/state" \ "$HOST_DATA_DIR/runs" \ "$HOST_DATA_DIR/logs" \ "$HOST_DATA_DIR/tmp" \ "$HOST_DATA_DIR/prometheus" \ "$HOST_DATA_DIR/grafana" chmod 755 "$HOST_DATA_DIR" "$HOST_DATA_DIR/state" "$HOST_DATA_DIR/runs" "$HOST_DATA_DIR/logs" "$HOST_DATA_DIR/tmp" || true chmod 777 "$HOST_DATA_DIR/prometheus" "$HOST_DATA_DIR/grafana" || true } latest_run_dir() { load_env find "$HOST_DATA_DIR/runs" -maxdepth 1 -mindepth 1 -type d -name 'run_*' 2>/dev/null | sort | tail -1 } latest_success_run_dir() { load_env find "$HOST_DATA_DIR/runs" -maxdepth 2 -type f -path '*/run-summary.json' 2>/dev/null \ | while read -r summary; do if jq -e '.status == "success"' "$summary" >/dev/null 2>&1; then dirname "$summary" fi done | sort | tail -1 } has_success_run() { [[ -n "$(latest_success_run_dir)" ]] } print_run_summary() { local run_dir="$1" local summary="$run_dir/run-summary.json" local meta="$run_dir/run-meta.json" local timing="$run_dir/stage-timing.json" local process_time="$run_dir/process-time.txt" local vrps_file="$run_dir/vrps.csv" local vaps_file="$run_dir/vaps.csv" local status="unknown" local sync_mode="unknown" local wall_ms="null" local validation_ms="null" local repo_sync_ms="null" local max_rss_kb="null" local publication_points="null" local vrps="null" local vaps="null" local warnings="null" [[ -f "$summary" ]] || { warn "missing run-summary.json in $run_dir" return 1 } status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)" wall_ms="$(jq -r '.wallMs // .wall_ms // "null"' "$summary" 2>/dev/null || echo null)" warnings="$(jq -r '.warningCount // .warnings // "null"' "$summary" 2>/dev/null || echo null)" if [[ -f "$meta" ]]; then sync_mode="$(jq -r '.sync_mode // .syncMode // "unknown"' "$meta" 2>/dev/null || echo unknown)" status="$(jq -r --arg fallback "$status" '.status // $fallback' "$meta" 2>/dev/null || echo "$status")" fi if [[ -f "$timing" ]]; then validation_ms="$(jq -r '.validation_ms // "null"' "$timing" 2>/dev/null || echo null)" repo_sync_ms="$(jq -r '.repo_sync_ms_total // "null"' "$timing" 2>/dev/null || echo null)" publication_points="$(jq -r '.publication_points // "null"' "$timing" 2>/dev/null || echo null)" fi if [[ -f "$process_time" ]]; then max_rss_kb="$(awk -F': ' '/Maximum resident set size/ {print $2; found=1} END {if (!found) print "null"}' "$process_time")" fi if [[ -f "$vrps_file" ]]; then vrps="$(( $(wc -l < "$vrps_file") > 0 ? $(wc -l < "$vrps_file") - 1 : 0 ))" fi if [[ -f "$vaps_file" ]]; then vaps="$(( $(wc -l < "$vaps_file") > 0 ? $(wc -l < "$vaps_file") - 1 : 0 ))" fi jq -n \ --arg run "$(basename "$run_dir")" \ --arg status "$status" \ --arg syncMode "$sync_mode" \ --argjson wallMs "$wall_ms" \ --argjson validationMs "$validation_ms" \ --argjson repoSyncMs "$repo_sync_ms" \ --argjson maxRssKb "$max_rss_kb" \ --argjson vrps "$vrps" \ --argjson vaps "$vaps" \ --argjson publicationPoints "$publication_points" \ --argjson warnings "$warnings" \ '{run:$run,status:$status,syncMode:$syncMode,wallMs:$wallMs,validationMs:$validationMs,repoSyncMs:$repoSyncMs,maxRssKb:$maxRssKb,vrps:$vrps,vaps:$vaps,publicationPoints:$publicationPoints,warnings:$warnings}' } wait_for_new_success_run() { local before_latest="$1" local timeout_secs="$2" local start_epoch now run_dir summary meta status meta_status start_epoch="$(date +%s)" while true; do run_dir="$(latest_run_dir || true)" if [[ -n "$run_dir" && "$run_dir" != "$before_latest" ]]; then summary="$run_dir/run-summary.json" meta="$run_dir/run-meta.json" if [[ -f "$summary" ]]; then status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)" if [[ "$status" == "success" ]]; then meta_status="unknown" if [[ -f "$meta" ]]; then meta_status="$(jq -r '.status // "unknown"' "$meta" 2>/dev/null || echo unknown)" fi if [[ "$meta_status" == "success" ]]; then print_run_summary "$run_dir" || true return 0 fi fi if [[ "$status" == "failed" || "$status" == "error" ]]; then print_run_summary "$run_dir" || true die "run failed: $run_dir" fi fi fi now="$(date +%s)" if (( now - start_epoch > timeout_secs )); then die "timed out waiting for first successful run after ${timeout_secs}s" fi sleep 10 done } docker_compose_available() { docker compose version >/dev/null 2>&1 } install_docker_if_missing() { if command -v docker >/dev/null 2>&1 && docker_compose_available && command -v jq >/dev/null 2>&1 && command -v rsync >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then log "docker and docker compose are already installed" return 0 fi if [[ "${SKIP_DEP_INSTALL:-0}" == "1" ]]; then die "docker/docker compose missing and SKIP_DEP_INSTALL=1" fi if ! command -v apt-get >/dev/null 2>&1; then die "docker/docker compose missing; automatic install currently supports apt-get only" fi log "installing missing runtime packages via apt" apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl jq rsync gzip tar docker.io if ! docker_compose_available; then if apt-cache show docker-compose-v2 >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-v2 elif apt-cache show docker-compose-plugin >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin elif apt-cache show docker-compose >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose fi fi systemctl enable --now docker >/dev/null 2>&1 || true docker_compose_available || die "docker compose is still unavailable after install" } load_installer_images() { require_cmd docker shopt -s nullglob local image load_output desired_tag local found=0 for image in "$INSTALLER_ROOT"/images/*.tar "$INSTALLER_ROOT"/images/*.tar.gz; do found=1 log "loading docker image: $image" if [[ "$image" == *.gz ]]; then load_output="$(gzip -dc "$image" | docker load)" else load_output="$(docker load -i "$image")" fi printf '%s\n' "$load_output" desired_tag="" case "$(basename "$image")" in "${image_tar:-}") desired_tag="${image_tag:-$RPKI_IMAGE}" ;; "${prometheus_image_tar:-}") desired_tag="${prometheus_image:-$PROMETHEUS_IMAGE}" ;; "${grafana_image_tar:-}") desired_tag="${grafana_image:-$GRAFANA_IMAGE}" ;; esac if [[ -n "$desired_tag" ]]; then ensure_loaded_image_tag "$desired_tag" "$load_output" "$image" fi done shopt -u nullglob (( found == 1 )) || warn "no image tar found under $INSTALLER_ROOT/images" } ensure_loaded_image_tag() { local desired_tag="$1" local load_output="$2" local image_path="$3" if docker image inspect "$desired_tag" >/dev/null 2>&1; then return 0 fi local loaded_ref="" loaded_ref="$(printf '%s\n' "$load_output" | awk -F'Loaded image: ' '/Loaded image: / {print $2; exit}')" if [[ -z "$loaded_ref" ]]; then loaded_ref="$(printf '%s\n' "$load_output" | awk -F'Loaded image ID: ' '/Loaded image ID: / {print $2; exit}')" fi [[ -n "$loaded_ref" ]] || die "unable to determine loaded image reference for $image_path" log "tagging loaded image for package use: source=$loaded_ref target=$desired_tag" docker tag "$loaded_ref" "$desired_tag" } ensure_binfmt_if_needed() { require_cmd docker load_env local host_arch package_arch host_arch="$(detect_host_arch)" package_arch="$(effective_package_arch)" if [[ "$host_arch" == "$package_arch" ]]; then return 0 fi assert_arch_compatibility log "host arch is $host_arch; enabling binfmt/qemu for package arch $package_arch" docker run --rm --privileged tonistiigi/binfmt --install "$package_arch" } verify_runtime_image() { load_env require_cmd docker local help_path help_path="$(mktemp "${TMPDIR:-/tmp}/ours-rp-rpki-help.XXXXXX")" log "verifying runtime image $RPKI_IMAGE on $RPKI_PLATFORM" docker image inspect "$RPKI_IMAGE" >/dev/null docker run --rm --platform "$RPKI_PLATFORM" "$RPKI_IMAGE" /opt/ours-rp/bin/rpki --help >"$help_path" head -5 "$help_path" || true rm -f "$help_path" } verify_image_platform() { local image="$1" local expected_platform="$2" local role="$3" local actual_platform docker image inspect "$image" >/dev/null actual_platform="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null || echo unknown)" [[ "$actual_platform" == "$expected_platform" ]] || die "$role image platform mismatch: image=$image expected=$expected_platform actual=$actual_platform" } verify_monitor_images() { load_env require_cmd docker verify_image_platform "$PROMETHEUS_IMAGE" "$MONITOR_PLATFORM" "prometheus" verify_image_platform "$GRAFANA_IMAGE" "$MONITOR_PLATFORM" "grafana" } endpoint_ok() { local url="$1" curl -fsS --max-time 5 "$url" >/dev/null 2>&1 }