|
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
@@ -206,6 +207,227 @@ func (h *HelmProvisioner) Teardown(d Demo) error {
|
|
|
|
|
return h.deleteNamespace(d.Namespace)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MigrateToPremiumNamespace reconstruit l'intégralité de la démo dans
|
|
|
|
|
// newNamespace ("premium-<client>") et y migre les données postgres —
|
|
|
|
|
// Kubernetes ne permet pas de renommer un namespace, il faut donc
|
|
|
|
|
// recréer toute la stack (postgres/redis/backend/frontend/ingressroute/
|
|
|
|
|
// lbtelegram) ailleurs.
|
|
|
|
|
//
|
|
|
|
|
// La configuration (bot Telegram, NowPayments, load-balancer...) n'est
|
|
|
|
|
// jamais persistée en base (voir ProvisionConfig) : elle est relue depuis
|
|
|
|
|
// les valeurs Helm de l'ancien déploiement ("helm get values"), qui
|
|
|
|
|
// contiennent exactement ce qui a été installé à l'origine, secrets
|
|
|
|
|
// inclus — jamais journalisés ni interprétés ici, seulement retransmis
|
|
|
|
|
// tels quels au nouveau déploiement. Seuls les champs qui référencent
|
|
|
|
|
// l'ancien namespace (DNS internes, URL du webhook Telegram) sont
|
|
|
|
|
// recalculés pour le nouveau (voir patchBackendValuesForNamespace /
|
|
|
|
|
// patchLBTelegramValuesForNamespace).
|
|
|
|
|
//
|
|
|
|
|
// L'ancien namespace n'est supprimé qu'une fois le nouveau confirmé
|
|
|
|
|
// opérationnel (rollout réussi) : en cas d'échec à n'importe quelle étape,
|
|
|
|
|
// la démo continue de fonctionner sous son ancienne adresse, rien n'est
|
|
|
|
|
// perdu. No-op si newNamespace == d.Namespace (déjà migrée, ex.
|
|
|
|
|
// renouvellement d'un client déjà premium).
|
|
|
|
|
func (h *HelmProvisioner) MigrateToPremiumNamespace(d Demo, newNamespace, newURL string) error {
|
|
|
|
|
oldNamespace := d.Namespace
|
|
|
|
|
if newNamespace == oldNamespace {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
backendValues, err := h.getReleaseValues(oldNamespace, oldNamespace+"-backend")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("lecture config backend existante: %w", err)
|
|
|
|
|
}
|
|
|
|
|
lbValues, lbEnabled := h.getReleaseValuesOptional(oldNamespace, oldNamespace+"-lbtelegram")
|
|
|
|
|
|
|
|
|
|
if err := h.createNamespace(newNamespace); err != nil {
|
|
|
|
|
return fmt.Errorf("création namespace %s: %w", newNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newDemo := d
|
|
|
|
|
newDemo.Namespace = newNamespace
|
|
|
|
|
newDemo.URL = newURL
|
|
|
|
|
|
|
|
|
|
if err := h.installChart(newNamespace, "postgresql", h.buildPostgresValues(newDemo)); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("déploiement postgresql: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := h.installChart(newNamespace, "redis", h.buildRedisValues(newDemo)); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("déploiement redis: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Données postgres : migrées par dump/restore (le nouveau PVC est
|
|
|
|
|
// 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 {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("migration données postgres: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var backendLinkSecret string
|
|
|
|
|
if lbEnabled {
|
|
|
|
|
backendLinkSecret = asStringMap(backendValues["secrets"])["BACKEND_LINK_SECRET"]
|
|
|
|
|
if backendLinkSecret == "" {
|
|
|
|
|
backendLinkSecret = randomSecret()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backendPatched := h.patchBackendValuesForNamespace(backendValues, newDemo, lbEnabled, backendLinkSecret)
|
|
|
|
|
if err := h.installChart(newNamespace, "backend", backendPatched); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("déploiement backend: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := h.installChart(newNamespace, "frontend", h.buildFrontendValues(newDemo)); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("déploiement frontend: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := h.installChart(newNamespace, "ingressroute", h.buildIngressRouteValues(newDemo)); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("déploiement ingressroute: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if lbEnabled {
|
|
|
|
|
lbPatched := h.patchLBTelegramValuesForNamespace(lbValues, newDemo, backendLinkSecret)
|
|
|
|
|
if err := h.installChart(newNamespace, "lbtelegram", lbPatched); err != nil {
|
|
|
|
|
h.deleteNamespace(newNamespace)
|
|
|
|
|
return fmt.Errorf("déploiement lbtelegram: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.waitForRollout(newNamespace); err != nil {
|
|
|
|
|
// Le nouveau namespace n'est PAS supprimé : les données y sont déjà
|
|
|
|
|
// migrées, un rollout en échec est réparable manuellement sans tout
|
|
|
|
|
// reperdre. L'ancien namespace reste lui aussi en place tant que le
|
|
|
|
|
// nouveau n'est pas confirmé opérationnel.
|
|
|
|
|
return fmt.Errorf("rollout du nouveau namespace %s: %w", newNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.deleteNamespace(oldNamespace); err != nil {
|
|
|
|
|
log.Printf("Warning: suppression ancien namespace %s échouée (nouveau namespace %s opérationnel): %v", oldNamespace, newNamespace, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Printf("Démo %s migrée vers le namespace premium %s", oldNamespace, newNamespace)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(dump) == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newPod, err := h.findPod(ctx, newNamespace, "postgresql")
|
|
|
|
|
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)
|
|
|
|
|
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 nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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{} {
|
|
|
|
|
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)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
if secrets["TELEGRAM_BOT_TOKEN"] != "" {
|
|
|
|
|
secrets["TELEGRAM_WEBHOOK_URL"] = newDemo.URL + "/webhook/telegram"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
values["env"] = env
|
|
|
|
|
values["secrets"] = secrets
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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{} {
|
|
|
|
|
env := asStringMap(values["env"])
|
|
|
|
|
secrets := asStringMap(values["secrets"])
|
|
|
|
|
|
|
|
|
|
host := fmt.Sprintf("%s.%s", newDemo.Namespace, h.baseDomain)
|
|
|
|
|
env["GATEWAY_URL"] = "https://" + host
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
values["host"] = host
|
|
|
|
|
values["env"] = env
|
|
|
|
|
values["secrets"] = secrets
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getReleaseValues récupère les valeurs Helm exactes utilisées par une
|
|
|
|
|
// release déjà installée ("helm get values -o json").
|
|
|
|
|
func (h *HelmProvisioner) getReleaseValues(namespace, release string) (map[string]interface{}, error) {
|
|
|
|
|
out, err := h.runHelmOutput("get", "values", release, "--namespace", namespace, "-o", "json")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var values map[string]interface{}
|
|
|
|
|
if err := json.Unmarshal(out, &values); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse valeurs helm de %s: %w", release, err)
|
|
|
|
|
}
|
|
|
|
|
return values, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getReleaseValuesOptional : comme getReleaseValues, mais renvoie
|
|
|
|
|
// simplement (nil, false) si la release n'existe pas (ex. lbtelegram non
|
|
|
|
|
// activé pour cette démo) plutôt qu'une erreur.
|
|
|
|
|
func (h *HelmProvisioner) getReleaseValuesOptional(namespace, release string) (map[string]interface{}, bool) {
|
|
|
|
|
values, err := h.getReleaseValues(namespace, release)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
return values, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// asStringMap convertit une valeur JSON décodée (map[string]interface{})
|
|
|
|
|
// en map[string]string, en ignorant silencieusement les clés dont la
|
|
|
|
|
// valeur n'est pas une chaîne.
|
|
|
|
|
func asStringMap(v interface{}) map[string]string {
|
|
|
|
|
out := map[string]string{}
|
|
|
|
|
m, ok := v.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
for k, val := range m {
|
|
|
|
|
if s, ok := val.(string); ok {
|
|
|
|
|
out[k] = s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createGestionAdmin crée le compte admin de l'application "gestion"
|
|
|
|
|
// déployée dans cette démo, en insérant directement dans la table "users"
|
|
|
|
|
// (aucune route API ne le permet : CreateUser refuse explicitement de créer
|
|
|
|
@@ -405,15 +627,22 @@ func (h *HelmProvisioner) writeValuesFile(namespace, chartName string, values ma
|
|
|
|
|
|
|
|
|
|
// runHelm exécute une commande helm et remonte stdout/stderr en cas d'erreur.
|
|
|
|
|
func (h *HelmProvisioner) runHelm(args ...string) error {
|
|
|
|
|
_, err := h.runHelmOutput(args...)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runHelmOutput exécute une commande helm et retourne son stdout (ex. "helm
|
|
|
|
|
// get values -o json").
|
|
|
|
|
func (h *HelmProvisioner) runHelmOutput(args ...string) ([]byte, error) {
|
|
|
|
|
cmd := exec.Command(h.helmPath, args...)
|
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("%v (stdout: %s, stderr: %s)", err, stdout.String(), stderr.String())
|
|
|
|
|
return nil, fmt.Errorf("%v (stdout: %s, stderr: %s)", err, stdout.String(), stderr.String())
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
return stdout.Bytes(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseImage(image string) (repo string, tag string) {
|
|
|
|
|