Files
omnex/control-plane/api/internal/demos/project_provisioner.go
T
Xor290 9e11f01c2c
ci-api / test (push) Failing after 14m25s
ci-web / test (push) Failing after 8m17s
chore: build
2026-09-20 16:14:29 +02:00

190 lines
7.4 KiB
Go

package demos
import (
"fmt"
"log"
"strconv"
)
// Projets vitrine (voir internal/projects) : même infrastructure que les
// démos (namespace isolé, NetworkPolicy, ResourceQuota, Traefik + WAF
// partagés, sauvegardes Velero par namespace) mais avec les charts du
// produit vitrine (h.vitrineChartsDir) et une durée d'abonnement en mois.
// Le cycle de vie (durée, prolongation, suppression) vit dans internal/projects ;
// ce fichier ne contient que ce qui touche au cluster.
// Noms de release/chart utilisés par les charts vitrine (dossier
// deploy/chart-vitrine). Le nom de Service produit par le chart est
// "<release>-<nom du chart>" avec release = "<namespace>-<chart>" (voir
// installChartFrom) : ces deux fonctions doivent rester synchronisées avec les
// defaults de deploy/chart-vitrine/ingressroute/templates/_helpers.tpl.
func vitrineBackendService(namespace string) string {
return namespace + "-backend-vitrine-backend"
}
func vitrineFrontendService(namespace string) string {
return namespace + "-frontend-vitrine-frontend"
}
// ProjectSpec : ce dont le provisioner a besoin pour déployer un projet
// vitrine. AdminPassword ne doit jamais être persisté : il ne transite que
// par le fichier de valeurs Helm temporaire puis le Secret k8s du chart.
type ProjectSpec struct {
Namespace string
Host string // nom d'hôte public, sans protocole (ex. vitrine-ab12cd34.vitrine-omnex.club)
URL string // URL publique complète (https://<Host>)
AdminUsername string
AdminPassword string
// AdminNumber : nombre maximum de comptes admin du backend (ADMIN_NUMBER),
// choisi par l'admin Omnex au déploiement.
AdminNumber int
}
// ProvisionProject déploie un projet vitrine :
// 1. namespace + Schedule Velero (identique aux démos : voir
// ensureDemoBackupSchedule),
// 2. isolation réseau, quota, identifiants du registry (charts des démos),
// 3. postgresql, redis, backend (migrations + compte admin via hooks Helm),
// frontend puis ingressroute (charts vitrine).
//
// En cas d'échec, le namespace et le Schedule sont supprimés.
func (h *HelmProvisioner) ProvisionProject(p ProjectSpec) error {
ns := p.Namespace
// Sans image, les valeurs Helm écraseraient les défauts des charts par
// une chaîne vide : échec explicite avant de créer quoi que ce soit.
if h.cfg.VitrineBackendImage == "" || h.cfg.VitrineFrontendImage == "" {
return fmt.Errorf("VITRINE_BACKEND_IMAGE et VITRINE_FRONTEND_IMAGE doivent être définies")
}
if err := h.createNamespace(ns); err != nil {
return fmt.Errorf("création namespace %s: %w", ns, err)
}
// Non-fatal : un projet doit pouvoir démarrer même si Velero est
// momentanément indisponible (même règle que pour les démos).
if err := h.ensureDemoBackupSchedule(ns); err != nil {
log.Printf("Warning: planning de sauvegarde échoué pour %s: %v", ns, err)
}
fail := func(step string, err error) error {
if derr := h.deleteDemoBackups(ns); derr != nil {
log.Printf("Warning: nettoyage des sauvegardes de %s après échec: %v", ns, derr)
}
_ = h.deleteNamespace(ns)
return fmt.Errorf("déploiement %s: %w", step, err)
}
// Isolation réseau AVANT tout workload, plafond de ressources, puis
// identifiants du registry avant tout pull d'image : mêmes charts que les
// démos (h.chartsDir).
if err := h.installChart(ns, "network-policy", h.buildNetworkPolicyValues()); err != nil {
return fail("network-policy", err)
}
if err := h.installChart(ns, "resource-quota", nil); err != nil {
return fail("resource-quota", err)
}
if err := h.installChart(ns, "registry-credentials", h.buildRegistryCredentialsValues()); err != nil {
return fail("registry-credentials", err)
}
// Secrets générés par projet, jamais partagés ni codés en dur.
pgPassword := randomSecret()
redisPassword := randomSecret()
jwtSecret := randomSecret()
steps := []struct {
chart string
values map[string]interface{}
}{
{"postgresql", h.buildVitrinePostgresValues(pgPassword)},
{"redis", h.buildVitrineRedisValues(redisPassword)},
{"backend", h.buildVitrineBackendValues(p, pgPassword, redisPassword, jwtSecret)},
{"frontend", h.buildVitrineFrontendValues()},
{"ingressroute", h.buildVitrineIngressRouteValues(p)},
}
for _, s := range steps {
if err := h.installChartFrom(h.vitrineChartsDir, ns, s.chart, s.values); err != nil {
return fail(s.chart, err)
}
}
if err := h.waitForRollout(ns); err != nil {
// Le déploiement Helm a réussi (--wait --atomic) : on ne détruit pas
// pour un simple dépassement de délai de cette vérification.
log.Printf("Warning: rollout check échoué pour %s: %v", ns, err)
}
return nil
}
// TeardownProject supprime le namespace du projet (PVC compris) et ses
// sauvegardes Velero — même chemin que Teardown pour une démo.
func (h *HelmProvisioner) TeardownProject(namespace string) error {
return h.Teardown(Demo{Namespace: namespace})
}
func (h *HelmProvisioner) buildVitrinePostgresValues(password string) map[string]interface{} {
// Nom de base et utilisateur : défauts du chart (vitrine_db / postgres),
// alignés sur database.name / database.user du chart backend.
return map[string]interface{}{
"imagePullSecrets": h.imagePullSecretsValue(),
"auth": map[string]interface{}{"password": password},
}
}
func (h *HelmProvisioner) buildVitrineRedisValues(password string) map[string]interface{} {
return map[string]interface{}{
"imagePullSecrets": h.imagePullSecretsValue(),
"auth": map[string]interface{}{"password": password},
}
}
func (h *HelmProvisioner) buildVitrineBackendValues(p ProjectSpec, pgPassword, redisPassword, jwtSecret string) map[string]interface{} {
repo, tag := parseImage(h.cfg.VitrineBackendImage)
return map[string]interface{}{
"replicaCount": 1, // volumes ReadWriteOnce : un seul replica (voir le chart)
"imagePullSecrets": h.imagePullSecretsValue(),
"image": map[string]interface{}{
"repository": repo,
"tag": tag,
"pullPolicy": "Always",
},
"autoscaling": map[string]interface{}{"enabled": false},
"env": map[string]interface{}{
"ADMIN_NUMBER": strconv.Itoa(p.AdminNumber),
"SEED_ADMIN_USERNAME": p.AdminUsername,
"MEDIA_LOCAL_BASE_URL": p.URL + "/uploads",
},
"secrets": map[string]interface{}{
"JWT_SECRET": jwtSecret,
"DB_PASSWORD": pgPassword,
"REDIS_PASSWORD": redisPassword,
"SEED_ADMIN_PASSWORD": p.AdminPassword,
},
// Compte admin créé par le hook post-install du chart (jamais rejoué
// à l'upgrade : cmd/seed n'est pas idempotent).
"seedJob": map[string]interface{}{"enabled": true},
}
}
func (h *HelmProvisioner) buildVitrineFrontendValues() map[string]interface{} {
repo, tag := parseImage(h.cfg.VitrineFrontendImage)
return map[string]interface{}{
"replicaCount": 1,
"imagePullSecrets": h.imagePullSecretsValue(),
"image": map[string]interface{}{
"repository": repo,
"tag": tag,
"pullPolicy": "Always",
},
"autoscaling": map[string]interface{}{"enabled": false},
}
}
func (h *HelmProvisioner) buildVitrineIngressRouteValues(p ProjectSpec) map[string]interface{} {
return map[string]interface{}{
"host": p.Host,
"traefikNamespace": sharedTraefikNamespace,
"backend": map[string]interface{}{"serviceName": vitrineBackendService(p.Namespace)},
"frontend": map[string]interface{}{"serviceName": vitrineFrontendService(p.Namespace)},
}
}