chore: update
This commit is contained in:
@@ -104,6 +104,13 @@ func main() {
|
|||||||
log.Fatalf("k8s client: %v", err)
|
log.Fatalf("k8s client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config REST brute — nécessaire pour l'exec dans les pods (pg_dump /
|
||||||
|
// restore lors du passage d'une démo en abonnement payant).
|
||||||
|
restConfig, err := k8s.NewRESTConfig(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("k8s rest config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Client metrics.k8s.io (usage CPU/mémoire live) : optionnel, ne bloque
|
// Client metrics.k8s.io (usage CPU/mémoire live) : optionnel, ne bloque
|
||||||
// pas le démarrage si metrics-server n'est pas déployé sur le cluster.
|
// pas le démarrage si metrics-server n'est pas déployé sur le cluster.
|
||||||
metricsClient, err := k8s.NewMetricsClient(&cfg)
|
metricsClient, err := k8s.NewMetricsClient(&cfg)
|
||||||
@@ -112,7 +119,7 @@ func main() {
|
|||||||
metricsClient = nil
|
metricsClient = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
helmProv, err := demos.NewHelmProvisioner(&cfg, k8sClient, metricsClient, "/charts", "helm")
|
helmProv, err := demos.NewHelmProvisioner(&cfg, k8sClient, restConfig, metricsClient, "/charts", "helm")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("helm provisioner: %v", err)
|
log.Fatalf("helm provisioner: %v", err)
|
||||||
}
|
}
|
||||||
@@ -136,7 +143,6 @@ func main() {
|
|||||||
BaseDomain: cfg.DemoDomain,
|
BaseDomain: cfg.DemoDomain,
|
||||||
HTTPSPort: cfg.DemoHTTPSPort,
|
HTTPSPort: cfg.DemoHTTPSPort,
|
||||||
TTL: demos.TTL,
|
TTL: demos.TTL,
|
||||||
MaxDemos: demos.MaxConcurrentDemos,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
deps := router.Deps{
|
deps := router.Deps{
|
||||||
@@ -146,7 +152,7 @@ func main() {
|
|||||||
AuthH: auth.NewHandler(userStore, sessions, iss, cfg.Secure()),
|
AuthH: auth.NewHandler(userStore, sessions, iss, cfg.Secure()),
|
||||||
LeadsH: leads.NewHandler(leadStore),
|
LeadsH: leads.NewHandler(leadStore),
|
||||||
DemosH: demos.NewHandler(demoSvc, helmProv),
|
DemosH: demos.NewHandler(demoSvc, helmProv),
|
||||||
SubH: sub.NewHandler(codeStore),
|
SubH: sub.NewHandler(codeStore, demoSvc),
|
||||||
ProfileH: profile.NewHandler(profileStore),
|
ProfileH: profile.NewHandler(profileStore),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ require (
|
|||||||
k8s.io/api v0.36.3
|
k8s.io/api v0.36.3
|
||||||
k8s.io/apimachinery v0.36.3
|
k8s.io/apimachinery v0.36.3
|
||||||
k8s.io/client-go v0.36.3
|
k8s.io/client-go v0.36.3
|
||||||
|
k8s.io/metrics v0.36.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -38,6 +39,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/gnostic-models v0.7.0 // indirect
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
@@ -50,6 +52,7 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/moby/spdystream v0.5.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
@@ -72,7 +75,7 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/klog/v2 v2.140.0 // indirect
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||||
k8s.io/metrics v0.36.3 // indirect
|
k8s.io/streaming v0.36.3 // indirect
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
@@ -58,6 +60,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
@@ -91,6 +95,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
|||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
|
||||||
|
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -183,6 +189,8 @@ k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hk
|
|||||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||||
k8s.io/metrics v0.36.3 h1:NDKceAgWS8CJCdDtM5kFACkBOa9Lxia1jUiibJfvUgQ=
|
k8s.io/metrics v0.36.3 h1:NDKceAgWS8CJCdDtM5kFACkBOa9Lxia1jUiibJfvUgQ=
|
||||||
k8s.io/metrics v0.36.3/go.mod h1:NTLS8ybwn+zYGwKqYublWPvmnNp8N4pV3etjtx7XWaM=
|
k8s.io/metrics v0.36.3/go.mod h1:NTLS8ybwn+zYGwKqYublWPvmnNp8N4pV3etjtx7XWaM=
|
||||||
|
k8s.io/streaming v0.36.3 h1:9rAaqBk0C0Pc7+/fqGekj07NV+/Xrew58p647A0JT8w=
|
||||||
|
k8s.io/streaming v0.36.3/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
// dans le cluster (une démo = un namespace isolé, TTL 30 jours).
|
// dans le cluster (une démo = un namespace isolé, TTL 30 jours).
|
||||||
package demos
|
package demos
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Paramètres capacité / durée de vie.
|
// Paramètres durée de vie. Pas de quota sur le nombre de démos : seule
|
||||||
|
// limite réelle, le pool de ressources externes (voir PoolSizePerService).
|
||||||
const (
|
const (
|
||||||
TTL = 30 * 24 * time.Hour // durée de vie d'une démo
|
TTL = 30 * 24 * time.Hour // durée de vie d'une démo
|
||||||
MaxConcurrentDemos = 5 // capacité cible
|
|
||||||
PoolSizePerService = 5 // slots par service externe poolé
|
PoolSizePerService = 5 // slots par service externe poolé
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +24,48 @@ const (
|
|||||||
// PooledServices : services nécessitant un slot de pool par démo.
|
// PooledServices : services nécessitant un slot de pool par démo.
|
||||||
var PooledServices = []string{ServiceTelegram, ServiceNowPayments}
|
var PooledServices = []string{ServiceTelegram, ServiceNowPayments}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StorageLocal = "local"
|
||||||
|
StorageS3 = "s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidStorageDriver = errors.New("storage_driver doit être 'local' ou 's3'")
|
||||||
|
ErrS3ConfigIncomplete = errors.New("s3_bucket et s3_endpoint sont requis pour le stockage S3")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProvisionConfig : réglages saisis par l'admin à la création (popup de
|
||||||
|
// déploiement), propagés jusqu'au chart backend. Jamais persisté en base —
|
||||||
|
// notamment les secrets (token Telegram, clés NowPayments), qui ne
|
||||||
|
// transitent que par le fichier de valeurs Helm temporaire puis le Secret
|
||||||
|
// k8s du chart, comme les autres secrets.
|
||||||
|
type ProvisionConfig struct {
|
||||||
|
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é.
|
||||||
|
NowPaymentsAPIKey string
|
||||||
|
NowPaymentsIPNSecret string
|
||||||
|
StorageDriver string // "local" | "s3", "" => "local"
|
||||||
|
S3Bucket string
|
||||||
|
S3Endpoint string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate vérifie la cohérence du choix de stockage.
|
||||||
|
func (c ProvisionConfig) Validate() error {
|
||||||
|
switch c.StorageDriver {
|
||||||
|
case "", StorageLocal:
|
||||||
|
return nil
|
||||||
|
case StorageS3:
|
||||||
|
if c.S3Bucket == "" || c.S3Endpoint == "" {
|
||||||
|
return ErrS3ConfigIncomplete
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return ErrInvalidStorageDriver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Status : état d'une démo.
|
// Status : état d'une démo.
|
||||||
type Status string
|
type Status string
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ func NewHandler(svc *Service, helm *HelmProvisioner) *Handler {
|
|||||||
type createRequest struct {
|
type createRequest struct {
|
||||||
LeadID string `json:"lead_id" binding:"omitempty,uuid4"`
|
LeadID string `json:"lead_id" binding:"omitempty,uuid4"`
|
||||||
Username string `json:"username" binding:"omitempty,min=3,max=64,alphanum"`
|
Username string `json:"username" binding:"omitempty,min=3,max=64,alphanum"`
|
||||||
|
|
||||||
|
// Réglages saisis dans le popup de déploiement (voir ProvisionConfig).
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DetailDemoUserRequest struct {
|
type DetailDemoUserRequest struct {
|
||||||
@@ -43,13 +51,23 @@ func (h *Handler) Create(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "requête invalide"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d, err := h.svc.Create(req.LeadID, req.Username)
|
cfg := ProvisionConfig{
|
||||||
|
TelegramBotToken: req.TelegramBotToken,
|
||||||
|
NowPaymentsAPIKey: req.NowPaymentsAPIKey,
|
||||||
|
NowPaymentsIPNSecret: req.NowPaymentsIPNSecret,
|
||||||
|
StorageDriver: req.StorageDriver,
|
||||||
|
S3Bucket: req.S3Bucket,
|
||||||
|
S3Endpoint: req.S3Endpoint,
|
||||||
|
}
|
||||||
|
d, err := h.svc.Create(req.LeadID, req.Username, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, ErrCapacityReached):
|
case errors.Is(err, ErrPoolExhausted):
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "capacité maximale de démos atteinte"})
|
c.JSON(http.StatusConflict, gin.H{"error": "plus de ressources externes disponibles pour le moment (slots Telegram/NowPayments épuisés)"})
|
||||||
case errors.Is(err, ErrUserHasActiveDemo):
|
case errors.Is(err, ErrUserHasActiveDemo):
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "ce client a déjà une démo active"})
|
c.JSON(http.StatusConflict, gin.H{"error": "ce client a déjà une démo active"})
|
||||||
|
case errors.Is(err, ErrS3ConfigIncomplete), errors.Is(err, ErrInvalidStorageDriver):
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
default:
|
default:
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -17,6 +18,9 @@ import (
|
|||||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes"
|
"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"
|
metricsclient "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@ import (
|
|||||||
type HelmProvisioner struct {
|
type HelmProvisioner struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
k8sClient *kubernetes.Clientset
|
k8sClient *kubernetes.Clientset
|
||||||
|
restConfig *rest.Config // pour l'exec dans les pods (pg_dump/restore)
|
||||||
metricsClient *metricsclient.Clientset // optionnel : nil si metrics-server indisponible
|
metricsClient *metricsclient.Clientset // optionnel : nil si metrics-server indisponible
|
||||||
chartsDir string // chemin vers le dossier des charts (ex: /charts)
|
chartsDir string // chemin vers le dossier des charts (ex: /charts)
|
||||||
frontendImage string
|
frontendImage string
|
||||||
@@ -40,6 +45,7 @@ type HelmProvisioner struct {
|
|||||||
func NewHelmProvisioner(
|
func NewHelmProvisioner(
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
k8sClient *kubernetes.Clientset,
|
k8sClient *kubernetes.Clientset,
|
||||||
|
restConfig *rest.Config,
|
||||||
metricsClient *metricsclient.Clientset,
|
metricsClient *metricsclient.Clientset,
|
||||||
chartsDir string,
|
chartsDir string,
|
||||||
helmPath string,
|
helmPath string,
|
||||||
@@ -59,6 +65,7 @@ func NewHelmProvisioner(
|
|||||||
return &HelmProvisioner{
|
return &HelmProvisioner{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
k8sClient: k8sClient,
|
k8sClient: k8sClient,
|
||||||
|
restConfig: restConfig,
|
||||||
metricsClient: metricsClient,
|
metricsClient: metricsClient,
|
||||||
chartsDir: chartsDir,
|
chartsDir: chartsDir,
|
||||||
frontendImage: cfg.FrontendImage,
|
frontendImage: cfg.FrontendImage,
|
||||||
@@ -84,16 +91,18 @@ const (
|
|||||||
// 4. Installe le chart backend
|
// 4. Installe le chart backend
|
||||||
// 5. Installe le chart frontend
|
// 5. Installe le chart frontend
|
||||||
// 6. Raccorde la démo au Traefik partagé (WAF + routage HTTPS du sous-domaine)
|
// 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
|
// Créer le namespace
|
||||||
if err := h.createNamespace(d.Namespace); err != nil {
|
if err := h.createNamespace(d.Namespace); err != nil {
|
||||||
return fmt.Errorf("création namespace %s: %w", d.Namespace, err)
|
return fmt.Errorf("création namespace %s: %w", d.Namespace, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Préparer les valeurs
|
// Préparer les valeurs. Pas de persistence à la création : les démos
|
||||||
postgresValues := h.buildPostgresValues(d)
|
// d'essai sont éphémères (TTL 30j) — voir UpgradeToPaid pour le passage
|
||||||
redisValues := h.buildRedisValues(d)
|
// en stockage persistant lorsqu'un client devient payant.
|
||||||
backendValues := h.buildBackendValues(d, resources)
|
postgresValues := h.buildPostgresValues(d, false)
|
||||||
|
redisValues := h.buildRedisValues(d, false)
|
||||||
|
backendValues := h.buildBackendValues(d, resources, cfg)
|
||||||
frontendValues := h.buildFrontendValues(d)
|
frontendValues := h.buildFrontendValues(d)
|
||||||
ingressValues := h.buildIngressRouteValues(d)
|
ingressValues := h.buildIngressRouteValues(d)
|
||||||
|
|
||||||
@@ -162,6 +171,111 @@ func (h *HelmProvisioner) Teardown(d Demo) error {
|
|||||||
return h.deleteNamespace(d.Namespace)
|
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.
|
// createNamespace crée un namespace Kubernetes.
|
||||||
func (h *HelmProvisioner) createNamespace(name string) error {
|
func (h *HelmProvisioner) createNamespace(name string) error {
|
||||||
ns := &k8sCoreV1.Namespace{
|
ns := &k8sCoreV1.Namespace{
|
||||||
@@ -283,10 +397,53 @@ func parseImage(image string) (repo string, tag string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildBackendValues construit les valeurs pour le chart backend.
|
// 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
|
// Parser l'image backend pour séparer repository et tag
|
||||||
backendRepo, backendTag := parseImage(h.backendImage)
|
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{}{
|
values := map[string]interface{}{
|
||||||
"replicaCount": 1,
|
"replicaCount": 1,
|
||||||
"image": map[string]interface{}{
|
"image": map[string]interface{}{
|
||||||
@@ -303,23 +460,11 @@ func (h *HelmProvisioner) buildBackendValues(d Demo, resources []ExternalResourc
|
|||||||
},
|
},
|
||||||
"persistence": map[string]interface{}{
|
"persistence": map[string]interface{}{
|
||||||
"uploads": map[string]interface{}{
|
"uploads": map[string]interface{}{
|
||||||
"enabled": false,
|
"enabled": uploadsEnabled,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"env": map[string]string{
|
"env": env,
|
||||||
"DB_HOST": fmt.Sprintf("%s-postgresql-postgresql", d.Namespace),
|
"secrets": secrets,
|
||||||
"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),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return values
|
return values
|
||||||
@@ -375,7 +520,7 @@ func (h *HelmProvisioner) buildSecrets(resources []ExternalResource) map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildPostgresValues construit les valeurs pour le chart postgresql.
|
// 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{}{
|
return map[string]interface{}{
|
||||||
"auth": map[string]interface{}{
|
"auth": map[string]interface{}{
|
||||||
"password": "demo-postgres-pass", // Mot de passe OBLIGATOIRE (champ correct pour le chart)
|
"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,
|
"port": 5432,
|
||||||
},
|
},
|
||||||
"persistence": map[string]interface{}{
|
"persistence": map[string]interface{}{
|
||||||
"enabled": false,
|
"enabled": persistent,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRedisValues construit les valeurs pour le chart redis.
|
// 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{}{
|
return map[string]interface{}{
|
||||||
"service": map[string]interface{}{
|
"service": map[string]interface{}{
|
||||||
"type": "ClusterIP",
|
"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
|
"password": "demo-redis-pass", // Mot de passe simple pour les démos
|
||||||
},
|
},
|
||||||
"persistence": map[string]interface{}{
|
"persistence": map[string]interface{}{
|
||||||
"enabled": false, // Pas de persistence pour les démos
|
"enabled": persistent,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import "context"
|
|||||||
// contente d'enfiler les intentions.
|
// contente d'enfiler les intentions.
|
||||||
type Provisioner interface {
|
type Provisioner interface {
|
||||||
// Provision demande le déploiement d'une démo (asynchrone).
|
// Provision demande le déploiement d'une démo (asynchrone).
|
||||||
Provision(d Demo, resources []ExternalResource) error
|
Provision(d Demo, resources []ExternalResource, cfg ProvisionConfig) error
|
||||||
// Teardown demande la destruction d'une démo.
|
// Teardown demande la destruction d'une démo.
|
||||||
Teardown(d Demo) error
|
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)
|
GetResourceState(ctx context.Context, namespace string) (ResourceState, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,8 +20,9 @@ type Provisioner interface {
|
|||||||
// Sert au scaffolding et aux tests tant que le worker n'existe pas.
|
// Sert au scaffolding et aux tests tant que le worker n'existe pas.
|
||||||
type NoopProvisioner struct{}
|
type NoopProvisioner struct{}
|
||||||
|
|
||||||
func (NoopProvisioner) Provision(Demo, []ExternalResource) error { return nil }
|
func (NoopProvisioner) Provision(Demo, []ExternalResource, ProvisionConfig) error { return nil }
|
||||||
func (NoopProvisioner) Teardown(Demo) 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) {
|
func (NoopProvisioner) GetResourceState(ctx context.Context, namespace string) (ResourceState, error) {
|
||||||
return ResourceState{}, nil
|
return ResourceState{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrCapacityReached = errors.New("capacité maximale de démos atteinte")
|
|
||||||
ErrNotFound = errors.New("démo introuvable")
|
ErrNotFound = errors.New("démo introuvable")
|
||||||
ErrNotExtendable = errors.New("démo non prolongeable dans son état actuel")
|
ErrNotExtendable = errors.New("démo non prolongeable dans son état actuel")
|
||||||
ErrUserHasActiveDemo = errors.New("ce client a déjà une démo active")
|
ErrUserHasActiveDemo = errors.New("ce client a déjà une démo active")
|
||||||
@@ -22,7 +21,6 @@ type Config struct {
|
|||||||
BaseDomain string // ex. "demo.omnex.app"
|
BaseDomain string // ex. "demo.omnex.app"
|
||||||
HTTPSPort string // port public HTTPS pour l'URL affichée ("443" = omis, sinon ex. NodePort)
|
HTTPSPort string // port public HTTPS pour l'URL affichée ("443" = omis, sinon ex. NodePort)
|
||||||
TTL time.Duration // durée de vie
|
TTL time.Duration // durée de vie
|
||||||
MaxDemos int // capacité
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service : logique métier des démos (indépendante du transport HTTP).
|
// Service : logique métier des démos (indépendante du transport HTTP).
|
||||||
@@ -38,9 +36,6 @@ func NewService(store Store, pool Pool, prov Provisioner, cfg Config) *Service {
|
|||||||
if cfg.TTL == 0 {
|
if cfg.TTL == 0 {
|
||||||
cfg.TTL = TTL
|
cfg.TTL = TTL
|
||||||
}
|
}
|
||||||
if cfg.MaxDemos == 0 {
|
|
||||||
cfg.MaxDemos = MaxConcurrentDemos
|
|
||||||
}
|
|
||||||
if cfg.BaseDomain == "" {
|
if cfg.BaseDomain == "" {
|
||||||
cfg.BaseDomain = "demo.omnex.app"
|
cfg.BaseDomain = "demo.omnex.app"
|
||||||
}
|
}
|
||||||
@@ -52,8 +47,12 @@ func NewService(store Store, pool Pool, prov Provisioner, cfg Config) *Service {
|
|||||||
|
|
||||||
// Create réserve la capacité + le pool, persiste la démo et déclenche le worker.
|
// Create réserve la capacité + le pool, persiste la démo et déclenche le worker.
|
||||||
// username (optionnel) rattache la démo à un client existant. Un client n'a
|
// username (optionnel) rattache la démo à un client existant. Un client n'a
|
||||||
// droit qu'à une seule démo active à la fois.
|
// droit qu'à une seule démo active à la fois. cfg (bot Telegram, stockage)
|
||||||
func (s *Service) Create(leadID, username string) (Demo, error) {
|
// n'est jamais persisté : transmis tel quel au provisioning.
|
||||||
|
func (s *Service) Create(leadID, username string, cfg ProvisionConfig) (Demo, error) {
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return Demo{}, err
|
||||||
|
}
|
||||||
normalizedUsername := auth.NormalizeUsername(username)
|
normalizedUsername := auth.NormalizeUsername(username)
|
||||||
if normalizedUsername != "" {
|
if normalizedUsername != "" {
|
||||||
existing, err := s.store.ListByUsername(normalizedUsername)
|
existing, err := s.store.ListByUsername(normalizedUsername)
|
||||||
@@ -67,14 +66,6 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
active, err := s.store.CountActive()
|
|
||||||
if err != nil {
|
|
||||||
return Demo{}, err
|
|
||||||
}
|
|
||||||
if active >= s.cfg.MaxDemos {
|
|
||||||
return Demo{}, ErrCapacityReached
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uuid.NewString()
|
id := uuid.NewString()
|
||||||
demo := Demo{
|
demo := Demo{
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -95,12 +86,11 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
|
|||||||
}
|
}
|
||||||
demo.URL = "https://" + demo.Namespace + "." + s.cfg.BaseDomain + portSuffix
|
demo.URL = "https://" + demo.Namespace + "." + s.cfg.BaseDomain + portSuffix
|
||||||
|
|
||||||
// Réserve les ressources externes ; ErrPoolExhausted => capacité atteinte.
|
// Réserve les ressources externes (slots Telegram/NowPayments pré-
|
||||||
|
// provisionnés) — contrainte réelle indépendante du nombre de démos,
|
||||||
|
// non concernée par la suppression du quota.
|
||||||
resources, err := s.pool.Borrow(id)
|
resources, err := s.pool.Borrow(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrPoolExhausted) {
|
|
||||||
return Demo{}, ErrCapacityReached
|
|
||||||
}
|
|
||||||
return Demo{}, err
|
return Demo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +103,7 @@ func (s *Service) Create(leadID, username string) (Demo, error) {
|
|||||||
|
|
||||||
// Lance le déploiement de manière asynchrone (worker inline via goroutine).
|
// Lance le déploiement de manière asynchrone (worker inline via goroutine).
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.prov.Provision(created, resources); err != nil {
|
if err := s.prov.Provision(created, resources, cfg); err != nil {
|
||||||
log.Printf("Provisioning échoué pour %s: %v", created.ID, err)
|
log.Printf("Provisioning échoué pour %s: %v", created.ID, err)
|
||||||
created.Status = StatusFailed
|
created.Status = StatusFailed
|
||||||
_, _ = s.store.Update(created)
|
_, _ = s.store.Update(created)
|
||||||
@@ -144,6 +134,45 @@ func (s *Service) ListForUser(username string) ([]Demo, error) {
|
|||||||
return s.store.ListByUsername(auth.NormalizeUsername(username))
|
return s.store.ListByUsername(auth.NormalizeUsername(username))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (s *Service) TransferToPaid(username string) error {
|
||||||
|
existing, err := s.store.ListByUsername(auth.NormalizeUsername(username))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var active *Demo
|
||||||
|
for i := range existing {
|
||||||
|
if existing[i].Status.Active() {
|
||||||
|
active = &existing[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if active == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Delete déclenche le teardown et libère le pool.
|
// Delete déclenche le teardown et libère le pool.
|
||||||
func (s *Service) Delete(id string) (Demo, error) {
|
func (s *Service) Delete(id string) (Demo, error) {
|
||||||
d, ok := s.store.Get(id)
|
d, ok := s.store.Get(id)
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ func restConfig(cfg *config.Config) (*rest.Config, error) {
|
|||||||
return rest.InClusterConfig()
|
return rest.InClusterConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRESTConfig expose la config REST brute — nécessaire pour les appels qui
|
||||||
|
// ne passent pas par le Clientset généré (ex: exec dans un pod via
|
||||||
|
// remotecommand, utilisé pour la migration pg_dump/restore).
|
||||||
|
func NewRESTConfig(cfg *config.Config) (*rest.Config, error) {
|
||||||
|
return restConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
func NewClient(cfg *config.Config) (*kubernetes.Clientset, error) {
|
func NewClient(cfg *config.Config) (*kubernetes.Clientset, error) {
|
||||||
k8sCfg, err := restConfig(cfg)
|
k8sCfg, err := restConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,21 +2,24 @@ package sub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/omnex/control-plane/api/internal/auth"
|
"github.com/omnex/control-plane/api/internal/auth"
|
||||||
|
"github.com/omnex/control-plane/api/internal/demos"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler gère les requêtes HTTP pour les codes de souscription.
|
// Handler gère les requêtes HTTP pour les codes de souscription.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
store Store
|
store Store
|
||||||
|
demos *demos.Service // optionnel : nil => pas de transfert démo automatique
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler crée un nouveau Handler.
|
// NewHandler crée un nouveau Handler.
|
||||||
func NewHandler(store Store) *Handler {
|
func NewHandler(store Store, demoSvc *demos.Service) *Handler {
|
||||||
return &Handler{store: store}
|
return &Handler{store: store, demos: demoSvc}
|
||||||
}
|
}
|
||||||
|
|
||||||
// createCodeRequest représente la requête pour créer un code.
|
// createCodeRequest représente la requête pour créer un code.
|
||||||
@@ -122,5 +125,15 @@ func (h *Handler) AddCode(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "code invalide"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "code invalide"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transfert de la démo active (si elle existe) vers la version payante :
|
||||||
|
// n'expire plus + bascule stockage persistant en tâche de fond. Ne fait
|
||||||
|
// jamais échouer la validation du code — le client est déjà premium
|
||||||
|
// même si sa démo (optionnelle) pose problème.
|
||||||
|
if h.demos != nil {
|
||||||
|
if err := h.demos.TransferToPaid(p.Username); err != nil {
|
||||||
|
log.Printf("Transfert démo vers payant échoué pour %s: %v", p.Username, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"success": "vous passez en abonnement premium"})
|
c.JSON(http.StatusOK, gin.H{"success": "vous passez en abonnement premium"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,20 +39,34 @@ env:
|
|||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
REDIS_PORT: "6379"
|
REDIS_PORT: "6379"
|
||||||
API_PORT: "8080"
|
API_PORT: "8080"
|
||||||
|
STORAGE_DRIVER: local
|
||||||
|
S3_REGION: us-east-1
|
||||||
|
S3_BUCKET: ""
|
||||||
|
S3_ENDPOINT: ""
|
||||||
|
TELEGRAM_BOT_USERNAME: ""
|
||||||
|
LBTELEGRAM_URL: ""
|
||||||
|
LBTELEGRAM_BOT1_USERNAME: ""
|
||||||
|
LBTELEGRAM_BOT2_USERNAME: ""
|
||||||
|
|
||||||
# Valeurs sensibles — à surcharger via --set-string ou un Secret Manager externe
|
# Valeurs sensibles — à surcharger via --set-string ou un Secret Manager externe
|
||||||
secrets:
|
secrets:
|
||||||
DB_PASSWORD: ""
|
DB_PASSWORD: ""
|
||||||
SESSION_SECRET: ""
|
SESSION_SECRET: ""
|
||||||
USER_JWT_SECRET: ""
|
USER_JWT_SECRET: ""
|
||||||
USER_JWT_SECRET_OLD: ""
|
|
||||||
ADMIN_JWT_SECRET: ""
|
ADMIN_JWT_SECRET: ""
|
||||||
ADMIN_JWT_SECRET_OLD: ""
|
|
||||||
REDIS_PASSWORD: ""
|
REDIS_PASSWORD: ""
|
||||||
TOMTOM_API_KEY: ""
|
TOMTOM_API_KEY: ""
|
||||||
|
TOMTOM_API_KEY_1: ""
|
||||||
|
TOMTOM_API_KEY_2: ""
|
||||||
|
TOMTOM_API_KEY_3: ""
|
||||||
|
TELEGRAM_BOT_TOKEN: ""
|
||||||
TELEGRAM_WEBHOOK_URL: ""
|
TELEGRAM_WEBHOOK_URL: ""
|
||||||
TELEGRAM_WEBHOOK_SECRET: ""
|
TELEGRAM_WEBHOOK_SECRET: ""
|
||||||
|
NOWPAYMENTS_API_KEY: ""
|
||||||
NOWPAYMENTS_IPN_SECRET: ""
|
NOWPAYMENTS_IPN_SECRET: ""
|
||||||
|
RUSTFS_ACCESS_KEY: ""
|
||||||
|
RUSTFS_SECRET_KEY: ""
|
||||||
|
BACKEND_LINK_SECRET: ""
|
||||||
|
|
||||||
podAnnotations: {}
|
podAnnotations: {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: lbtelegram
|
||||||
|
description: LoadBalancer telegram
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "1.0.0"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{{- define "lbtelegram.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "lbtelegram.fullname" -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "lbtelegram.labels" -}}
|
||||||
|
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
|
||||||
|
app.kubernetes.io/name: {{ include "lbtelegram.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "lbtelegram.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "lbtelegram.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "lbtelegram.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "lbtelegram.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "lbtelegram.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "lbtelegram.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: lbtelegram
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ .Values.service.port }}
|
||||||
|
protocol: TCP
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: {{ include "lbtelegram.fullname" . }}-secrets
|
||||||
|
env:
|
||||||
|
{{- range $k, $v := .Values.env }}
|
||||||
|
- name: {{ $k }}
|
||||||
|
value: {{ $v | quote }}
|
||||||
|
{{- end }}
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "lbtelegram.fullname" . }}-secrets
|
||||||
|
labels:
|
||||||
|
{{- include "lbtelegram.labels" . | nindent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
{{- range $k, $v := .Values.secrets }}
|
||||||
|
{{ $k }}: {{ $v | quote }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "lbtelegram.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "lbtelegram.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
selector:
|
||||||
|
{{- include "lbtelegram.selectorLabels" . | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: lbtelegram
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
|
||||||
|
env:
|
||||||
|
PORT: "8081"
|
||||||
|
ENV: ""
|
||||||
|
BOT_COUNT: ""
|
||||||
|
GATEWAY_URL: ""
|
||||||
|
BOT1_USERNAME: ""
|
||||||
|
BOT2_USERNAME: ""
|
||||||
|
JWT_TTL_SECONDS: ""
|
||||||
|
LB_STRATEGY: ""
|
||||||
|
HEALTH_CHECK_INTERVAL: ""
|
||||||
|
BACKEND_LINK_URL: ""
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
BOT1_TOKEN: ""
|
||||||
|
BOT2_TOKEN: ""
|
||||||
|
BOT1_WEBHOOK_SECRET: ""
|
||||||
|
BOT2_WEBHOOK_SECRET: ""
|
||||||
|
BACKEND_LINK_SECRET: ""
|
||||||
|
JWT_SECRET: ""
|
||||||
|
# DSN complets (identifiants inclus) — à générer par le worker de provisioning
|
||||||
|
# vers les services postgresql/redis du même namespace de démo.
|
||||||
|
DATABASE_URL: ""
|
||||||
|
REDIS_URL: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
podSecurityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8081
|
||||||
+2
-1
@@ -17,7 +17,8 @@ REDIS_PASSWORD=630fa654c76f1256fea3ba3c51a718a4
|
|||||||
OMNEX_ENV=dev
|
OMNEX_ENV=dev
|
||||||
OMNEX_SEED_USERNAME=admin
|
OMNEX_SEED_USERNAME=admin
|
||||||
OMNEX_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
|
OMNEX_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||||
OMNEX_DEMO_DOMAIN=demo.omnex.app
|
OMNEX_DEMO_DOMAIN=192.168.1.31.nip.io
|
||||||
|
OMNEX_DEMO_HTTPS_PORT=31908
|
||||||
KUBECONFIG=/home/xor_fakers/.kube/config
|
KUBECONFIG=/home/xor_fakers/.kube/config
|
||||||
FRONTEND_IMAGE_APP=xor1234/frontend-mln:helm
|
FRONTEND_IMAGE_APP=xor1234/frontend-mln:helm
|
||||||
BACKEND_IMAGE_APP=xor1234/backend-mln:helm
|
BACKEND_IMAGE_APP=xor1234/backend-mln:helm
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ services:
|
|||||||
OMNEX_REDIS_URL: "redis://:${REDIS_PASSWORD:-omnexredis}@redis:6379/0"
|
OMNEX_REDIS_URL: "redis://:${REDIS_PASSWORD:-omnexredis}@redis:6379/0"
|
||||||
OMNEX_SEED_USERNAME: ${OMNEX_SEED_USERNAME:-admin}
|
OMNEX_SEED_USERNAME: ${OMNEX_SEED_USERNAME:-admin}
|
||||||
OMNEX_SEED_PASSWORD: ${OMNEX_SEED_PASSWORD}
|
OMNEX_SEED_PASSWORD: ${OMNEX_SEED_PASSWORD}
|
||||||
OMNEX_DEMO_DOMAIN: ${OMNEX_DEMO_DOMAIN:-demo.omnex.app}
|
OMNEX_DEMO_DOMAIN: ${OMNEX_DEMO_DOMAIN:-192.168.1.31.nip.io}
|
||||||
|
OMNEX_DEMO_HTTPS_PORT: ${OMNEX_DEMO_HTTPS_PORT:-443}
|
||||||
FRONTEND_IMAGE_APP: ${FRONTEND_IMAGE_APP:-xor1234/frontend-mln:latest}
|
FRONTEND_IMAGE_APP: ${FRONTEND_IMAGE_APP:-xor1234/frontend-mln:latest}
|
||||||
BACKEND_IMAGE_APP: ${BACKEND_IMAGE_APP:-xor1234/backend-mln:latest}
|
BACKEND_IMAGE_APP: ${BACKEND_IMAGE_APP:-xor1234/backend-mln:latest}
|
||||||
KUBECONFIG: /kubeconfig/config
|
KUBECONFIG: /kubeconfig/config
|
||||||
@@ -65,6 +66,12 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
# Le temps que le fix "EnsureSharedInfra en arrière-plan" soit déployé
|
||||||
|
# (voir control-plane/api/cmd/api/main.go) : laisse la marge pour que
|
||||||
|
# le "helm upgrade --install --wait --timeout 5m" bloquant échoue tout
|
||||||
|
# seul si le cluster est injoignable, plutôt que de faire échouer
|
||||||
|
# `docker compose up` avant même que le serveur HTTP démarre.
|
||||||
|
start_period: 330s
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
HStack,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@@ -14,6 +16,7 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Stack,
|
Stack,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
@@ -32,7 +35,14 @@ interface CreateDemoModalProps {
|
|||||||
export function CreateDemoModal({ isOpen, onClose, onCreated, leadId, lockedUsername }: CreateDemoModalProps) {
|
export function CreateDemoModal({ isOpen, onClose, onCreated, leadId, lockedUsername }: CreateDemoModalProps) {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const [username, setUsername] = useState(lockedUsername ?? '')
|
const [username, setUsername] = useState(lockedUsername ?? '')
|
||||||
|
|
||||||
|
const [enableTelegram, setEnableTelegram] = useState(false)
|
||||||
const [telegramBotToken, setTelegramBotToken] = useState('')
|
const [telegramBotToken, setTelegramBotToken] = useState('')
|
||||||
|
|
||||||
|
const [enableNowPayments, setEnableNowPayments] = useState(false)
|
||||||
|
const [nowPaymentsApiKey, setNowPaymentsApiKey] = useState('')
|
||||||
|
const [nowPaymentsIpnSecret, setNowPaymentsIpnSecret] = useState('')
|
||||||
|
|
||||||
const [storageDriver, setStorageDriver] = useState<StorageDriver>('local')
|
const [storageDriver, setStorageDriver] = useState<StorageDriver>('local')
|
||||||
const [s3Bucket, setS3Bucket] = useState('')
|
const [s3Bucket, setS3Bucket] = useState('')
|
||||||
const [s3Endpoint, setS3Endpoint] = useState('')
|
const [s3Endpoint, setS3Endpoint] = useState('')
|
||||||
@@ -40,7 +50,11 @@ export function CreateDemoModal({ isOpen, onClose, onCreated, leadId, lockedUser
|
|||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setUsername(lockedUsername ?? '')
|
setUsername(lockedUsername ?? '')
|
||||||
|
setEnableTelegram(false)
|
||||||
setTelegramBotToken('')
|
setTelegramBotToken('')
|
||||||
|
setEnableNowPayments(false)
|
||||||
|
setNowPaymentsApiKey('')
|
||||||
|
setNowPaymentsIpnSecret('')
|
||||||
setStorageDriver('local')
|
setStorageDriver('local')
|
||||||
setS3Bucket('')
|
setS3Bucket('')
|
||||||
setS3Endpoint('')
|
setS3Endpoint('')
|
||||||
@@ -65,7 +79,9 @@ export function CreateDemoModal({ isOpen, onClose, onCreated, leadId, lockedUser
|
|||||||
const params: CreateDemoParams = {
|
const params: CreateDemoParams = {
|
||||||
username: username.trim(),
|
username: username.trim(),
|
||||||
leadId,
|
leadId,
|
||||||
telegramBotToken: telegramBotToken.trim() || undefined,
|
telegramBotToken: enableTelegram ? telegramBotToken.trim() || undefined : undefined,
|
||||||
|
nowPaymentsApiKey: enableNowPayments ? nowPaymentsApiKey.trim() || undefined : undefined,
|
||||||
|
nowPaymentsIpnSecret: enableNowPayments ? nowPaymentsIpnSecret.trim() || undefined : undefined,
|
||||||
storageDriver,
|
storageDriver,
|
||||||
...(storageDriver === 's3'
|
...(storageDriver === 's3'
|
||||||
? { s3Bucket: s3Bucket.trim(), s3Endpoint: s3Endpoint.trim() }
|
? { s3Bucket: s3Bucket.trim(), s3Endpoint: s3Endpoint.trim() }
|
||||||
@@ -104,20 +120,70 @@ export function CreateDemoModal({ isOpen, onClose, onCreated, leadId, lockedUser
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{/* --- Telegram --- */}
|
||||||
<FormControl isDisabled={submitting}>
|
<FormControl isDisabled={submitting}>
|
||||||
<FormLabel>Token bot Telegram</FormLabel>
|
<HStack justify="space-between">
|
||||||
<Input
|
<FormLabel mb={0}>Bot Telegram</FormLabel>
|
||||||
placeholder="123456:ABC-DEF..."
|
<Switch
|
||||||
value={telegramBotToken}
|
isChecked={enableTelegram}
|
||||||
onChange={(e) => setTelegramBotToken(e.target.value)}
|
onChange={(e) => setEnableTelegram(e.target.checked)}
|
||||||
type="password"
|
/>
|
||||||
autoComplete="off"
|
</HStack>
|
||||||
/>
|
|
||||||
<Text fontSize="xs" color="gray.500" mt={1}>
|
|
||||||
Optionnel — laissez vide si le bot n'est pas encore prêt.
|
|
||||||
</Text>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{enableTelegram && (
|
||||||
|
<Box pl={3} borderLeftWidth="2px" borderColor="primary.500">
|
||||||
|
<FormControl isDisabled={submitting}>
|
||||||
|
<FormLabel fontSize="sm">Token bot Telegram</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder="123456:ABC-DEF..."
|
||||||
|
value={telegramBotToken}
|
||||||
|
onChange={(e) => setTelegramBotToken(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- NowPayments --- */}
|
||||||
|
<FormControl isDisabled={submitting}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<FormLabel mb={0}>NowPayments (paiement crypto)</FormLabel>
|
||||||
|
<Switch
|
||||||
|
isChecked={enableNowPayments}
|
||||||
|
onChange={(e) => setEnableNowPayments(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</FormControl>
|
||||||
|
{enableNowPayments && (
|
||||||
|
<Stack spacing={3} pl={3} borderLeftWidth="2px" borderColor="primary.500">
|
||||||
|
<FormControl isDisabled={submitting}>
|
||||||
|
<FormLabel fontSize="sm">Clé API NowPayments</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder="clé API du compte marchand"
|
||||||
|
value={nowPaymentsApiKey}
|
||||||
|
onChange={(e) => setNowPaymentsApiKey(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl isDisabled={submitting}>
|
||||||
|
<FormLabel fontSize="sm">Secret IPN NowPayments</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder="secret configuré côté NowPayments"
|
||||||
|
value={nowPaymentsIpnSecret}
|
||||||
|
onChange={(e) => setNowPaymentsIpnSecret(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" color="gray.500" mt={1}>
|
||||||
|
Laissez vide pour garder celui pré-généré automatiquement.
|
||||||
|
</Text>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- Stockage --- */}
|
||||||
<FormControl isDisabled={submitting}>
|
<FormControl isDisabled={submitting}>
|
||||||
<FormLabel>Stockage des fichiers</FormLabel>
|
<FormLabel>Stockage des fichiers</FormLabel>
|
||||||
<RadioGroup value={storageDriver} onChange={(v) => setStorageDriver(v as StorageDriver)}>
|
<RadioGroup value={storageDriver} onChange={(v) => setStorageDriver(v as StorageDriver)}>
|
||||||
@@ -129,17 +195,17 @@ export function CreateDemoModal({ isOpen, onClose, onCreated, leadId, lockedUser
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{storageDriver === 's3' && (
|
{storageDriver === 's3' && (
|
||||||
<Stack spacing={4} pl={1} borderLeftWidth="2px" borderColor="primary.500">
|
<Stack spacing={4} pl={3} borderLeftWidth="2px" borderColor="primary.500">
|
||||||
<FormControl isRequired isDisabled={submitting} pl={3}>
|
<FormControl isRequired isDisabled={submitting}>
|
||||||
<FormLabel>Nom du bucket</FormLabel>
|
<FormLabel fontSize="sm">Nom du bucket</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
placeholder="mon-bucket-demo"
|
placeholder="mon-bucket-demo"
|
||||||
value={s3Bucket}
|
value={s3Bucket}
|
||||||
onChange={(e) => setS3Bucket(e.target.value)}
|
onChange={(e) => setS3Bucket(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl isRequired isDisabled={submitting} pl={3}>
|
<FormControl isRequired isDisabled={submitting}>
|
||||||
<FormLabel>Endpoint S3</FormLabel>
|
<FormLabel fontSize="sm">Endpoint S3</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
placeholder="https://s3.exemple.com"
|
placeholder="https://s3.exemple.com"
|
||||||
value={s3Endpoint}
|
value={s3Endpoint}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ export interface CreateDemoParams {
|
|||||||
username: string
|
username: string
|
||||||
leadId?: string
|
leadId?: string
|
||||||
telegramBotToken?: string
|
telegramBotToken?: string
|
||||||
|
nowPaymentsApiKey?: string
|
||||||
|
nowPaymentsIpnSecret?: string
|
||||||
storageDriver: StorageDriver
|
storageDriver: StorageDriver
|
||||||
s3Bucket?: string
|
s3Bucket?: string
|
||||||
s3Endpoint?: string
|
s3Endpoint?: string
|
||||||
@@ -167,6 +169,8 @@ export const api = {
|
|||||||
username: params.username,
|
username: params.username,
|
||||||
...(params.leadId ? { lead_id: params.leadId } : {}),
|
...(params.leadId ? { lead_id: params.leadId } : {}),
|
||||||
...(params.telegramBotToken ? { telegram_bot_token: params.telegramBotToken } : {}),
|
...(params.telegramBotToken ? { telegram_bot_token: params.telegramBotToken } : {}),
|
||||||
|
...(params.nowPaymentsApiKey ? { nowpayments_api_key: params.nowPaymentsApiKey } : {}),
|
||||||
|
...(params.nowPaymentsIpnSecret ? { nowpayments_ipn_secret: params.nowPaymentsIpnSecret } : {}),
|
||||||
storage_driver: params.storageDriver,
|
storage_driver: params.storageDriver,
|
||||||
...(params.storageDriver === 's3'
|
...(params.storageDriver === 's3'
|
||||||
? { s3_bucket: params.s3Bucket, s3_endpoint: params.s3Endpoint }
|
? { s3_bucket: params.s3Bucket, s3_endpoint: params.s3Endpoint }
|
||||||
|
|||||||
Reference in New Issue
Block a user