@@ -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" }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user