@@ -59,21 +59,6 @@ func (s Status) Extendable() bool {
|
|||||||
return false
|
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.
|
// ResourceStatus : état d'un slot de pool.
|
||||||
type ResourceStatus string
|
type ResourceStatus string
|
||||||
|
|
||||||
@@ -81,16 +66,3 @@ const (
|
|||||||
ResourceFree ResourceStatus = "free"
|
ResourceFree ResourceStatus = "free"
|
||||||
ResourceBorrowed ResourceStatus = "borrowed"
|
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"
|
"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} }
|
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"`
|
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).
|
// Create : POST /demos — provisionne une démo (202 Accepted, worker asynchrone).
|
||||||
func (h *Handler) Create(c *gin.Context) {
|
func (h *Handler) Create(c *gin.Context) {
|
||||||
var req createRequest
|
var req createRequest
|
||||||
@@ -47,19 +60,40 @@ func (h *Handler) List(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListMine : GET /demos/mine — démos rattachées au client authentifié.
|
func (h *Handler) ListDetails(c *gin.Context) {
|
||||||
func (h *Handler) ListMine(c *gin.Context) {
|
|
||||||
p := auth.PrincipalFrom(c)
|
p := auth.PrincipalFrom(c)
|
||||||
if p == nil {
|
if p == nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "non authentifié"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items, err := h.svc.ListForUser(p.Username)
|
if p.Role != "admin" {
|
||||||
if err != nil {
|
c.JSON(http.StatusForbidden, gin.H{"error": "vous n'avez pas les droits"})
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur serveur"})
|
|
||||||
return
|
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.
|
// 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
|
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.
|
// MemPool : Pool en mémoire (dev/tests), PoolSizePerService slots par service.
|
||||||
type MemPool struct {
|
type MemPool struct {
|
||||||
mu sync.Mutex
|
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
|
package demos
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
// Provisioner : abstraction du worker qui déploie/détruit la stack dans le cluster.
|
// 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
|
// L'implémentation réelle (Helm + client-go) sera branchée plus tard ; l'API se
|
||||||
// contente d'enfiler les intentions.
|
// contente d'enfiler les intentions.
|
||||||
@@ -8,6 +10,7 @@ type Provisioner interface {
|
|||||||
Provision(d Demo, resources []ExternalResource) error
|
Provision(d Demo, resources []ExternalResource) 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
|
||||||
|
GetResourceState(ctx context.Context, namespace string) (ResourceState, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoopProvisioner : implémentation neutre (aucun déploiement réel).
|
// NoopProvisioner : implémentation neutre (aucun déploiement réel).
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Store interface {
|
|||||||
ListByUsername(username string) ([]Demo, error)
|
ListByUsername(username string) ([]Demo, error)
|
||||||
Update(d Demo) (Demo, error)
|
Update(d Demo) (Demo, error)
|
||||||
CountActive() (int, error)
|
CountActive() (int, error)
|
||||||
|
GetDemoByUsername(username string) (Demo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GormStore : Store adossé à PostgreSQL via GORM.
|
// GormStore : Store adossé à PostgreSQL via GORM.
|
||||||
@@ -68,3 +69,14 @@ func (s *GormStore) CountActive() (int, error) {
|
|||||||
Count(&n).Error
|
Count(&n).Error
|
||||||
return int(n), err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,9 +61,6 @@ func New(d Deps) *gin.Engine {
|
|||||||
client.GET("/leads", d.LeadsH.List)
|
client.GET("/leads", d.LeadsH.List)
|
||||||
client.PATCH("/leads/:id/status", d.LeadsH.SetStatus)
|
client.PATCH("/leads/:id/status", d.LeadsH.SetStatus)
|
||||||
client.POST("/subscription", d.SubH.AddCode)
|
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).
|
// 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.GET("/demos/:id", d.DemosH.Get)
|
||||||
admin.DELETE("/demos/:id", d.DemosH.Delete)
|
admin.DELETE("/demos/:id", d.DemosH.Delete)
|
||||||
admin.POST("/demos/:id/extend", d.DemosH.Extend)
|
admin.POST("/demos/:id/extend", d.DemosH.Extend)
|
||||||
|
admin.POST("/demos/details", d.DemosH.ListDetails)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user