commit 468d4562c74c797408da3b06e729d446b2902924 Author: Luca Date: Wed May 6 11:44:04 2026 +0200 Initial commit — LoginMaster tenant deployment toolkit 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63f1deb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.claude/ +.deploy-credentials.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..1048f9a --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +# LoginMaster Tenant — Deployment Tools + +Toolkit per deployare un tenant LoginMaster (api-tenant + admin-tenant + MongoDB replica set) su qualsiasi cluster Kubernetes — vSphere, AWS EKS, Azure AKS, DigitalOcean DOKS, Scaleway Kapsule, Hetzner, ecc. + +## Cosa viene deployato + +``` +namespace: +│ +├── MongoDB replica set rs0 (3 pod, StatefulSet) +│ ├── TLS server cert via cert-manager (CA interna self-signed, 10 anni) +│ ├── auth replica set via keyFile, auth client via password +│ └── PVC RWO 20Gi per nodo (storage class scelta dall'utente) +│ +├── tenant-api (Deployment, 2 repliche, rolling update zero-downtime) +│ └── connessione Mongo via TLS, valida server cert via la CA interna +│ +├── tenant-admin (Deployment, 1 replica) +│ +├── Ingress nginx con cert Let's Encrypt per i due hostname pubblici +│ +├── NetworkPolicy che isola il tenant: +│ ├── Mongo raggiungibile solo dai pod tenant-api e dai membri replica set +│ └── api/admin raggiungibili solo dall'ingress controller +│ +└── Secret per: credenziali Mongo, MASTER_ENCRYPTION_KEY, registry, SMTP password +``` + +## Prerequisiti + +### Sul cluster + +| Cosa | Note | +|---|---| +| **cert-manager** | Con `ClusterIssuer` `letsencrypt-prod` per i certificati Ingress pubblici. Gli `Issuer` interni per il TLS Mongo sono creati dallo script. | +| **ingress-nginx** | Installato in un namespace con la label `kubernetes.io/metadata.name: ingress-nginx` (default da K8s 1.22+). | +| **CNI con NetworkPolicy** | Calico, Cilium, weave, kube-router. Le `NetworkPolicy` non sono enforcement-effective senza un CNI che le implementi. | +| **Storage class default** | Auto-rilevata dallo script via annotation `storageclass.kubernetes.io/is-default-class`. Se il cluster non ne ha una marcata, lo script chiede manualmente. | + +### Sulla macchina + +``` +brew install kubectl gettext openssl +brew link --force gettext # per envsubst +``` + +### Credenziali + +- Accesso al registry `hub.codebaker.it/loginmaster-tenant/` (username + password) +- Context kubectl configurato per il cluster target + +## Deploy iniziale + +```bash +./deploy.sh +``` + +Lo script chiede 14 parametri in modo interattivo. Default sensati per Codebaker (in `[parentesi]`): + +| Prompt | Default | Note | +|---|---|---| +| Context kubectl | corrente | Quello selezionato in `~/.kube/config` | +| Namespace | `loginmaster-tenant` | Uno per tenant. Lo script aborta se esiste già | +| Tag api-tenant | `prod-1.3.8` | | +| Tag admin-tenant | `prod-1.3.7` | Le due immagini possono essere a versioni diverse | +| Hostname API | `api.tenant.example.com` | Da sostituire col proprio dominio. Deve risolvere all'ingress controller | +| Hostname admin | `admin.tenant.example.com` | Idem | +| `LOGINMASTER_API_URL` | `https://api.loginmaster.it/1.3/` | API LoginMaster centrale | +| `ADMIN_ALLOWED_ORIGIN` | `https://admin.loginmaster.it` | Origin del pannello LoginMaster globale (≠ pannello del tenant) | +| Storage class | default cluster | Auto-rilevata | +| Storage size | `20Gi` | Per ogni membro Mongo (totale: 3× questo) | +| Mongo CPU/mem | 250m–2 / 1Gi–2Gi | Aumenta su carichi reali (Mongo cache cresce con la RAM) | +| SMTP | `N` | Se sì, chiede host/port/secure/user/pass | +| Registry user/password | — / — | Credenziali fornite per accedere a `hub.codebaker.it` | +| Confirm | `N` | `Y` per procedere | + +A fine deploy lo script salva `.deploy-credentials.txt` (chmod 600) con MASTER_ENCRYPTION_KEY, password root + applicativa Mongo, URI di connessione. **Spostalo in un password manager e cancellalo dal disco.** + +### Re-run protection + +`deploy.sh` aborta se nel namespace esiste già il Secret `mongodb-tenant-auth`: rigenerarlo causerebbe la perdita del replica set (nuovo keyfile → membri non si parlano più). + +Per ridistribuire da zero: `kubectl delete namespace ` e rilancia. +Per aggiornare le immagini: usa `update.sh`. + +## Aggiornare un tenant esistente + +```bash +./update.sh # interattivo, prefilla con i tag attuali del cluster +./update.sh prod-1.3.9 # stesso tag per api e admin +./update.sh prod-1.3.9 prod-1.3.8 # tag separati +``` + +Lo script: +1. Mostra le immagini attuali e chiede conferma per il rolling update di api e/o admin. +2. Se l'update fallisce, stampa il comando `kubectl rollout undo` da copiare. +3. In coda chiede se **riapplicare il ConfigMap** dal template — utile quando una nuova versione dell'API legge env var nuove. Riapplica `templates/03-configmap.yaml` usando i valori correnti dal cluster e fa `rollout restart` di tenant-api per propagarli. + +### Rollback manuale + +`kubectl` mantiene le ultime 10 revisioni dei Deployment: + +```bash +kubectl -n rollout history deployment/tenant-api +kubectl -n rollout undo deployment/tenant-api +kubectl -n rollout undo deployment/tenant-api --to-revision=N +``` + +## Operazioni comuni + +### Verificare lo stato + +```bash +kubectl -n get pods,ingress,certificate +kubectl -n logs deploy/tenant-api --tail=100 -f +``` + +### Stato replica set Mongo + +```bash +ROOT_PASS=$(kubectl -n get secret mongodb-tenant-auth -o jsonpath='{.data.root-password}' | base64 -d) +kubectl -n exec mongodb-tenant-0 -c mongodb -- mongosh \ + --tls --tlsCAFile /etc/mongo-tls/ca.crt --tlsAllowInvalidHostnames --host 127.0.0.1 \ + -u admin -p "$ROOT_PASS" --authenticationDatabase admin \ + --eval 'rs.status()' +``` + +### Backup / restore + +Lo script non gestisce backup: usa `mongodump`/`mongorestore` con le credenziali applicative, oppure snapshot lato cloud sui PV Mongo. + +### Scale orizzontale + +```bash +kubectl -n scale deployment/tenant-api --replicas=4 +``` + +Mongo è un replica set fisso a 3 membri (PDB `minAvailable: 2` lo presuppone): non scalare a meno di non rivedere PDB e quorum. + +## Cleanup + +```bash +kubectl delete namespace +``` + +> **Attenzione**: i PV con `reclaim policy: Retain` (es. `vsphere-csi`, `sbs-default-retain`) **sopravvivono** alla cancellazione del namespace e dei PVC. Vanno rimossi a mano (`kubectl delete pv `) e i dischi sottostanti dal pannello del cloud provider per non lasciare costi residui. + +## Sicurezza + +- **TLS interno Mongo**: cert-manager emette CA self-signed (10 anni) e cert membri (1 anno). Mongo è in `requireTLS`. Niente traffico in chiaro nel namespace. +- **Auth replica set**: keyFile (HMAC condiviso). I membri non si parlano se il keyfile non combacia. +- **Auth client**: password generata, mai in argv di kubectl, mai in log audit (le `mongo_eval*` di `deploy.sh` autenticano via `db.auth()` su stdin). +- **NetworkPolicy**: blocca traffico cross-namespace e intra-namespace non autorizzato. +- **Pod Mongo non-root** (uid/gid 999) con `fsGroup` per ownership PVC. +- **Secret K8s** (base64-encoded, non encrypted at rest se non configurato a livello cluster). MASTER_ENCRYPTION_KEY, password Mongo, password registry e SMTP risiedono qui — abilita encryption-at-rest a livello etcd se la compliance lo richiede. + +### Limiti noti + +- **Rinnovo cert TLS Mongo**: cert-manager rinnova il Secret automaticamente, ma **mongod non ricarica il cert a runtime**. Quando cert-manager riemette il Secret (default 30 giorni prima della scadenza, quindi ~ogni 11 mesi sui cert membri), serve `kubectl rollout restart statefulset/mongodb-tenant`. Il PDB garantisce che resti almeno 1 PRIMARY + 1 SECONDARY → zero downtime sul replica set, ma è una azione manuale. +- **Warning `PodSecurity "restricted"`**: il pod Mongo richiede uno user-defined `runAsUser` e l'init come root, quindi triggera un warn (non enforce). Se la policy è in enforce mode, il deploy va rivisto (init come root non è compatibile con `restricted` puro). + +## Note per cloud provider specifici + +| Provider | Storage class default tipica | Reclaim default | +|---|---|---| +| AWS EKS | `gp3` (recenti) o `gp2` | `Delete` | +| Azure AKS | `default` o `managed-csi` | `Delete` | +| DigitalOcean DOKS | `do-block-storage` | `Delete` | +| Scaleway Kapsule | `sbs-default-retain`, `scw-bssd` | varia | +| GCP GKE | `standard-rwo`, `premium-rwo` | `Delete` | +| vSphere | `vsphere-csi` | `Retain` | +| Hetzner | `hcloud-volumes` | `Delete` | + +**Reclaim `Delete`** = cancellando il namespace perdi i dati Mongo. Per produzione: scegliere SC con `Retain` o avere snapshot/backup esterni. Lo script non sovrascrive il reclaim della SC. + +**Volume binding mode**: tutte le SC cloud moderne usano `WaitForFirstConsumer`, che fa partire i 3 pod Mongo correttamente in cluster multi-AZ. Le SC custom con `Immediate` su cluster multi-AZ rischiano cross-zone mount fail. + +## Struttura del repo + +``` +. +├── deploy.sh # bootstrap di un nuovo tenant (idempotente: aborta su rerun) +├── update.sh # rolling update + rollback hint + apply ConfigMap opzionale +└── templates/ + ├── 00-namespace.yaml + ├── 01-mongodb-tls.yaml # cert-manager Issuer + CA + cert membri + ├── 02-networkpolicy.yaml # isolamento intra-namespace + ├── 03-configmap.yaml # tenant-api-config + ├── 04-mongodb.yaml # StatefulSet 3 repliche, init container per keyfile/PEM tmpfs + ├── 05-tenant-api.yaml # Deployment 2 repliche + Service + PDB + ├── 06-tenant-admin.yaml # Deployment 1 replica + Service + └── 07-ingress.yaml # ingress-nginx + Let's Encrypt +``` + +## Troubleshooting + +### Pod Mongo in CrashLoopBackOff +Probabilmente cert-manager non ha emesso `mongodb-tenant-tls`. Verifica: +```bash +kubectl -n get certificate +kubectl -n describe certificate mongodb-tenant-tls +``` + +### Cert ingress `tenant-tls` resta `Ready: False` +DNS dei due hostname non risolve all'IP dell'ingress controller, oppure la HTTP-01 challenge fallisce per altri motivi: +```bash +kubectl -n describe certificate tenant-tls +kubectl -n get challenge +``` + +### Replica set non elegge un PRIMARY +Verifica connettività intra-namespace (NetworkPolicy può bloccare se la label `app: mongodb-tenant` viene cambiata) e che i 3 pod si vedano via DNS: +```bash +kubectl -n exec mongodb-tenant-0 -c mongodb -- nslookup mongodb-tenant-1.mongodb-tenant-headless +``` + +### `ImagePullBackOff` su admin/api +Tag inesistente sul registry. Verifica: +```bash +curl -u USER:PASS https://hub.codebaker.it/v2/loginmaster-tenant/admin-tenant/tags/list +``` +Storicamente `api-tenant` esce più frequentemente di `admin-tenant`: usa tag separati con `./update.sh API_TAG ADMIN_TAG`. + +### Errori PVC su provisioning +Storage class non esistente o quota cloud esaurita: +```bash +kubectl get sc +kubectl -n describe pvc mongodb-data-mongodb-tenant-0 +``` diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..7576251 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,524 @@ +#!/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 ')" "" + 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 <}${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 <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 <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" <- + cp /etc/secrets/keyfile /run/mongo/mongodb-keyfile && + cat /etc/mongo-tls/tls.crt /etc/mongo-tls/tls.key > /run/mongo/mongo-server.pem && + chown 999:999 /run/mongo/mongodb-keyfile /run/mongo/mongo-server.pem && + chmod 400 /run/mongo/mongodb-keyfile /run/mongo/mongo-server.pem + volumeMounts: + - name: mongo-runtime + mountPath: /run/mongo + - name: mongo-keyfile + mountPath: /etc/secrets + readOnly: true + - name: mongo-tls + mountPath: /etc/mongo-tls + readOnly: true + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app: mongodb-tenant + topologyKey: kubernetes.io/hostname + containers: + - name: mongodb + image: mongo:6.0 + # I file runtime (keyfile + PEM cert+key) sono già stati preparati dall' + # initContainer in /run/mongo con owner mongodb:mongodb mode 0400. + command: + - mongod + - --bind_ip_all + - --replSet + - rs0 + - --auth + - --keyFile + - /run/mongo/mongodb-keyfile + - --tlsMode + - requireTLS + - --tlsCertificateKeyFile + - /run/mongo/mongo-server.pem + - --tlsCAFile + - /etc/mongo-tls/ca.crt + # Senza questo flag, --tlsCAFile mette mongod in mTLS e rifiuta ogni + # client che non presenta un cert. I membri del replica set si autenticano + # via keyFile (clusterAuthMode keyFile, default), non x.509: quindi non + # serve mTLS. I client (API, probe, mongosh) presentano solo password. + - --tlsAllowConnectionsWithoutCertificates + ports: + - containerPort: 27017 + volumeMounts: + - name: mongodb-data + mountPath: /data/db + - name: mongo-runtime + mountPath: /run/mongo + readOnly: true + - name: mongo-tls + mountPath: /etc/mongo-tls + readOnly: true + resources: + requests: + cpu: "${MONGO_CPU_REQUEST}" + memory: "${MONGO_MEM_REQUEST}" + limits: + cpu: "${MONGO_CPU_LIMIT}" + memory: "${MONGO_MEM_LIMIT}" + # quit(N) propaga N come exit code: probe fallisce solo se ping.ok != 1. + # Connessione su 127.0.0.1 con --tlsAllowInvalidHostnames perché il cert + # non include localhost nei SAN (la CA però viene validata). + livenessProbe: + exec: + command: + - mongosh + - --quiet + - --tls + - --tlsCAFile + - /etc/mongo-tls/ca.crt + - --tlsAllowInvalidHostnames + - --host + - "127.0.0.1" + - --eval + - "quit(db.adminCommand('ping').ok===1?0:1)" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - mongosh + - --quiet + - --tls + - --tlsCAFile + - /etc/mongo-tls/ca.crt + - --tlsAllowInvalidHostnames + - --host + - "127.0.0.1" + - --eval + - "quit(db.adminCommand('ping').ok===1?0:1)" + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + volumes: + - name: mongo-runtime + emptyDir: + medium: Memory + sizeLimit: 16Mi + - name: mongo-keyfile + secret: + secretName: mongodb-tenant-auth + items: + - key: keyfile + path: keyfile + # Con fsGroup=999 il file è root:mongodb 0440 — leggibile solo dal gruppo + # mongodb (uid/gid 999), non world-readable. Il container poi lo copia in + # /data/db/ e fa chmod 400 perché mongod richiede mode 0400/0600 owner-only. + defaultMode: 0440 + - name: mongo-tls + secret: + # Secret popolato da cert-manager (vedi templates/01-mongodb-tls.yaml). + # Contiene tls.crt, tls.key, ca.crt — leggibili dal gruppo 999 via fsGroup. + secretName: mongodb-tenant-tls + defaultMode: 0440 + volumeClaimTemplates: + - metadata: + name: mongodb-data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: "${STORAGE_CLASS}" + resources: + requests: + storage: ${STORAGE_SIZE} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: mongodb-tenant-pdb + namespace: ${NAMESPACE} +spec: + # Il replica set tollera la perdita di 1 membro (quorum = 2/3). + # minAvailable: 2 evita che drain/voluntary disruption tolga il quorum. + minAvailable: 2 + selector: + matchLabels: + app: mongodb-tenant +# NB: rs.initiate e la creazione degli utenti root/app sono eseguite da +# `deploy.sh` via `kubectl exec` nel pod mongodb-tenant-0 (sfrutta la +# "localhost exception" di Mongo per creare il primo utente senza auth). diff --git a/templates/05-tenant-api.yaml b/templates/05-tenant-api.yaml new file mode 100644 index 0000000..48a702e --- /dev/null +++ b/templates/05-tenant-api.yaml @@ -0,0 +1,111 @@ +apiVersion: v1 +kind: Service +metadata: + name: tenant-api + namespace: ${NAMESPACE} +spec: + selector: + app: tenant-api + ports: + - port: 3000 + targetPort: 3000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tenant-api + namespace: ${NAMESPACE} +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: tenant-api + template: + metadata: + labels: + app: tenant-api + spec: + imagePullSecrets: + - name: registry-codebaker + containers: + - name: tenant-api + image: hub.codebaker.it/loginmaster-tenant/api-tenant:${IMAGE_TAG_API} + ports: + - containerPort: 3000 + envFrom: + - configMapRef: + name: tenant-api-config + env: + - name: MONGODB_URI + valueFrom: + secretKeyRef: + name: mongodb-tenant-auth + key: MONGODB_URI + - name: MASTER_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: tenant-api-secrets + key: MASTER_ENCRYPTION_KEY + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: tenant-api-secrets + key: SMTP_PASSWORD + volumeMounts: + - name: mongo-ca + mountPath: /etc/mongo-tls + readOnly: true + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + startupProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /health + port: 3000 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: 3000 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: mongo-ca + secret: + # Solo la ca.crt dal Secret cert-manager; il cert di servizio non serve + # all'API (non fa mTLS, valida solo il server). + secretName: mongodb-tenant-tls + items: + - key: ca.crt + path: ca.crt + defaultMode: 0444 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: tenant-api-pdb + namespace: ${NAMESPACE} +spec: + minAvailable: 1 + selector: + matchLabels: + app: tenant-api diff --git a/templates/06-tenant-admin.yaml b/templates/06-tenant-admin.yaml new file mode 100644 index 0000000..c55a550 --- /dev/null +++ b/templates/06-tenant-admin.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +kind: Service +metadata: + name: tenant-admin + namespace: ${NAMESPACE} +spec: + selector: + app: tenant-admin + ports: + - port: 80 + targetPort: 80 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tenant-admin + namespace: ${NAMESPACE} +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: tenant-admin + template: + metadata: + labels: + app: tenant-admin + spec: + imagePullSecrets: + - name: registry-codebaker + containers: + - name: tenant-admin + image: hub.codebaker.it/loginmaster-tenant/admin-tenant:${IMAGE_TAG_ADMIN} + env: + - name: VITE_APP_NAME + value: "LoginMaster Tenant Admin" + - name: VITE_API_BASE_URL + value: "https://${DOMAIN_API}" + ports: + - containerPort: 80 + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + startupProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 3 + periodSeconds: 5 + failureThreshold: 20 + readinessProbe: + httpGet: + path: / + port: 80 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: 80 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 diff --git a/templates/07-ingress.yaml b/templates/07-ingress.yaml new file mode 100644 index 0000000..2e3bb1b --- /dev/null +++ b/templates/07-ingress.yaml @@ -0,0 +1,36 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: tenant-ingress + namespace: ${NAMESPACE} + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "10m" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + tls: + - hosts: + - ${DOMAIN_API} + - ${DOMAIN_ADMIN} + secretName: tenant-tls + rules: + - host: ${DOMAIN_API} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: tenant-api + port: + number: 3000 + - host: ${DOMAIN_ADMIN} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: tenant-admin + port: + number: 80 diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..e87192a --- /dev/null +++ b/update.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# ============================================================================= +# update.sh — Aggiorna un tenant LoginMaster esistente. +# +# 1) Rolling update zero-downtime di api-tenant e/o admin-tenant ad un nuovo tag. +# 2) Opzionalmente riapplica il ConfigMap dal template (necessario se l'API ha +# iniziato a leggere nuove env var) e fa rollout restart per propagarle. +# 3) Su errore stampa il comando di rollback (kubectl rollout undo). +# +# Uso: +# ./update.sh # interattivo (chiede tutto) +# ./update.sh prod-1.3.8 # tag uguale per api+admin +# ./update.sh prod-1.3.8 prod-1.3.7 # tag api + tag admin separati +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATES_DIR="$SCRIPT_DIR/templates" + +REGISTRY="hub.codebaker.it/loginmaster-tenant" +DEF_NAMESPACE="loginmaster-tenant" + +# ----------------------------------------------------------------------------- +# 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() { + 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 +} + +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])?$ ]] +} + +kc() { kubectl --context "$CONTEXT" -n "$NAMESPACE" "$@"; } + +# ----------------------------------------------------------------------------- +# Parametri +# ----------------------------------------------------------------------------- +log "Parametri aggiornamento" + +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" + +kubectl --context "$CONTEXT" cluster-info >/dev/null 2>&1 \ + || err "Impossibile contattare il cluster sul context '$CONTEXT'" +ok "Connesso a $CONTEXT" + +ask "Namespace del tenant" "$DEF_NAMESPACE" +NAMESPACE="$REPLY" + +# Verifica che il namespace esista e abbia i deployment +kc get deployment tenant-api >/dev/null 2>&1 \ + || err "Deployment tenant-api non trovato nel namespace '$NAMESPACE'" + +# Mostra stato attuale +log "Stato attuale" +CURRENT_API=$(kc get deployment tenant-api -o jsonpath='{.spec.template.spec.containers[0].image}') +CURRENT_ADMIN=$(kc get deployment tenant-admin -o jsonpath='{.spec.template.spec.containers[0].image}') +echo " tenant-api: $CURRENT_API" +echo " tenant-admin: $CURRENT_ADMIN" +echo + +# Tag da argomenti posizionali o prompt. Permettiamo tag separati perché +# api-tenant e admin-tenant possono essere a versioni diverse. +if [[ -n "${1:-}" && -n "${2:-}" ]]; then + NEW_TAG_API="$1" + NEW_TAG_ADMIN="$2" +elif [[ -n "${1:-}" ]]; then + NEW_TAG_API="$1" + NEW_TAG_ADMIN="$1" +else + # Default = tag attuali estratti dal cluster (rimangono se l'utente conferma) + CUR_API_TAG="${CURRENT_API##*:}" + CUR_ADMIN_TAG="${CURRENT_ADMIN##*:}" + ask "Nuovo tag api-tenant" "$CUR_API_TAG" + NEW_TAG_API="$REPLY" + ask "Nuovo tag admin-tenant" "$CUR_ADMIN_TAG" + NEW_TAG_ADMIN="$REPLY" +fi +[[ -z "$NEW_TAG_API" ]] && err "Tag api-tenant obbligatorio" +[[ -z "$NEW_TAG_ADMIN" ]] && err "Tag admin-tenant obbligatorio" + +NEW_API="${REGISTRY}/api-tenant:${NEW_TAG_API}" +NEW_ADMIN="${REGISTRY}/admin-tenant:${NEW_TAG_ADMIN}" + +# Scelta componenti +UPDATE_API=true +UPDATE_ADMIN=true +if ! confirm "Aggiornare tenant-api a ${NEW_API}?" "Y"; then + UPDATE_API=false +fi +if ! confirm "Aggiornare tenant-admin a ${NEW_ADMIN}?" "Y"; then + UPDATE_ADMIN=false +fi + +$UPDATE_API || $UPDATE_ADMIN || { echo "Nessun aggiornamento selezionato."; exit 0; } + +# Riepilogo +log "Riepilogo aggiornamento" +echo " Context: $CONTEXT" +echo " Namespace: $NAMESPACE" +$UPDATE_API && echo " tenant-api: $CURRENT_API --> $NEW_API" +$UPDATE_ADMIN && echo " tenant-admin: $CURRENT_ADMIN --> $NEW_ADMIN" +echo +confirm "Procedere?" "N" || { echo "Annullato."; exit 0; } + +# ----------------------------------------------------------------------------- +# Aggiornamento +# ----------------------------------------------------------------------------- + +# Suggerisce il rollback se lo script fallisce a metà strada (rollout fail, kubectl +# error, etc.). Viene disarmato solo dopo il rollout completato con successo. +print_rollback_hint() { + echo + warn "Aggiornamento interrotto. Per riportare i Deployment alla revisione precedente:" + $UPDATE_API && echo " kubectl --context '$CONTEXT' -n '$NAMESPACE' rollout undo deployment/tenant-api" + $UPDATE_ADMIN && echo " kubectl --context '$CONTEXT' -n '$NAMESPACE' rollout undo deployment/tenant-admin" + echo " (kubectl rollout undo usa la revision history del Deployment, non richiede il tag precedente)" +} +trap print_rollback_hint ERR + +if $UPDATE_API; then + log "Aggiorno tenant-api" + kc set image deployment/tenant-api tenant-api="$NEW_API" + kc rollout status deployment/tenant-api --timeout=300s + ok "tenant-api aggiornato a $NEW_TAG_API" +fi + +if $UPDATE_ADMIN; then + log "Aggiorno tenant-admin" + kc set image deployment/tenant-admin tenant-admin="$NEW_ADMIN" + kc rollout status deployment/tenant-admin --timeout=300s + ok "tenant-admin aggiornato a $NEW_TAG_ADMIN" +fi + +trap - ERR + +# ----------------------------------------------------------------------------- +# ConfigMap (opzionale — per propagare nuove env var introdotte dal template) +# ----------------------------------------------------------------------------- +# Le env var iniettate via envFrom configMapRef NON si aggiornano automaticamente +# quando il ConfigMap cambia: i pod vanno riavviati. Questo blocco riapplica il +# template usando i valori CORRENTI dal cluster (preserva le scelte fatte al +# deploy iniziale o tramite kubectl edit) e fa rollout restart. +if confirm "Riapplicare anche il ConfigMap dal template (necessario se l'API legge nuove env var)?" "N"; then + command -v envsubst >/dev/null || err "envsubst non trovato (brew install gettext && brew link gettext --force)" + + log "Leggo i valori correnti dal ConfigMap tenant-api-config" + cm_get() { kc get configmap tenant-api-config -o jsonpath="{.data.$1}" 2>/dev/null || true; } + + NAMESPACE="$NAMESPACE" \ + LOGINMASTER_API_URL=$(cm_get LOGINMASTER_API_URL) \ + ADMIN_ALLOWED_ORIGIN=$(cm_get ADMIN_ALLOWED_ORIGIN) \ + TENANT_ADMIN_URL=$(cm_get TENANT_ADMIN_URL) \ + SMTP_HOST=$(cm_get SMTP_HOST) \ + SMTP_PORT=$(cm_get SMTP_PORT) \ + SMTP_SECURE=$(cm_get SMTP_SECURE) \ + SMTP_USER=$(cm_get SMTP_USER) \ + EMAIL_FROM=$(cm_get EMAIL_FROM) \ + envsubst < "$TEMPLATES_DIR/03-configmap.yaml" \ + | kc apply -f - + ok "ConfigMap riapplicato dal template" + + log "Restart deployment/tenant-api (necessario per propagare env var)" + kc rollout restart deployment/tenant-api + kc rollout status deployment/tenant-api --timeout=300s + ok "tenant-api riavviato con il nuovo ConfigMap" +fi + +# ----------------------------------------------------------------------------- +# Verifica finale +# ----------------------------------------------------------------------------- +log "Verifica finale" +echo +kc get pods -l 'app in (tenant-api, tenant-admin)' +echo +echo "Immagini attuali:" +echo " tenant-api: $(kc get deployment tenant-api -o jsonpath='{.spec.template.spec.containers[0].image}')" +echo " tenant-admin: $(kc get deployment tenant-admin -o jsonpath='{.spec.template.spec.containers[0].image}')" +echo +echo "Per fare rollback alla revisione precedente:" +$UPDATE_API && echo " kubectl --context '$CONTEXT' -n '$NAMESPACE' rollout undo deployment/tenant-api" +$UPDATE_ADMIN && echo " kubectl --context '$CONTEXT' -n '$NAMESPACE' rollout undo deployment/tenant-admin" +echo +ok "Aggiornamento completato."