portal.sh

#!/usr/bin/env bash
# Portal de actualizaciones de los nodos (Semaphore UI) en el master-1. Como root.
#
#   ./scripts/portal.sh instalar    primera vez: secretos, usuario admin, claves, arranque y botones
#   ./scripts/portal.sh botones     crea o completa los botones (pide la contraseña del admin)
#   ./scripts/portal.sh repo        pone el portal al día con main (tras mergear cambios en ansible/)
#   ./scripts/portal.sh claves      vuelve a copiar la clave SSH, known_hosts y kubeconfig
#   ./scripts/portal.sh estado | registros | parar | arrancar
#
# Web: http://203.0.113.121:3010 (solo por Tailscale). Ver ansible/README.md.
set -euo pipefail

RAIZ="$(cd "$(dirname "$0")/.." && pwd)"
COMPOSE=(docker compose -f "$RAIZ/ansible/semaphore/compose.yaml")
CONF=/root/.config/semaphore
ENV_FILE="$CONF/semaphore.env"
CLAVES_ORIGEN=/root/.config/mantenimiento
REPO=/opt/portal-nodos/repo
URL="http://${PORTAL_IP:-203.0.113.121}:${PORTAL_PUERTO:-3010}"
UID_SEMAPHORE=1001
umask 077

[ "$(id -u)" -eq 0 ] || { echo "Lánzalo como root" >&2; exit 1; }

copiar_claves() {
  install -d -m 700 -o "$UID_SEMAPHORE" -g "$UID_SEMAPHORE" "$CONF/claves"
  for f in mantenimiento_ed25519 known_hosts kubeconfig; do
    [ -r "$CLAVES_ORIGEN/$f" ] || { echo "Falta $CLAVES_ORIGEN/$f (ver ansible/README.md)" >&2; exit 1; }
    install -m 600 -o "$UID_SEMAPHORE" -g "$UID_SEMAPHORE" "$CLAVES_ORIGEN/$f" "$CONF/claves/$f"
  done
  echo "✓ claves copiadas a $CONF/claves (solo el usuario del contenedor)"
}

# Copia de main, independiente de la rama en la que esté /root/k3s: el portal
# solo ejecuta lo que ya se ha revisado y mergeado.
poner_repo() {
  # El contenedor (uid 1001) tiene que poder leerla: no es secreta, es main.
  local umask_antes; umask_antes=$(umask); umask 022
  git -C "$RAIZ" fetch -q origin main
  if [ ! -d "$REPO/.git" ]; then
    install -d -m 755 "$(dirname "$REPO")"
    git clone -q --no-checkout "$RAIZ" "$REPO"   # clon completo: Semaphore no ve /root/k3s
  fi
  git -C "$REPO" fetch -q "$RAIZ" refs/remotes/origin/main
  git -C "$REPO" checkout -q --detach FETCH_HEAD
  chmod -R go+rX,go-w "$REPO"
  umask "$umask_antes"
  echo "✓ portal en main: $(git -C "$REPO" log -1 --format='%h %s')"
}

esperar() {
  for _ in $(seq 60); do
    curl -fs "$URL/api/ping" >/dev/null 2>&1 && return 0
    sleep 2
  done
  echo "Semaphore no responde en $URL: ./scripts/portal.sh registros" >&2; exit 1
}

case "${1:-}" in
  instalar)
    install -d -m 700 "$CONF"
    if [ ! -f "$ENV_FILE" ]; then
      echo "Usuario administrador del portal (el 2FA se activa después, en la web)."
      read -rp "  Usuario: " usuario
      read -rsp "  Contraseña (mín. 12 caracteres): " clave; echo
      read -rsp "  Repite la contraseña: " clave2; echo
      [ "$clave" = "$clave2" ] || { echo "No coinciden" >&2; exit 1; }
      [ "${#clave}" -ge 12 ] || { echo "Demasiado corta" >&2; exit 1; }
      {
        echo "SEMAPHORE_COOKIE_HASH=$(head -c 32 /dev/urandom | base64)"
        echo "SEMAPHORE_COOKIE_ENCRYPTION=$(head -c 32 /dev/urandom | base64)"
        echo "SEMAPHORE_ACCESS_KEY_ENCRYPTION=$(head -c 32 /dev/urandom | base64)"
        echo "# Avisos por Telegram (opcional): rellena y ./scripts/portal.sh arrancar"
        echo "#SEMAPHORE_TELEGRAM_ALERT=true"
        echo "#SEMAPHORE_TELEGRAM_CHAT="
        echo "#SEMAPHORE_TELEGRAM_TOKEN="
        echo "SEMAPHORE_ADMIN=$usuario"
        echo "SEMAPHORE_ADMIN_NAME=$usuario"
        echo "SEMAPHORE_ADMIN_EMAIL=$usuario@localhost"
        echo "SEMAPHORE_ADMIN_PASSWORD=$clave"
      } > "$ENV_FILE"
      chmod 600 "$ENV_FILE"
    fi
    copiar_claves
    poner_repo
    "${COMPOSE[@]}" up -d --build
    esperar
    if grep -q '^SEMAPHORE_ADMIN_PASSWORD=' "$ENV_FILE"; then
      usuario="$(sed -n 's/^SEMAPHORE_ADMIN=//p' "$ENV_FILE")"
      clave="$(sed -n 's/^SEMAPHORE_ADMIN_PASSWORD=//p' "$ENV_FILE")"
      # El admin ya existe en la base de datos: la contraseña sale del disco pase lo
      # que pase con los botones (si fallan: ./scripts/portal.sh botones).
      sed -i '/^SEMAPHORE_ADMIN_PASSWORD=/d' "$ENV_FILE"
      PORTAL_URL="$URL" PORTAL_USUARIO="$usuario" PORTAL_CLAVE="$clave" \
        python3 "$RAIZ/ansible/semaphore/configurar.py" \
        || { echo "✘ No se crearon los botones: ./scripts/portal.sh botones" >&2; exit 1; }
      unset clave
    fi
    echo
    echo "✓ Portal en $URL"
    echo "  Entra, y en tu usuario (arriba a la derecha) activa la verificación en dos pasos (TOTP)."
    ;;
  botones)
    # Crea o completa el proyecto y los botones (se puede repetir: no duplica).
    read -rp "Usuario admin del portal: " usuario
    read -rsp "Contraseña: " clave; echo
    read -rp "Código 2FA de la app (6 dígitos; vacío si no tienes 2FA): " codigo
    PORTAL_URL="$URL" PORTAL_USUARIO="$usuario" PORTAL_CLAVE="$clave" PORTAL_CODIGO="$codigo" \
      python3 "$RAIZ/ansible/semaphore/configurar.py"
    ;;
  repo)      poner_repo ;;
  claves)    copiar_claves ;;
  estado)    "${COMPOSE[@]}" ps; curl -fs "$URL/api/ping" && echo " ← $URL" ;;
  registros) "${COMPOSE[@]}" logs --tail 50 ;;
  parar)     "${COMPOSE[@]}" stop ;;
  arrancar)  "${COMPOSE[@]}" up -d; esperar; echo "✓ $URL" ;;
  *) sed -n '2,10p' "$0"; exit 64 ;;
esac