Files
OmniOilPersonal/scripts/setup-domains.sh
Wilman Yesid Farfan Diaz 978eac5ed9
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
feat: add automation script for Cloudflare DNS provisioning and Dokploy deployment integration
2026-07-12 23:03:58 -05:00

514 lines
21 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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