Files
LOGINMASTER-DeploymentScript/deploy.sh
T
Luca d3a9739f2f Fix race su rs.initiate: attendi i 3 pod Mongo + directConnection in mongosh
Due bug che bloccavano deploy.sh su tenant con provisioning PVC lento:

1. Race DNS: lo step 8 aspettava solo mongodb-tenant-0 prima di rs.initiate.
   Se i pod 1/2 erano ancora in provisioning, i loro record DNS nel service
   headless non esistevano e rs.initiate falliva il quorum check con
   "Could not find address for mongodb-tenant-1...". Ora si attende che tutti
   e 3 i pod siano Ready.

2. mongosh "connection <monitor> closed": con --host 127.0.0.1, appena il
   replica set è inizializzato mongosh passa in modalità topology e prova a
   monitorare il PRIMARY via hostname annunciato, chiudendo la connessione.
   Sostituito con URI directConnection=true, compatibile con la localhost
   exception per la creazione del primo utente.

Allineati gli hint diagnostici in deploy.sh e l'esempio rs.status() nel README
al nuovo form directConnection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:18:14 +02:00

539 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# deploy.sh — Deploy di un nuovo tenant LoginMaster su Kubernetes.
#
# Embeddato (hardcoded):
# - database LoginMaster-Tenant
# - replica set name rs0
# - replicas 3 mongo / 2 api / 1 admin
# - resources, probe, PDB, anti-affinity, TLS issuer letsencrypt-prod
# - registry hub.codebaker.it/loginmaster-tenant/
#
# Chiesto a runtime:
# - namespace (default: loginmaster-tenant)
# - context kubectl, storage class/size
# - tag immagini
# - domini pubblici (api + admin)
# - LOGINMASTER_API_URL, ADMIN_ALLOWED_ORIGIN
# - SMTP config (opzionale)
# - credenziali registry codebaker
#
# Generato automaticamente:
# - MASTER_ENCRYPTION_KEY, mongo root pwd, mongo app pwd, mongo keyfile
# (salvate in .deploy-credentials.txt con chmod 600)
#
# Idempotente: rilanciabile in sicurezza.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATES_DIR="$SCRIPT_DIR/templates"
CREDS_FILE="$SCRIPT_DIR/.deploy-credentials.txt"
# -----------------------------------------------------------------------------
# Costanti (embedded)
# -----------------------------------------------------------------------------
DB_NAME="LoginMaster-Tenant"
RS_NAME="rs0"
REGISTRY_SERVER="hub.codebaker.it"
SVC_HEADLESS="mongodb-tenant-headless"
POD0="mongodb-tenant-0"
POD1="mongodb-tenant-1"
POD2="mongodb-tenant-2"
# Default per i prompt
DEF_NAMESPACE="loginmaster-tenant"
DEF_IMAGE_TAG_API="prod-1.3.8"
DEF_IMAGE_TAG_ADMIN="prod-1.3.7"
DEF_DOMAIN_API="api.tenant.example.com"
DEF_DOMAIN_ADMIN="admin.tenant.example.com"
DEF_LOGINMASTER_API_URL="https://api.loginmaster.it/1.3/"
DEF_ADMIN_ALLOWED_ORIGIN="https://admin.loginmaster.it"
DEF_STORAGE_SIZE="20Gi"
# Mongo: il limite memoria deve coprire la WiredTiger cache (default ~50% RAM)
# + working set + connessioni. 1Gi è insufficiente per qualsiasi carico reale.
DEF_MONGO_CPU_REQUEST="250m"
DEF_MONGO_CPU_LIMIT="2"
DEF_MONGO_MEM_REQUEST="1Gi"
DEF_MONGO_MEM_LIMIT="2Gi"
# DEF_REGISTRY_USERNAME volutamente vuoto: ogni cliente ha le sue credenziali
# di accesso al registry, non c'è un default sensato.
DEF_REGISTRY_USERNAME=""
DEF_ROOT_USER="admin"
DEF_APP_USER="lmtenant"
# -----------------------------------------------------------------------------
# Helper
# -----------------------------------------------------------------------------
BLUE='\033[0;34m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m'
log() { echo -e "\n${BLUE}==>${NC} $*"; }
ok() { echo -e "${GREEN}${NC} $*"; }
warn() { echo -e "${YELLOW}!${NC} $*"; }
err() { echo -e "${RED}ERRORE:${NC} $*" >&2; exit 1; }
ask() {
# ask "Prompt" "default" -> popola REPLY
local prompt="$1" def="${2:-}" input
if [[ -n "$def" ]]; then
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${def}]: ")" input
REPLY="${input:-$def}"
else
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" input
REPLY="$input"
fi
}
ask_secret() {
local prompt="$1" input
read -r -s -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" input
echo
REPLY="$input"
}
confirm() {
local prompt="$1" def="${2:-N}" input suffix
[[ "$def" == "Y" ]] && suffix="[Y/n]" || suffix="[y/N]"
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} ${suffix}: ")" input
input="${input:-$def}"
[[ "$input" =~ ^[yY]([eE][sS])?$ ]]
}
render() {
# Sostituisce ${VAR} nel template con le variabili esportate, via envsubst.
# Il fallback sed è stato rimosso perché non gestiva correttamente caratteri
# speciali (|, &, \, newline) in valori realistici come EMAIL_FROM.
envsubst < "$1"
}
kc() { kubectl --context "$CONTEXT" -n "$NAMESPACE" "$@"; }
kc_cluster() { kubectl --context "$CONTEXT" "$@"; }
# mongosh dentro pod-0 — JS letto da stdin per non esporre credenziali in argv
# (kubectl exec mette argv nei log audit dell'API server e nel command line del kubelet).
# Connessione mongosh dentro pod-0 con TLS verso 127.0.0.1.
# - tls=true + tlsCAFile: valida il cert via la CA interna.
# - tlsAllowInvalidHostnames: il cert non include 'localhost' nei SAN; la CA però
# è validata, quindi MITM resta impossibile (siamo già dentro al pod).
# - 127.0.0.1: necessario per la "localhost exception" di mongo (creazione primo utente).
# - directConnection=true: OBBLIGATORIO. Appena il replica set è inizializzato,
# senza directConnection mongosh passa in modalità topology e prova a monitorare
# il PRIMARY tramite l'hostname annunciato (mongodb-tenant-0.<headless>): la
# connessione di monitoring si chiude e ogni comando fallisce con
# "connection <monitor> closed". directConnection forza la sessione sul solo
# 127.0.0.1, restando compatibile con la localhost exception.
MONGO_TLS_ARGS=('mongodb://127.0.0.1:27017/?directConnection=true&tls=true&tlsCAFile=/etc/mongo-tls/ca.crt&tlsAllowInvalidHostnames=true')
mongo_eval() {
# Localhost exception: usabile solo prima della creazione del primo utente.
kc exec -i "$POD0" -c mongodb -- mongosh --quiet "${MONGO_TLS_ARGS[@]}"
}
mongo_eval_root() {
# Autentica via db.auth() inline così -p non finisce in argv.
# ROOT_USER/ROOT_PASS sono alfanumerici (openssl rand -hex), single-quote-safe.
{
printf "db.getSiblingDB('admin').auth('%s', '%s');\n" "$ROOT_USER" "$ROOT_PASS"
cat
} | kc exec -i "$POD0" -c mongodb -- mongosh --quiet "${MONGO_TLS_ARGS[@]}"
}
# Estrai l'unico valore stampato dal JS come `print('@@RES@@' + value)`.
# Robusto rispetto a warning/deprecation/banner che mongosh può emettere.
# Lo script JS passato deve usare la sentinella esplicitamente.
mongo_get() { mongo_eval 2>/dev/null | sed -n 's/^@@RES@@//p' | head -1; }
mongo_get_root() { mongo_eval_root 2>/dev/null | sed -n 's/^@@RES@@//p' | head -1; }
# -----------------------------------------------------------------------------
# Pre-flight
# -----------------------------------------------------------------------------
log "Pre-flight checks"
command -v kubectl >/dev/null || err "kubectl non trovato in PATH"
command -v openssl >/dev/null || err "openssl non trovato in PATH"
command -v envsubst >/dev/null || err "envsubst non trovato in PATH (brew install gettext && brew link gettext --force)"
ok "Strumenti disponibili"
# -----------------------------------------------------------------------------
# Prompt dei parametri
# -----------------------------------------------------------------------------
log "Parametri del deploy"
CURRENT_CTX=$(kubectl config current-context 2>/dev/null || echo "")
echo "Context kubectl disponibili:"
kubectl config get-contexts -o name | sed 's/^/ - /'
echo
ask "Context da usare" "$CURRENT_CTX"
CONTEXT="$REPLY"
[[ -z "$CONTEXT" ]] && err "Context obbligatorio"
# Verifica accesso al cluster
kubectl --context "$CONTEXT" cluster-info >/dev/null 2>&1 \
|| err "Impossibile contattare il cluster sul context '$CONTEXT'"
ok "Connesso a $CONTEXT"
ask "Nome del namespace" "$DEF_NAMESPACE"
NAMESPACE="$REPLY"
[[ -z "$NAMESPACE" ]] && err "Namespace obbligatorio"
# Re-run protection: questo script genera segreti freschi ad ogni esecuzione.
# Se il namespace contiene già un deploy, sovrascrivere i Secret causerebbe:
# - keyfile Mongo nuovo → mismatch fra membri del replica set → perdita di quorum.
# - password app/root nel Secret diverse da quelle in DB → API e mongo_eval_root rotti.
# Per fresh install: cancellare il namespace. Per update immagini: usare update.sh.
if kubectl --context "$CONTEXT" -n "$NAMESPACE" get secret mongodb-tenant-auth >/dev/null 2>&1; then
err "Il namespace '$NAMESPACE' contiene già un deploy (Secret 'mongodb-tenant-auth' presente).
Rilanciare deploy.sh genererebbe nuovi segreti e rompere il replica set.
- Per aggiornare le immagini: ./update.sh
- Per ridistribuire da zero: kubectl --context '$CONTEXT' delete namespace '$NAMESPACE'"
fi
# Calcolo FQDN MongoDB (dipende dal namespace scelto)
FQDN_SUFFIX="${SVC_HEADLESS}.${NAMESPACE}.svc.cluster.local"
HOST0="${POD0}.${FQDN_SUFFIX}:27017"
HOST1="${POD1}.${FQDN_SUFFIX}:27017"
HOST2="${POD2}.${FQDN_SUFFIX}:27017"
# I due tag possono divergere: storicamente api-tenant viene rilasciata più
# spesso di admin-tenant, quindi due prompt separati invece di uno unico.
ask "Tag api-tenant" "$DEF_IMAGE_TAG_API"
IMAGE_TAG_API="$REPLY"
ask "Tag admin-tenant" "$DEF_IMAGE_TAG_ADMIN"
IMAGE_TAG_ADMIN="$REPLY"
ask "Hostname pubblico API" "$DEF_DOMAIN_API"
DOMAIN_API="$REPLY"
ask "Hostname pubblico Admin" "$DEF_DOMAIN_ADMIN"
DOMAIN_ADMIN="$REPLY"
TENANT_ADMIN_URL="https://${DOMAIN_ADMIN}"
ask "LOGINMASTER_API_URL (API LoginMaster centrale)" "$DEF_LOGINMASTER_API_URL"
LOGINMASTER_API_URL="$REPLY"
# ADMIN_ALLOWED_ORIGIN è l'URL del pannello admin GLOBALE di LoginMaster (es.
# https://admin.loginmaster.it), non del tenant: serve all'API per accettare
# richieste cross-origin di amministrazione centralizzata. È deliberatamente
# diverso da TENANT_ADMIN_URL (https://${DOMAIN_ADMIN}, calcolato sotto).
ask "ADMIN_ALLOWED_ORIGIN (origin del pannello LoginMaster globale)" "$DEF_ADMIN_ALLOWED_ORIGIN"
ADMIN_ALLOWED_ORIGIN="$REPLY"
# Default suggerito = la storage class marcata come default sul cluster (annotazione
# storageclass.kubernetes.io/is-default-class). Funziona indipendentemente dal cloud
# provider (gp3 su EKS, managed-csi su AKS, do-block-storage su DOKS, sbs-default-retain
# su Scaleway, vsphere-csi su vSphere, ecc.). Se il cluster non ne ha una marcata, niente default.
DETECTED_SC=$(kubectl --context "$CONTEXT" get storageclass -o jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}' 2>/dev/null | awk '{print $1}')
ask "Storage class per i PVC Mongo" "${DETECTED_SC:-}"
STORAGE_CLASS="$REPLY"
[[ -z "$STORAGE_CLASS" ]] && err "Storage class obbligatoria"
ask "Storage size per ogni membro Mongo" "$DEF_STORAGE_SIZE"
STORAGE_SIZE="$REPLY"
ask "Mongo CPU request" "$DEF_MONGO_CPU_REQUEST"
MONGO_CPU_REQUEST="$REPLY"
ask "Mongo CPU limit" "$DEF_MONGO_CPU_LIMIT"
MONGO_CPU_LIMIT="$REPLY"
ask "Mongo memory request" "$DEF_MONGO_MEM_REQUEST"
MONGO_MEM_REQUEST="$REPLY"
ask "Mongo memory limit" "$DEF_MONGO_MEM_LIMIT"
MONGO_MEM_LIMIT="$REPLY"
# SMTP opzionale
SMTP_HOST=""; SMTP_PORT="587"; SMTP_SECURE="false"; SMTP_USER=""; SMTP_PASSWORD=""; EMAIL_FROM=""
if confirm "Configurare SMTP ora?" "N"; then
ask "SMTP host (es. smtp.gmail.com)" ""
SMTP_HOST="$REPLY"
ask "SMTP port" "587"
SMTP_PORT="$REPLY"
ask "SMTP secure (true/false)" "false"
SMTP_SECURE="$REPLY"
ask "SMTP user" ""
SMTP_USER="$REPLY"
ask_secret "SMTP password"
SMTP_PASSWORD="$REPLY"
ask "EMAIL_FROM (es. 'Tenant <no-reply@example.com>')" ""
EMAIL_FROM="$REPLY"
fi
# Registry codebaker
log "Credenziali registry ${REGISTRY_SERVER}"
ask "Registry username" "$DEF_REGISTRY_USERNAME"
REGISTRY_USERNAME="$REPLY"
[[ -z "$REGISTRY_USERNAME" ]] && err "Username registry obbligatorio"
ask_secret "Registry password"
REGISTRY_PASSWORD="$REPLY"
[[ -z "$REGISTRY_PASSWORD" ]] && err "Password registry obbligatoria"
# -----------------------------------------------------------------------------
# Generazione segreti
# -----------------------------------------------------------------------------
log "Genero segreti crittografici"
MASTER_ENCRYPTION_KEY=$(openssl rand -hex 32)
MONGO_KEYFILE=$(openssl rand -base64 756 | tr -d '\n')
ROOT_USER="$DEF_ROOT_USER"
ROOT_PASS=$(openssl rand -hex 24)
APP_USER="$DEF_APP_USER"
APP_PASS=$(openssl rand -hex 24)
MONGODB_URI="mongodb://${APP_USER}:${APP_PASS}@${HOST0},${HOST1},${HOST2}/${DB_NAME}?replicaSet=${RS_NAME}&authSource=admin&readPreference=nearest&retryWrites=true&w=majority&tls=true&tlsCAFile=/etc/mongo-tls/ca.crt"
ok "Segreti generati (verranno salvati in $CREDS_FILE al termine)"
# -----------------------------------------------------------------------------
# Summary + conferma finale
# -----------------------------------------------------------------------------
log "Riepilogo deploy"
cat <<EOF
Context: $CONTEXT
Namespace: $NAMESPACE
Image tag api: $IMAGE_TAG_API
Image tag admin: $IMAGE_TAG_ADMIN
API host: $DOMAIN_API
Admin host: $DOMAIN_ADMIN
LOGINMASTER_API_URL: $LOGINMASTER_API_URL
ADMIN_ALLOWED_ORIGIN: $ADMIN_ALLOWED_ORIGIN
TENANT_ADMIN_URL: $TENANT_ADMIN_URL
Storage: ${STORAGE_SIZE} su ${STORAGE_CLASS}
Mongo resources: cpu ${MONGO_CPU_REQUEST}/${MONGO_CPU_LIMIT}, mem ${MONGO_MEM_REQUEST}/${MONGO_MEM_LIMIT}
SMTP: ${SMTP_HOST:-<disabilitato>}${SMTP_HOST:+ (user=$SMTP_USER)}
Registry: ${REGISTRY_SERVER} (user=$REGISTRY_USERNAME)
Workloads: mongodb=3 (replica set rs0) / tenant-api=2 / tenant-admin=1
Mongo root user: $ROOT_USER
Mongo app user: $APP_USER (db=$DB_NAME)
EOF
confirm "Procedere con il deploy?" "N" || { echo "Annullato."; exit 0; }
# -----------------------------------------------------------------------------
# Apply risorse
# -----------------------------------------------------------------------------
# Esporta tutte le variabili per envsubst (usato da render in ogni step)
export NAMESPACE LOGINMASTER_API_URL ADMIN_ALLOWED_ORIGIN TENANT_ADMIN_URL \
SMTP_HOST SMTP_PORT SMTP_SECURE SMTP_USER EMAIL_FROM \
STORAGE_CLASS STORAGE_SIZE IMAGE_TAG_API IMAGE_TAG_ADMIN DOMAIN_API DOMAIN_ADMIN \
MONGO_CPU_REQUEST MONGO_CPU_LIMIT MONGO_MEM_REQUEST MONGO_MEM_LIMIT
log "1/11 Namespace"
render "$TEMPLATES_DIR/00-namespace.yaml" | kc_cluster apply -f -
log "2/11 NetworkPolicy (isolamento intra-namespace)"
render "$TEMPLATES_DIR/02-networkpolicy.yaml" | kc_cluster apply -f -
log "3/11 cert-manager: CA interna + cert membri replica set"
render "$TEMPLATES_DIR/01-mongodb-tls.yaml" | kc_cluster apply -f -
log "4/11 Secret registry-codebaker"
kc create secret docker-registry registry-codebaker \
--docker-server="$REGISTRY_SERVER" \
--docker-username="$REGISTRY_USERNAME" \
--docker-password="$REGISTRY_PASSWORD" \
--dry-run=client -o yaml | kc apply -f -
log "5/11 Secret tenant-api-secrets (MASTER_ENCRYPTION_KEY + SMTP_PASSWORD)"
kc create secret generic tenant-api-secrets \
--from-literal=MASTER_ENCRYPTION_KEY="$MASTER_ENCRYPTION_KEY" \
--from-literal=SMTP_PASSWORD="$SMTP_PASSWORD" \
--dry-run=client -o yaml | kc apply -f -
log "6/11 Secret mongodb-tenant-auth (keyfile + credenziali + URI)"
kc create secret generic mongodb-tenant-auth \
--from-literal=keyfile="$MONGO_KEYFILE" \
--from-literal=root-username="$ROOT_USER" \
--from-literal=root-password="$ROOT_PASS" \
--from-literal=app-username="$APP_USER" \
--from-literal=app-password="$APP_PASS" \
--from-literal=app-database="$DB_NAME" \
--from-literal=MONGODB_URI="$MONGODB_URI" \
--dry-run=client -o yaml | kc apply -f -
log "7/11 ConfigMap tenant-api-config"
render "$TEMPLATES_DIR/03-configmap.yaml" | kc_cluster apply -f -
log "8/11 MongoDB StatefulSet + Service + PDB"
log " Attendo che cert-manager emetta il Secret 'mongodb-tenant-tls'"
for i in $(seq 1 60); do
if kc get secret mongodb-tenant-tls >/dev/null 2>&1; then
ok "Cert TLS emesso"
break
fi
[[ $i -eq 60 ]] && err "cert-manager non ha emesso 'mongodb-tenant-tls' entro 2 minuti.
Verifica: kubectl --context '$CONTEXT' -n '$NAMESPACE' get certificate,certificaterequest"
sleep 2
done
render "$TEMPLATES_DIR/04-mongodb.yaml" | kc_cluster apply -f -
log " Attendo che i 3 membri ($POD0, $POD1, $POD2) siano Ready (timeout 10 min — il provisioning PVC+attach può essere lento)"
# kubectl wait sfrutta direttamente la readiness probe dei pod. Dieci minuti
# coprono provisioning PVC + attach + boot Mongo anche su CSI lenti (vSphere,
# Scaleway block storage in cold start, ecc.).
# IMPORTANTE: aspettiamo TUTTI e 3 i pod, non solo pod-0. Il record DNS per-pod
# del service headless (es. mongodb-tenant-1.<headless>) viene pubblicato solo
# quando il pod è Ready: lanciare rs.initiate con il solo pod-0 pronto fa fallire
# il quorum check con "Could not find address for mongodb-tenant-1...". Questo
# accade quando pod-0 parte veloce e 1/2 sono ancora in provisioning PVC.
if ! kc wait "pod/$POD0" "pod/$POD1" "pod/$POD2" --for=condition=Ready --timeout=600s 2>&1; then
err "Uno dei pod mongodb-tenant-{0,1,2} non è Ready dopo 10 minuti. Diagnostica:
kubectl --context '$CONTEXT' -n '$NAMESPACE' get pods
kubectl --context '$CONTEXT' -n '$NAMESPACE' describe pod $POD0
kubectl --context '$CONTEXT' -n '$NAMESPACE' logs $POD0 -c mongodb"
fi
ok "I 3 membri del replica set sono Ready"
# -----------------------------------------------------------------------------
# Init replica set + utenti
# -----------------------------------------------------------------------------
log "9/11 Inizializzo il replica set $RS_NAME"
set +e
RS_OK=$(echo "try { print('@@RES@@' + rs.status().ok) } catch(e) { print('@@RES@@0') }" | mongo_get)
set -e
if [[ "$RS_OK" != "1" ]]; then
log " rs.initiate con 3 membri (fresh install, tutti i PVC sono vuoti)"
mongo_eval <<EOF
rs.initiate({
_id: '${RS_NAME}',
members: [
{ _id: 0, host: '${HOST0}' },
{ _id: 1, host: '${HOST1}' },
{ _id: 2, host: '${HOST2}' }
]
})
EOF
log " Attendo PRIMARY"
# 5 min: l'elezione di solito è < 15s, ma su CSI lenti il primo heartbeat tra
# i membri può ritardare; preferiamo abbondare al primo init.
for i in $(seq 1 150); do
STATE=$(echo "try { print('@@RES@@' + rs.status().members.find(m=>m.self).stateStr) } catch(e) { print('@@RES@@') }" | mongo_get)
[[ "$STATE" == *"PRIMARY"* ]] && { ok "$POD0 è PRIMARY"; break; }
[[ $i -eq 150 ]] && err "Nessun PRIMARY eletto entro 5 minuti.
Diagnostica: kc exec $POD0 -c mongodb -- mongosh --quiet \\
'mongodb://127.0.0.1:27017/?directConnection=true&tls=true&tlsCAFile=/etc/mongo-tls/ca.crt&tlsAllowInvalidHostnames=true' \\
--eval 'rs.status()'"
sleep 2
done
else
ok "Replica set $RS_NAME già inizializzato"
fi
log " Verifico/creo utente root '$ROOT_USER' (localhost exception)"
HAS_ROOT=$(echo "try { print('@@RES@@' + db.getSiblingDB('admin').system.users.countDocuments({user:'${ROOT_USER}'})) } catch(e) { print('@@RES@@0') }" | mongo_get || echo 0)
if [[ "$HAS_ROOT" != "1" ]]; then
mongo_eval <<EOF
db.getSiblingDB('admin').createUser({
user: '${ROOT_USER}',
pwd: '${ROOT_PASS}',
roles: [ { role: 'root', db: 'admin' } ]
})
EOF
ok "Utente root creato"
else
# Re-run protection a inizio script dovrebbe averlo già intercettato; se siamo qui
# il Secret è stato cancellato a mano lasciando il DB vivo: stato incoerente.
err "Utente root '$ROOT_USER' già presente in MongoDB ma il Secret era stato (ri)generato.
Stato incoerente: la nuova password non combacia con quella in DB.
Recovery: ripristina il Secret 'mongodb-tenant-auth' originale, oppure cancella il
namespace e rifai il deploy da zero."
fi
log " Verifico/creo utente applicativo '$APP_USER'"
HAS_APP=$(echo "print('@@RES@@' + db.getSiblingDB('admin').system.users.countDocuments({user:'${APP_USER}'}))" | mongo_get_root || echo 0)
if [[ "$HAS_APP" != "1" ]]; then
mongo_eval_root <<EOF
db.getSiblingDB('admin').createUser({
user: '${APP_USER}',
pwd: '${APP_PASS}',
roles: [ { role: 'readWrite', db: '${DB_NAME}' } ]
})
EOF
ok "Utente app creato"
else
err "Utente app '$APP_USER' già presente in MongoDB ma il Secret era stato (ri)generato.
Stato incoerente — vedi messaggio precedente."
fi
log " Attendo che i 3 membri siano PRIMARY/SECONDARY"
for i in $(seq 1 120); do
HEALTHY=$(echo "print('@@RES@@' + rs.status().members.filter(m=>m.stateStr=='PRIMARY'||m.stateStr=='SECONDARY').length)" | mongo_get_root || echo 0)
if [[ "$HEALTHY" == "3" ]]; then
ok "Replica set sano: 1 PRIMARY + 2 SECONDARY"
break
fi
echo " membri sani: ${HEALTHY}/3 — attendo..."
[[ $i -eq 120 ]] && err "Replica set non sano dopo 10 min"
sleep 5
done
# -----------------------------------------------------------------------------
# App workloads
# -----------------------------------------------------------------------------
log "10/11 tenant-api (Service + Deployment + PDB)"
render "$TEMPLATES_DIR/05-tenant-api.yaml" | kc_cluster apply -f -
kc rollout status deployment/tenant-api --timeout=300s
log " tenant-admin (Service + Deployment)"
render "$TEMPLATES_DIR/06-tenant-admin.yaml" | kc_cluster apply -f -
kc rollout status deployment/tenant-admin --timeout=300s
log "11/11 Ingress"
render "$TEMPLATES_DIR/07-ingress.yaml" | kc_cluster apply -f -
# -----------------------------------------------------------------------------
# Salvataggio credenziali
# -----------------------------------------------------------------------------
log "Salvo le credenziali in $CREDS_FILE"
umask 077
cat > "$CREDS_FILE" <<EOF
# Credenziali generate da deploy.sh il $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Context: $CONTEXT
# Namespace: $NAMESPACE
#
# ATTENZIONE: salvale in un password manager e CANCELLA questo file.
# Non committarlo da nessuna parte.
MASTER_ENCRYPTION_KEY=$MASTER_ENCRYPTION_KEY
MONGO_ROOT_USER=$ROOT_USER
MONGO_ROOT_PASSWORD=$ROOT_PASS
MONGO_APP_USER=$APP_USER
MONGO_APP_PASSWORD=$APP_PASS
MONGO_APP_DATABASE=$DB_NAME
MONGODB_URI=$MONGODB_URI
REGISTRY_SERVER=$REGISTRY_SERVER
REGISTRY_USERNAME=$REGISTRY_USERNAME
# Password registry: non salvata in chiaro (inserita a runtime).
EOF
chmod 600 "$CREDS_FILE"
ok "Credenziali scritte (chmod 600)"
# -----------------------------------------------------------------------------
# Summary finale
# -----------------------------------------------------------------------------
echo
echo -e "${GREEN}===================== DEPLOY COMPLETATO =====================${NC}"
echo
kc get pods
echo
echo "Endpoint pubblici (dopo che cert-manager avrà emesso i certificati):"
echo " API → https://${DOMAIN_API}"
echo " Admin → https://${DOMAIN_ADMIN}"
echo
echo "Verifiche:"
echo " kc get all"
echo " kc logs deploy/tenant-api --tail=100 -f"
echo " # Per rs.status() recupera la root password da $CREDS_FILE:"
echo " kc exec $POD0 -c mongodb -- mongosh --quiet \\"
echo " 'mongodb://127.0.0.1:27017/?directConnection=true&tls=true&tlsCAFile=/etc/mongo-tls/ca.crt&tlsAllowInvalidHostnames=true' \\"
echo " -u '$ROOT_USER' -p \"\$MONGO_ROOT_PASSWORD\" --authenticationDatabase admin \\"
echo " --eval 'rs.status()'"
echo
warn "Credenziali in $CREDS_FILE (password in chiaro, chmod 600)."
warn "Trasferiscile in un password manager e CANCELLA il file."