chore: update
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -17,6 +18,9 @@ import (
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||
)
|
||||
|
||||
@@ -25,6 +29,7 @@ import (
|
||||
type HelmProvisioner struct {
|
||||
cfg *config.Config
|
||||
k8sClient *kubernetes.Clientset
|
||||
restConfig *rest.Config // pour l'exec dans les pods (pg_dump/restore)
|
||||
metricsClient *metricsclient.Clientset // optionnel : nil si metrics-server indisponible
|
||||
chartsDir string // chemin vers le dossier des charts (ex: /charts)
|
||||
frontendImage string
|
||||
@@ -40,6 +45,7 @@ type HelmProvisioner struct {
|
||||
func NewHelmProvisioner(
|
||||
cfg *config.Config,
|
||||
k8sClient *kubernetes.Clientset,
|
||||
restConfig *rest.Config,
|
||||
metricsClient *metricsclient.Clientset,
|
||||
chartsDir string,
|
||||
helmPath string,
|
||||
@@ -59,6 +65,7 @@ func NewHelmProvisioner(
|
||||
return &HelmProvisioner{
|
||||
cfg: cfg,
|
||||
k8sClient: k8sClient,
|
||||
restConfig: restConfig,
|
||||
metricsClient: metricsClient,
|
||||
chartsDir: chartsDir,
|
||||
frontendImage: cfg.FrontendImage,
|
||||
@@ -84,16 +91,18 @@ const (
|
||||
// 4. Installe le chart backend
|
||||
// 5. Installe le chart frontend
|
||||
// 6. Raccorde la démo au Traefik partagé (WAF + routage HTTPS du sous-domaine)
|
||||
func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource) error {
|
||||
func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg ProvisionConfig) error {
|
||||
// Créer le namespace
|
||||
if err := h.createNamespace(d.Namespace); err != nil {
|
||||
return fmt.Errorf("création namespace %s: %w", d.Namespace, err)
|
||||
}
|
||||
|
||||
// Préparer les valeurs
|
||||
postgresValues := h.buildPostgresValues(d)
|
||||
redisValues := h.buildRedisValues(d)
|
||||
backendValues := h.buildBackendValues(d, resources)
|
||||
// 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)
|
||||
backendValues := h.buildBackendValues(d, resources, cfg)
|
||||
frontendValues := h.buildFrontendValues(d)
|
||||
ingressValues := h.buildIngressRouteValues(d)
|
||||
|
||||
@@ -162,6 +171,111 @@ func (h *HelmProvisioner) Teardown(d Demo) error {
|
||||
return h.deleteNamespace(d.Namespace)
|
||||
}
|
||||
|
||||
// Identifiants postgres des démos — mêmes valeurs codées en dur que
|
||||
// buildPostgresValues/buildBackendValues (voir ces fonctions).
|
||||
const (
|
||||
demoDBUser = "postgres"
|
||||
demoDBName = "demo_db"
|
||||
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{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, p := range pods.Items {
|
||||
if strings.Contains(p.Name, nameContains) {
|
||||
return p.Name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("aucun pod contenant %q dans %s", nameContains, namespace)
|
||||
}
|
||||
|
||||
// execInPod exécute une commande dans un conteneur et capture stdout/stderr.
|
||||
// stdin peut être nil (aucune entrée à fournir).
|
||||
func (h *HelmProvisioner) execInPod(ctx context.Context, namespace, pod, container string, command []string, stdin io.Reader) (string, string, error) {
|
||||
req := h.k8sClient.CoreV1().RESTClient().Post().
|
||||
Resource("pods").
|
||||
Name(pod).
|
||||
Namespace(namespace).
|
||||
SubResource("exec").
|
||||
VersionedParams(&k8sCoreV1.PodExecOptions{
|
||||
Container: container,
|
||||
Command: command,
|
||||
Stdin: stdin != nil,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
executor, err := remotecommand.NewSPDYExecutor(h.restConfig, "POST", req.URL())
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("exec setup: %w", err)
|
||||
}
|
||||
|
||||
var stdout, stderrBuf bytes.Buffer
|
||||
err = executor.StreamWithContext(ctx, remotecommand.StreamOptions{
|
||||
Stdin: stdin,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderrBuf,
|
||||
})
|
||||
if err != nil {
|
||||
return stdout.String(), stderrBuf.String(), err
|
||||
}
|
||||
return stdout.String(), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
// createNamespace crée un namespace Kubernetes.
|
||||
func (h *HelmProvisioner) createNamespace(name string) error {
|
||||
ns := &k8sCoreV1.Namespace{
|
||||
@@ -283,10 +397,53 @@ func parseImage(image string) (repo string, tag string) {
|
||||
}
|
||||
|
||||
// buildBackendValues construit les valeurs pour le chart backend.
|
||||
func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResource) map[string]interface{} {
|
||||
func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResource, cfg ProvisionConfig) map[string]interface{} {
|
||||
// Parser l'image backend pour séparer repository et tag
|
||||
backendRepo, backendTag := parseImage(h.backendImage)
|
||||
|
||||
storageDriver := cfg.StorageDriver
|
||||
if storageDriver == "" {
|
||||
storageDriver = StorageLocal
|
||||
}
|
||||
|
||||
// PVC uploads inutile si le stockage est S3 (rien n'est écrit sur le
|
||||
// disque local dans ce cas).
|
||||
uploadsEnabled := storageDriver == StorageLocal
|
||||
|
||||
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,
|
||||
}
|
||||
if storageDriver == StorageS3 {
|
||||
env["S3_BUCKET"] = cfg.S3Bucket
|
||||
env["S3_ENDPOINT"] = cfg.S3Endpoint
|
||||
}
|
||||
|
||||
secrets := h.buildSecrets(resources)
|
||||
if cfg.TelegramBotToken != "" {
|
||||
secrets["TELEGRAM_BOT_TOKEN"] = cfg.TelegramBotToken
|
||||
}
|
||||
if cfg.NowPaymentsAPIKey != "" {
|
||||
secrets["NOWPAYMENTS_API_KEY"] = cfg.NowPaymentsAPIKey
|
||||
}
|
||||
if cfg.NowPaymentsIPNSecret != "" {
|
||||
// Écrase le secret pré-provisionné par le pool (ServiceNowPayments) :
|
||||
// contrairement au webhook Telegram, l'IPN NowPayments doit
|
||||
// correspondre à ce qui est réellement configuré sur le compte
|
||||
// marchand NowPayments, pas à une valeur générée par le pool.
|
||||
secrets["NOWPAYMENTS_IPN_SECRET"] = cfg.NowPaymentsIPNSecret
|
||||
}
|
||||
|
||||
values := map[string]interface{}{
|
||||
"replicaCount": 1,
|
||||
"image": map[string]interface{}{
|
||||
@@ -303,23 +460,11 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
||||
},
|
||||
"persistence": map[string]interface{}{
|
||||
"uploads": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"enabled": uploadsEnabled,
|
||||
},
|
||||
},
|
||||
"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",
|
||||
},
|
||||
"secrets": h.buildSecrets(resources),
|
||||
"env": env,
|
||||
"secrets": secrets,
|
||||
}
|
||||
|
||||
return values
|
||||
@@ -375,7 +520,7 @@ func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]
|
||||
}
|
||||
|
||||
// buildPostgresValues construit les valeurs pour le chart postgresql.
|
||||
func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} {
|
||||
func (h *HelmProvisioner) buildPostgresValues(d Demo, persistent bool) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"auth": map[string]interface{}{
|
||||
"password": "demo-postgres-pass", // Mot de passe OBLIGATOIRE (champ correct pour le chart)
|
||||
@@ -387,13 +532,13 @@ func (h *HelmProvisioner) buildPostgresValues(d Demo) map[string]interface{} {
|
||||
"port": 5432,
|
||||
},
|
||||
"persistence": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"enabled": persistent,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildRedisValues construit les valeurs pour le chart redis.
|
||||
func (h *HelmProvisioner) buildRedisValues(d Demo) map[string]interface{} {
|
||||
func (h *HelmProvisioner) buildRedisValues(d Demo, persistent bool) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"service": map[string]interface{}{
|
||||
"type": "ClusterIP",
|
||||
@@ -403,7 +548,7 @@ func (h *HelmProvisioner) buildRedisValues(d Demo) map[string]interface{} {
|
||||
"password": "demo-redis-pass", // Mot de passe simple pour les démos
|
||||
},
|
||||
"persistence": map[string]interface{}{
|
||||
"enabled": false, // Pas de persistence pour les démos
|
||||
"enabled": persistent,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user