chore: update
This commit is contained in:
@@ -40,7 +40,12 @@ var (
|
||||
// transitent que par le fichier de valeurs Helm temporaire puis le Secret
|
||||
// k8s du chart, comme les autres secrets.
|
||||
type ProvisionConfig struct {
|
||||
TelegramBotToken string
|
||||
// TelegramBotUsername alimente TELEGRAM_BOT_USERNAME (lu par le backend
|
||||
// au démarrage). TELEGRAM_WEBHOOK_URL est calculée automatiquement
|
||||
// (URL publique de la démo) ; TELEGRAM_WEBHOOK_SECRET reste généré par
|
||||
// le pool de ressources (ServiceTelegram), jamais saisi par l'admin.
|
||||
TelegramBotUsername string
|
||||
TelegramBotToken string
|
||||
// NowPaymentsAPIKey écrase NOWPAYMENTS_API_KEY. NowPaymentsIPNSecret,
|
||||
// s'il est fourni, écrase le secret pré-provisionné par le pool
|
||||
// (ServiceNowPayments) — sinon celui du pool reste utilisé.
|
||||
@@ -49,6 +54,29 @@ type ProvisionConfig struct {
|
||||
StorageDriver string // "local" | "s3", "" => "local"
|
||||
S3Bucket string
|
||||
S3Endpoint string
|
||||
|
||||
// Load-balancer Telegram (chart lbtelegram) : installé seulement si au
|
||||
// moins un bot est renseigné. Le reste de la config du chart (DSN
|
||||
// postgres/redis, secrets JWT/webhook/backend-link) est généré par le
|
||||
// provisioner, pas saisi par l'admin.
|
||||
LBBot1Username string
|
||||
LBBot1Token string
|
||||
LBBot2Username string
|
||||
LBBot2Token string
|
||||
|
||||
// Compte admin de l'application déployée (pas le compte omnex de
|
||||
// l'admin qui lance la démo) — requis : sans lui, personne ne peut se
|
||||
// connecter au backoffice de la démo. Créé directement en base une fois
|
||||
// postgres/redis/backend/frontend Running (voir HelmProvisioner.Provision),
|
||||
// car la table users n'existe qu'après la migration du backend au
|
||||
// premier démarrage.
|
||||
AdminUsername string
|
||||
AdminPassword string
|
||||
}
|
||||
|
||||
// LBTelegramEnabled indique si au moins un bot du load-balancer est configuré.
|
||||
func (c ProvisionConfig) LBTelegramEnabled() bool {
|
||||
return c.LBBot1Username != "" || c.LBBot2Username != ""
|
||||
}
|
||||
|
||||
// Validate vérifie la cohérence du choix de stockage.
|
||||
|
||||
@@ -23,12 +23,23 @@ type createRequest struct {
|
||||
Username string `json:"username" binding:"omitempty,min=3,max=64,alphanum"`
|
||||
|
||||
// Réglages saisis dans le popup de déploiement (voir ProvisionConfig).
|
||||
TelegramBotUsername string `json:"telegram_bot_username" binding:"omitempty,max=64"`
|
||||
TelegramBotToken string `json:"telegram_bot_token" binding:"omitempty"`
|
||||
NowPaymentsAPIKey string `json:"nowpayments_api_key" binding:"omitempty"`
|
||||
NowPaymentsIPNSecret string `json:"nowpayments_ipn_secret" binding:"omitempty"`
|
||||
StorageDriver string `json:"storage_driver" binding:"omitempty,oneof=local s3"`
|
||||
S3Bucket string `json:"s3_bucket" binding:"omitempty,max=63"`
|
||||
S3Endpoint string `json:"s3_endpoint" binding:"omitempty,max=255"`
|
||||
|
||||
LBBot1Username string `json:"lb_bot1_username" binding:"omitempty,max=64"`
|
||||
LBBot1Token string `json:"lb_bot1_token" binding:"omitempty"`
|
||||
LBBot2Username string `json:"lb_bot2_username" binding:"omitempty,max=64"`
|
||||
LBBot2Token string `json:"lb_bot2_token" binding:"omitempty"`
|
||||
|
||||
// Compte admin de l'application déployée — requis, sans lui personne ne
|
||||
// peut se connecter au backoffice de la démo.
|
||||
AdminUsername string `json:"admin_username" binding:"required,min=3,max=64,alphanum"`
|
||||
AdminPassword string `json:"admin_password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
type DetailDemoUserRequest struct {
|
||||
@@ -52,12 +63,19 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
cfg := ProvisionConfig{
|
||||
TelegramBotUsername: req.TelegramBotUsername,
|
||||
TelegramBotToken: req.TelegramBotToken,
|
||||
NowPaymentsAPIKey: req.NowPaymentsAPIKey,
|
||||
NowPaymentsIPNSecret: req.NowPaymentsIPNSecret,
|
||||
StorageDriver: req.StorageDriver,
|
||||
S3Bucket: req.S3Bucket,
|
||||
S3Endpoint: req.S3Endpoint,
|
||||
LBBot1Username: req.LBBot1Username,
|
||||
LBBot1Token: req.LBBot1Token,
|
||||
LBBot2Username: req.LBBot2Username,
|
||||
LBBot2Token: req.LBBot2Token,
|
||||
AdminUsername: req.AdminUsername,
|
||||
AdminPassword: req.AdminPassword,
|
||||
}
|
||||
d, err := h.svc.Create(req.LeadID, req.Username, cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,8 @@ package demos
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
|
||||
"github.com/omnex/control-plane/api/internal/config"
|
||||
"go.yaml.in/yaml/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
k8sCoreV1 "k8s.io/api/core/v1"
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -102,7 +105,16 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
|
||||
// en stockage persistant lorsqu'un client devient payant.
|
||||
postgresValues := h.buildPostgresValues(d, false)
|
||||
redisValues := h.buildRedisValues(d, false)
|
||||
backendValues := h.buildBackendValues(d, resources, cfg)
|
||||
|
||||
// Secret partagé backend <-> lbtelegram (authentifie les appels de
|
||||
// lbtelegram vers backend) — généré une seule fois si le load-balancer
|
||||
// Telegram est activé, vide sinon (le chart lbtelegram n'est pas installé).
|
||||
var backendLinkSecret string
|
||||
if cfg.LBTelegramEnabled() {
|
||||
backendLinkSecret = randomSecret()
|
||||
}
|
||||
|
||||
backendValues := h.buildBackendValues(d, resources, cfg, backendLinkSecret)
|
||||
frontendValues := h.buildFrontendValues(d)
|
||||
ingressValues := h.buildIngressRouteValues(d)
|
||||
|
||||
@@ -137,9 +149,28 @@ func (h *HelmProvisioner) Provision(d Demo, resources []ExternalResource, cfg Pr
|
||||
return fmt.Errorf("déploiement ingressroute: %w", err)
|
||||
}
|
||||
|
||||
// Attendre que les pods soient prêts
|
||||
// Load-balancer Telegram : optionnel, seulement si l'admin a renseigné
|
||||
// au moins un bot.
|
||||
if cfg.LBTelegramEnabled() {
|
||||
lbValues := h.buildLBTelegramValues(d, cfg, backendLinkSecret)
|
||||
if err := h.installChart(d.Namespace, "lbtelegram", lbValues); err != nil {
|
||||
h.deleteNamespace(d.Namespace)
|
||||
return fmt.Errorf("déploiement lbtelegram: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attendre que postgres/redis/backend/frontend soient tous Running avant
|
||||
// de créer le compte admin : la table "users" n'existe qu'une fois que
|
||||
// le backend a fait tourner sa migration au démarrage, donc tenter
|
||||
// l'insertion avant que le rollout ne soit confirmé échouerait
|
||||
// ("relation users does not exist").
|
||||
if err := h.waitForRollout(d.Namespace); err != nil {
|
||||
log.Printf("Warning: rollout check échoué pour %s: %v", d.Namespace, err)
|
||||
log.Printf("Warning: rollout check échoué pour %s: %v — compte admin non créé", d.Namespace, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.createGestionAdmin(context.Background(), d.Namespace, cfg.AdminUsername, cfg.AdminPassword); err != nil {
|
||||
log.Printf("Warning: création du compte admin échouée pour %s: %v", d.Namespace, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -171,6 +202,53 @@ func (h *HelmProvisioner) Teardown(d Demo) error {
|
||||
return h.deleteNamespace(d.Namespace)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// (bcrypt.CompareHashAndPassword côté LoginAdmin).
|
||||
func (h *HelmProvisioner) createGestionAdmin(ctx context.Context, namespace, username, password string) error {
|
||||
if username == "" || password == "" {
|
||||
return fmt.Errorf("username/password admin manquants")
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash bcrypt: %w", err)
|
||||
}
|
||||
|
||||
pgPod, err := h.findPod(ctx, namespace, "postgresql")
|
||||
if err != nil {
|
||||
return fmt.Errorf("pod postgresql introuvable: %w", err)
|
||||
}
|
||||
|
||||
// Échappement basique des quotes simples : le username est déjà validé
|
||||
// alphanumérique côté handler HTTP, le hash bcrypt ne contient jamais de
|
||||
// guillemet (alphabet base64 restreint ./A-Za-z0-9).
|
||||
escapedUsername := strings.ReplaceAll(username, "'", "''")
|
||||
sql := fmt.Sprintf(
|
||||
`INSERT INTO users (username, password, role) VALUES ('%s', '%s', 'admin') ON CONFLICT (username) DO NOTHING;`,
|
||||
escapedUsername, string(hash),
|
||||
)
|
||||
shellCmd := fmt.Sprintf("PGPASSWORD=%s psql -h localhost -U %s -d %s -v ON_ERROR_STOP=1 -c %s",
|
||||
demoDBPass, demoDBUser, demoDBName, shellQuote(sql))
|
||||
|
||||
_, stderr, err := h.execInPod(ctx, namespace, pgPod, "postgresql", []string{"sh", "-c", shellCmd}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insertion compte admin: %w (%s)", err, stderr)
|
||||
}
|
||||
|
||||
log.Printf("Compte admin %q créé pour la démo %s", username, namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// shellQuote entoure une chaîne de guillemets simples pour un usage sûr dans
|
||||
// une commande sh -c (échappe les guillemets simples qu'elle contient).
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// Identifiants postgres des démos — mêmes valeurs codées en dur que
|
||||
// buildPostgresValues/buildBackendValues (voir ces fonctions).
|
||||
const (
|
||||
@@ -397,7 +475,9 @@ func parseImage(image string) (repo string, tag string) {
|
||||
}
|
||||
|
||||
// buildBackendValues construit les valeurs pour le chart backend.
|
||||
func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResource, cfg ProvisionConfig) map[string]interface{} {
|
||||
// backendLinkSecret : partagé avec le chart lbtelegram (voir
|
||||
// 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{} {
|
||||
// Parser l'image backend pour séparer repository et tag
|
||||
backendRepo, backendTag := parseImage(h.backendImage)
|
||||
|
||||
@@ -429,9 +509,17 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
||||
env["S3_ENDPOINT"] = cfg.S3Endpoint
|
||||
}
|
||||
|
||||
if cfg.TelegramBotUsername != "" {
|
||||
env["TELEGRAM_BOT_USERNAME"] = cfg.TelegramBotUsername
|
||||
}
|
||||
|
||||
secrets := h.buildSecrets(resources)
|
||||
if cfg.TelegramBotToken != "" {
|
||||
secrets["TELEGRAM_BOT_TOKEN"] = cfg.TelegramBotToken
|
||||
// URL publique du webhook Telegram de cette démo (route backend :
|
||||
// POST /webhook/telegram, sans préfixe /api/v1). TELEGRAM_WEBHOOK_SECRET
|
||||
// reste celui généré par le pool (h.buildSecrets ci-dessus).
|
||||
secrets["TELEGRAM_WEBHOOK_URL"] = d.URL + "/webhook/telegram"
|
||||
}
|
||||
if cfg.NowPaymentsAPIKey != "" {
|
||||
secrets["NOWPAYMENTS_API_KEY"] = cfg.NowPaymentsAPIKey
|
||||
@@ -443,6 +531,12 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
||||
// marchand NowPayments, pas à une valeur générée par le pool.
|
||||
secrets["NOWPAYMENTS_IPN_SECRET"] = cfg.NowPaymentsIPNSecret
|
||||
}
|
||||
if cfg.LBTelegramEnabled() {
|
||||
env["LBTELEGRAM_URL"] = fmt.Sprintf("http://%s-lbtelegram-lbtelegram.%s.svc.cluster.local:8081", d.Namespace, d.Namespace)
|
||||
env["LBTELEGRAM_BOT1_USERNAME"] = cfg.LBBot1Username
|
||||
env["LBTELEGRAM_BOT2_USERNAME"] = cfg.LBBot2Username
|
||||
secrets["BACKEND_LINK_SECRET"] = backendLinkSecret
|
||||
}
|
||||
|
||||
values := map[string]interface{}{
|
||||
"replicaCount": 1,
|
||||
@@ -505,6 +599,58 @@ func (h *HelmProvisioner) buildIngressRouteValues(d Demo) map[string]interface{}
|
||||
}
|
||||
}
|
||||
|
||||
// buildLBTelegramValues construit les valeurs pour le chart lbtelegram
|
||||
// (load-balancer multi-bots Telegram). N'est appelé que si
|
||||
// cfg.LBTelegramEnabled() — au moins un bot renseigné par l'admin. Les
|
||||
// identifiants des bots (username/token) viennent de l'admin ; le reste
|
||||
// (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).
|
||||
func (h *HelmProvisioner) buildLBTelegramValues(d Demo, cfg ProvisionConfig, backendLinkSecret string) map[string]interface{} {
|
||||
botCount := 0
|
||||
if cfg.LBBot1Username != "" {
|
||||
botCount++
|
||||
}
|
||||
if cfg.LBBot2Username != "" {
|
||||
botCount++
|
||||
}
|
||||
|
||||
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)
|
||||
redisURL := fmt.Sprintf("redis://:demo-redis-pass@%s-redis-redis:6379/0", d.Namespace)
|
||||
|
||||
return map[string]interface{}{
|
||||
"env": map[string]string{
|
||||
"PORT": "8081",
|
||||
"ENV": "production",
|
||||
"BOT_COUNT": fmt.Sprintf("%d", botCount),
|
||||
"BOT1_USERNAME": cfg.LBBot1Username,
|
||||
"BOT2_USERNAME": cfg.LBBot2Username,
|
||||
"BACKEND_LINK_URL": backendURL,
|
||||
},
|
||||
"secrets": map[string]string{
|
||||
"BOT1_TOKEN": cfg.LBBot1Token,
|
||||
"BOT2_TOKEN": cfg.LBBot2Token,
|
||||
"BOT1_WEBHOOK_SECRET": randomSecret(),
|
||||
"BOT2_WEBHOOK_SECRET": randomSecret(),
|
||||
"BACKEND_LINK_SECRET": backendLinkSecret,
|
||||
"JWT_SECRET": randomSecret(),
|
||||
"DATABASE_URL": dbURL,
|
||||
"REDIS_URL": redisURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// randomSecret génère un secret hexadécimal aléatoire de 32 octets (256 bits).
|
||||
func randomSecret() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// crypto/rand.Read ne devrait jamais échouer sur un système normal ;
|
||||
// fail-secure plutôt que de retourner un secret prévisible.
|
||||
panic(fmt.Sprintf("randomSecret: %v", err))
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// buildSecrets extrait les références des secrets des ressources externes.
|
||||
func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]string {
|
||||
secrets := map[string]string{}
|
||||
|
||||
Reference in New Issue
Block a user