#!/bin/bash # # Local Orchestrator Bootstrap (with License Validation) # # This script runs AFTER docker-compose up to: # 1. VALIDATE LICENSE with LetsBe Hub (REQUIRED for official installations) # 2. Wait for orchestrator health check (includes migrations via container startup) # 3. Get local tenant ID (for verification/logging) # 4. Write simplified credentials file # # NOTE: Database migrations are now run by the orchestrator container on startup # # IMPORTANT: Agent registration is handled via LOCAL_AGENT_KEY # from docker-compose.yml environment, NOT registration tokens. # # This script is idempotent - safe to run multiple times. # # Usage: # HUB_URL="https://hub.letsbe.biz" \ # LICENSE_KEY="lb_inst_..." \ # INSTANCE_ID="acme-orchestrator" \ # ADMIN_API_KEY="admin_key" \ # CUSTOMER="acme" \ # bash local_bootstrap.sh set -euo pipefail # ============ CONFIGURATION ============ HUB_URL="${HUB_URL:-https://hub.letsbe.biz}" LICENSE_KEY="${LICENSE_KEY:-}" INSTANCE_ID="${INSTANCE_ID:?INSTANCE_ID required}" ORCHESTRATOR_URL="${ORCHESTRATOR_URL:-http://localhost:8100}" ADMIN_API_KEY="${ADMIN_API_KEY:?ADMIN_API_KEY required}" CUSTOMER="${CUSTOMER:?CUSTOMER required}" CREDENTIALS_DIR="${CREDENTIALS_DIR:-/opt/letsbe/env}" # ============ LOGGING ============ log_info() { echo "[BOOTSTRAP] $(date '+%Y-%m-%d %H:%M:%S') $*"; } log_error() { echo "[BOOTSTRAP-ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; } log_success() { echo "[BOOTSTRAP-OK] $(date '+%Y-%m-%d %H:%M:%S') $*"; } log_warn() { echo "[BOOTSTRAP-WARN] $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; } # ============ LICENSE VALIDATION (FIRST STEP) ============ validate_license() { log_info "Validating license with LetsBe Hub..." # Check if license key is provided if [ -z "$LICENSE_KEY" ]; then log_error "LICENSE_KEY is required but not provided." log_error "Please obtain a license key from LetsBe Hub." log_error "Add 'license_key' to your config.json and re-run provisioning." exit 1 fi # Check if Hub URL is configured if [ -z "$HUB_URL" ]; then log_error "HUB_URL is required but not provided." exit 1 fi # Call Hub activation endpoint local http_code http_code=$(curl -s -o /tmp/activation_response.json -w "%{http_code}" \ -X POST "${HUB_URL}/api/v1/instances/activate" \ -H "Content-Type: application/json" \ -d "{\"license_key\": \"${LICENSE_KEY}\", \"instance_id\": \"${INSTANCE_ID}\"}") if [ "$http_code" != "200" ]; then log_error "License validation failed (HTTP $http_code)" # Parse error response if [ -f /tmp/activation_response.json ]; then local error_msg local error_code # Try to parse JSON error response error_msg=$(jq -r '.error // .detail.error // "Unknown error"' /tmp/activation_response.json 2>/dev/null || echo "Unknown error") error_code=$(jq -r '.code // .detail.code // "unknown"' /tmp/activation_response.json 2>/dev/null || echo "unknown") log_error "Error: $error_msg (code: $error_code)" case "$error_code" in "invalid_license") log_error "The provided license key is invalid." log_error "Please verify your license_key in config.json." ;; "expired") log_error "Your license has expired." log_error "Please contact LetsBe to renew your license." ;; "suspended") log_error "Your license has been suspended." log_error "Please contact LetsBe support." ;; "instance_not_found") log_error "Instance ID '$INSTANCE_ID' not found in Hub." log_error "Please ensure your instance was created in LetsBe Hub." ;; *) log_error "Please contact LetsBe support with error code: $error_code" ;; esac fi rm -f /tmp/activation_response.json exit 1 fi log_success "License validated successfully!" # Extract hub_api_key from response if provided local hub_api_key hub_api_key=$(jq -r '.hub_api_key // empty' /tmp/activation_response.json 2>/dev/null || echo "") if [ -n "$hub_api_key" ] && [ "$hub_api_key" != "USE_EXISTING" ]; then log_info "Received hub_api_key from activation" export HUB_API_KEY="$hub_api_key" # Save to credentials file mkdir -p "${CREDENTIALS_DIR}" echo "HUB_API_KEY=${hub_api_key}" >> "${CREDENTIALS_DIR}/hub-credentials.env" chmod 600 "${CREDENTIALS_DIR}/hub-credentials.env" log_info "Hub API key saved to ${CREDENTIALS_DIR}/hub-credentials.env" fi rm -f /tmp/activation_response.json } # ============ ORCHESTRATOR FUNCTIONS ============ wait_for_orchestrator() { log_info "Waiting for orchestrator to be ready..." local max_attempts=60 local attempt=0 while [[ $attempt -lt $max_attempts ]]; do if curl -sf "${ORCHESTRATOR_URL}/health" > /dev/null 2>&1; then log_success "Orchestrator is ready" return 0 fi attempt=$((attempt + 1)) log_info "Attempt $attempt/$max_attempts - waiting..." sleep 2 done log_error "Orchestrator not ready after ${max_attempts} attempts" return 1 } run_migrations() { log_info "Running database migrations..." # Find the orchestrator container local orchestrator_container orchestrator_container=$(docker ps --format '{{.Names}}' | grep -E "(orchestrator|${CUSTOMER}.*orchestrator)" | head -1) if [ -z "$orchestrator_container" ]; then log_error "Could not find orchestrator container" return 1 fi docker exec "$orchestrator_container" alembic upgrade head log_success "Migrations complete" } get_local_tenant_id() { log_info "Getting local tenant ID..." local response response=$(curl -sf "${ORCHESTRATOR_URL}/api/v1/meta/instance") local tenant_id tenant_id=$(echo "$response" | jq -r '.tenant_id') if [ "$tenant_id" == "null" ] || [ -z "$tenant_id" ]; then log_error "Failed to get tenant_id from /api/v1/meta/instance" log_error "Response: $response" return 1 fi log_success "Tenant ID: $tenant_id" echo "$tenant_id" } write_credentials() { local tenant_id=$1 local credentials_file="${CREDENTIALS_DIR}/sysadmin-credentials.env" log_info "Writing credentials to ${credentials_file}..." mkdir -p "${CREDENTIALS_DIR}" # NOTE: In LOCAL_MODE, agent uses LOCAL_AGENT_KEY from docker-compose.yml # We do NOT write ADMIN_API_KEY here - agent doesn't need it # We do NOT write registration tokens - LOCAL_MODE uses direct key auth cat > "${credentials_file}" < "${admin_creds}" </dev/null || true log_success "Admin credentials written (root-only access)" } # ============ MAIN ============ main() { log_info "Starting local orchestrator bootstrap for: ${CUSTOMER}" log_info "Instance ID: ${INSTANCE_ID}" # STEP 1: License validation (REQUIRED for official installations) # This is the gating step - if license fails, nothing else runs validate_license # STEP 2: Wait for orchestrator (migrations run on container startup) wait_for_orchestrator # STEP 3: Get tenant ID (for verification) local tenant_id tenant_id=$(get_local_tenant_id) # STEP 4: Write credentials # NOTE: No registration token creation - LOCAL_MODE uses LOCAL_AGENT_KEY write_credentials "${tenant_id}" write_admin_credentials log_success "Bootstrap complete!" log_info "Instance '${INSTANCE_ID}' is now licensed and activated" log_info "" log_info "Agent registration: Uses LOCAL_AGENT_KEY from docker-compose.yml" log_info "Agent should register with orchestrator within 30 seconds" } main "$@"