From 4b3a5720a38c0b80d10ac948cb571d82cdf39bab Mon Sep 17 00:00:00 2001 From: Nuxgrid Date: Tue, 28 Jul 2026 14:44:48 +0200 Subject: [PATCH] feat: add details demo --- control-plane/api/internal/demos/demos.go | 28 ----------- control-plane/api/internal/demos/handler.go | 48 ++++++++++++++++--- .../api/internal/demos/helm_provisioner.go | 36 ++++++++++++++ control-plane/api/internal/demos/mem.go | 12 +++++ control-plane/api/internal/demos/models.go | 37 ++++++++++++++ .../api/internal/demos/provisioner.go | 3 ++ control-plane/api/internal/demos/store.go | 12 +++++ control-plane/api/internal/router/router.go | 5 +- 8 files changed, 143 insertions(+), 38 deletions(-) create mode 100644 control-plane/api/internal/demos/models.go diff --git a/control-plane/api/internal/demos/demos.go b/control-plane/api/internal/demos/demos.go index d2e0be9..77707d4 100644 --- a/control-plane/api/internal/demos/demos.go +++ b/control-plane/api/internal/demos/demos.go @@ -59,21 +59,6 @@ func (s Status) Extendable() bool { return false } -// Demo : modèle GORM d'une démo. -type Demo struct { - ID string `gorm:"type:uuid;primaryKey" json:"id"` - Username string `gorm:"type:varchar(64);index" json:"username,omitempty"` - LeadID string `gorm:"type:varchar(36);index" json:"lead_id,omitempty"` - Status Status `gorm:"size:20;not null;index" json:"status"` - Namespace string `gorm:"size:63;uniqueIndex" json:"namespace"` - URL string `gorm:"size:255" json:"url"` - TypeAbo string `gorm:"type:varchar(35)" json:"type_abonnement"` - CreatedAt time.Time `json:"created_at"` - ExpiresAt time.Time `json:"expires_at"` -} - -func (Demo) TableName() string { return "demos" } - // ResourceStatus : état d'un slot de pool. type ResourceStatus string @@ -81,16 +66,3 @@ const ( ResourceFree ResourceStatus = "free" ResourceBorrowed ResourceStatus = "borrowed" ) - -// ExternalResource : slot pré-provisionné (table external_pool). -// On stocke une *référence* au secret (SecretRef), jamais le secret en clair. -type ExternalResource struct { - ID uint `gorm:"primaryKey" json:"id"` - Service string `gorm:"size:20;not null;index:idx_service_status" json:"service"` - Label string `gorm:"size:120" json:"label"` // ex. "@omnex_demo_bot_1" - SecretRef string `gorm:"size:200;not null" json:"-"` // clé dans le secret manager - Status ResourceStatus `gorm:"size:20;not null;index:idx_service_status" json:"status"` - DemoID string `gorm:"type:varchar(36);index" json:"demo_id,omitempty"` // FK optionnelle (vide si libre) -} - -func (ExternalResource) TableName() string { return "external_pool" } diff --git a/control-plane/api/internal/demos/handler.go b/control-plane/api/internal/demos/handler.go index 5794d1b..ef4e628 100644 --- a/control-plane/api/internal/demos/handler.go +++ b/control-plane/api/internal/demos/handler.go @@ -9,7 +9,10 @@ import ( "github.com/omnex/control-plane/api/internal/auth" ) -type Handler struct{ svc *Service } +type Handler struct { + svc *Service + helm *HelmProvisioner // interface déjà implémentée par HelmProvisioner +} func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } @@ -18,6 +21,16 @@ type createRequest struct { Username string `json:"username" binding:"omitempty,min=3,max=64,alphanum"` } +type DetailDemoUserRequest struct { + Username string `json:"username" binding:"required,min=3,max=64,alphanum"` +} + +type DemoDetailsResponse struct { + Username string `json:"username"` + Namespace string `json:"namespace"` + State ResourceState `json:"state"` +} + // Create : POST /demos — provisionne une démo (202 Accepted, worker asynchrone). func (h *Handler) Create(c *gin.Context) { var req createRequest @@ -47,19 +60,40 @@ func (h *Handler) List(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"items": items}) } -// ListMine : GET /demos/mine — démos rattachées au client authentifié. -func (h *Handler) ListMine(c *gin.Context) { +func (h *Handler) ListDetails(c *gin.Context) { p := auth.PrincipalFrom(c) if p == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"}) return } - items, err := h.svc.ListForUser(p.Username) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"}) + if p.Role != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "vous n'avez pas les droits"}) return } - c.JSON(http.StatusOK, gin.H{"items": items}) + + var req DetailDemoUserRequest + if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "username requis"}) + return + } + + demo, err := h.svc.store.GetDemoByUsername(req.Username) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "démo introuvable"}) + return + } + + state, err := h.helm.GetResourceState(c.Request.Context(), demo.Namespace) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "impossible de récupérer l'état des ressources"}) + return + } + + c.JSON(http.StatusOK, DemoDetailsResponse{ + Username: req.Username, + Namespace: demo.Namespace, + State: state, + }) } // Get : GET /demos/:id. diff --git a/control-plane/api/internal/demos/helm_provisioner.go b/control-plane/api/internal/demos/helm_provisioner.go index 855fba1..9f88a82 100644 --- a/control-plane/api/internal/demos/helm_provisioner.go +++ b/control-plane/api/internal/demos/helm_provisioner.go @@ -335,3 +335,39 @@ func (h *HelmProvisioner) waitForRollout(namespace string) error { } } } + +func (h *HelmProvisioner) GetResourceState( + ctx context.Context, + namespace string, +) (ResourceState, error) { + pods, err := h.k8sClient. + CoreV1(). + Pods(namespace). + List(ctx, metav1.ListOptions{}) + + if err != nil { + return ResourceState{}, err + } + + var state ResourceState + + for _, pod := range pods.Items { + status := string(pod.Status.Phase) + + switch { + case strings.Contains(pod.Name, "backend"): + state.APIState = status + + case strings.Contains(pod.Name, "frontend"): + state.WebState = status + + case strings.Contains(pod.Name, "postgresql"): + state.DBState = status + + case strings.Contains(pod.Name, "redis"): + state.DBMemoryState = status + } + } + + return state, nil +} diff --git a/control-plane/api/internal/demos/mem.go b/control-plane/api/internal/demos/mem.go index 45c64a1..f1b8c36 100644 --- a/control-plane/api/internal/demos/mem.go +++ b/control-plane/api/internal/demos/mem.go @@ -65,6 +65,18 @@ func (m *MemStore) CountActive() (int, error) { return n, nil } +func (m *MemStore) GetDemoByUsername(username string) (Demo, error) { + m.mu.Lock() + defer m.mu.Unlock() + + for _, demo := range m.items { + if demo.Username == username { + return demo, nil + } + } + return Demo{}, ErrNotFound +} + // MemPool : Pool en mémoire (dev/tests), PoolSizePerService slots par service. type MemPool struct { mu sync.Mutex diff --git a/control-plane/api/internal/demos/models.go b/control-plane/api/internal/demos/models.go new file mode 100644 index 0000000..319e027 --- /dev/null +++ b/control-plane/api/internal/demos/models.go @@ -0,0 +1,37 @@ +package demos + +import "time" + +type Demo struct { + ID string `gorm:"type:uuid;primaryKey" json:"id"` + Username string `gorm:"type:varchar(64);index" json:"username,omitempty"` + LeadID string `gorm:"type:varchar(36);index" json:"lead_id,omitempty"` + Status Status `gorm:"size:20;not null;index" json:"status"` + Namespace string `gorm:"size:63;uniqueIndex" json:"namespace"` + URL string `gorm:"size:255" json:"url"` + TypeAbo string `gorm:"type:varchar(35)" json:"type_abonnement"` + CreatedAt time.Time `json:"created_at"` + ExpiresAt time.Time `json:"expires_at"` +} + +func (Demo) TableName() string { return "demos" } + +type ExternalResource struct { + ID uint `gorm:"primaryKey" json:"id"` + Service string `gorm:"size:20;not null;index:idx_service_status" json:"service"` + Label string `gorm:"size:120" json:"label"` // ex. "@omnex_demo_bot_1" + SecretRef string `gorm:"size:200;not null" json:"-"` // clé dans le secret manager + Status ResourceStatus `gorm:"size:20;not null;index:idx_service_status" json:"status"` + DemoID string `gorm:"type:varchar(36);index" json:"demo_id,omitempty"` // FK optionnelle (vide si libre) +} + +func (ExternalResource) TableName() string { return "external_pool" } + +type ResourceState struct { + APIState string `gorm:"size:20;not null" json:"api_state"` + WebState string `gorm:"size:20;not null" json:"web_state"` + DBState string `gorm:"size:20;not null" json:"db_state"` + DBMemoryState string `gorm:"size:20;not null" json:"dbm_state"` +} + +func (ResourceState) TableName() string { return "ressource_state" } diff --git a/control-plane/api/internal/demos/provisioner.go b/control-plane/api/internal/demos/provisioner.go index eb59823..972373f 100644 --- a/control-plane/api/internal/demos/provisioner.go +++ b/control-plane/api/internal/demos/provisioner.go @@ -1,5 +1,7 @@ package demos +import "context" + // Provisioner : abstraction du worker qui déploie/détruit la stack dans le cluster. // L'implémentation réelle (Helm + client-go) sera branchée plus tard ; l'API se // contente d'enfiler les intentions. @@ -8,6 +10,7 @@ type Provisioner interface { Provision(d Demo, resources []ExternalResource) error // Teardown demande la destruction d'une démo. Teardown(d Demo) error + GetResourceState(ctx context.Context, namespace string) (ResourceState, error) } // NoopProvisioner : implémentation neutre (aucun déploiement réel). diff --git a/control-plane/api/internal/demos/store.go b/control-plane/api/internal/demos/store.go index 7b7af11..8e8068d 100644 --- a/control-plane/api/internal/demos/store.go +++ b/control-plane/api/internal/demos/store.go @@ -14,6 +14,7 @@ type Store interface { ListByUsername(username string) ([]Demo, error) Update(d Demo) (Demo, error) CountActive() (int, error) + GetDemoByUsername(username string) (Demo, error) } // GormStore : Store adossé à PostgreSQL via GORM. @@ -68,3 +69,14 @@ func (s *GormStore) CountActive() (int, error) { Count(&n).Error return int(n), err } + +func (s *GormStore) GetDemoByUsername(username string) (Demo, error) { + var demo Demo + + err := s.db. + Where("username = ?", username). + First(&demo). + Error + + return demo, err +} diff --git a/control-plane/api/internal/router/router.go b/control-plane/api/internal/router/router.go index da43f86..635da61 100644 --- a/control-plane/api/internal/router/router.go +++ b/control-plane/api/internal/router/router.go @@ -61,9 +61,6 @@ func New(d Deps) *gin.Engine { client.GET("/leads", d.LeadsH.List) client.PATCH("/leads/:id/status", d.LeadsH.SetStatus) client.POST("/subscription", d.SubH.AddCode) - if d.DemosH != nil { - client.GET("/demos/mine", d.DemosH.ListMine) - } } // Espace admin : provisioning des démos (admin uniquement). @@ -79,6 +76,8 @@ func New(d Deps) *gin.Engine { admin.GET("/demos/:id", d.DemosH.Get) admin.DELETE("/demos/:id", d.DemosH.Delete) admin.POST("/demos/:id/extend", d.DemosH.Extend) + admin.POST("/demos/details", d.DemosH.ListDetails) + } }