diff --git a/control-plane/api/internal/demos/helm_provisioner.go b/control-plane/api/internal/demos/helm_provisioner.go index 6baa477..80da5d0 100644 --- a/control-plane/api/internal/demos/helm_provisioner.go +++ b/control-plane/api/internal/demos/helm_provisioner.go @@ -102,11 +102,12 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr return fmt.Errorf("création namespace %s: %w", d.Namespace, err) } - // Préparer les valeurs. Pas de persistence à la création : les démos - // d'essai sont éphémères (TTL 30j) — voir UpgradeToPaid pour le passage - // en stockage persistant lorsqu'un client devient payant. - postgresValues := h.buildPostgresValues(d, false) - redisValues := h.buildRedisValues(d, false) + // 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) // Secret partagé backend <-> lbtelegram (authentifie les appels de // lbtelegram vers backend) — généré une seule fois si le load-balancer @@ -199,7 +200,8 @@ func (h *HelmProvisioner) EnsureSharedInfra() error { return nil } -// Teardown détruit une démo en supprimant son namespace (cascading). +// Teardown détruit une démo en supprimant son namespace (cascading) — les +// PVC postgres/redis de la démo partent avec, puisqu'ils sont namespaced. func (h *HelmProvisioner) Teardown(d Demo) error { return h.deleteNamespace(d.Namespace) } @@ -259,56 +261,6 @@ const ( demoDBPass = "demo-postgres-pass" ) -// UpgradeToPaid bascule postgresql/redis d'une démo en stockage persistant -// lorsqu'un client passe en abonnement payant, en préservant les données -// postgres déjà créées pendant l'essai : pg_dump avant la bascule (le volume -// actuel est un emptyDir éphémère), puis restore une fois le nouveau volume -// persistant en place. Redis n'est pas migré (cache/sessions — reconstruit -// naturellement), seule sa persistence est activée pour la suite. -func (h *HelmProvisioner) UpgradeToPaid(d Demo) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - pgPod, err := h.findPod(ctx, d.Namespace, "postgresql") - if err != nil { - return fmt.Errorf("pod postgresql introuvable: %w", err) - } - - dumpCmd := fmt.Sprintf("PGPASSWORD=%s pg_dump -h localhost -U %s %s", demoDBPass, demoDBUser, demoDBName) - dump, stderr, err := h.execInPod(ctx, d.Namespace, pgPod, "postgresql", []string{"sh", "-c", dumpCmd}, nil) - if err != nil { - return fmt.Errorf("pg_dump: %w (%s)", err, stderr) - } - - // Bascule postgresql en stockage persistant : le pod redémarre (nouveau - // PVC, initialement vide) — c'est pour ça que le dump ci-dessus doit - // être pris AVANT cet appel. helm upgrade --wait attend que le nouveau - // pod soit prêt (readinessProbe = pg_isready) avant de retourner. - if err := h.upgradeInstallChart(d.Namespace, d.Namespace+"-postgresql", "postgresql", h.buildPostgresValues(d, true)); err != nil { - return fmt.Errorf("upgrade postgresql (persistence): %w", err) - } - - newPgPod, err := h.findPod(ctx, d.Namespace, "postgresql") - if err != nil { - return fmt.Errorf("pod postgresql introuvable après upgrade: %w", err) - } - - if strings.TrimSpace(dump) != "" { - restoreCmd := fmt.Sprintf("PGPASSWORD=%s psql -h localhost -U %s %s", demoDBPass, demoDBUser, demoDBName) - _, stderr, err := h.execInPod(ctx, d.Namespace, newPgPod, "postgresql", []string{"sh", "-c", restoreCmd}, strings.NewReader(dump)) - if err != nil { - return fmt.Errorf("restore pg_dump: %w (%s)", err, stderr) - } - } - - if err := h.upgradeInstallChart(d.Namespace, d.Namespace+"-redis", "redis", h.buildRedisValues(d, true)); err != nil { - return fmt.Errorf("upgrade redis (persistence): %w", err) - } - - log.Printf("Démo %s basculée en stockage persistant (données postgres migrées)", d.Namespace) - return nil -} - // findPod retourne le nom du premier pod du namespace dont le nom contient nameContains. func (h *HelmProvisioner) findPod(ctx context.Context, namespace, nameContains string) (string, error) { pods, err := h.k8sClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) @@ -694,7 +646,8 @@ func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string] } // buildPostgresValues construit les valeurs pour le chart postgresql. -func (h *HelmProvisioner) buildPostgresValues(d Demo, persistent bool) map[string]interface{} { +// Toujours persistant (PVC) : voir Provision. +func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} { return map[string]interface{}{ "auth": map[string]interface{}{ "password": "demo-postgres-pass", // Mot de passe OBLIGATOIRE (champ correct pour le chart) @@ -706,13 +659,14 @@ func (h *HelmProvisioner) buildPostgresValues(d Demo, persistent bool) map[strin "port": 5432, }, "persistence": map[string]interface{}{ - "enabled": persistent, + "enabled": true, }, } } // buildRedisValues construit les valeurs pour le chart redis. -func (h *HelmProvisioner) buildRedisValues(d Demo, persistent bool) map[string]interface{} { +// Toujours persistant (PVC) : voir Provision. +func (h *HelmProvisioner) buildRedisValues(d Demo) map[string]interface{} { return map[string]interface{}{ "service": map[string]interface{}{ "type": "ClusterIP", @@ -722,7 +676,7 @@ func (h *HelmProvisioner) buildRedisValues(d Demo, persistent bool) map[string]i "password": "demo-redis-pass", // Mot de passe simple pour les démos }, "persistence": map[string]interface{}{ - "enabled": persistent, + "enabled": true, }, } } diff --git a/control-plane/api/internal/demos/provisioner.go b/control-plane/api/internal/demos/provisioner.go index 529dff8..e37954c 100644 --- a/control-plane/api/internal/demos/provisioner.go +++ b/control-plane/api/internal/demos/provisioner.go @@ -10,9 +10,6 @@ type Provisioner interface { Provision(d Demo, resources []ExternalResource, cfg ProvisionConfig) error // Teardown demande la destruction d'une démo. Teardown(d Demo) error - // UpgradeToPaid bascule postgresql/redis en stockage persistant, en - // migrant les données postgres déjà créées (pg_dump/restore). - UpgradeToPaid(d Demo) error GetResourceState(ctx context.Context, namespace string) (ResourceState, error) } @@ -22,7 +19,6 @@ type NoopProvisioner struct{} func (NoopProvisioner) Provision(Demo, []ExternalResource, ProvisionConfig) error { return nil } func (NoopProvisioner) Teardown(Demo) error { return nil } -func (NoopProvisioner) UpgradeToPaid(Demo) error { return nil } func (NoopProvisioner) GetResourceState(ctx context.Context, namespace string) (ResourceState, error) { return ResourceState{}, nil } diff --git a/control-plane/api/internal/demos/service.go b/control-plane/api/internal/demos/service.go index 0f1e295..c11034c 100644 --- a/control-plane/api/internal/demos/service.go +++ b/control-plane/api/internal/demos/service.go @@ -135,11 +135,11 @@ func (s *Service) ListForUser(username string) ([]Demo, error) { } // TransferToPaid marque la démo active du client comme abonnement payant -// (n'expire plus) et déclenche la bascule en stockage persistant (migration -// des données postgres incluse, voir Provisioner.UpgradeToPaid). No-op si le -// client n'a pas de démo active — ce n'est pas une erreur (ex: compte créé -// sans jamais avoir demandé de démo). L'upgrade infra est lent (dump/ -// restore/helm) : il tourne en tâche de fond, cette méthode ne bloque pas. +// (n'expire plus). Le stockage (PVC postgres/redis) est déjà en place depuis +// la création de la démo (voir HelmProvisioner.Provision) : rien à basculer +// côté infra, seul le statut change. No-op si le client n'a pas de démo +// active — ce n'est pas une erreur (ex: compte créé sans jamais avoir +// demandé de démo). func (s *Service) TransferToPaid(username string) error { existing, err := s.store.ListByUsername(auth.NormalizeUsername(username)) if err != nil { @@ -159,17 +159,10 @@ func (s *Service) TransferToPaid(username string) error { active.TypeAbo = "premium" active.ExpiresAt = s.now().UTC().AddDate(10, 0, 0) // n'expire plus, en pratique - updated, err := s.store.Update(*active) - if err != nil { + if _, err := s.store.Update(*active); err != nil { return err } - go func() { - if err := s.prov.UpgradeToPaid(updated); err != nil { - log.Printf("Transfert payant échoué pour %s: %v", updated.Namespace, err) - } - }() - return nil }