feat: add details demo
ci-api / test (push) Has been cancelled

This commit is contained in:
Nuxgrid
2026-07-28 14:44:48 +02:00
parent 65ca24b6eb
commit 4b3a5720a3
8 changed files with 143 additions and 38 deletions
-28
View File
@@ -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" }
+41 -7
View File
@@ -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.
@@ -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
}
+12
View File
@@ -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
@@ -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" }
@@ -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).
+12
View File
@@ -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
}