#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Defaults MODE="" WITH_LIVEKIT=false CUSTOM_VALUES="" NODES="" NAMESPACE="loop" RELEASE_NAME="loop-stack" LIVEKIT_RELEASE_NAME="loop-livekit" usage() { cat < Loop Air-Gapped Installer" echo " Mode: ${MODE}" echo " LiveKit: ${WITH_LIVEKIT}" # ---- Detect container runtime ---- detect_runtime() { if command -v docker &>/dev/null; then echo "docker" elif command -v ctr &>/dev/null; then echo "containerd" elif command -v crictl &>/dev/null; then echo "crictl" else echo "none" fi } # ---- Load images locally ---- load_images_local() { local dir="$1" local runtime runtime=$(detect_runtime) echo "==> Loading images from ${dir} (runtime: ${runtime})..." for tarfile in "$dir"/*.tar; do [[ -f "$tarfile" ]] || continue echo " Loading: $(basename "$tarfile")" case "$runtime" in docker) docker load -i "$tarfile" ;; containerd) ctr -n k8s.io images import "$tarfile" ;; crictl) if command -v ctr &>/dev/null; then ctr -n k8s.io images import "$tarfile" else echo " WARNING: crictl detected but ctr not found. Please load manually:" echo " ctr -n k8s.io images import $tarfile" fi ;; *) echo " ERROR: No container runtime found. Install docker or containerd." exit 1 ;; esac done } # ---- Load images on remote nodes via SSH ---- load_images_remote() { local dir="$1" local nodes_csv="$2" IFS=',' read -ra NODE_LIST <<< "$nodes_csv" for node in "${NODE_LIST[@]}"; do node=$(echo "$node" | xargs) echo "==> Loading images on remote node: ${node}" ssh "$node" "mkdir -p /tmp/loop-airgap-images" echo " Copying image tars to ${node}..." scp "$dir"/*.tar "${node}:/tmp/loop-airgap-images/" ssh "$node" bash -s <<'REMOTE_SCRIPT' set -euo pipefail if command -v ctr &>/dev/null; then for f in /tmp/loop-airgap-images/*.tar; do echo " Loading: $(basename "$f")" ctr -n k8s.io images import "$f" done elif command -v docker &>/dev/null; then for f in /tmp/loop-airgap-images/*.tar; do echo " Loading: $(basename "$f")" docker load -i "$f" done else echo "ERROR: No container runtime found on $(hostname)" exit 1 fi rm -rf /tmp/loop-airgap-images REMOTE_SCRIPT echo " Done: ${node}" done } # ==== MAIN INSTALLATION ==== # ---- Load core images locally ---- load_images_local "$SCRIPT_DIR/images" # ---- Load LiveKit images if requested ---- if [[ "$WITH_LIVEKIT" == "true" ]] && [[ -d "$SCRIPT_DIR/images/livekit" ]]; then load_images_local "$SCRIPT_DIR/images/livekit" fi # ---- Load images on remote nodes (Helm mode only) ---- if [[ "$MODE" == "helm" ]] && [[ -n "$NODES" ]]; then load_images_remote "$SCRIPT_DIR/images" "$NODES" if [[ "$WITH_LIVEKIT" == "true" ]] && [[ -d "$SCRIPT_DIR/images/livekit" ]]; then load_images_remote "$SCRIPT_DIR/images/livekit" "$NODES" fi fi # ==== HELM MODE ==== if [[ "$MODE" == "helm" ]]; then if ! command -v helm &>/dev/null; then echo "ERROR: 'helm' is required but not found in PATH" exit 1 fi CHART_DIR="$SCRIPT_DIR/helm/loop-enterprise-stack" AIRGAP_VALUES="$CHART_DIR/values-airgap.yaml" echo "==> Installing Loop via Helm..." echo " Namespace: ${NAMESPACE}" echo " Release: ${RELEASE_NAME}" HELM_CMD="helm upgrade --install ${RELEASE_NAME} ${CHART_DIR}" HELM_CMD+=" --namespace ${NAMESPACE} --create-namespace" if [[ -f "$AIRGAP_VALUES" ]]; then HELM_CMD+=" -f ${AIRGAP_VALUES}" fi if [[ -n "$CUSTOM_VALUES" ]]; then HELM_CMD+=" -f ${CUSTOM_VALUES}" fi echo " Running: ${HELM_CMD}" eval "$HELM_CMD" if [[ "$WITH_LIVEKIT" == "true" ]]; then LK_CHART_DIR="$SCRIPT_DIR/helm/loop-livekit-server" LK_AIRGAP_VALUES="$LK_CHART_DIR/values-airgap.yaml" echo "==> Installing LiveKit via Helm..." LK_CMD="helm upgrade --install ${LIVEKIT_RELEASE_NAME} ${LK_CHART_DIR}" LK_CMD+=" --namespace ${NAMESPACE} --create-namespace" if [[ -f "$LK_AIRGAP_VALUES" ]]; then LK_CMD+=" -f ${LK_AIRGAP_VALUES}" fi echo " Running: ${LK_CMD}" eval "$LK_CMD" fi echo "" echo "==> Helm installation complete!" echo " Check status: helm list -n ${NAMESPACE}" echo " Check pods: kubectl get pods -n ${NAMESPACE}" fi # ==== COMPOSE MODE ==== if [[ "$MODE" == "compose" ]]; then COMPOSE_DIR="$SCRIPT_DIR/docker-compose" AIRGAP_OVERRIDE="$COMPOSE_DIR/docker-compose-airgap.override.yml" echo "==> Installing Loop via Docker Compose..." COMPOSE_CMD="docker compose -f ${COMPOSE_DIR}/docker-compose.yml" if [[ -f "$AIRGAP_OVERRIDE" ]]; then COMPOSE_CMD+=" -f ${AIRGAP_OVERRIDE}" fi COMPOSE_CMD+=" up -d" echo " Running: ${COMPOSE_CMD}" eval "$COMPOSE_CMD" echo "" echo "==> Docker Compose installation complete!" echo " Check status: docker compose -f ${COMPOSE_DIR}/docker-compose.yml ps" fi