// 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-" 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 }