259 lines
8.7 KiB
Bash
259 lines
8.7 KiB
Bash
#!/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 "$@"
|