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) 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 // 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) // souscrit pas, le teardown du namespace (TTL ou suppression admin)
// supprime le PVC avec le reste. S'il souscrit, TransferToPaid n'a rien // 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. // d'autre à faire côté stockage — les données sont déjà en place.
postgresValues := h.buildPostgresValues(d) postgresValues := h.buildPostgresValues(d, pgPassword)
redisValues := h.buildRedisValues(d) redisValues := h.buildRedisValues(d, redisPassword)
// Secret partagé backend <-> lbtelegram (authentifie les appels de // Secret partagé backend <-> lbtelegram (authentifie les appels de
// lbtelegram vers backend) — généré une seule fois si le load-balancer // 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() backendLinkSecret = randomSecret()
} }
backendValues := h.buildBackendValues(d, resources, cfg, backendLinkSecret) backendValues := h.buildBackendValues(d, resources, cfg, backendLinkSecret, pgPassword, redisPassword)
frontendValues := h.buildFrontendValues(d) frontendValues := h.buildFrontendValues(d)
ingressValues := h.buildIngressRouteValues(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é // Load-balancer Telegram : optionnel, seulement si l'admin a renseigné
// au moins un bot. // au moins un bot.
if cfg.LBTelegramEnabled() { 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 { if err := h.installChart(d.Namespace, "lbtelegram", lbValues); err != nil {
h.deleteNamespace(d.Namespace) h.deleteNamespace(d.Namespace)
return fmt.Errorf("déploiement lbtelegram: %w", err) return fmt.Errorf("déploiement lbtelegram: %w", err)
@@ -174,7 +191,7 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
return nil 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) 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") 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 { if err := h.createNamespace(newNamespace); err != nil {
return fmt.Errorf("création namespace %s: %w", newNamespace, err) 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 := d
newDemo.Namespace = newNamespace newDemo.Namespace = newNamespace
newDemo.URL = newURL 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) h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement postgresql: %w", err) 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) h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement redis: %w", err) 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 // vide). Redis (cache/sessions) n'est pas migré, reconstruit
// naturellement — même convention que l'historique passage en // naturellement — même convention que l'historique passage en
// stockage persistant. // 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) h.deleteNamespace(newNamespace)
return fmt.Errorf("migration données postgres: %w", err) 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 { if err := h.installChart(newNamespace, "backend", backendPatched); err != nil {
h.deleteNamespace(newNamespace) h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement backend: %w", err) 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) return fmt.Errorf("déploiement ingressroute: %w", err)
} }
if lbEnabled { 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 { if err := h.installChart(newNamespace, "lbtelegram", lbPatched); err != nil {
h.deleteNamespace(newNamespace) h.deleteNamespace(newNamespace)
return fmt.Errorf("déploiement lbtelegram: %w", err) 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 // migratePostgresData copie les données postgres d'un namespace à l'autre
// (pg_dump / psql restore), utilisé par MigrateToPremiumNamespace. // (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") oldPod, err := h.findPod(ctx, oldNamespace, "postgresql")
if err != nil { if err != nil {
return fmt.Errorf("pod postgresql source introuvable: %w", err) 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) dump, stderr, err := h.execInPod(ctx, oldNamespace, oldPod, "postgresql", []string{"sh", "-c", dumpCmd}, nil)
if err != nil { if err != nil {
return fmt.Errorf("pg_dump: %w (%s)", err, stderr) return fmt.Errorf("pg_dump: %w (%s)", err, stderr)
@@ -409,7 +447,7 @@ func (h *HelmProvisioner) migratePostgresData(ctx context.Context, oldNamespace,
if err != nil { if err != nil {
return fmt.Errorf("pod postgresql cible introuvable: %w", err) 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 { 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) 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 // patchBackendValuesForNamespace réutilise les valeurs Helm existantes du
// backend (image, secrets, réglages métier...) en ne recalculant que ce qui // 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 // référence le nom du namespace : DNS internes postgres/redis/lbtelegram,
// URL du webhook Telegram (dépend de l'URL publique de la démo). // URL du webhook Telegram, et les nouveaux mots de passe postgres/redis
func (h *HelmProvisioner) patchBackendValuesForNamespace(values map[string]interface{}, newDemo Demo, lbEnabled bool, backendLinkSecret string) map[string]interface{} { // (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"]) env := asStringMap(values["env"])
secrets := asStringMap(values["secrets"]) secrets := asStringMap(values["secrets"])
env["DB_HOST"] = fmt.Sprintf("%s-postgresql-postgresql", newDemo.Namespace) env["DB_HOST"] = fmt.Sprintf("%s-postgresql-postgresql", newDemo.Namespace)
env["REDIS_HOST"] = fmt.Sprintf("%s-redis-redis", 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 { if lbEnabled {
env["LBTELEGRAM_URL"] = fmt.Sprintf("http://%s-lbtelegram-lbtelegram.%s.svc.cluster.local:8081", newDemo.Namespace, newDemo.Namespace) env["LBTELEGRAM_URL"] = fmt.Sprintf("http://%s-lbtelegram-lbtelegram.%s.svc.cluster.local:8081", newDemo.Namespace, newDemo.Namespace)
secrets["BACKEND_LINK_SECRET"] = backendLinkSecret secrets["BACKEND_LINK_SECRET"] = backendLinkSecret
@@ -442,8 +485,8 @@ func (h *HelmProvisioner) patchBackendValuesForNamespace(values map[string]inter
// patchLBTelegramValuesForNamespace réutilise les valeurs Helm existantes // patchLBTelegramValuesForNamespace réutilise les valeurs Helm existantes
// du chart lbtelegram (bots, stratégie, tokens...) en ne recalculant que ce // 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 // qui référence le nom du namespace : host public, GATEWAY_URL, DSN
// postgres/redis et URL interne du backend. // postgres/redis (avec les nouveaux mots de passe) et URL interne du backend.
func (h *HelmProvisioner) patchLBTelegramValuesForNamespace(values map[string]interface{}, newDemo Demo, backendLinkSecret string) map[string]interface{} { func (h *HelmProvisioner) patchLBTelegramValuesForNamespace(values map[string]interface{}, newDemo Demo, backendLinkSecret, pgPassword, redisPassword string) map[string]interface{} {
env := asStringMap(values["env"]) env := asStringMap(values["env"])
secrets := asStringMap(values["secrets"]) 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) 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["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["DATABASE_URL"] = fmt.Sprintf("postgres://postgres:%s@%s-postgresql-postgresql:5432/demo_db?sslmode=disable", pgPassword, newDemo.Namespace)
secrets["REDIS_URL"] = fmt.Sprintf("redis://:demo-redis-pass@%s-redis-redis:6379/0", newDemo.Namespace) secrets["REDIS_URL"] = fmt.Sprintf("redis://:%s@%s-redis-redis:6379/0", redisPassword, newDemo.Namespace)
values["host"] = host values["host"] = host
values["env"] = env 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 // 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 // (golang.org/x/crypto/bcrypt), comme le fait l'app elle-même
// (bcrypt.CompareHashAndPassword côté LoginAdmin). // (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 == "" { if username == "" || password == "" {
return fmt.Errorf("username/password admin manquants") return fmt.Errorf("username/password admin manquants")
} }
@@ -533,7 +576,7 @@ func (h *HelmProvisioner) createGestionAdmin(ctx context.Context, namespace, use
escapedUsername, string(hash), escapedUsername, string(hash),
) )
shellCmd := fmt.Sprintf("PGPASSWORD=%s psql -h localhost -U %s -d %s -v ON_ERROR_STOP=1 -c %s", 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) _, stderr, err := h.execInPod(ctx, namespace, pgPod, "postgresql", []string{"sh", "-c", shellCmd}, nil)
if err != nil { if err != nil {
@@ -550,12 +593,12 @@ func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
} }
// Identifiants postgres des démos — mêmes valeurs codées en dur que // Identifiants postgres des démos — utilisateur/base fixes (pas des
// buildPostgresValues/buildBackendValues (voir ces fonctions). // 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 ( const (
demoDBUser = "postgres" demoDBUser = "postgres"
demoDBName = "demo_db" demoDBName = "demo_db"
demoDBPass = "demo-postgres-pass"
) )
// findPod retourne le nom du premier pod du namespace dont le nom contient nameContains. // 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. // buildBackendValues construit les valeurs pour le chart backend.
// backendLinkSecret : partagé avec le chart lbtelegram (voir // backendLinkSecret : partagé avec le chart lbtelegram (voir
// buildLBTelegramValues) — vide si le load-balancer Telegram n'est pas activé. // 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 // Parser l'image backend pour séparer repository et tag
backendRepo, backendTag := parseImage(h.backendImage) backendRepo, backendTag := parseImage(h.backendImage)
@@ -751,13 +797,11 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
env := map[string]string{ env := map[string]string{
"DB_HOST": fmt.Sprintf("%s-postgresql-postgresql", d.Namespace), "DB_HOST": fmt.Sprintf("%s-postgresql-postgresql", d.Namespace),
"DB_PORT": "5432", "DB_PORT": "5432",
"DB_PASSWORD": "demo-postgres-pass",
"DB_USER": "postgres", "DB_USER": "postgres",
"DB_NAME": "demo_db", "DB_NAME": "demo_db",
"DB_SSLMODE": "disable", "DB_SSLMODE": "disable",
"REDIS_HOST": fmt.Sprintf("%s-redis-redis", d.Namespace), "REDIS_HOST": fmt.Sprintf("%s-redis-redis", d.Namespace),
"REDIS_PORT": "6379", "REDIS_PORT": "6379",
"REDIS_PASSWORD": "demo-redis-pass", // Mot de passe Redis
"API_PORT": "8080", "API_PORT": "8080",
"NODE_ENV": "production", "NODE_ENV": "production",
"STORAGE_DRIVER": storageDriver, "STORAGE_DRIVER": storageDriver,
@@ -772,6 +816,8 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
} }
secrets := h.buildSecrets(resources) secrets := h.buildSecrets(resources)
secrets["DB_PASSWORD"] = pgPassword
secrets["REDIS_PASSWORD"] = redisPassword
if cfg.TelegramBotToken != "" { if cfg.TelegramBotToken != "" {
secrets["TELEGRAM_BOT_TOKEN"] = cfg.TelegramBotToken secrets["TELEGRAM_BOT_TOKEN"] = cfg.TelegramBotToken
// URL publique du webhook Telegram de cette démo (route backend : // 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 // buildLBTelegramValues construit les valeurs pour le chart lbtelegram
// (load-balancer multi-bots Telegram). N'est appelé que si // (load-balancer multi-bots Telegram). N'est appelé que si
// cfg.LBTelegramEnabled() — au moins un bot renseigné par l'admin. Les // cfg.LBTelegramEnabled() — au moins un bot renseigné par l'admin. Les
// identifiants des bots (username/token) viennent de l'admin ; le reste // identifiants des bots (username/token) viennent de l'admin ; le reste
// (DSN postgres/redis, secrets JWT/webhook/backend-link) est généré ici, // (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). // 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 botCount := 0
if cfg.LBBot1Username != "" { if cfg.LBBot1Username != "" {
botCount++ 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) 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) dbURL := fmt.Sprintf("postgres://postgres:%s@%s-postgresql-postgresql:5432/demo_db?sslmode=disable", pgPassword, d.Namespace)
redisURL := fmt.Sprintf("redis://:demo-redis-pass@%s-redis-redis:6379/0", d.Namespace) redisURL := fmt.Sprintf("redis://:%s@%s-redis-redis:6379/0", redisPassword, d.Namespace)
lbRepo, lbTag := parseImage(h.lbtelegramImage) lbRepo, lbTag := parseImage(h.lbtelegramImage)
host := fmt.Sprintf("%s.%s", d.Namespace, h.baseDomain) 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. // buildPostgresValues construit les valeurs pour le chart postgresql.
// Toujours persistant (PVC) : voir Provision. // Toujours persistant (PVC) : voir Provision. password : généré par démo,
func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} { // jamais partagé (voir Provision/MigrateToPremiumNamespace).
func (h *HelmProvisioner) buildPostgresValues(d Demo, password string) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"auth": map[string]interface{}{ "auth": map[string]interface{}{
"password": "demo-postgres-pass", // Mot de passe OBLIGATOIRE (champ correct pour le chart) "password": password,
"username": "postgres", "username": "postgres",
"database": "demo_db", "database": "demo_db",
}, },
@@ -969,15 +1027,16 @@ func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} {
} }
// buildRedisValues construit les valeurs pour le chart redis. // buildRedisValues construit les valeurs pour le chart redis.
// Toujours persistant (PVC) : voir Provision. // Toujours persistant (PVC) : voir Provision. password : généré par démo,
func (h *HelmProvisioner) buildRedisValues(d Demo) map[string]interface{} { // jamais partagé (voir Provision/MigrateToPremiumNamespace).
func (h *HelmProvisioner) buildRedisValues(d Demo, password string) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"service": map[string]interface{}{ "service": map[string]interface{}{
"type": "ClusterIP", "type": "ClusterIP",
"port": 6379, "port": 6379,
}, },
"auth": map[string]interface{}{ "auth": map[string]interface{}{
"password": "demo-redis-pass", // Mot de passe simple pour les démos "password": password,
}, },
"persistence": map[string]interface{}{ "persistence": map[string]interface{}{
"enabled": true, "enabled": true,
+1510
View File
File diff suppressed because one or more lines are too long
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Omnex — Plateforme de gestion de commandes & livraison</title>
<meta name="description" content="Déployez en un clic une démo complète de la plateforme de gestion de commandes et de livraison." />
<script type="module" crossorigin src="/assets/index-CvCTqtVE.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
+4 -1
View File
@@ -45,7 +45,10 @@ export function BackofficeLayout() {
const roleColor = isAdmin ? 'purple' : isClient ? 'gray' : 'gray' const roleColor = isAdmin ? 'purple' : isClient ? 'gray' : 'gray'
return ( return (
<Box minH="100vh" bg="chakra-subtle-bg"> // overflowX hidden : garde-fou global — si un élément interne déborde
// malgré tout (contenu imprévu, régression future), le contenu est
// rogné plutôt que d'ouvrir un scroll horizontal sur toute la page.
<Box minH="100vh" bg="chakra-subtle-bg" overflowX="hidden">
<Flex as="header" px={6} py={3} borderBottomWidth="1px" align="center" gap={6}> <Flex as="header" px={6} py={3} borderBottomWidth="1px" align="center" gap={6}>
<Heading size="sm" as={RouterLink} to="/app"> <Heading size="sm" as={RouterLink} to="/app">
Omnex · {isAdmin ? 'Espace admin' : 'Espace client'} Omnex · {isAdmin ? 'Espace admin' : 'Espace client'}
+6 -4
View File
@@ -47,7 +47,7 @@ export function DemoCard({
<Box borderWidth="1px" borderRadius="lg" overflow="hidden" bg="bg-surface"> <Box borderWidth="1px" borderRadius="lg" overflow="hidden" bg="bg-surface">
<Box p={4} cursor="pointer" onClick={onToggle} _active={{ bg: 'chakra-subtle-bg' }}> <Box p={4} cursor="pointer" onClick={onToggle} _active={{ bg: 'chakra-subtle-bg' }}>
<HStack justify="space-between" align="start"> <HStack justify="space-between" align="start">
<HStack spacing={2} minW={0}> <HStack spacing={2} minW={0} flex="1">
<Icon <Icon
as={ChevronIcon} as={ChevronIcon}
boxSize={3} boxSize={3}
@@ -56,7 +56,7 @@ export function DemoCard({
transform={isOpen ? 'rotate(90deg)' : undefined} transform={isOpen ? 'rotate(90deg)' : undefined}
transition="transform 0.15s" transition="transform 0.15s"
/> />
<Text fontFamily="mono" fontSize="sm" noOfLines={1} wordBreak="break-all"> <Text fontFamily="mono" fontSize="sm" noOfLines={1} wordBreak="break-all" minW={0} flex="1">
{demo.namespace} {demo.namespace}
</Text> </Text>
</HStack> </HStack>
@@ -74,7 +74,7 @@ export function DemoCard({
<Text color="gray.500" flexShrink={0}> <Text color="gray.500" flexShrink={0}>
URL URL
</Text> </Text>
<HStack spacing={1} minW={0}> <HStack spacing={1} minW={0} flex="1" justify="flex-end">
{demo.status === 'ready' ? ( {demo.status === 'ready' ? (
<Link <Link
href={demo.url} href={demo.url}
@@ -82,6 +82,8 @@ export function DemoCard({
isExternal isExternal
noOfLines={1} noOfLines={1}
wordBreak="break-all" wordBreak="break-all"
minW={0}
flex="1"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{demo.url} {demo.url}
@@ -111,7 +113,7 @@ export function DemoCard({
</Stack> </Stack>
{actions && ( {actions && (
<HStack mt={3} spacing={2} onClick={(e) => e.stopPropagation()}> <HStack mt={3} spacing={2} flexWrap="wrap" rowGap={2} onClick={(e) => e.stopPropagation()}>
{actions} {actions}
</HStack> </HStack>
)} )}
+7 -3
View File
@@ -30,7 +30,11 @@ export function PodStatusPanel({ state }: { state: DemoDetails['state'] }) {
: `${downCount} service${downCount > 1 ? 's' : ''} indisponible${downCount > 1 ? 's' : ''}`} : `${downCount} service${downCount > 1 ? 's' : ''} indisponible${downCount > 1 ? 's' : ''}`}
</Text> </Text>
</HStack> </HStack>
<SimpleGrid columns={{ base: 1, sm: 2, lg: 4 }} spacing={3}> {/* 1 colonne jusqu'à lg : à "sm" (480px), 2 colonnes serraient trop les
cartes (CPU/mémoire en mono + badge) et forçaient un débordement
horizontal — surtout dans le contexte carte mobile, déjà à l'étroit
avec son propre padding. */}
<SimpleGrid columns={{ base: 1, lg: 4 }} spacing={3}>
{PODSTATUS_COMPONENTS.map((c) => ( {PODSTATUS_COMPONENTS.map((c) => (
<ComponentCard key={c.key} title={c.label} cs={state[c.key]} /> <ComponentCard key={c.key} title={c.label} cs={state[c.key]} />
))} ))}
@@ -44,9 +48,9 @@ function ComponentCard({ title, cs }: { title: string; cs: ComponentState }) {
const memPct = cs.memory_limit_mi > 0 ? Math.min(100, Math.round((cs.memory_mi / cs.memory_limit_mi) * 100)) : 0 const memPct = cs.memory_limit_mi > 0 ? Math.min(100, Math.round((cs.memory_mi / cs.memory_limit_mi) * 100)) : 0
return ( return (
<Box p={3} borderWidth="1px" borderRadius="lg" bg="bg-surface"> <Box p={3} borderWidth="1px" borderRadius="lg" bg="bg-surface" minW={0}>
<HStack justify="space-between" mb={3}> <HStack justify="space-between" mb={3}>
<Text fontSize="sm" fontWeight="semibold"> <Text fontSize="sm" fontWeight="semibold" noOfLines={1}>
{title} {title}
</Text> </Text>
<HStack spacing={1.5}> <HStack spacing={1.5}>
+1 -1
View File
@@ -53,7 +53,7 @@ const plans: Plan[] = [
description: 'Passez en production : stockage persistant, votre démo n\'expire plus.', description: 'Passez en production : stockage persistant, votre démo n\'expire plus.',
features: [ features: [
'Tout ce qui est inclus dans Démo', 'Tout ce qui est inclus dans Démo',
'Stockage persistant, n\'expire plus', 'Votre nom de domaine',
'Support prioritaire', 'Support prioritaire',
], ],
cta: 'Nous contacter', cta: 'Nous contacter',