#!/usr/bin/env bash set -euo pipefail # ORCH-SVC-34-004: Build air-gap bundle for Orchestrator service # Packages container images, configs, and manifests for offline deployment. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" VERSION="${VERSION:-2025.10.0-edge}" CHANNEL="${CHANNEL:-edge}" BUNDLE_DIR="${BUNDLE_DIR:-$REPO_ROOT/out/bundles/orchestrator-${VERSION}}" SRC_DIR="${SRC_DIR:-$REPO_ROOT/out/buildx/orchestrator}" usage() { cat <&2; usage 64 ;; esac done BUNDLE_DIR="${BUNDLE_DIR:-$REPO_ROOT/out/bundles/orchestrator-${VERSION}}" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "[orchestrator-airgap] Building bundle v${VERSION} (${CHANNEL})" echo "[orchestrator-airgap] Output: ${BUNDLE_DIR}" mkdir -p "$BUNDLE_DIR"/{images,configs,manifests,docs} # ------------------------------------------------------------------------------ # Stage 1: Export container images as OCI archives # ------------------------------------------------------------------------------ if [[ "$SKIP_IMAGES" == "false" ]]; then echo "[orchestrator-airgap] Exporting container images..." IMAGES=( "orchestrator-web:${VERSION}" "orchestrator-worker:${VERSION}" ) for img in "${IMAGES[@]}"; do img_name="${img%%:*}" img_file="${BUNDLE_DIR}/images/${img_name}.oci.tar.gz" if [[ -f "${SRC_DIR}/${img_name}/image.oci" ]]; then echo "[orchestrator-airgap] Packaging ${img_name} from buildx output..." gzip -c "${SRC_DIR}/${img_name}/image.oci" > "$img_file" else echo "[orchestrator-airgap] Exporting ${img_name} via docker save..." docker save "registry.stella-ops.org/stellaops/${img}" | gzip > "$img_file" fi # Generate checksum sha256sum "$img_file" | cut -d' ' -f1 > "${img_file}.sha256" # Copy SBOM if available if [[ -f "${SRC_DIR}/${img_name}/sbom.syft.json" ]]; then cp "${SRC_DIR}/${img_name}/sbom.syft.json" "${BUNDLE_DIR}/manifests/${img_name}.sbom.json" fi done else echo "[orchestrator-airgap] Skipping image export (--skip-images)" fi # ------------------------------------------------------------------------------ # Stage 2: Copy configuration templates # ------------------------------------------------------------------------------ echo "[orchestrator-airgap] Copying configuration templates..." # Helm values overlay if [[ -f "$REPO_ROOT/deploy/helm/stellaops/values-orchestrator.yaml" ]]; then cp "$REPO_ROOT/deploy/helm/stellaops/values-orchestrator.yaml" \ "${BUNDLE_DIR}/configs/values-orchestrator.yaml" fi # Sample configuration if [[ -f "$REPO_ROOT/etc/orchestrator.yaml.sample" ]]; then cp "$REPO_ROOT/etc/orchestrator.yaml.sample" \ "${BUNDLE_DIR}/configs/orchestrator.yaml.sample" fi # PostgreSQL migration scripts if [[ -d "$REPO_ROOT/src/Orchestrator/StellaOps.Orchestrator/migrations" ]]; then mkdir -p "${BUNDLE_DIR}/configs/migrations" cp "$REPO_ROOT/src/Orchestrator/StellaOps.Orchestrator/migrations/"*.sql \ "${BUNDLE_DIR}/configs/migrations/" 2>/dev/null || true fi # Bootstrap secrets template cat > "${BUNDLE_DIR}/configs/secrets.env.example" <<'SECRETS_EOF' # Orchestrator Secrets Template # Copy to secrets.env and fill in values before deployment # PostgreSQL password (required) POSTGRES_PASSWORD= # Authority JWT signing key (if using local Authority) AUTHORITY_SIGNING_KEY= # OpenTelemetry endpoint (optional) OTEL_EXPORTER_OTLP_ENDPOINT= # Tenant encryption key for multi-tenant isolation (optional) TENANT_ENCRYPTION_KEY= SECRETS_EOF # ------------------------------------------------------------------------------ # Stage 3: Generate bundle manifest # ------------------------------------------------------------------------------ echo "[orchestrator-airgap] Generating bundle manifest..." # Calculate checksums for all bundle files MANIFEST_FILE="${BUNDLE_DIR}/manifests/bundle-manifest.json" # Build file list with checksums FILES_JSON="[]" while IFS= read -r -d '' file; do rel_path="${file#$BUNDLE_DIR/}" if [[ "$rel_path" != "manifests/bundle-manifest.json" ]]; then sha=$(sha256sum "$file" | cut -d' ' -f1) size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo "0") FILES_JSON=$(echo "$FILES_JSON" | jq --arg name "$rel_path" --arg sha "$sha" --arg size "$size" \ '. + [{"name": $name, "sha256": $sha, "size": ($size | tonumber)}]') fi done < <(find "$BUNDLE_DIR" -type f -print0 | sort -z) cat > "$MANIFEST_FILE" < "${MANIFEST_FILE}.sha256" # ------------------------------------------------------------------------------ # Stage 4: Copy documentation # ------------------------------------------------------------------------------ echo "[orchestrator-airgap] Copying documentation..." # Module architecture if [[ -f "$REPO_ROOT/docs/modules/orchestrator/architecture.md" ]]; then cp "$REPO_ROOT/docs/modules/orchestrator/architecture.md" \ "${BUNDLE_DIR}/docs/architecture.md" fi # GA checklist if [[ -f "$REPO_ROOT/ops/orchestrator/GA_CHECKLIST.md" ]]; then cp "$REPO_ROOT/ops/orchestrator/GA_CHECKLIST.md" \ "${BUNDLE_DIR}/docs/GA_CHECKLIST.md" fi # Quick deployment guide cat > "${BUNDLE_DIR}/docs/DEPLOY.md" <<'DEPLOY_EOF' # Orchestrator Air-Gap Deployment Guide ## Prerequisites - Docker or containerd runtime - Kubernetes 1.28+ (for Helm deployment) or Docker Compose - PostgreSQL 16+ (included as container or external) ## Quick Start (Docker) 1. Load images: ```bash for img in images/*.oci.tar.gz; do gunzip -c "$img" | docker load done ``` 2. Configure secrets: ```bash cp configs/secrets.env.example secrets.env # Edit secrets.env with your values ``` 3. Start services: ```bash docker compose -f docker-compose.orchestrator.yaml up -d ``` ## Helm Deployment 1. Import images to registry: ```bash for img in images/*.oci.tar.gz; do crane push "$img" your-registry.local/stellaops/$(basename "$img" .oci.tar.gz) done ``` 2. Install chart: ```bash helm upgrade --install stellaops ./stellaops \ -f configs/values-orchestrator.yaml \ --set global.imageRegistry=your-registry.local ``` ## Verification Check health endpoints: ```bash curl http://localhost:8080/healthz curl http://localhost:8080/readyz ``` DEPLOY_EOF # ------------------------------------------------------------------------------ # Stage 5: Create final tarball # ------------------------------------------------------------------------------ echo "[orchestrator-airgap] Creating final tarball..." TARBALL="${BUNDLE_DIR}.tar.gz" tar -C "$(dirname "$BUNDLE_DIR")" -czf "$TARBALL" "$(basename "$BUNDLE_DIR")" # Checksum the tarball sha256sum "$TARBALL" | cut -d' ' -f1 > "${TARBALL}.sha256" echo "[orchestrator-airgap] Bundle created successfully:" echo " Tarball: ${TARBALL}" echo " SHA256: $(cat "${TARBALL}.sha256")" echo " Size: $(du -h "$TARBALL" | cut -f1)"