196 lines
6.8 KiB
Go
196 lines
6.8 KiB
Go
// Package demos : cœur métier — cycle de vie des démos client déployées
|
|
// dans le cluster (une démo = un namespace isolé, TTL 30 jours).
|
|
package demos
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Paramètres durée de vie. Pas de quota sur le nombre de démos : seule
|
|
// limite réelle, le pool de ressources externes (voir PoolSizePerService).
|
|
const (
|
|
TTL = 30 * 24 * time.Hour // durée de vie d'une démo
|
|
PoolSizePerService = 5 // slots par service externe poolé
|
|
)
|
|
|
|
// Services externes gérés par pool pré-provisionné (non auto-créables par
|
|
// API). TomTom n'y figure pas : ses clés sont saisies directement par
|
|
// l'admin au moment du déploiement (voir ProvisionConfig.TomTomAPIKey*).
|
|
const (
|
|
ServiceTelegram = "telegram"
|
|
ServiceNowPayments = "nowpayments"
|
|
)
|
|
|
|
// PooledServices : services nécessitant un slot de pool par démo.
|
|
var PooledServices = []string{ServiceTelegram, ServiceNowPayments}
|
|
|
|
const (
|
|
StorageLocal = "local"
|
|
StorageS3 = "s3"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidStorageDriver = errors.New("storage_driver doit être 'local' ou 's3'")
|
|
ErrS3ConfigIncomplete = errors.New("s3_bucket et s3_endpoint sont requis pour le stockage S3")
|
|
ErrInvalidDomain = errors.New("nom de domaine invalide")
|
|
)
|
|
|
|
// domainPattern : nom d'hôte RFC 1123 (labels alphanumériques + tirets,
|
|
// séparés par des points, au moins un point — pas de protocole ni de chemin).
|
|
var domainPattern = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$`)
|
|
|
|
// ValidateDomain vérifie le format d'un domaine personnalisé. Une chaîne
|
|
// vide est valide : elle signifie "revenir au domaine par défaut" (voir
|
|
// Service.SetDomain).
|
|
func ValidateDomain(domain string) error {
|
|
if domain == "" {
|
|
return nil
|
|
}
|
|
if len(domain) > 253 || !domainPattern.MatchString(domain) {
|
|
return ErrInvalidDomain
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ProvisionConfig : réglages saisis par l'admin à la création (popup de
|
|
// déploiement), propagés jusqu'au chart backend. Jamais persisté en base —
|
|
// notamment les secrets (token Telegram, clés NowPayments), qui ne
|
|
// transitent que par le fichier de valeurs Helm temporaire puis le Secret
|
|
// k8s du chart, comme les autres secrets.
|
|
type ProvisionConfig struct {
|
|
// TelegramBotUsername alimente TELEGRAM_BOT_USERNAME (lu par le backend
|
|
// au démarrage). TELEGRAM_WEBHOOK_URL est calculée automatiquement
|
|
// (URL publique de la démo) ; TELEGRAM_WEBHOOK_SECRET reste généré par
|
|
// le pool de ressources (ServiceTelegram), jamais saisi par l'admin.
|
|
TelegramBotUsername string
|
|
TelegramBotToken string
|
|
// NowPaymentsAPIKey écrase NOWPAYMENTS_API_KEY. NowPaymentsIPNSecret,
|
|
// s'il est fourni, écrase le secret pré-provisionné par le pool
|
|
// (ServiceNowPayments) — sinon celui du pool reste utilisé.
|
|
NowPaymentsAPIKey string
|
|
NowPaymentsIPNSecret string
|
|
StorageDriver string // "local" | "s3", "" => "local"
|
|
S3Bucket string
|
|
S3Endpoint string
|
|
|
|
// TomTom (GPS livreur : géocodage, itinéraire, ETA — voir
|
|
// services/tomtom_keys.go côté gestion). Jusqu'à 4 clés, utilisées en
|
|
// rotation automatique par le backend si l'une atteint son quota
|
|
// (403/429) ; seule TomTomAPIKey est requise, les 3 suivantes sont des
|
|
// clés de secours optionnelles.
|
|
TomTomAPIKey string
|
|
TomTomAPIKey1 string
|
|
TomTomAPIKey2 string
|
|
TomTomAPIKey3 string
|
|
|
|
// Load-balancer Telegram (chart lbtelegram) : installé seulement si au
|
|
// moins un bot est renseigné. GATEWAY_URL (URL publique de la démo) et
|
|
// les DSN/secrets sont générés par le provisioner. Les 3 réglages
|
|
// ci-dessous sont optionnels — laissés vides, lbtelegram applique ses
|
|
// propres défauts (failover, 300s, 30s — voir internal/config/config.go
|
|
// du service lbtelegram).
|
|
LBBot1Username string
|
|
LBBot1Token string
|
|
LBBot2Username string
|
|
LBBot2Token string
|
|
// LBStrategy : "failover" (défaut) | "roundrobin" | "leastconn"
|
|
LBStrategy string
|
|
LBJWTTTLSeconds string
|
|
LBHealthCheckInterval string // secondes
|
|
|
|
// Compte admin de l'application déployée (pas le compte omnex de
|
|
// l'admin qui lance la démo) — requis : sans lui, personne ne peut se
|
|
// connecter au backoffice de la démo. Créé directement en base une fois
|
|
// postgres/redis/backend/frontend Running (voir HelmProvisioner.Provision),
|
|
// car la table users n'existe qu'après la migration du backend au
|
|
// premier démarrage.
|
|
AdminUsername string
|
|
AdminPassword string
|
|
}
|
|
|
|
// LBTelegramEnabled indique si au moins un bot du load-balancer est configuré.
|
|
func (c ProvisionConfig) LBTelegramEnabled() bool {
|
|
return c.LBBot1Username != "" || c.LBBot2Username != ""
|
|
}
|
|
|
|
// Validate vérifie la cohérence du choix de stockage.
|
|
func (c ProvisionConfig) Validate() error {
|
|
switch c.StorageDriver {
|
|
case "", StorageLocal:
|
|
return nil
|
|
case StorageS3:
|
|
if c.S3Bucket == "" || c.S3Endpoint == "" {
|
|
return ErrS3ConfigIncomplete
|
|
}
|
|
return nil
|
|
default:
|
|
return ErrInvalidStorageDriver
|
|
}
|
|
}
|
|
|
|
// Status : état d'une démo.
|
|
type Status string
|
|
|
|
const (
|
|
StatusPending Status = "pending" // créée, pas encore prise par le worker
|
|
StatusProvisioning Status = "provisioning" // worker en train de déployer
|
|
StatusReady Status = "ready" // démo accessible
|
|
StatusExpiring Status = "expiring" // teardown en cours
|
|
StatusExpired Status = "expired" // détruite / TTL atteint
|
|
StatusFailed Status = "failed" // échec de provisioning
|
|
)
|
|
|
|
func (s Status) Valid() bool {
|
|
switch s {
|
|
case StatusPending, StatusProvisioning, StatusReady, StatusExpiring, StatusExpired, StatusFailed:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Active indique si la démo occupe des ressources (compte dans la capacité).
|
|
func (s Status) Active() bool {
|
|
switch s {
|
|
case StatusPending, StatusProvisioning, StatusReady, StatusExpiring:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Extendable : on ne prolonge que des démos encore vivantes.
|
|
func (s Status) Extendable() bool {
|
|
switch s {
|
|
case StatusProvisioning, StatusReady:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ResourceStatus : état d'un slot de pool.
|
|
type ResourceStatus string
|
|
|
|
const (
|
|
ResourceFree ResourceStatus = "free"
|
|
ResourceBorrowed ResourceStatus = "borrowed"
|
|
)
|
|
|
|
// premiumNamespace calcule le namespace "premium-<client>" utilisé une fois
|
|
// la démo passée en abonnement payant (voir Service.TransferToPaid et
|
|
// HelmProvisioner.MigrateToPremiumNamespace) — Kubernetes ne permet pas de
|
|
// renommer un namespace, seulement d'en créer un nouveau et d'y migrer les
|
|
// données. Contrainte K8s : nom en minuscules, 63 caractères max ; le
|
|
// username est déjà alphanumérique (voir handler.go), il suffit de le
|
|
// mettre en minuscule et de le tronquer si besoin.
|
|
func premiumNamespace(username string) string {
|
|
const prefix = "premium-"
|
|
maxLen := 63 - len(prefix)
|
|
u := strings.ToLower(username)
|
|
if len(u) > maxLen {
|
|
u = u[:maxLen]
|
|
}
|
|
return prefix + u
|
|
}
|