chore: update
ci-api / test (push) Successful in 23m31s
ci-web / test (push) Successful in 14m33s

This commit is contained in:
Xor290
2026-08-03 12:11:14 +02:00
parent e2802aeb59
commit a9811fbb10
7 changed files with 1637 additions and 46 deletions
@@ -104,12 +104,29 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
return fmt.Errorf("création namespace %s: %w", d.Namespace, err)
}
// Isolation réseau AVANT tout workload : deny-by-default, aucune
// communication inter-démos, ingress limité au Traefik partagé (voir
// deploy/chart-gestion/network-policy). Sans ça, tout pod d'une démo
// peut atteindre le réseau de n'importe quelle autre démo.
if err := h.installChart(d.Namespace, "network-policy", h.buildNetworkPolicyValues()); err != nil {
h.deleteNamespace(d.Namespace)
return fmt.Errorf("déploiement network-policy: %w", err)
}
// Mot de passe postgres/redis généré par démo (jamais réutilisé d'une
// démo à l'autre) : avec des identifiants partagés, un gap réseau
// (CNI, règle manquante) donnerait un accès direct aux données de
// n'importe quelle autre démo. Le NetworkPolicy ci-dessus est la
// première ligne de défense, ce mot de passe unique la seconde.
pgPassword := randomSecret()
redisPassword := randomSecret()
// PVC dès la création de la démo (pas d'emptyDir) : si le client ne
// souscrit pas, le teardown du namespace (TTL ou suppression admin)
// supprime le PVC avec le reste. S'il souscrit, TransferToPaid n'a rien
// d'autre à faire côté stockage — les données sont déjà en place.
postgresValues := h.buildPostgresValues(d)
redisValues := h.buildRedisValues(d)
postgresValues := h.buildPostgresValues(d, pgPassword)
redisValues := h.buildRedisValues(d, redisPassword)
// Secret partagé backend <-> lbtelegram (authentifie les appels de
// lbtelegram vers backend) — généré une seule fois si le load-balancer
@@ -119,7 +136,7 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
backendLinkSecret = randomSecret()
}
backendValues := h.buildBackendValues(d, resources, cfg, backendLinkSecret)
backendValues := h.buildBackendValues(d, resources, cfg, backendLinkSecret, pgPassword, redisPassword)
frontendValues := h.buildFrontendValues(d)
ingressValues := h.buildIngressRouteValues(d)
@@ -157,7 +174,7 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
// Load-balancer Telegram : optionnel, seulement si l'admin a renseigné
// au moins un bot.
if cfg.LBTelegramEnabled() {
lbValues := h.buildLBTelegramValues(d, cfg, backendLinkSecret)
lbValues := h.buildLBTelegramValues(d, cfg, backendLinkSecret, pgPassword, redisPassword)
if err := h.installChart(d.Namespace, "lbtelegram", lbValues); err != nil {
h.deleteNamespace(d.Namespace)
return fmt.Errorf("déploiement lbtelegram: %w", err)
@@ -174,7 +191,7 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
return nil
}
if err := h.createGestionAdmin(context.Background(), d.Namespace, cfg.AdminUsername, cfg.AdminPassword); err != nil {
if err := h.createGestionAdmin(context.Background(), d.Namespace, cfg.AdminUsername, cfg.AdminPassword, pgPassword); err != nil {
log.Printf("Warning: création du compte admin échouée pour %s: %v", d.Namespace, err)
}
@@ -244,19 +261,38 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
}
lbValues, lbEnabled := h.getReleaseValuesOptional(oldNamespace, oldNamespace+"-lbtelegram")
// Mot de passe postgres actuel (pour le pg_dump) — jamais persisté nulle
// part, relu directement depuis la release Helm en place.
oldPostgresValues, err := h.getReleaseValues(oldNamespace, oldNamespace+"-postgresql")
if err != nil {
return fmt.Errorf("lecture config postgresql existante: %w", err)
}
oldPgPassword := asStringMap(oldPostgresValues["auth"])["password"]
if err := h.createNamespace(newNamespace); err != nil {
return fmt.Errorf("création namespace %s: %w", newNamespace, err)
}
// Isolation réseau AVANT tout workload, comme pour Provision.
if err := h.installChart(newNamespace, "network-policy", h.buildNetworkPolicyValues()); err != nil {
h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement network-policy: %w", err)
}
newDemo := d
newDemo.Namespace = newNamespace
newDemo.URL = newURL
if err := h.installChart(newNamespace, "postgresql", h.buildPostgresValues(newDemo)); err != nil {
// Nouveau mot de passe postgres/redis pour le nouveau namespace (pas de
// réutilisation de l'ancien) : la donnée est migrée, pas l'identifiant.
pgPassword := randomSecret()
redisPassword := randomSecret()
if err := h.installChart(newNamespace, "postgresql", h.buildPostgresValues(newDemo, pgPassword)); err != nil {
h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement postgresql: %w", err)
}
if err := h.installChart(newNamespace, "redis", h.buildRedisValues(newDemo)); err != nil {
if err := h.installChart(newNamespace, "redis", h.buildRedisValues(newDemo, redisPassword)); err != nil {
h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement redis: %w", err)
}
@@ -265,7 +301,7 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
// vide). Redis (cache/sessions) n'est pas migré, reconstruit
// naturellement — même convention que l'historique passage en
// stockage persistant.
if err := h.migratePostgresData(ctx, oldNamespace, newNamespace); err != nil {
if err := h.migratePostgresData(ctx, oldNamespace, newNamespace, oldPgPassword, pgPassword); err != nil {
h.deleteNamespace(newNamespace)
return fmt.Errorf("migration données postgres: %w", err)
}
@@ -278,7 +314,7 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
}
}
backendPatched := h.patchBackendValuesForNamespace(backendValues, newDemo, lbEnabled, backendLinkSecret)
backendPatched := h.patchBackendValuesForNamespace(backendValues, newDemo, lbEnabled, backendLinkSecret, pgPassword, redisPassword)
if err := h.installChart(newNamespace, "backend", backendPatched); err != nil {
h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement backend: %w", err)
@@ -292,7 +328,7 @@ func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL
return fmt.Errorf("déploiement ingressroute: %w", err)
}
if lbEnabled {
lbPatched := h.patchLBTelegramValuesForNamespace(lbValues, newDemo, backendLinkSecret)
lbPatched := h.patchLBTelegramValuesForNamespace(lbValues, newDemo, backendLinkSecret, pgPassword, redisPassword)
if err := h.installChart(newNamespace, "lbtelegram", lbPatched); err != nil {
h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement lbtelegram: %w", err)
@@ -391,12 +427,14 @@ func (h *HelmProvisioner) restartDeployment(ctx context.Context, namespace, name
// migratePostgresData copie les données postgres d'un namespace à l'autre
// (pg_dump / psql restore), utilisé par MigrateToPremiumNamespace.
func (h *HelmProvisioner) migratePostgresData(ctx context.Context, oldNamespace, newNamespace string) error {
// oldPassword/newPassword : mots de passe respectifs de chaque instance
// postgres (uniques par démo, jamais réutilisés — voir Provision).
func (h *HelmProvisioner) migratePostgresData(ctx context.Context, oldNamespace, newNamespace, oldPassword, newPassword string) error {
oldPod, err := h.findPod(ctx, oldNamespace, "postgresql")
if err != nil {
return fmt.Errorf("pod postgresql source introuvable: %w", err)
}
dumpCmd := fmt.Sprintf("PGPASSWORD=%s pg_dump -h localhost -U %s %s", demoDBPass, demoDBUser, demoDBName)
dumpCmd := fmt.Sprintf("PGPASSWORD=%s pg_dump -h localhost -U %s %s", oldPassword, demoDBUser, demoDBName)
dump, stderr, err := h.execInPod(ctx, oldNamespace, oldPod, "postgresql", []string{"sh", "-c", dumpCmd}, nil)
if err != nil {
return fmt.Errorf("pg_dump: %w (%s)", err, stderr)
@@ -409,7 +447,7 @@ func (h *HelmProvisioner) migratePostgresData(ctx context.Context, oldNamespace,
if err != nil {
return fmt.Errorf("pod postgresql cible introuvable: %w", err)
}
restoreCmd := fmt.Sprintf("PGPASSWORD=%s psql -h localhost -U %s %s", demoDBPass, demoDBUser, demoDBName)
restoreCmd := fmt.Sprintf("PGPASSWORD=%s psql -h localhost -U %s %s", newPassword, demoDBUser, demoDBName)
if _, stderr, err := h.execInPod(ctx, newNamespace, newPod, "postgresql", []string{"sh", "-c", restoreCmd}, strings.NewReader(dump)); err != nil {
return fmt.Errorf("restore pg_dump: %w (%s)", err, stderr)
}
@@ -418,14 +456,19 @@ func (h *HelmProvisioner) migratePostgresData(ctx context.Context, oldNamespace,
// patchBackendValuesForNamespace réutilise les valeurs Helm existantes du
// backend (image, secrets, réglages métier...) en ne recalculant que ce qui
// référence le nom du namespace : DNS internes postgres/redis/lbtelegram et
// URL du webhook Telegram (dépend de l'URL publique de la démo).
func (h *HelmProvisioner) patchBackendValuesForNamespace(values map[string]interface{}, newDemo Demo, lbEnabled bool, backendLinkSecret string) map[string]interface{} {
// référence le nom du namespace : DNS internes postgres/redis/lbtelegram,
// URL du webhook Telegram, et les nouveaux mots de passe postgres/redis
// (uniques au nouveau namespace, voir MigrateToPremiumNamespace).
func (h *HelmProvisioner) patchBackendValuesForNamespace(values map[string]interface{}, newDemo Demo, lbEnabled bool, backendLinkSecret, pgPassword, redisPassword string) map[string]interface{} {
env := asStringMap(values["env"])
secrets := asStringMap(values["secrets"])
env["DB_HOST"] = fmt.Sprintf("%s-postgresql-postgresql", newDemo.Namespace)
env["REDIS_HOST"] = fmt.Sprintf("%s-redis-redis", newDemo.Namespace)
delete(env, "DB_PASSWORD") // désormais dans secrets, voir buildBackendValues
delete(env, "REDIS_PASSWORD") // idem
secrets["DB_PASSWORD"] = pgPassword
secrets["REDIS_PASSWORD"] = redisPassword
if lbEnabled {
env["LBTELEGRAM_URL"] = fmt.Sprintf("http://%s-lbtelegram-lbtelegram.%s.svc.cluster.local:8081", newDemo.Namespace, newDemo.Namespace)
secrets["BACKEND_LINK_SECRET"] = backendLinkSecret
@@ -442,8 +485,8 @@ func (h *HelmProvisioner) patchBackendValuesForNamespace(values map[string]inter
// patchLBTelegramValuesForNamespace réutilise les valeurs Helm existantes
// du chart lbtelegram (bots, stratégie, tokens...) en ne recalculant que ce
// qui référence le nom du namespace : host public, GATEWAY_URL, DSN
// postgres/redis et URL interne du backend.
func (h *HelmProvisioner) patchLBTelegramValuesForNamespace(values map[string]interface{}, newDemo Demo, backendLinkSecret string) map[string]interface{} {
// postgres/redis (avec les nouveaux mots de passe) et URL interne du backend.
func (h *HelmProvisioner) patchLBTelegramValuesForNamespace(values map[string]interface{}, newDemo Demo, backendLinkSecret, pgPassword, redisPassword string) map[string]interface{} {
env := asStringMap(values["env"])
secrets := asStringMap(values["secrets"])
@@ -452,8 +495,8 @@ func (h *HelmProvisioner) patchLBTelegramValuesForNamespace(values map[string]in
env["BACKEND_LINK_URL"] = fmt.Sprintf("http://%s-backend-gestion-backend.%s.svc.cluster.local:8080", newDemo.Namespace, newDemo.Namespace)
secrets["BACKEND_LINK_SECRET"] = backendLinkSecret
secrets["DATABASE_URL"] = fmt.Sprintf("postgres://postgres:demo-postgres-pass@%s-postgresql-postgresql:5432/demo_db?sslmode=disable", newDemo.Namespace)
secrets["REDIS_URL"] = fmt.Sprintf("redis://:demo-redis-pass@%s-redis-redis:6379/0", newDemo.Namespace)
secrets["DATABASE_URL"] = fmt.Sprintf("postgres://postgres:%s@%s-postgresql-postgresql:5432/demo_db?sslmode=disable", pgPassword, newDemo.Namespace)
secrets["REDIS_URL"] = fmt.Sprintf("redis://:%s@%s-redis-redis:6379/0", redisPassword, newDemo.Namespace)
values["host"] = host
values["env"] = env
@@ -509,7 +552,7 @@ func asStringMap(v interface{}) map[string]string {
// un compte admin, par design de l'app). Mot de passe haché en bcrypt
// (golang.org/x/crypto/bcrypt), comme le fait l'app elle-même
// (bcrypt.CompareHashAndPassword côté LoginAdmin).
func (h *HelmProvisioner) createGestionAdmin(ctx context.Context, namespace, username, password string) error {
func (h *HelmProvisioner) createGestionAdmin(ctx context.Context, namespace, username, password, pgPassword string) error {
if username == "" || password == "" {
return fmt.Errorf("username/password admin manquants")
}
@@ -533,7 +576,7 @@ func (h *HelmProvisioner) createGestionAdmin(ctx context.Context, namespace, use
escapedUsername, string(hash),
)
shellCmd := fmt.Sprintf("PGPASSWORD=%s psql -h localhost -U %s -d %s -v ON_ERROR_STOP=1 -c %s",
demoDBPass, demoDBUser, demoDBName, shellQuote(sql))
pgPassword, demoDBUser, demoDBName, shellQuote(sql))
_, stderr, err := h.execInPod(ctx, namespace, pgPod, "postgresql", []string{"sh", "-c", shellCmd}, nil)
if err != nil {
@@ -550,12 +593,12 @@ func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}
// Identifiants postgres des démos — mêmes valeurs codées en dur que
// buildPostgresValues/buildBackendValues (voir ces fonctions).
// Identifiants postgres des démos — utilisateur/base fixes (pas des
// secrets), le mot de passe est généré par démo (voir Provision et
// MigrateToPremiumNamespace, jamais codé en dur ni partagé entre démos).
const (
demoDBUser = "postgres"
demoDBName = "demo_db"
demoDBPass = "demo-postgres-pass"
)
// findPod retourne le nom du premier pod du namespace dont le nom contient nameContains.
@@ -735,7 +778,10 @@ func parseImage(image string) (repo string, tag string) {
// buildBackendValues construit les valeurs pour le chart backend.
// backendLinkSecret : partagé avec le chart lbtelegram (voir
// buildLBTelegramValues) — vide si le load-balancer Telegram n'est pas activé.
func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResource, cfg ProvisionConfig, backendLinkSecret string) map[string]interface{} {
// pgPassword/redisPassword : générés par démo (voir Provision), jamais
// codés en dur — placés dans "secrets" (Secret k8s), pas "env" (visible en
// clair sur le Deployment sans droit de lecture des Secrets).
func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResource, cfg ProvisionConfig, backendLinkSecret, pgPassword, redisPassword string) map[string]interface{} {
// Parser l'image backend pour séparer repository et tag
backendRepo, backendTag := parseImage(h.backendImage)
@@ -751,13 +797,11 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
env := map[string]string{
"DB_HOST": fmt.Sprintf("%s-postgresql-postgresql", d.Namespace),
"DB_PORT": "5432",
"DB_PASSWORD": "demo-postgres-pass",
"DB_USER": "postgres",
"DB_NAME": "demo_db",
"DB_SSLMODE": "disable",
"REDIS_HOST": fmt.Sprintf("%s-redis-redis", d.Namespace),
"REDIS_PORT": "6379",
"REDIS_PASSWORD": "demo-redis-pass", // Mot de passe Redis
"API_PORT": "8080",
"NODE_ENV": "production",
"STORAGE_DRIVER": storageDriver,
@@ -772,6 +816,8 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
}
secrets := h.buildSecrets(resources)
secrets["DB_PASSWORD"] = pgPassword
secrets["REDIS_PASSWORD"] = redisPassword
if cfg.TelegramBotToken != "" {
secrets["TELEGRAM_BOT_TOKEN"] = cfg.TelegramBotToken
// URL publique du webhook Telegram de cette démo (route backend :
@@ -857,13 +903,24 @@ func (h *HelmProvisioner) buildIngressRouteValues(d Demo) map[string]interface{}
}
}
// buildNetworkPolicyValues construit les valeurs pour le chart
// network-policy (isolation réseau stricte d'une démo, voir Provision).
// kubeSystemNamespace/blockedEgressCIDRs gardent les défauts du chart
// (deploy/chart-gestion/network-policy/values.yaml) ; seul traefikNamespace
// est explicité pour rester synchronisé avec sharedTraefikNamespace.
func (h *HelmProvisioner) buildNetworkPolicyValues() map[string]interface{} {
return map[string]interface{}{
"traefikNamespace": sharedTraefikNamespace,
}
}
// buildLBTelegramValues construit les valeurs pour le chart lbtelegram
// (load-balancer multi-bots Telegram). N'est appelé que si
// cfg.LBTelegramEnabled() — au moins un bot renseigné par l'admin. Les
// identifiants des bots (username/token) viennent de l'admin ; le reste
// (DSN postgres/redis, secrets JWT/webhook/backend-link) est généré ici,
// comme indiqué par le commentaire du chart (deploy/chart-gestion/lbtelegram/values.yaml).
func (h *HelmProvisioner) buildLBTelegramValues(d Demo, cfg ProvisionConfig, backendLinkSecret string) map[string]interface{} {
func (h *HelmProvisioner) buildLBTelegramValues(d Demo, cfg ProvisionConfig, backendLinkSecret, pgPassword, redisPassword string) map[string]interface{} {
botCount := 0
if cfg.LBBot1Username != "" {
botCount++
@@ -873,8 +930,8 @@ func (h *HelmProvisioner) buildLBTelegramValues(d Demo, cfg ProvisionConfig, bac
}
backendURL := fmt.Sprintf("http://%s-backend-gestion-backend.%s.svc.cluster.local:8080", d.Namespace, d.Namespace)
dbURL := fmt.Sprintf("postgres://postgres:demo-postgres-pass@%s-postgresql-postgresql:5432/demo_db?sslmode=disable", d.Namespace)
redisURL := fmt.Sprintf("redis://:demo-redis-pass@%s-redis-redis:6379/0", d.Namespace)
dbURL := fmt.Sprintf("postgres://postgres:%s@%s-postgresql-postgresql:5432/demo_db?sslmode=disable", pgPassword, d.Namespace)
redisURL := fmt.Sprintf("redis://:%s@%s-redis-redis:6379/0", redisPassword, d.Namespace)
lbRepo, lbTag := parseImage(h.lbtelegramImage)
host := fmt.Sprintf("%s.%s", d.Namespace, h.baseDomain)
@@ -950,11 +1007,12 @@ func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]
}
// buildPostgresValues construit les valeurs pour le chart postgresql.
// Toujours persistant (PVC) : voir Provision.
func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} {
// Toujours persistant (PVC) : voir Provision. password : généré par démo,
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[string]interface{} {
return map[string]interface{}{
"auth": map[string]interface{}{
"password": "demo-postgres-pass", // Mot de passe OBLIGATOIRE (champ correct pour le chart)
"password": password,
"username": "postgres",
"database": "demo_db",
},
@@ -969,15 +1027,16 @@ func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} {
}
// buildRedisValues construit les valeurs pour le chart redis.
// Toujours persistant (PVC) : voir Provision.
func (h *HelmProvisioner) buildRedisValues(d Demo) map[string]interface{} {
// Toujours persistant (PVC) : voir Provision. password : généré par démo,
// jamais partagé (voir Provision/MigrateToPremiumNamespace).
func (h *HelmProvisioner) buildRedisValues(d Demo, password string) map[string]interface{} {
return map[string]interface{}{
"service": map[string]interface{}{
"type": "ClusterIP",
"port": 6379,
},
"auth": map[string]interface{}{
"password": "demo-redis-pass", // Mot de passe simple pour les démos
"password": password,
},
"persistence": map[string]interface{}{
"enabled": true,