LetsBeBiz-Redesign/letsbe-ansible-runner/scripts/local_bootstrap.sh

259 lines
8.7 KiB
Bash
Raw Permalink Normal View History

#!/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}" <<EOF
# LetsBe LOCAL_MODE Credentials
# Generated by local_bootstrap.sh at $(date -u '+%Y-%m-%dT%H:%M:%SZ')
#
# Agent Registration: Uses LOCAL_AGENT_KEY from docker-compose.yml
# NOT stored here to minimize secrets at rest
TENANT_ID=${tenant_id}
INSTANCE_ID=${INSTANCE_ID}
EOF
chmod 600 "${credentials_file}"
log_success "Credentials written"
}
write_admin_credentials() {
# Separate file for admin scripts only - NOT for agent
local admin_creds="${CREDENTIALS_DIR}/admin-credentials.env"
log_info "Writing admin credentials to ${admin_creds}..."
mkdir -p "${CREDENTIALS_DIR}"
cat > "${admin_creds}" <<EOF
# LetsBe Admin Credentials (root only)
# Generated by local_bootstrap.sh at $(date -u '+%Y-%m-%dT%H:%M:%SZ')
ADMIN_API_KEY=${ADMIN_API_KEY}
EOF
chmod 600 "${admin_creds}"
chown root:root "${admin_creds}" 2>/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 "$@"