chore: build
ci-api / test (push) Failing after 14m25s
ci-web / test (push) Failing after 8m17s

This commit is contained in:
Xor290
2026-09-20 16:14:29 +02:00
parent 76b1412968
commit 9e11f01c2c
15 changed files with 65 additions and 359 deletions
+12 -48
View File
@@ -31,8 +31,6 @@ var adminUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9]{3,64}$`)
type Provisioner interface {
ProvisionProject(spec demos.ProjectSpec) error
TeardownProject(namespace string) error
SuspendProject(namespace string) error
ResumeProject(namespace string) error
}
// NoopProvisioner : aucun déploiement réel (dev en mémoire, tests).
@@ -40,13 +38,10 @@ type NoopProvisioner struct{}
func (NoopProvisioner) ProvisionProject(demos.ProjectSpec) error { return nil }
func (NoopProvisioner) TeardownProject(string) error { return nil }
func (NoopProvisioner) SuspendProject(string) error { return nil }
func (NoopProvisioner) ResumeProject(string) error { return nil }
// Config du service (injectable pour les tests).
type Config struct {
Domain string // domaine des projets : <namespace>.<Domain>
Grace time.Duration // délai entre l'échéance (suspension) et la suppression
Domain string // domaine des projets : <namespace>.<Domain>
}
// CreateInput : saisie de l'admin Omnex au déploiement (dashboard).
@@ -135,7 +130,6 @@ func (s *Service) Create(in CreateInput) (Project, error) {
MonthsPurchased: in.Months,
CreatedAt: now,
ExpiresAt: expires,
DeleteAt: expires.Add(s.cfg.Grace),
}
created, err := s.store.Create(p)
if err != nil {
@@ -183,7 +177,7 @@ func (s *Service) Get(id string) (Project, error) {
func (s *Service) List() ([]Project, error) { return s.store.List() }
// Extend ajoute months mois à l'abonnement, à partir de l'échéance courante
// (ou de maintenant si elle est dépassée). Un projet suspendu est relancé.
// (ou de maintenant si elle est dépassée).
func (s *Service) Extend(id string, months int) (Project, error) {
if err := ValidateMonths(months); err != nil {
return Project{}, err
@@ -205,21 +199,13 @@ func (s *Service) Extend(id string, months int) (Project, error) {
base = now
}
if p.Status == StatusSuspended {
if err := s.prov.ResumeProject(p.Namespace); err != nil {
return Project{}, err
}
p.Status = StatusReady
p.SuspendedAt = nil
}
p.ExpiresAt = base.AddDate(0, months, 0)
p.DeleteAt = p.ExpiresAt.Add(s.cfg.Grace)
p.MonthsPurchased += months
return s.store.Update(p)
}
// Delete détruit le projet (namespace, données, sauvegardes) sans attendre le
// délai de grâce — action explicite de l'admin.
// Delete détruit le projet (namespace, données, sauvegardes) sans attendre
// l'échéance — action explicite de l'admin.
func (s *Service) Delete(id string) (Project, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -246,7 +232,7 @@ func (s *Service) deleteLocked(id string) (Project, error) {
}
// RunExpirationLoop applique le cycle de vie à intervalle régulier jusqu'à
// l'annulation de ctx : suspension à l'échéance, suppression à DeleteAt.
// l'annulation de ctx : suppression des projets dont l'abonnement est échu.
func (s *Service) RunExpirationLoop(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -273,40 +259,18 @@ func (s *Service) expireOverdue() {
}
}
// applyLifecycle relit le projet (il a pu changer depuis List) et applique la
// transition due. À appeler avec s.mu tenu.
// applyLifecycle relit le projet (il a pu changer depuis List) et le supprime
// si son abonnement est échu. À appeler avec s.mu tenu.
func (s *Service) applyLifecycle(id string) {
p, ok := s.store.Get(id)
if !ok {
if !ok || p.Status != StatusReady || p.ExpiresAt.After(s.now().UTC()) {
return
}
now := s.now().UTC()
switch p.Status {
case StatusReady:
if p.ExpiresAt.After(now) {
return
}
if err := s.prov.SuspendProject(p.Namespace); err != nil {
log.Printf("projets: suspension de %s échouée: %v", p.Namespace, err)
return
}
p.Status = StatusSuspended
p.SuspendedAt = &now
if _, err := s.store.Update(p); err != nil {
log.Printf("projets: enregistrement de la suspension de %s échoué: %v", p.Namespace, err)
return
}
log.Printf("projets: %s suspendu (abonnement échu, suppression le %s)", p.Namespace, p.DeleteAt.Format(time.RFC3339))
case StatusSuspended:
if p.DeleteAt.After(now) {
return
}
if _, err := s.deleteLocked(p.ID); err != nil {
log.Printf("projets: suppression de %s échouée: %v", p.Namespace, err)
return
}
log.Printf("projets: %s supprimé (délai de grâce dépassé)", p.Namespace)
if _, err := s.deleteLocked(p.ID); err != nil {
log.Printf("projets: suppression de %s échouée: %v", p.Namespace, err)
return
}
log.Printf("projets: %s supprimé (abonnement échu)", p.Namespace)
}
// shortID : 8 premiers caractères hex d'un uuid pour un nom de namespace court.