468d4562c7
Toolkit per deployare/aggiornare un tenant LoginMaster su qualsiasi Kubernetes (EKS/AKS/DOKS/Scaleway/vSphere/...). Contiene: - deploy.sh: bootstrap di un nuovo tenant (idempotente, re-run protection, storage class auto-rilevata, prompt separati api/admin tag, generazione segreti crittografici via openssl rand). - update.sh: rolling update zero-downtime con tag api/admin separati, rollback hint via 'kubectl rollout undo', riapplicazione opzionale del ConfigMap. - templates/: 8 manifest parametrici (envsubst): namespace, cert-manager TLS Mongo, NetworkPolicy intra-namespace, ConfigMap, MongoDB StatefulSet 3 repliche con TLS interno + initContainer per keyfile/PEM, tenant-api Deployment 2 repliche con CA validation, tenant-admin, ingress nginx + Let's Encrypt. Sicurezza: TLS interno Mongo (cert-manager CA self-signed 10y), keyFile per auth replica set, password client mai in argv, NetworkPolicy che isola il tenant, pod Mongo non-root (uid 999) con initContainer come root per i file runtime in tmpfs.
525 lines
21 KiB
Bash
Executable File
525 lines
21 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 + --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).
|
|
MONGO_TLS_ARGS=(--tls --tlsCAFile /etc/mongo-tls/ca.crt --tlsAllowInvalidHostnames --host 127.0.0.1)
|
|
|
|
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 $POD0 sia Ready (timeout 10 min — il provisioning PVC+attach può essere lento)"
|
|
# kubectl wait sfrutta direttamente la readiness probe del pod. Dieci minuti
|
|
# coprono provisioning PVC + attach + boot Mongo anche su CSI lenti (vSphere,
|
|
# Scaleway block storage in cold start, ecc.).
|
|
if ! kc wait "pod/$POD0" --for=condition=Ready --timeout=600s 2>&1; then
|
|
err "$POD0 non è Ready dopo 10 minuti. Diagnostica:
|
|
kubectl --context '$CONTEXT' -n '$NAMESPACE' describe pod $POD0
|
|
kubectl --context '$CONTEXT' -n '$NAMESPACE' logs $POD0 -c mongodb"
|
|
fi
|
|
ok "$POD0 pronto"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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 --tls --tlsCAFile /etc/mongo-tls/ca.crt \\
|
|
--tlsAllowInvalidHostnames --host 127.0.0.1 --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 \\"
|
|
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."
|