Compare commits
1
Commits
6323fe8f0e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc326e8ac |
+22
-142
@@ -1,153 +1,33 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# deploy.sh — Despliega la rama actual a <slug>.localesp.es
|
# deploy.sh — Despliega la rama actual a <slug>.localesp.es
|
||||||
#
|
# Toda la lógica vive en lib/env.sh (modelo OOP). Éste es sólo el entrypoint.
|
||||||
# Estrategia A (entorno por rama):
|
|
||||||
# /opt/localesp-<slug> + systemd unit localesp-<slug> + vhost nginx + cert Let's Encrypt.
|
|
||||||
# Reutiliza el puerto de la unit existente (adopta entornos legacy) o asigna uno libre (>=8082).
|
|
||||||
#
|
|
||||||
# PRECONDICIÓN MANUAL: el registro A <slug>.localesp.es debe existir (se gestiona a mano).
|
|
||||||
# Si no resuelve aún, la app queda servida por HTTP y se emite el cert cuando el DNS apunte aquí.
|
|
||||||
#
|
|
||||||
# Ejecutar como root (host runner). Idempotente.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BRANCH="${1:-${GITHUB_REF_NAME:-}}"
|
|
||||||
[ -n "$BRANCH" ] || { echo "usage: $0 <branch>" >&2; exit 2; }
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
# shellcheck source=lib/env.sh
|
||||||
DOMAIN="localesp.es"
|
. "$SCRIPT_DIR/lib/env.sh"
|
||||||
PORT_BASE=8082 # 8080/8081 son entornos legacy
|
|
||||||
|
|
||||||
log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; }
|
usage() {
|
||||||
err() { printf '\033[1;31m[deploy:ERROR]\033[0m %s\n' "$*" >&2; }
|
echo "usage: $0 <branch>" >&2
|
||||||
|
exit 2
|
||||||
slugify() {
|
|
||||||
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
|
|
||||||
}
|
}
|
||||||
SLUG="$(slugify "$BRANCH")"
|
|
||||||
[ -n "$SLUG" ] || { err "slug vacío para la rama '$BRANCH'"; exit 2; }
|
|
||||||
|
|
||||||
DEST="/opt/localesp/$SLUG"
|
main() {
|
||||||
UNIT="localesp-$SLUG"
|
local branch root
|
||||||
HOST="$SLUG.$DOMAIN"
|
|
||||||
|
|
||||||
# --- asignación de puerto ---
|
branch="${1:-${GITHUB_REF_NAME:-}}"
|
||||||
allocate_port() {
|
if [ -z "$branch" ]; then
|
||||||
local u="/etc/systemd/system/$UNIT.service"
|
usage
|
||||||
if [ -f "$u" ]; then # adopta entorno existente
|
|
||||||
local p; p=$(grep -oE 'PORT=[0-9]+' "$u" | head -1 | cut -d= -f2 || true)
|
|
||||||
[ -n "$p" ] && { echo "$p"; return; }
|
|
||||||
fi
|
|
||||||
if [ -f "$DEST/.port" ]; then cat "$DEST/.port"; return; fi
|
|
||||||
local used; used=$( { grep -rhoE 'PORT=[0-9]+' /etc/systemd/system/localesp*.service 2>/dev/null | cut -d= -f2;
|
|
||||||
grep -rhoE 'localhost:[0-9]+' /etc/nginx/conf.d/*.conf 2>/dev/null | cut -d: -f2; } | sort -un)
|
|
||||||
local p=$PORT_BASE
|
|
||||||
while printf '%s\n' "$used" | grep -qx "$p"; do p=$((p+1)); done
|
|
||||||
echo "$p"
|
|
||||||
}
|
|
||||||
PORT="$(allocate_port)"
|
|
||||||
log "rama=$BRANCH slug=$SLUG host=$HOST dest=$DEST puerto=$PORT"
|
|
||||||
|
|
||||||
# --- 1. build artifacts ---
|
|
||||||
[ -d "$ROOT/dist" ] || { err "falta dist/ — ¿se ejecutó el build?"; exit 1; }
|
|
||||||
log "sincronizando artefactos -> $DEST"
|
|
||||||
mkdir -p "$DEST/dist" "$DEST/server"
|
|
||||||
rsync -a --delete "$ROOT/dist/" "$DEST/dist/"
|
|
||||||
rsync -a --delete "$ROOT/server/" "$DEST/server/"
|
|
||||||
cp -a "$ROOT/package.json" "$ROOT/package-lock.json" "$DEST/"
|
|
||||||
# localesp.db se PRESERVA (datos de usuarios). No se toca aquí.
|
|
||||||
|
|
||||||
# --- 2. dependencias de producción ---
|
|
||||||
# Reutiliza los node_modules ya compilados por el workflow (better-sqlite3 v12 exige
|
|
||||||
# C++20, GCC 12). Copiar + npm prune evita recompilar en cada despliegue.
|
|
||||||
# Fallback a npm ci --omit=dev si se ejecuta a mano sin build previo.
|
|
||||||
log "instalando dependencias (prod)"
|
|
||||||
if [ -d "$ROOT/node_modules" ]; then
|
|
||||||
mkdir -p "$DEST/node_modules"
|
|
||||||
rsync -a --delete "$ROOT/node_modules/" "$DEST/node_modules/"
|
|
||||||
( cd "$DEST" && npm prune --omit=dev --no-audit --no-fund )
|
|
||||||
else
|
|
||||||
( cd "$DEST" && npm ci --omit=dev --no-audit --no-fund )
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- 3. systemd unit ---
|
|
||||||
log "escribiendo unit $UNIT (PORT=$PORT)"
|
|
||||||
cat > "/etc/systemd/system/$UNIT.service" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=LocaleSP env rama '$BRANCH' ($HOST)
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=root
|
|
||||||
WorkingDirectory=$DEST
|
|
||||||
ExecStart=/usr/bin/node server/index.js
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5s
|
|
||||||
Environment=NODE_ENV=production
|
|
||||||
Environment=PORT=$PORT
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
echo "$PORT" > "$DEST/.port"
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable "$UNIT" >/dev/null 2>&1 || true
|
|
||||||
systemctl restart "$UNIT"
|
|
||||||
log "$UNIT arrancada"
|
|
||||||
|
|
||||||
# --- 4. vhost nginx (busca por server_name; crea o reajusta proxy_pass) ---
|
|
||||||
log "configurando nginx para $HOST"
|
|
||||||
HOST_RE="$(printf '%s' "$HOST" | sed 's/\./\\./g')"
|
|
||||||
VHOST="$(grep -rlE "server_name[[:space:]]+$HOST_RE[[:space:]]*;" /etc/nginx/conf.d/*.conf 2>/dev/null | head -1 || true)"
|
|
||||||
if [ -z "$VHOST" ]; then
|
|
||||||
VHOST="/etc/nginx/conf.d/$SLUG.conf"
|
|
||||||
log "creando vhost HTTP $VHOST"
|
|
||||||
cat > "$VHOST" <<EOF
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name $HOST;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:$PORT;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
log "vhost existente: $VHOST — forzando proxy_pass -> :$PORT"
|
|
||||||
sed -i -E "s|proxy_pass http://localhost:[0-9]+;|proxy_pass http://localhost:$PORT;|g" "$VHOST"
|
|
||||||
fi
|
|
||||||
nginx -t 2>&1 | tail -2
|
|
||||||
systemctl reload nginx
|
|
||||||
log "nginx recargado"
|
|
||||||
|
|
||||||
# --- 5. TLS (sólo si no hay cert y el DNS ya apunta aquí) ---
|
|
||||||
if [ -d "/etc/letsencrypt/live/$HOST" ]; then
|
|
||||||
log "cert TLS ya presente para $HOST"
|
|
||||||
else
|
|
||||||
log "comprobando DNS de $HOST"
|
|
||||||
PUBIP="$(curl -s4 --max-time 5 ifconfig.me || true)"
|
|
||||||
RESOLVED="$(getent hosts "$HOST" | awk '{print $1}' | head -1 || true)"
|
|
||||||
if [ -n "$PUBIP" ] && [ "$RESOLVED" = "$PUBIP" ]; then
|
|
||||||
log "emitiendo cert con certbot --nginx"
|
|
||||||
if certbot --nginx -d "$HOST" -n --redirect --keep-until-expiring; then
|
|
||||||
log "cert emitido ✓"
|
|
||||||
else
|
|
||||||
err "certbot falló; $HOST sigue en HTTP. Revisa y vuelve a lanzar el workflow."
|
|
||||||
fi
|
fi
|
||||||
else
|
root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
err "DNS de $HOST -> '${RESOLVED:-<sin resolver>}', esperado $PUBIP."
|
|
||||||
err "Crea el registro A $HOST -> $PUBIP y, tras propagar, re-lanza el workflow"
|
|
||||||
err "(o ejecuta: certbot --nginx -d $HOST). La app ya vive en http://$HOST"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
PROTO=https; [ ! -d "/etc/letsencrypt/live/$HOST" ] && PROTO=http
|
Logger::set_tag deploy
|
||||||
log "LISTO: $PROTO://$HOST (rama=$BRANCH unit=$UNIT puerto=$PORT)"
|
Env::new env "$branch" "$root"
|
||||||
|
Logger::info "$(Env::banner env)"
|
||||||
|
|
||||||
|
Env::deploy env
|
||||||
|
|
||||||
|
Logger::info "LISTO: $(Env::url env) ($(Env::summary env))"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|||||||
@@ -0,0 +1,450 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/env.sh — Modelo OOP de entornos de previsualización por rama.
|
||||||
|
#
|
||||||
|
# Estrategia (entorno por rama):
|
||||||
|
# /opt/localesp/<slug> + systemd unit localesp-<slug> + vhost nginx + cert Let's Encrypt.
|
||||||
|
# Reutiliza el puerto de la unit existente (adopta entornos legacy) o asigna uno libre (>=8082).
|
||||||
|
#
|
||||||
|
# PRECONDICIÓN MANUAL: el registro A <slug>.localesp.es debe existir (se gestiona a mano).
|
||||||
|
# Si no resuelve aún, la app queda servida por HTTP y se emite el cert cuando el DNS apunte aquí.
|
||||||
|
#
|
||||||
|
# Representación OOP en bash:
|
||||||
|
# - Instancia = array asociativo global, referenciado por NOMBRE.
|
||||||
|
# - Constructor = Class::new <nombre> <args> (declara el array y establece invariantes).
|
||||||
|
# - Método = Class::method <nombre> <args>; accede al estado vía `local -n self="$1"`.
|
||||||
|
# - Composición = Env posee los nombres de sus colaboradores (<env>__unit, __vhost, __cert).
|
||||||
|
# - Servicios sin estado (Slug, Host, Port, Artifacts, Deps, Logger) = métodos estáticos.
|
||||||
|
#
|
||||||
|
# Requiere bash >= 4.3 (namerefs). Sin efectos al sourcear: sólo define funciones/constantes.
|
||||||
|
#
|
||||||
|
# CONTRATO (importante): el NOMBRE de instancia pasado a Env::new (p.ej. "env") es el
|
||||||
|
# nombre de un array asociativo global. NUNCA declares en el caller una `local` con ese
|
||||||
|
# mismo nombre: bash usa ámbito dinámico y la local sombrearía al array global, rompiendo
|
||||||
|
# el nameref `local -n self=env`. Pasa el nombre como literal: Env::new env ...
|
||||||
|
|
||||||
|
DOMAIN="localesp.es"
|
||||||
|
PORT_BASE=8082 # 8080/8081 son entornos legacy
|
||||||
|
LOGGER_TAG="env"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logger — servicio singleton (tag configurable según entrypoint).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Logger::set_tag() {
|
||||||
|
LOGGER_TAG="$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info() {
|
||||||
|
printf '\033[1;34m[%s]\033[0m %s\n' "$LOGGER_TAG" "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::error() {
|
||||||
|
printf '\033[1;31m[%s:ERROR]\033[0m %s\n' "$LOGGER_TAG" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Slug — value object estático: normaliza un nombre de rama a slug.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Slug::from() {
|
||||||
|
local branch="$1"
|
||||||
|
printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Host — servicio estático: IP pública y resolución DNS.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Host::public_ip() {
|
||||||
|
curl -s4 --max-time 5 ifconfig.me || true
|
||||||
|
}
|
||||||
|
|
||||||
|
Host::resolved_ip() {
|
||||||
|
local host="$1"
|
||||||
|
getent hosts "$host" | awk '{print $1}' | head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Port — servicio estático: asigna/reutiliza puerto para una unit+dest.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Port::acquire() {
|
||||||
|
local unit="$1"
|
||||||
|
local dest="$2"
|
||||||
|
local unit_file="/etc/systemd/system/$unit.service"
|
||||||
|
local used
|
||||||
|
local port
|
||||||
|
|
||||||
|
# Adopta entorno existente: reutiliza el puerto de la unit ya instalada.
|
||||||
|
if [ -f "$unit_file" ]; then
|
||||||
|
port="$(grep -oE 'PORT=[0-9]+' "$unit_file" | head -1 | cut -d= -f2 || true)"
|
||||||
|
if [ -n "$port" ]; then
|
||||||
|
echo "$port"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -f "$dest/.port" ]; then
|
||||||
|
cat "$dest/.port"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
used="$( { grep -rhoE 'PORT=[0-9]+' /etc/systemd/system/localesp*.service 2>/dev/null | cut -d= -f2
|
||||||
|
grep -rhoE 'localhost:[0-9]+' /etc/nginx/conf.d/*.conf 2>/dev/null | cut -d: -f2; } | sort -un)"
|
||||||
|
port="$PORT_BASE"
|
||||||
|
while printf '%s\n' "$used" | grep -qx "$port"; do
|
||||||
|
port=$((port+1))
|
||||||
|
done
|
||||||
|
echo "$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Artifacts — servicio estático: artefactos buildados.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Artifacts::require_built() {
|
||||||
|
local root="$1"
|
||||||
|
|
||||||
|
if [ ! -d "$root/dist" ]; then
|
||||||
|
Logger::error "falta dist/ — ¿se ejecutó el build?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
Artifacts::sync() {
|
||||||
|
local root="$1"
|
||||||
|
local dest="$2"
|
||||||
|
|
||||||
|
mkdir -p "$dest/dist" "$dest/server"
|
||||||
|
rsync -a --delete "$root/dist/" "$dest/dist/"
|
||||||
|
rsync -a --delete "$root/server/" "$dest/server/"
|
||||||
|
cp -a "$root/package.json" "$root/package-lock.json" "$dest/"
|
||||||
|
# localesp.db se PRESERVA (datos de usuarios). No se toca aquí.
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Deps — servicio estático: dependencias de producción.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Deps::install() {
|
||||||
|
local root="$1"
|
||||||
|
local dest="$2"
|
||||||
|
|
||||||
|
# Reutiliza los node_modules ya compilados por el workflow (better-sqlite3 v12 exige
|
||||||
|
# C++20, GCC 12). Copiar + npm prune evita recompilar en cada despliegue.
|
||||||
|
# Fallback a npm ci --omit=dev si se ejecuta a mano sin build previo.
|
||||||
|
if [ -d "$root/node_modules" ]; then
|
||||||
|
mkdir -p "$dest/node_modules"
|
||||||
|
rsync -a --delete "$root/node_modules/" "$dest/node_modules/"
|
||||||
|
( cd "$dest" && npm prune --omit=dev --no-audit --no-fund )
|
||||||
|
else
|
||||||
|
( cd "$dest" && npm ci --omit=dev --no-audit --no-fund )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SystemdUnit — entidad: fichero .service de un entorno.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SystemdUnit::new() {
|
||||||
|
local self="$1"
|
||||||
|
local name="$2"
|
||||||
|
local dest="$3"
|
||||||
|
local branch="$4"
|
||||||
|
local host="$5"
|
||||||
|
local port="$6"
|
||||||
|
|
||||||
|
declare -gA "$self"
|
||||||
|
local -n u="$self"
|
||||||
|
u[name]="$name"
|
||||||
|
u[dest]="$dest"
|
||||||
|
u[branch]="$branch"
|
||||||
|
u[host]="$host"
|
||||||
|
u[port]="$port"
|
||||||
|
u[file]="/etc/systemd/system/${name}.service"
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemdUnit::render() {
|
||||||
|
local self="$1"
|
||||||
|
local -n u="$self"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=LocaleSP env rama '${u[branch]}' (${u[host]})
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=${u[dest]}
|
||||||
|
ExecStart=/usr/bin/node server/index.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
Environment=PORT=${u[port]}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemdUnit::write() {
|
||||||
|
local self="$1"
|
||||||
|
local -n u="$self"
|
||||||
|
|
||||||
|
Logger::info "escribiendo unit ${u[name]} (PORT=${u[port]})"
|
||||||
|
SystemdUnit::render "$self" > "${u[file]}"
|
||||||
|
echo "${u[port]}" > "${u[dest]}/.port"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "${u[name]}" >/dev/null 2>&1 || true
|
||||||
|
systemctl restart "${u[name]}"
|
||||||
|
Logger::info "${u[name]} arrancada"
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemdUnit::remove() {
|
||||||
|
local self="$1"
|
||||||
|
local -n u="$self"
|
||||||
|
|
||||||
|
systemctl disable --now "${u[name]}" 2>/dev/null || true
|
||||||
|
rm -f "${u[file]}"
|
||||||
|
systemctl daemon-reload
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# NginxVhost — entidad: vhost nginx de un entorno.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
NginxVhost::new() {
|
||||||
|
local self="$1"
|
||||||
|
local slug="$2"
|
||||||
|
local host="$3"
|
||||||
|
local port="$4"
|
||||||
|
|
||||||
|
declare -gA "$self"
|
||||||
|
local -n v="$self"
|
||||||
|
v[slug]="$slug"
|
||||||
|
v[host]="$host"
|
||||||
|
v[port]="$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
NginxVhost::find_path() {
|
||||||
|
local self="$1"
|
||||||
|
local -n v="$self"
|
||||||
|
local host_re
|
||||||
|
host_re="$(printf '%s' "${v[host]}" | sed 's/\./\\./g')"
|
||||||
|
grep -rlE "server_name[[:space:]]+$host_re[[:space:]]*;" /etc/nginx/conf.d/*.conf 2>/dev/null | head -1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
NginxVhost::render() {
|
||||||
|
local self="$1"
|
||||||
|
local -n v="$self"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${v[host]};
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:${v[port]};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
NginxVhost::create() {
|
||||||
|
local self="$1"
|
||||||
|
local -n v="$self"
|
||||||
|
local path="/etc/nginx/conf.d/${v[slug]}.conf"
|
||||||
|
|
||||||
|
Logger::info "creando vhost HTTP $path"
|
||||||
|
NginxVhost::render "$self" > "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
NginxVhost::repoint() {
|
||||||
|
local self="$1"
|
||||||
|
local -n v="$self"
|
||||||
|
local path="$2"
|
||||||
|
|
||||||
|
Logger::info "vhost existente: $path — forzando proxy_pass -> :${v[port]}"
|
||||||
|
sed -i -E "s|proxy_pass http://localhost:[0-9]+;|proxy_pass http://localhost:${v[port]};|g" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
NginxVhost::configure() {
|
||||||
|
local self="$1"
|
||||||
|
local existing
|
||||||
|
|
||||||
|
existing="$(NginxVhost::find_path "$self")"
|
||||||
|
if [ -z "$existing" ]; then
|
||||||
|
NginxVhost::create "$self"
|
||||||
|
else
|
||||||
|
NginxVhost::repoint "$self" "$existing"
|
||||||
|
fi
|
||||||
|
nginx -t 2>&1 | tail -2
|
||||||
|
systemctl reload nginx
|
||||||
|
Logger::info "nginx recargado"
|
||||||
|
}
|
||||||
|
|
||||||
|
NginxVhost::remove() {
|
||||||
|
local self="$1"
|
||||||
|
local path
|
||||||
|
|
||||||
|
path="$(NginxVhost::find_path "$self")"
|
||||||
|
if [ -n "$path" ]; then
|
||||||
|
Logger::info "borrando vhost $path"
|
||||||
|
rm -f "$path"
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TlsCert — entidad: certificado Let's Encrypt de un entorno.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TlsCert::new() {
|
||||||
|
local self="$1"
|
||||||
|
local host="$2"
|
||||||
|
|
||||||
|
declare -gA "$self"
|
||||||
|
local -n c="$self"
|
||||||
|
c[host]="$host"
|
||||||
|
}
|
||||||
|
|
||||||
|
TlsCert::exists() {
|
||||||
|
local self="$1"
|
||||||
|
local -n c="$self"
|
||||||
|
[ -d "/etc/letsencrypt/live/${c[host]}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
TlsCert::ensure() {
|
||||||
|
local self="$1"
|
||||||
|
local -n c="$self"
|
||||||
|
local host="${c[host]}"
|
||||||
|
local pub_ip resolved
|
||||||
|
|
||||||
|
if TlsCert::exists "$self"; then
|
||||||
|
Logger::info "cert TLS ya presente para $host"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
Logger::info "comprobando DNS de $host"
|
||||||
|
pub_ip="$(Host::public_ip)"
|
||||||
|
resolved="$(Host::resolved_ip "$host")"
|
||||||
|
|
||||||
|
if [ -n "$pub_ip" ] && [ "$resolved" = "$pub_ip" ]; then
|
||||||
|
Logger::info "emitiendo cert con certbot --nginx"
|
||||||
|
if certbot --nginx -d "$host" -n --redirect --keep-until-expiring; then
|
||||||
|
Logger::info "cert emitido ✓"
|
||||||
|
else
|
||||||
|
Logger::error "certbot falló; $host sigue en HTTP. Revisa y vuelve a lanzar el workflow."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
Logger::error "DNS de $host -> '${resolved:-<sin resolver>}', esperado $pub_ip."
|
||||||
|
Logger::error "Crea el registro A $host -> $pub_ip y, tras propagar, re-lanza el workflow"
|
||||||
|
Logger::error "(o ejecuta: certbot --nginx -d $host). La app ya vive en http://$host"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TlsCert::delete() {
|
||||||
|
local self="$1"
|
||||||
|
local -n c="$self"
|
||||||
|
local host="${c[host]}"
|
||||||
|
|
||||||
|
if TlsCert::exists "$self"; then
|
||||||
|
Logger::info "borrando cert $host"
|
||||||
|
certbot delete --cert-name "$host" -n 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TlsCert::protocol() {
|
||||||
|
local self="$1"
|
||||||
|
|
||||||
|
if TlsCert::exists "$self"; then
|
||||||
|
echo "https"
|
||||||
|
else
|
||||||
|
echo "http"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Env — aggregate root: un entorno de rama. Compone SystemdUnit, NginxVhost, TlsCert.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
Env::new() {
|
||||||
|
local self="$1"
|
||||||
|
local branch="$2"
|
||||||
|
local root="$3"
|
||||||
|
|
||||||
|
declare -gA "$self"
|
||||||
|
local -n s="$self"
|
||||||
|
|
||||||
|
s[branch]="$branch"
|
||||||
|
s[root]="$root"
|
||||||
|
s[slug]="$(Slug::from "$branch")"
|
||||||
|
if [ -z "${s[slug]}" ]; then
|
||||||
|
Logger::error "slug vacío para la rama '$branch'"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
s[host]="${s[slug]}.$DOMAIN"
|
||||||
|
s[dest]="/opt/localesp/${s[slug]}"
|
||||||
|
s[unit_name]="localesp-${s[slug]}"
|
||||||
|
s[port]="$(Port::acquire "${s[unit_name]}" "${s[dest]}")"
|
||||||
|
|
||||||
|
# Composición: crea los colaboradores y guarda sus nombres en el agregado.
|
||||||
|
SystemdUnit::new "${self}__unit" "${s[unit_name]}" "${s[dest]}" "$branch" "${s[host]}" "${s[port]}"
|
||||||
|
NginxVhost::new "${self}__vhost" "${s[slug]}" "${s[host]}" "${s[port]}"
|
||||||
|
TlsCert::new "${self}__cert" "${s[host]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Env::deploy() {
|
||||||
|
local self="$1"
|
||||||
|
local -n s="$self"
|
||||||
|
|
||||||
|
Artifacts::require_built "${s[root]}"
|
||||||
|
Logger::info "sincronizando artefactos -> ${s[dest]}"
|
||||||
|
Artifacts::sync "${s[root]}" "${s[dest]}"
|
||||||
|
|
||||||
|
Logger::info "instalando dependencias (prod)"
|
||||||
|
Deps::install "${s[root]}" "${s[dest]}"
|
||||||
|
|
||||||
|
SystemdUnit::write "${self}__unit"
|
||||||
|
|
||||||
|
Logger::info "configurando nginx para ${s[host]}"
|
||||||
|
NginxVhost::configure "${self}__vhost"
|
||||||
|
|
||||||
|
TlsCert::ensure "${self}__cert"
|
||||||
|
}
|
||||||
|
|
||||||
|
Env::teardown() {
|
||||||
|
local self="$1"
|
||||||
|
local -n s="$self"
|
||||||
|
|
||||||
|
SystemdUnit::remove "${self}__unit"
|
||||||
|
TlsCert::delete "${self}__cert"
|
||||||
|
NginxVhost::remove "${self}__vhost"
|
||||||
|
rm -rf "${s[dest]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Env::url() {
|
||||||
|
local self="$1"
|
||||||
|
local -n s="$self"
|
||||||
|
local proto
|
||||||
|
|
||||||
|
proto="$(TlsCert::protocol "${self}__cert")"
|
||||||
|
echo "$proto://${s[host]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Env::banner() {
|
||||||
|
local self="$1"
|
||||||
|
local -n s="$self"
|
||||||
|
|
||||||
|
echo "rama=${s[branch]} slug=${s[slug]} host=${s[host]} dest=${s[dest]} puerto=${s[port]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Env::identity() {
|
||||||
|
local self="$1"
|
||||||
|
local -n s="$self"
|
||||||
|
|
||||||
|
echo "${s[host]} (${s[dest]}, ${s[unit_name]})"
|
||||||
|
}
|
||||||
|
|
||||||
|
Env::summary() {
|
||||||
|
local self="$1"
|
||||||
|
local -n s="$self"
|
||||||
|
|
||||||
|
echo "rama=${s[branch]} unit=${s[unit_name]} puerto=${s[port]}"
|
||||||
|
}
|
||||||
+22
-26
@@ -1,37 +1,33 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# teardown.sh — Elimina el entorno de una rama (al borrar la rama o a mano).
|
# teardown.sh — Elimina el entorno de una rama (al borrar la rama o a mano).
|
||||||
# usage: teardown.sh <branch>
|
# Toda la lógica vive en lib/env.sh (modelo OOP). Éste es sólo el entrypoint.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BRANCH="${1:?usage: $0 <branch>}"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
DOMAIN="localesp.es"
|
# shellcheck source=lib/env.sh
|
||||||
|
. "$SCRIPT_DIR/lib/env.sh"
|
||||||
|
|
||||||
log() { printf '\033[1;34m[teardown]\033[0m %s\n' "$*"; }
|
usage() {
|
||||||
|
echo "usage: $0 <branch>" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
slugify() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'; }
|
main() {
|
||||||
SLUG="$(slugify "$BRANCH")"
|
local branch root
|
||||||
UNIT="localesp-$SLUG"
|
|
||||||
HOST="$SLUG.$DOMAIN"
|
|
||||||
DEST="/opt/localesp/$SLUG"
|
|
||||||
|
|
||||||
log "$HOST ($DEST, $UNIT)"
|
branch="${1:-}"
|
||||||
|
if [ -z "$branch" ]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
systemctl disable --now "$UNIT" 2>/dev/null || true
|
Logger::set_tag teardown
|
||||||
rm -f "/etc/systemd/system/$UNIT.service"
|
Env::new env "$branch" "$root"
|
||||||
systemctl daemon-reload
|
Logger::info "$(Env::identity env)"
|
||||||
|
|
||||||
if [ -d "/etc/letsencrypt/live/$HOST" ]; then
|
Env::teardown env
|
||||||
log "borrando cert $HOST"
|
|
||||||
certbot delete --cert-name "$HOST" -n 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
HOST_RE="$(printf '%s' "$HOST" | sed 's/\./\\./g')"
|
Logger::info "entorno eliminado ✓"
|
||||||
VHOST="$(grep -rlE "server_name[[:space:]]+$HOST_RE[[:space:]]*;" /etc/nginx/conf.d/*.conf 2>/dev/null | head -1 || true)"
|
}
|
||||||
if [ -n "$VHOST" ]; then
|
|
||||||
log "borrando vhost $VHOST"
|
|
||||||
rm -f "$VHOST"
|
|
||||||
nginx -t && systemctl reload nginx
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf "$DEST"
|
main "$@"
|
||||||
log "entorno eliminado ✓"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user