feat: add automation script for Cloudflare DNS provisioning and Dokploy deployment integration
Some checks failed
Build, MSI & Deploy / Build Edge Agent Binaries (push) Has been cancelled
Build, MSI & Deploy / Build Edge Agent MSI (push) Has been cancelled
Build, MSI & Deploy / Deploy to Dokploy (push) Has been cancelled
Build, MSI & Deploy / Send notification (push) Has been cancelled
Build, MSI & Deploy / Build Docker images (push) Has been cancelled

This commit is contained in:
Wilman Yesid Farfan Diaz
2026-07-12 23:03:53 -05:00
parent 0d5edfe872
commit 978eac5ed9

513
scripts/setup-domains.sh Normal file
View File

@@ -0,0 +1,513 @@
#!/usr/bin/env bash
# =============================================================================
# setup-domains.sh — Script de Automatización de Dominios (Cloudflare y Dokploy)
# =============================================================================
#
# Propósito:
# Este script automatiza el flujo completo de enrutamiento y despliegue de
# los subdominios requeridos por la plataforma OmniOil en un entorno de producción.
#
# Realiza dos tareas principales:
# 1. Crea o actualiza registros DNS tipo "A" en Cloudflare apuntando a la
# IP pública del servidor VPS, con el Proxy (CDN) activo.
# 2. Registra los dominios en la API de Dokploy asociándolos a cada servicio
# específico dentro de la pila Docker Compose (con soporte SSL automático
# mediante Let's Encrypt), e inicia un re-despliegue de la pila para aplicar
# los cambios de enrutamiento en Traefik.
#
# Requisitos del Sistema:
# - curl: Para realizar peticiones HTTP/REST a las APIs.
# - jq: Para parsear e inyectar payloads JSON. El script tiene un buscador
# inteligente de jq en rutas locales (por si no está instalado a nivel global).
#
# Autenticación y Credenciales:
# Las credenciales se pueden pasar por línea de comandos o configurar en un
# archivo .env o .env.production local (el script los carga automáticamente).
#
# =============================================================================
set -euo pipefail
# -----------------------------------------------------------------------------
# 1. Configuración por Defecto y Variables de Estado
# -----------------------------------------------------------------------------
DOMAIN="" # Dominio base (ej. omnioil.app). Se solicita de forma interactiva si falta.
SERVER_IP="" # IP pública del VPS. Se solicita de forma interactiva si falta.
PROJECT_NAME="omnioil"# Nombre del proyecto dentro del panel de Dokploy.
COMPOSE_NAME="anh" # Nombre del Docker Compose en Dokploy (corresponde a la pila unificada).
CF_TOKEN="" # Token de API de Cloudflare con permisos de edición de registros DNS.
CF_ZONE_ID="" # ID de la Zona de Cloudflare asignada al dominio.
DOKPLOY_API_URL="" # URL base de la API de Dokploy.
DOKPLOY_API_KEY="" # Token/API Key generada en el perfil de administrador de Dokploy.
DRY_RUN=false # Si es true, corre en modo de simulación imprimiendo las acciones.
SKIP_REDEPLOY=false # Si es true, omite el comando de redeploy en Dokploy al finalizar.
CLOUDFLARE_API="https://api.cloudflare.com/client/v4"
# -----------------------------------------------------------------------------
# 2. Función de Carga Segura de Archivos de Entorno (.env)
# -----------------------------------------------------------------------------
# Esta función lee un archivo .env línea por línea de forma nativa en Bash.
# A diferencia de un "source" o "eval" directo en el archivo completo, esta lógica:
# - Evita la expansión accidental de caracteres comodín (*) comunes en expresiones
# cron como BACKUP_SCHEDULE="0 3 * * *".
# - Remueve de manera segura los comentarios al final de la línea.
# - Limpia comillas dobles y simples exteriores de los valores.
load_env_file() {
local env_file="$1"
if [ -f "$env_file" ]; then
echo " Cargando variables de entorno desde $env_file..."
while IFS= read -r line || [ -n "$line" ]; do
# Eliminar espacios en blanco al principio de la línea
line="${line##[[:space:]]}"
# Saltar comentarios y líneas vacías
[[ "$line" =~ ^# ]] && continue
[[ -z "$line" ]] && continue
# Procesar solo líneas que contengan una asignación '='
if [[ "$line" == *=* ]]; then
local key="${line%%=*}"
local value="${line#*=}"
# Quitar comentarios al final del valor (delimitados por '#')
value="${value%%#*}"
# Recortar espacios sobrantes al inicio y final en Bash puro (sin subshells)
key="${key#"${key%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
# Quitar comillas exteriores del valor si las tuviera
if [[ "$value" =~ ^\".*\"$ ]] || [[ "$value" =~ ^\'.*\'$ ]]; then
value="${value:1:${#value}-2}"
fi
# Exportar la variable al entorno actual usando declare (seguro contra globbing)
if [ -n "$key" ]; then
declare -x "$key=$value" 2>/dev/null || true
fi
fi
done < "$env_file"
fi
}
# Cargar configuración desde archivos locales
load_env_file ".env"
load_env_file ".env.production"
# Acoplar variables precargadas desde el entorno o archivos .env
DOMAIN="${DOMAIN:-${OMNIOIL_DOMAIN:-}}"
SERVER_IP="${SERVER_IP:-${OMNIOIL_IP:-}}"
CF_TOKEN="${CF_TOKEN:-}"
CF_ZONE_ID="${CF_ZONE_ID:-}"
DOKPLOY_API_URL="${DOKPLOY_API_URL:-}"
DOKPLOY_API_KEY="${DOKPLOY_API_KEY:-}"
# -----------------------------------------------------------------------------
# 3. Procesamiento de Argumentos de la Línea de Comandos
# -----------------------------------------------------------------------------
# Las opciones pasadas por la terminal tienen prioridad sobre los archivos .env.
while [[ $# -gt 0 ]]; do
case "$1" in
--domain)
DOMAIN="$2"
shift 2
;;
--ip)
SERVER_IP="$2"
shift 2
;;
--project)
PROJECT_NAME="$2"
shift 2
;;
--compose)
COMPOSE_NAME="$2"
shift 2
;;
--cf-token)
CF_TOKEN="$2"
shift 2
;;
--cf-zone-id)
CF_ZONE_ID="$2"
shift 2
;;
--dokploy-url)
DOKPLOY_API_URL="$2"
shift 2
;;
--dokploy-key)
DOKPLOY_API_KEY="$2"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
--skip-redeploy)
SKIP_REDEPLOY=true
shift
;;
-h|--help)
echo "Uso: $0 [opciones]"
echo ""
echo "Opciones:"
echo " --domain <dominio> Dominio principal (ej. omnioil.app)"
echo " --ip <ip> IP pública del servidor VPS"
echo " --project <nombre> Nombre del proyecto en Dokploy (default: omnioil)"
echo " --compose <nombre> Nombre de la app Compose en Dokploy (default: anh)"
echo " --cf-token <token> Token de Cloudflare API (permiso DNS Edit)"
echo " --cf-zone-id <id> Zone ID de Cloudflare"
echo " --dokploy-url <url> URL API Dokploy (default: http://<server_ip>:3000/api)"
echo " --dokploy-key <key> API Key de Dokploy"
echo " --dry-run Modo simulación (imprime acciones sin hacer cambios reales)"
echo " --skip-redeploy Evita el re-despliegue automático en Dokploy"
exit 0
;;
*)
echo "❌ Opción desconocida: $1"
echo "Ejecuta '$0 --help' para ver las opciones disponibles."
exit 1
;;
esac
done
# -----------------------------------------------------------------------------
# 4. Validaciones de Entorno e Interactividad
# -----------------------------------------------------------------------------
# El comando 'curl' es indispensable para las peticiones de red
if ! command -v curl &>/dev/null; then
echo "❌ Error: Se requiere 'curl' instalado en el sistema."
exit 1
fi
# Buscador tolerante de 'jq'
# Si no está instalado globalmente en el sistema, busca en carpetas de usuario
# locales de WSL o rutas del proyecto para mitigar problemas de dependencias.
JQ_CMD="jq"
if ! command -v jq &>/dev/null; then
if [ -f "/home/yesid_farfan/jq" ]; then
JQ_CMD="/home/yesid_farfan/jq"
elif [ -f "$HOME/jq" ]; then
JQ_CMD="$HOME/jq"
elif [ -f "./jq" ]; then
JQ_CMD="./jq"
elif [ -f "./scripts/jq" ]; then
JQ_CMD="./scripts/jq"
else
echo "❌ Error: Se requiere 'jq' instalado en el sistema para procesar respuestas JSON."
exit 1
fi
fi
# Solicitar dominio e IP al usuario interactivamente si no están configurados
if [ -z "$DOMAIN" ]; then
read -rp "Introduce el dominio principal (ej. omnioil.app): " DOMAIN
fi
if [ -z "$SERVER_IP" ]; then
read -rp "Introduce la IP pública del servidor VPS: " SERVER_IP
fi
# Cancelar si faltan los datos mínimos obligatorios
if [ -z "$DOMAIN" ] || [ -z "$SERVER_IP" ]; then
echo "❌ Error: El dominio y la IP del servidor son campos obligatorios."
exit 1
fi
# Asignar la URL de la API de Dokploy por defecto a partir de la IP del VPS si no se provee
if [ -z "$DOKPLOY_API_URL" ]; then
DOKPLOY_API_URL="http://$SERVER_IP:3000/api"
fi
# Evaluar credenciales para determinar qué plataformas se van a configurar
RUN_CLOUDFLARE=true
if [ -z "$CF_TOKEN" ] || [ -z "$CF_ZONE_ID" ]; then
echo "⚠️ Advertencia: Falta CF_TOKEN o CF_ZONE_ID. Se omitirá Cloudflare."
RUN_CLOUDFLARE=false
fi
RUN_DOKPLOY=true
if [ -z "$DOKPLOY_API_KEY" ]; then
echo "⚠️ Advertencia: Falta DOKPLOY_API_KEY. Se omitirá Dokploy."
RUN_DOKPLOY=false
fi
# Si no hay credenciales para ninguna plataforma, salimos
if [ "$RUN_CLOUDFLARE" = false ] && [ "$RUN_DOKPLOY" = false ]; then
echo "❌ Error: No hay credenciales configuradas para Cloudflare ni para Dokploy."
exit 1
fi
# -----------------------------------------------------------------------------
# 5. Mapeo de Subdominios a Contenedores Docker Compose
# -----------------------------------------------------------------------------
# Formato: "subdominio:nombre_servicio_docker:puerto_interno"
# Este mapeo se deriva del documento docs/DESPLIEGUE.md.
MAPPINGS=(
"@:landing:80" # omnioil.app -> contenedor landing puerto 80
"www:landing:80" # www.omnioil.app -> contenedor landing puerto 80
"app:frontend-admin:80" # app.omnioil.app -> contenedor frontend-admin puerto 80
"console:frontend-console:80" # console.omnioil.app -> contenedor frontend-console puerto 80
"mobile:frontend-pwa:80" # mobile.omnioil.app -> contenedor frontend-pwa puerto 80
"api:backend-api:8000" # api.omnioil.app -> contenedor backend-api puerto 8000
"storage:minio:9001" # storage.omnioil.app -> consola de administración de minio puerto 9001
"mqtt:mosquitto:9001" # mqtt.omnioil.app -> broker websocket mosquitto puerto 9001
)
# -----------------------------------------------------------------------------
# 6. Funciones de Integración con la API de Cloudflare (DNS)
# -----------------------------------------------------------------------------
# Crea o actualiza un registro DNS tipo A en Cloudflare
setup_cloudflare_dns() {
local name="$1" content="$2" type="${3:-A}" proxied="${4:-true}"
local record_name
# Manejar el dominio raíz (@) versus subdominios
if [ "$name" = "@" ]; then
record_name="$DOMAIN"
else
record_name="$name.$DOMAIN"
fi
echo " → Configurando DNS para $record_name -> $content"
# Buscar si el registro ya existe en el Zone ID
local search_res
search_res=$(curl -s -X GET "$CLOUDFLARE_API/zones/$CF_ZONE_ID/dns_records?name=$record_name&type=$type" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json")
# Validar que la respuesta sea un JSON de éxito
if ! echo "$search_res" | "$JQ_CMD" -e '.success' | grep -q "true"; then
local err
err=$(echo "$search_res" | "$JQ_CMD" -r '.errors[0].message // "Error en autenticación/red"')
echo " ❌ Falló la consulta a Cloudflare: $err"
return 1
fi
# Extraer el record_id si existe
local record_id
record_id=$(echo "$search_res" | "$JQ_CMD" -r '.result[0].id // empty')
# Generar el payload JSON de forma segura
local payload
payload=$("$JQ_CMD" -n \
--arg type "$type" \
--arg name "$record_name" \
--arg content "$content" \
--argjson proxied "$proxied" \
'{type: $type, name: $name, content: $content, proxied: $proxied, ttl: 1}')
local response
if [ -n "$record_id" ]; then
# Registro existente: Se actualiza con PUT
response=$(curl -s -X PUT "$CLOUDFLARE_API/zones/$CF_ZONE_ID/dns_records/$record_id" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d "$payload")
else
# Registro nuevo: Se crea con POST
response=$(curl -s -X POST "$CLOUDFLARE_API/zones/$CF_ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d "$payload")
fi
# Verificar resultado de la operación
if echo "$response" | "$JQ_CMD" -e '.success' | grep -q "true"; then
echo " ✅ DNS $record_name configurado con éxito."
else
local err
err=$(echo "$response" | "$JQ_CMD" -r '.errors[0].message // "Error desconocido"')
echo " ❌ Falló la creación/actualización de DNS $record_name: $err"
fi
}
# -----------------------------------------------------------------------------
# 7. Funciones de Integración con la API de Dokploy (Enrutamiento)
# -----------------------------------------------------------------------------
# Crea el enrutamiento de un dominio en Dokploy vinculándolo a un servicio del Docker Compose
create_dokploy_domain() {
local host="$1" service="$2" port="$3" compose_id="$4"
echo " → Registrando en Dokploy: $host -> servicio '$service' (puerto $port)"
# payload JSON para enrutar el tráfico al contenedor mediante Traefik.
# Se habilita SSL Let's Encrypt de forma automática (https: true, certificateType: letsencrypt)
local payload
payload=$("$JQ_CMD" -n \
--arg host "$host" \
--arg composeId "$compose_id" \
--arg serviceName "$service" \
--argjson port "$port" \
'{host: $host, composeId: $composeId, serviceName: $serviceName, port: $port, https: true, certificateType: "letsencrypt", path: "/"}')
local response
response=$(curl -s -X POST "$DOKPLOY_API_URL/domain.create" \
-H "x-api-key: $DOKPLOY_API_KEY" \
-H "Content-Type: application/json" \
-d "$payload")
# Evaluar respuestas de error
if echo "$response" | "$JQ_CMD" -e '.error // .message // empty' > /dev/null; then
local error_msg
error_msg=$(echo "$response" | "$JQ_CMD" -r '.message // .error')
# Si el dominio ya existe, el script es tolerante y no falla, simplemente salta al siguiente
if [[ "$error_msg" == *"already exists"* || "$error_msg" == *"Duplicate"* ]]; then
echo " ⚠️ El dominio $host ya está registrado en Dokploy. Saltando..."
else
echo " ❌ Error al registrar dominio $host en Dokploy: $error_msg"
fi
else
echo " ✅ Dominio $host enrutado con éxito."
fi
}
# Envía una señal de re-despliegue (redeploy) al Docker Compose en Dokploy.
# Esto es mandatorio porque Dokploy usa Traefik con enrutamiento por etiquetas (labels).
# Al añadir dominios a Compose, las etiquetas cambian y los contenedores deben recrearse.
redeploy_dokploy_compose() {
local compose_id="$1"
echo "=== Solicitando Re-despliegue de la pila en Dokploy ==="
local response
response=$(curl -s -X POST "$DOKPLOY_API_URL/compose.redeploy" \
-H "x-api-key: $DOKPLOY_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"composeId\":\"$compose_id\"}")
if echo "$response" | "$JQ_CMD" -e '.error // .message // empty' > /dev/null; then
local error_msg
error_msg=$(echo "$response" | "$JQ_CMD" -r '.message // .error')
echo " ❌ Error al solicitar re-despliegue: $error_msg"
else
echo " ✅ Re-despliegue iniciado con éxito en Dokploy."
echo " Traefik configurará SSL Let's Encrypt automáticamente una vez estén listos los servicios."
fi
}
# =============================================================================
# 8. Ejecución del Script (Flujo de Control)
# =============================================================================
echo "========================================================="
echo " AUTOMATIZACIÓN DE DOMINIOS Y ENRUTAMIENTO"
echo " Dominio: $DOMAIN"
echo " IP Servidor: $SERVER_IP"
if [ "$DRY_RUN" = true ]; then
echo " ⚠️ MODO SIMULACIÓN ACTIVO (No se harán cambios reales)"
fi
echo "========================================================="
# -----------------------------------------------------------------------------
# Paso 1: Configurar DNS en Cloudflare
# -----------------------------------------------------------------------------
if [ "$RUN_CLOUDFLARE" = true ]; then
echo ""
echo "=== Paso 1: Configurando registros DNS en Cloudflare ==="
for mapping in "${MAPPINGS[@]}"; do
# Separar el mapeo en variables
IFS=":" read -r sub service port <<< "$mapping"
record_name=""
if [ "$sub" = "@" ]; then record_name="$DOMAIN"; else record_name="$sub.$DOMAIN"; fi
if [ "$DRY_RUN" = true ]; then
echo " [SIMULACIÓN] Crear/Actualizar A: $record_name -> $SERVER_IP (proxied=true)"
else
setup_cloudflare_dns "$sub" "$SERVER_IP" "A" true
fi
done
fi
# -----------------------------------------------------------------------------
# Paso 2: Resolución de recursos e IDs en Dokploy
# -----------------------------------------------------------------------------
if [ "$RUN_DOKPLOY" = true ]; then
echo ""
echo "=== Paso 2: Conectando con Dokploy y buscando recursos ==="
compose_id="dummy-compose-id"
if [ "$DRY_RUN" = false ]; then
# 2a. Obtener la lista de todos los proyectos de la instancia
projects_json=$(curl -s -f -X GET "$DOKPLOY_API_URL/project.all" -H "x-api-key: $DOKPLOY_API_KEY")
if [ $? -ne 0 ] || [ -z "$projects_json" ]; then
echo "❌ Error: No se pudo conectar a Dokploy en $DOKPLOY_API_URL o la API key es inválida."
exit 1
fi
# 2b. Filtrar para hallar el projectId que coincida con PROJECT_NAME
project_id=$(echo "$projects_json" | "$JQ_CMD" -r --arg name "$PROJECT_NAME" '.[] | select(.name == $name) | .projectId // empty')
if [ -z "$project_id" ]; then
echo "❌ Error: El proyecto '$PROJECT_NAME' no fue encontrado en Dokploy."
echo "Proyectos detectados en Dokploy:"
echo "$projects_json" | "$JQ_CMD" -r '.[].name'
exit 1
fi
echo " ✅ Proyecto '$PROJECT_NAME' encontrado. (ID: $project_id)"
# 2c. Obtener los detalles del proyecto para buscar el recurso Compose
project_details=$(curl -s -f -X GET "$DOKPLOY_API_URL/project.one?projectId=$project_id" -H "x-api-key: $DOKPLOY_API_KEY")
# 2d. Filtrar para hallar el composeId o id del recurso Compose
compose_id=$(echo "$project_details" | "$JQ_CMD" -r --arg name "$COMPOSE_NAME" '.compose[] | select(.name == $name) | .composeId // .id // empty')
if [ -z "$compose_id" ]; then
echo "❌ Error: El recurso Docker Compose '$COMPOSE_NAME' no fue encontrado en el proyecto '$PROJECT_NAME'."
echo "Compose apps detectadas:"
echo "$project_details" | "$JQ_CMD" -r '.compose[].name // "Ninguno"'
exit 1
fi
echo " ✅ Docker Compose '$COMPOSE_NAME' encontrado. (ID: $compose_id)"
else
echo " [SIMULACIÓN] Buscado proyecto '$PROJECT_NAME' y Compose '$COMPOSE_NAME' (IDs simulados)."
fi
# -----------------------------------------------------------------------------
# Paso 3: Registrar dominios en la API de Dokploy
# -----------------------------------------------------------------------------
echo ""
echo "=== Paso 3: Registrando dominios en Dokploy ==="
for mapping in "${MAPPINGS[@]}"; do
# Separar mapeo
IFS=":" read -r sub service port <<< "$mapping"
host=""
if [ "$sub" = "@" ]; then
host="$DOMAIN"
else
host="$sub.$DOMAIN"
fi
if [ "$DRY_RUN" = true ]; then
echo " [SIMULACIÓN] Registrar dominio: $host -> servicio '$service' (puerto $port)"
else
create_dokploy_domain "$host" "$service" "$port" "$compose_id"
fi
done
# -----------------------------------------------------------------------------
# Paso 4: Re-despliegue de la pila para aplicar cambios en Traefik
# -----------------------------------------------------------------------------
if [ "$SKIP_REDEPLOY" = false ]; then
echo ""
if [ "$DRY_RUN" = true ]; then
echo " [SIMULACIÓN] Re-desplegar compose con ID $compose_id"
else
redeploy_dokploy_compose "$compose_id"
fi
fi
fi
echo ""
echo "========================================================="
echo " 🎉 PROCESO FINALIZADO CON ÉXITO"
echo "========================================================="