LetsBeBiz-Redesign/letsbe-ansible-runner/entrypoint.sh

323 lines
13 KiB
Bash

#!/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