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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.DS_Store
|
||||||
|
.claude/
|
||||||
|
.deploy-credentials.txt
|
||||||
@@ -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: <a tua scelta, es. loginmaster-tenant-acme>
|
||||||
|
│
|
||||||
|
├── 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 <ns>` 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 <ns> rollout history deployment/tenant-api
|
||||||
|
kubectl -n <ns> rollout undo deployment/tenant-api
|
||||||
|
kubectl -n <ns> rollout undo deployment/tenant-api --to-revision=N
|
||||||
|
```
|
||||||
|
|
||||||
|
## Operazioni comuni
|
||||||
|
|
||||||
|
### Verificare lo stato
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n <ns> get pods,ingress,certificate
|
||||||
|
kubectl -n <ns> logs deploy/tenant-api --tail=100 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stato replica set Mongo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ROOT_PASS=$(kubectl -n <ns> get secret mongodb-tenant-auth -o jsonpath='{.data.root-password}' | base64 -d)
|
||||||
|
kubectl -n <ns> 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 <ns> 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 <ns>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **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 <name>`) 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 <ns> get certificate
|
||||||
|
kubectl -n <ns> 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 <ns> describe certificate tenant-tls
|
||||||
|
kubectl -n <ns> 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 <ns> 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 <ns> describe pvc mongodb-data-mongodb-tenant-0
|
||||||
|
```
|
||||||
@@ -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 <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."
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: ${NAMESPACE}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# TLS interno MongoDB (cert-manager).
|
||||||
|
# Catena: Issuer self-signed → CA cert → Issuer CA → cert membri replica set.
|
||||||
|
# Il Secret 'mongodb-tenant-tls' (creato da cert-manager) contiene tls.crt, tls.key, ca.crt.
|
||||||
|
#
|
||||||
|
# Note operative:
|
||||||
|
# - mongod usa keyFile per cluster auth e TLS solo per il transport (non x.509).
|
||||||
|
# - Il rinnovo del cert membri (renewBefore: 30d, duration: 1y) NON riavvia
|
||||||
|
# automaticamente i pod mongo. Quando cert-manager riemette il Secret va
|
||||||
|
# fatto manualmente: kubectl rollout restart statefulset/mongodb-tenant.
|
||||||
|
# - SAN coprono: nome service headless, FQDN brevi e completi dei 3 pod.
|
||||||
|
# Per connessioni da deploy.sh via 127.0.0.1 si usa --tlsAllowInvalidHostnames
|
||||||
|
# (la CA è validata, solo l'hostname check è skippato).
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant-selfsigned
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
selfSigned: {}
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant-ca
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
isCA: true
|
||||||
|
commonName: mongodb-tenant-ca
|
||||||
|
duration: 87600h # 10 anni (CA interna)
|
||||||
|
renewBefore: 720h # 30 giorni
|
||||||
|
secretName: mongodb-tenant-ca
|
||||||
|
privateKey:
|
||||||
|
algorithm: ECDSA
|
||||||
|
size: 256
|
||||||
|
issuerRef:
|
||||||
|
name: mongodb-tenant-selfsigned
|
||||||
|
kind: Issuer
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant-ca
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
ca:
|
||||||
|
secretName: mongodb-tenant-ca
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant-tls
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
secretName: mongodb-tenant-tls
|
||||||
|
duration: 8760h # 1 anno
|
||||||
|
renewBefore: 720h # 30 giorni
|
||||||
|
privateKey:
|
||||||
|
algorithm: ECDSA
|
||||||
|
size: 256
|
||||||
|
issuerRef:
|
||||||
|
name: mongodb-tenant-ca
|
||||||
|
kind: Issuer
|
||||||
|
dnsNames:
|
||||||
|
- mongodb-tenant-headless
|
||||||
|
- mongodb-tenant-headless.${NAMESPACE}.svc
|
||||||
|
- mongodb-tenant-headless.${NAMESPACE}.svc.cluster.local
|
||||||
|
- mongodb-tenant-0
|
||||||
|
- mongodb-tenant-1
|
||||||
|
- mongodb-tenant-2
|
||||||
|
- mongodb-tenant-0.mongodb-tenant-headless
|
||||||
|
- mongodb-tenant-1.mongodb-tenant-headless
|
||||||
|
- mongodb-tenant-2.mongodb-tenant-headless
|
||||||
|
- mongodb-tenant-0.mongodb-tenant-headless.${NAMESPACE}.svc.cluster.local
|
||||||
|
- mongodb-tenant-1.mongodb-tenant-headless.${NAMESPACE}.svc.cluster.local
|
||||||
|
- mongodb-tenant-2.mongodb-tenant-headless.${NAMESPACE}.svc.cluster.local
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# NetworkPolicy: isolamento del namespace tenant.
|
||||||
|
# - mongodb-tenant: ingress solo dai pod tenant-api e dagli altri membri del replica set.
|
||||||
|
# - tenant-api: ingress solo dall'ingress controller (namespace 'ingress-nginx').
|
||||||
|
# - tenant-admin: ingress solo dall'ingress controller.
|
||||||
|
# Egress: non vincolato (DNS, registry, SMTP, LOGINMASTER_API_URL devono restare raggiungibili).
|
||||||
|
#
|
||||||
|
# Nota: la namespace label 'kubernetes.io/metadata.name' è automatica dal K8s 1.22+.
|
||||||
|
# Se l'ingress controller è installato in un namespace diverso, modificare il selector.
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: mongodb-tenant
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: mongodb-tenant
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: tenant-api
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 27017
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: tenant-api
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: tenant-api
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: ingress-nginx
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3000
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: tenant-admin
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: tenant-admin
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: ingress-nginx
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: tenant-api-config
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
data:
|
||||||
|
NODE_ENV: "production"
|
||||||
|
PORT: "3000"
|
||||||
|
# MONGODB_URI è nel Secret mongodb-tenant-auth (contiene credenziali)
|
||||||
|
LOGINMASTER_API_URL: "${LOGINMASTER_API_URL}"
|
||||||
|
ADMIN_ALLOWED_ORIGIN: "${ADMIN_ALLOWED_ORIGIN}"
|
||||||
|
TENANT_ADMIN_URL: "${TENANT_ADMIN_URL}"
|
||||||
|
# CORS_ORIGIN e MANAGE_ALLOWED_ORIGIN sono alias di TENANT_ADMIN_URL: nomi
|
||||||
|
# storici letti dal codice API. Tenuti per compat — se l'API ne legge solo
|
||||||
|
# uno è innocuo, ma è preferibile che il codice legga TENANT_ADMIN_URL.
|
||||||
|
CORS_ORIGIN: "${TENANT_ADMIN_URL}"
|
||||||
|
MANAGE_ALLOWED_ORIGIN: "${TENANT_ADMIN_URL}"
|
||||||
|
JWT_ACCESS_EXPIRY: "15m"
|
||||||
|
JWT_REFRESH_EXPIRY: "7d"
|
||||||
|
LOG_LEVEL: "info"
|
||||||
|
SMTP_HOST: "${SMTP_HOST}"
|
||||||
|
SMTP_PORT: "${SMTP_PORT}"
|
||||||
|
SMTP_SECURE: "${SMTP_SECURE}"
|
||||||
|
SMTP_USER: "${SMTP_USER}"
|
||||||
|
EMAIL_FROM: "${EMAIL_FROM}"
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant-headless
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: mongodb-tenant
|
||||||
|
ports:
|
||||||
|
- port: 27017
|
||||||
|
targetPort: 27017
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: mongodb-tenant
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
spec:
|
||||||
|
serviceName: mongodb-tenant-headless
|
||||||
|
replicas: 3
|
||||||
|
podManagementPolicy: Parallel
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: mongodb-tenant
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mongodb-tenant
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: registry-codebaker
|
||||||
|
securityContext:
|
||||||
|
# fsGroup garantisce ownership corretta sul PVC (mongo data) per uid 999.
|
||||||
|
# runAsUser/Group: il container principale gira come mongodb (uid 999),
|
||||||
|
# non root. L'init invece deve essere root per poter chmod 400 + chown.
|
||||||
|
runAsUser: 999
|
||||||
|
runAsGroup: 999
|
||||||
|
fsGroup: 999
|
||||||
|
initContainers:
|
||||||
|
# Prepara keyfile e PEM nel tmpfs /run/mongo con owner=mongodb mode=0400.
|
||||||
|
# Mongod richiede esattamente 0400/0600 owned-by-self e quel mode non si
|
||||||
|
# può ottenere via secret defaultMode senza far girare il container come
|
||||||
|
# root. Quindi: init come root, main come 999.
|
||||||
|
- name: init-tls
|
||||||
|
image: mongo:6.0
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
runAsGroup: 0
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- >-
|
||||||
|
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).
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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."
|
||||||
Reference in New Issue
Block a user