#!/bin/bash # # LetsBe Ansible Runner - Hub-Integrated Entrypoint # # This script is the entry point for the containerized Ansible runner. # It streams logs to the Hub API and updates job status on completion/failure. # set -euo pipefail # Configuration HUB_API_URL="${HUB_API_URL:-https://hub.letsbe.solutions}" JOB_ID="${JOB_ID:-}" RUNNER_TOKEN="${RUNNER_TOKEN:-}" JOB_CONFIG_PATH="${JOB_CONFIG_PATH:-/job/config.json}" CURRENT_STEP="init" COMPLETION_MARKED=false # Portainer credentials (read from remote server) PORTAINER_USER="" PORTAINER_PASS="" # Global file for passing result JSON (avoids bash variable expansion issues) RESULT_FILE="/tmp/job_result.json" # Logging to Hub API log_to_hub() { local level="$1" message="$2" step="${3:-$CURRENT_STEP}" progress="${4:-}" echo "[$(date '+%H:%M:%S')] [$level] [$step] $message" if [[ -n "$JOB_ID" && -n "$RUNNER_TOKEN" ]]; then local payload="{\"level\":\"${level}\",\"message\":\"${message}\",\"step\":\"${step}\"" [[ -n "$progress" ]] && payload="${payload},\"progress\":${progress}" payload="${payload}}" curl -s -X POST "${HUB_API_URL}/api/v1/jobs/${JOB_ID}/logs" \ -H "X-Runner-Token: ${RUNNER_TOKEN}" \ -H "Content-Type: application/json" \ -d "$payload" > /dev/null 2>&1 || true fi } log_info() { log_to_hub "info" "$1" "${2:-$CURRENT_STEP}" "${3:-}"; } log_warn() { log_to_hub "warn" "$1" "${2:-$CURRENT_STEP}" "${3:-}"; } log_error() { log_to_hub "error" "$1" "${2:-$CURRENT_STEP}" "${3:-}"; } update_job_status() { local status="$1" error="${2:-}" if [[ -n "$JOB_ID" && -n "$RUNNER_TOKEN" ]]; then local payload_file="/tmp/payload_$$.json" log_info "update_job_status called with status=$status" "debug" # Check if result file exists and has content if [[ -f "$RESULT_FILE" && -s "$RESULT_FILE" ]]; then log_info "Result file exists, size: $(wc -c < "$RESULT_FILE") bytes" "debug" # Validate it's proper JSON if jq -e . "$RESULT_FILE" > /dev/null 2>&1; then log_info "Result file contains valid JSON" "debug" # Build payload with result embedded jq -n --arg status "$status" --slurpfile result "$RESULT_FILE" \ '{status: $status, result: $result[0]}' > "$payload_file" else log_error "Result file is not valid JSON" "debug" jq -n --arg status "$status" '{status: $status}' > "$payload_file" fi elif [[ -n "$error" ]]; then log_info "Building error payload" "debug" jq -n --arg status "$status" --arg error "$error" \ '{status: $status, error: $error}' > "$payload_file" else log_info "Building status-only payload" "debug" jq -n --arg status "$status" '{status: $status}' > "$payload_file" fi # Show payload size (not content, to avoid log issues) log_info "Payload size: $(wc -c < "$payload_file") bytes" "debug" # Send to Hub local http_code http_code=$(curl -s -w "%{http_code}" -o /tmp/hub_response.txt -X PATCH "${HUB_API_URL}/api/v1/jobs/${JOB_ID}" \ -H "X-Runner-Token: ${RUNNER_TOKEN}" \ -H "Content-Type: application/json" \ -d "@$payload_file") || true log_info "Hub responded with HTTP $http_code" "debug" # Cleanup rm -f "$payload_file" /tmp/hub_response.txt fi } mark_completed() { log_info "mark_completed called" "complete" if [[ "$COMPLETION_MARKED" == "true" ]]; then log_info "Completion already marked, skipping" "complete" return fi COMPLETION_MARKED=true log_info "Job completed" "complete" "100" # Check if result file was prepared if [[ -f "$RESULT_FILE" ]]; then log_info "Result file ready: $(wc -c < "$RESULT_FILE") bytes" "complete" else log_warn "No result file found" "complete" fi log_info "Calling update_job_status" "complete" update_job_status "completed" log_info "update_job_status returned" "complete" } mark_failed() { if [[ "$COMPLETION_MARKED" == "true" ]]; then log_info "Completion already marked, skipping failure" "failed" return fi COMPLETION_MARKED=true log_error "Job failed: $1" "failed" update_job_status "failed" "$1" } cleanup() { local exit_code=$? if [[ $exit_code -ne 0 && "$COMPLETION_MARKED" != "true" ]]; then mark_failed "Script exited with code $exit_code" fi } trap cleanup EXIT # SSH helpers SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=30" ssh_run() { sshpass -p "$SERVER_PASSWORD" ssh $SSH_OPTS -p "$SERVER_PORT" root@"$SERVER_IP" "$@"; } scp_upload() { sshpass -p "$SERVER_PASSWORD" scp $SSH_OPTS -P "$SERVER_PORT" "$1" root@"$SERVER_IP":"$2"; } scp_upload_dir() { sshpass -p "$SERVER_PASSWORD" scp -r $SSH_OPTS -P "$SERVER_PORT" "$1" root@"$SERVER_IP":"$2"; } # Docker registry login on target server step_docker_login() { CURRENT_STEP="docker-login" # Read Docker Hub credentials from config local dockerhub_user=$(jq -r '.dockerHub.username // empty' "$JOB_CONFIG_PATH") local dockerhub_token=$(jq -r '.dockerHub.token // empty' "$JOB_CONFIG_PATH") local dockerhub_registry=$(jq -r '.dockerHub.registry // empty' "$JOB_CONFIG_PATH") # Read Gitea credentials from config local gitea_registry=$(jq -r '.gitea.registry // empty' "$JOB_CONFIG_PATH") local gitea_user=$(jq -r '.gitea.username // empty' "$JOB_CONFIG_PATH") local gitea_token=$(jq -r '.gitea.token // empty' "$JOB_CONFIG_PATH") # Login to Docker Hub if credentials provided if [[ -n "$dockerhub_user" && -n "$dockerhub_token" ]]; then log_info "Logging into Docker Hub" "docker-login" "12" if [[ -n "$dockerhub_registry" ]]; then ssh_run "echo '$dockerhub_token' | docker login -u '$dockerhub_user' --password-stdin '$dockerhub_registry'" 2>&1 || log_warn "Docker Hub login failed" else ssh_run "echo '$dockerhub_token' | docker login -u '$dockerhub_user' --password-stdin" 2>&1 || log_warn "Docker Hub login failed" fi log_info "Docker Hub login complete" "docker-login" else log_info "No Docker Hub credentials, skipping login" "docker-login" fi # Login to Gitea registry if credentials provided if [[ -n "$gitea_registry" && -n "$gitea_user" && -n "$gitea_token" ]]; then log_info "Logging into Gitea registry: $gitea_registry" "docker-login" "14" ssh_run "echo '$gitea_token' | docker login -u '$gitea_user' --password-stdin '$gitea_registry'" 2>&1 || log_warn "Gitea registry login failed" log_info "Gitea registry login complete" "docker-login" else log_info "No Gitea credentials, skipping registry login" "docker-login" fi } # Load config load_config() { CURRENT_STEP="config" log_info "Loading job configuration" [[ ! -f "$JOB_CONFIG_PATH" ]] && { log_error "Config not found"; exit 1; } SERVER_IP=$(jq -r '.server.ip' "$JOB_CONFIG_PATH") SERVER_PORT=$(jq -r '.server.port // 22' "$JOB_CONFIG_PATH") SERVER_PASSWORD=$(jq -r '.server.rootPassword' "$JOB_CONFIG_PATH") CUSTOMER=$(jq -r '.customer' "$JOB_CONFIG_PATH") DOMAIN=$(jq -r '.domain' "$JOB_CONFIG_PATH") COMPANY_NAME=$(jq -r '.companyName' "$JOB_CONFIG_PATH") LICENSE_KEY=$(jq -r '.licenseKey' "$JOB_CONFIG_PATH") DASHBOARD_TIER=$(jq -r '.dashboardTier' "$JOB_CONFIG_PATH") TOOLS=$(jq -r '.tools | join(",")' "$JOB_CONFIG_PATH") log_info "Config: $DOMAIN on $SERVER_IP" "config" "5" } # Provisioning steps step_prepare() { CURRENT_STEP="prepare"; log_info "Creating directories" "prepare" "10"; ssh_run "mkdir -p /opt/letsbe/{scripts,env,stacks,nginx,config}"; } step_upload() { CURRENT_STEP="upload" log_info "Uploading files" "upload" "20" scp_upload "/workspace/scripts/env_setup.sh" "/opt/letsbe/scripts/" scp_upload "/workspace/scripts/setup.sh" "/opt/letsbe/scripts/" ssh_run "chmod +x /opt/letsbe/scripts/*.sh" log_info "Uploading nginx" "upload" "30" scp_upload_dir "/workspace/nginx/." "/opt/letsbe/nginx/" log_info "Uploading stacks" "upload" "40" scp_upload_dir "/workspace/stacks/." "/opt/letsbe/stacks/" } step_env() { CURRENT_STEP="env" log_info "Setting up environment" "env" "50" ssh_run "bash /opt/letsbe/scripts/env_setup.sh --customer '$CUSTOMER' --domain '$DOMAIN' --company '$COMPANY_NAME' --license-key '$LICENSE_KEY'" } # Read Portainer credentials from the remote server read_portainer_credentials() { CURRENT_STEP="credentials" log_info "Reading Portainer credentials from server" "credentials" "55" # Read credentials.env from remote server local creds_content creds_content=$(ssh_run "cat /opt/letsbe/env/credentials.env 2>/dev/null" || echo "") if [[ -n "$creds_content" ]]; then # Extract Portainer credentials PORTAINER_USER=$(echo "$creds_content" | grep "^PORTAINER_ADMIN_USER=" | cut -d'=' -f2 | tr -d '\r\n' || echo "") PORTAINER_PASS=$(echo "$creds_content" | grep "^PORTAINER_ADMIN_PASSWORD=" | cut -d'=' -f2 | tr -d '\r\n' || echo "") log_info "Portainer user available: ${PORTAINER_USER:-none}" "credentials" log_info "Portainer pass available: $([ -n "$PORTAINER_PASS" ] && echo "yes" || echo "no")" "credentials" else log_warn "Could not read credentials.env from server" "credentials" fi } step_setup() { CURRENT_STEP="setup" log_info "Running setup (10-15 min)" "setup" "60" # Run setup script - note: setup.sh restarts SSH at the end which drops the connection # This is expected behavior, so we ignore the exit code from the SSH command set +e ssh_run "bash /opt/letsbe/scripts/setup.sh --tools '$TOOLS' --domain '$DOMAIN'" 2>&1 | while read -r line; do [[ -n "$line" ]] && log_info "$line" "setup" done local setup_exit=$? set -e # Give SSH time to restart on new port sleep 3 # Verify setup completed by checking if we can connect on new SSH port (22022) log_info "Verifying setup completion on new SSH port..." "setup" if sshpass -p "$SERVER_PASSWORD" ssh $SSH_OPTS -p 22022 root@"$SERVER_IP" "echo 'SSH reconnected on port 22022'" 2>/dev/null; then log_info "Setup complete - SSH accessible on port 22022" "setup" "90" else # Try original port as fallback (maybe SSH restart didn't happen) if ssh_run "echo 'SSH still on original port'" 2>/dev/null; then log_info "Setup complete - SSH still on original port" "setup" "90" else log_warn "Could not verify SSH connectivity, but setup logs indicate completion" "setup" fi fi } step_finalize() { CURRENT_STEP="finalize" log_info "Finalizing" "finalize" "95" log_info "Portainer user: '${PORTAINER_USER:-}'" "finalize" log_info "Portainer pass length: ${#PORTAINER_PASS}" "finalize" # Write result JSON directly to file (avoids all bash variable issues) if [[ -n "$PORTAINER_USER" && -n "$PORTAINER_PASS" ]]; then log_info "Building result with Portainer credentials" "finalize" jq -n -c \ --arg dashboard_url "https://dashboard.${DOMAIN}" \ --arg portainer_url "https://${SERVER_IP}:9443" \ --arg portainer_username "$PORTAINER_USER" \ --arg portainer_password "$PORTAINER_PASS" \ '{dashboard_url:$dashboard_url,portainer_url:$portainer_url,portainer_username:$portainer_username,portainer_password:$portainer_password}' \ > "$RESULT_FILE" else log_warn "No Portainer credentials available" "finalize" jq -n -c \ --arg dashboard_url "https://dashboard.${DOMAIN}" \ --arg portainer_url "https://${SERVER_IP}:9443" \ '{dashboard_url:$dashboard_url,portainer_url:$portainer_url}' \ > "$RESULT_FILE" fi # Verify the file was created and contains valid JSON if [[ -f "$RESULT_FILE" ]]; then log_info "Result file created: $(wc -c < "$RESULT_FILE") bytes" "finalize" if jq -e . "$RESULT_FILE" > /dev/null 2>&1; then log_info "Result file validated as JSON" "finalize" else log_error "Result file is NOT valid JSON!" "finalize" fi else log_error "Failed to create result file!" "finalize" fi log_info "Calling mark_completed" "finalize" mark_completed log_info "mark_completed returned" "finalize" } # Main main() { log_info "LetsBe Runner starting" load_config step_prepare step_docker_login step_upload step_env read_portainer_credentials step_setup step_finalize } main