summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore4
-rw-r--r--.gitkeep0
-rw-r--r--Dockerfile8
-rw-r--r--Makefile5
-rw-r--r--README.md236
-rw-r--r--cmd/server/main.go93
-rw-r--r--config.example.yaml32
-rw-r--r--config.toml22
-rw-r--r--config.yaml33
-rw-r--r--docker-compose.yml21
-rw-r--r--go.mod70
-rw-r--r--go.sum235
-rw-r--r--internal/config/config.go76
-rw-r--r--internal/middleware/cors.go122
-rw-r--r--internal/middleware/cors_test.go305
-rw-r--r--internal/orchestrator/orchestrator.go551
-rw-r--r--internal/orchestrator/test_orchestrator_api.go297
-rw-r--r--pkg/proto/sovrabase.pb.go215
-rw-r--r--pkg/proto/sovrabase_grpc.pb.go125
-rw-r--r--proto/sovrabase.proto22
20 files changed, 1962 insertions, 510 deletions
diff --git a/.dockerignore b/.dockerignore
index 5509e3c..929b30e 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,6 +12,10 @@ README.md
docs/
LICENSE
+# Configuration (should be mounted as volume)
+config.yaml
+config.*.yaml
+
# Development
.devenv
.direnv
diff --git a/.gitkeep b/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/.gitkeep
+++ /dev/null
diff --git a/Dockerfile b/Dockerfile
index 920a40e..1f19a96 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -38,12 +38,6 @@ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=builder /build/sovrabase /sovrabase
-
-# Copy config file
-COPY --from=builder /build/config.toml /config.toml
-
-# Expose gRPC port (adjust based on your config)
-EXPOSE 50051
-
# Run the application
+# The config file must be provided via volume mount at /config/config.yaml
ENTRYPOINT ["/sovrabase"]
diff --git a/Makefile b/Makefile
index e558582..b8726e1 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ run:
go run ./cmd/server
test:
- go test ./...
+ go test ./... -v
proto:
protoc \
@@ -15,6 +15,5 @@ proto:
--go-grpc_out=pkg --go-grpc_opt=paths=source_relative \
proto/*.proto
-
clean:
- rm -rf bin/
+ rm -rf bin/*
diff --git a/README.md b/README.md
index 6aca575..0f19d69 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,65 @@
# Sovrabase
-> Une plateforme Backend-as-a-Service (BaaS) open source, souveraine et composable — conçue pour reprendre le contrôle de votre infrastructure.
+> Une plateforme Backend-as-a-Service (BaaS) open source, souveraine et composable.
[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](LICENSE)
-[![Go Version](https://img.shields.io/badge/go-1.25.2-00ADD8.svg)](https://go.dev/)
-
----
## 🎯 Vision
-**Sovrabase** est une alternative moderne et souveraine aux plateformes BaaS existantes (Firebase, Supabase, Appwrite). Elle répond aux besoins des entreprises et développeurs qui cherchent :
+Sovrabase est une alternative souveraine à Firebase/Supabase : contrôle total, multi-tenant, multi-région, et extensible.
-- **L'indépendance technologique** : aucun vendor lock-in, aucune dépendance à Google Cloud ou AWS
-- **La souveraineté des données** : hébergement on-premises ou cloud privé (RGPD-friendly)
-- **La flexibilité architecturale** : infrastructure modulaire et composable
-- **La scalabilité multi-région** : distribution géographique native des données
-- **La transparence totale** : open source, auditable, gouvernance claire
+## 🚀 Quick Start
----
+```bash
+# 1. Cloner le dépôt
+git clone https://github.com/ketsuna-org/sovrabase.git
+cd sovrabase
+
+# 2. Créer votre fichier de configuration
+cp config.example.yaml config.yaml
+
+# 3. Éditer config.yaml et modifier au minimum :
+# - super_user.password (OBLIGATOIRE !)
+# - api.api_addr (par défaut "0.0.0.0:8080")
+# - orchestrator.docker_host (par défaut "unix:///var/run/docker.sock")
+
+# 4. Lancer Sovrabase avec Docker Compose
+docker compose up -d
+
+# 5. Vérifier que tout fonctionne
+curl http://localhost:8080/health
+```
+
+> **⚠️ Important** : Modifiez impérativement le mot de passe du super utilisateur dans `config.yaml` avant le premier lancement !
+
+Voir [docs/config.md](docs/config.md) pour la configuration détaillée.
+
+## 📦 Fonctionnalités principales
+
+- Authentication & Authorization
+- Database Management (PostgreSQL, MongoDB)
+- Storage S3-compatible
+- Real-time (WebSocket)
+- Multi-tenancy & Multi-region
+- RBAC avancé
+
+## 🛠️ Technologies
+
+- Backend : Go 1.25+
+- Infra : Docker, Kubernetes-ready
+- DB : PostgreSQL, MongoDB, Redis
+
+## 🚧 Statut
+
+En développement. Roadmap : [Phase 1-4 détaillée dans docs](docs/ROADMAP.md).
+
+## 🤝 Contribution
+
+Fork, branche, commit, PR. Voir [CONTRIBUTING.md](CONTRIBUTING.md).
+
+## 📄 Licence
+
+AGPLv3.
## 🚀 Pourquoi Sovrabase ?
@@ -116,7 +158,177 @@ Sovrabase est conçu pour les entreprises exigeantes en matière de conformité
---
-## 🚧 Statut du projet
+## 🐳 Installation et Déploiement
+
+Sovrabase utilise Docker pour orchestrer les bases de données des projets. L'application s'exécute dans un conteneur et communique avec le daemon Docker de l'hôte.
+
+### Prérequis
+
+- Docker Engine 20.10+
+- Docker Compose V2
+
+### Déploiement avec Docker Compose (recommandé)
+
+Le projet inclut un fichier `docker-compose.yml` prêt à l'emploi :
+
+```bash
+# 1. Cloner le dépôt
+git clone https://github.com/ketsuna-org/sovrabase.git
+cd sovrabase
+
+# 2. Créer votre fichier de configuration
+cp config.example.yaml config.yaml
+
+# 3. Éditer config.yaml et modifier :
+# - super_user.password (OBLIGATOIRE !)
+# - api.api_addr (par défaut "0.0.0.0:8080")
+# - orchestrator.docker_host (par défaut "unix:///var/run/docker.sock")
+# - region (ex: "eu-west-1")
+
+# 4. Lancer Sovrabase
+docker compose up -d
+
+# 5. Vérifier que tout fonctionne
+curl http://localhost:8080/health
+```
+
+> **⚠️ Important** : Modifiez impérativement le mot de passe du super utilisateur dans `config.yaml` avant le premier lancement !
+
+### Configuration des volumes
+
+Le `docker-compose.yml` configure automatiquement les volumes nécessaires :
+
+#### 1. Fichier de configuration : `config.yaml`
+- **Montage** : `./config.yaml:/config.yaml:ro` (lecture seule)
+- **Contenu** : configuration de l'API, orchestrateur, base de données interne, super utilisateur
+
+#### 2. Socket Docker
+- **Montage** : `/var/run/docker.sock:/var/run/docker.sock`
+- **Rôle** : permet à Sovrabase de créer et gérer les conteneurs de bases de données pour chaque projet
+
+#### 3. Volume de données : `sovrabase-data`
+- **Montage** : `sovrabase-data:/data`
+- **Contenu** : base de données interne SQLite (`/data/sovrabase.db` par défaut)
+- **Persistance** : les données survivent aux redémarrages et suppressions du conteneur
+
+### Structure du fichier `config.yaml`
+
+Voici les paramètres principaux à configurer :
+
+```yaml
+# Configuration de l'API
+api:
+ api_addr: "0.0.0.0:8080" # Adresse d'écoute
+ domain: "api.example.com" # Domaine public (optionnel)
+ cors_allow: # Origines CORS autorisées
+ - "http://localhost:3000"
+ - "https://example.com"
+
+# Orchestrateur (Docker ou Kubernetes)
+orchestrator:
+ type: "docker"
+ docker_host: "unix:///var/run/docker.sock"
+
+# Base de données interne (SQLite par défaut)
+internal_db:
+ manager: "sqlite"
+ uri: "/data/sovrabase.db"
+
+# Super utilisateur (à modifier OBLIGATOIREMENT)
+super_user:
+ username: "admin"
+ password: "CHANGE-THIS-TO-A-SECURE-PASSWORD"
+ email: "admin@example.com"
+
+# Région de déploiement
+region: "eu-west-1"
+```
+
+Voir [docs/config.md](docs/config.md) pour la documentation complète.
+
+### Commandes utiles
+
+```bash
+# Voir les logs en temps réel
+docker compose logs -f
+
+# Voir les logs du conteneur Sovrabase uniquement
+docker compose logs -f sovrabase
+
+# Arrêter Sovrabase
+docker compose down
+
+# Arrêter et supprimer les volumes (⚠️ perte de données)
+docker compose down -v
+
+# Redémarrer après modification de config.yaml
+docker compose restart
+
+# Mettre à jour vers la dernière version
+docker compose pull
+docker compose up -d
+```
+
+### Vérification de l'installation
+
+Une fois lancé, testez l'API :
+
+```bash
+# Health check
+curl http://localhost:8080/health
+# Réponse attendue : {"status":"ok"}
+
+# Vérifier les logs
+docker compose logs sovrabase
+```
+
+### ⚠️ Considérations de sécurité
+
+**Socket Docker** : Monter `/var/run/docker.sock` donne un accès privilégié au daemon Docker. Le conteneur peut créer, modifier et supprimer d'autres conteneurs.
+
+**Recommandations pour la production** :
+
+1. **Proxy Docker** : utilisez [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) pour limiter les permissions
+2. **Mot de passe fort** : changez `super_user.password` dans `config.yaml`
+3. **HTTPS** : utilisez un reverse proxy (Nginx, Caddy, Traefik) pour le TLS
+4. **Firewall** : restreignez l'accès au port 8080 aux IPs autorisées
+5. **Monitoring** : surveillez les logs et les actions Docker
+
+### Déploiement avec Docker Run
+
+Si vous préférez utiliser `docker run` directement :
+
+```bash
+# Créer un volume pour les données
+docker volume create sovrabase-data
+
+# Lancer Sovrabase
+docker run -d \
+ --name sovrabase \
+ --restart unless-stopped \
+ -p 8080:8080 \
+ -v $(pwd)/config.yaml:/config.yaml:ro \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v sovrabase-data:/data \
+ ghcr.io/ketsuna-org/sovrabase:latest
+```
+
+### Utilisation avec Podman
+
+Sovrabase est compatible avec Podman. Modifiez `orchestrator.docker_host` dans `config.yaml` :
+
+```yaml
+orchestrator:
+ type: "docker"
+ # Podman rootless
+ docker_host: "unix:///run/user/1000/podman/podman.sock"
+ # Podman root
+ # docker_host: "unix:///run/podman/podman.sock"
+```
+
+---
+
+## �🚧 Statut du projet
**⚠️ En développement actif** — Sovrabase est actuellement en phase de conception et développement.
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 532a059..870bd7c 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -1,80 +1,51 @@
package main
import (
- "context"
- "fmt"
"log"
- "net"
+ "net/http"
+ "os"
+ mux "github.com/gorilla/mux"
"github.com/ketsuna-org/sovrabase/internal/config"
- pb "github.com/ketsuna-org/sovrabase/pkg/proto"
- "google.golang.org/grpc"
- "google.golang.org/grpc/credentials/insecure"
+ "github.com/ketsuna-org/sovrabase/internal/middleware"
)
-type server struct {
- pb.UnimplementedForwardCommandServiceServer
- nodeClients map[string]pb.ForwardCommandServiceClient
-}
-
-func (s *server) ForwardCommand(ctx context.Context, req *pb.ForwardCommandRequest) (*pb.ForwardCommandResponse, error) {
- log.Printf("Received command: %s for node: %s", req.Command, req.TargetNode)
-
- // Si c'est pour ce node, traiter localement
- if req.TargetNode == "" || req.TargetNode == "self" {
- return &pb.ForwardCommandResponse{
- Success: true,
- Result: fmt.Sprintf("Command '%s' executed locally", req.Command),
- }, nil
+func main() {
+ // Get config path from environment variable or use default
+ configPath := os.Getenv("CONFIG_PATH")
+ if configPath == "" {
+ configPath = "config.yaml"
}
- // Sinon, forwarder au node cible
- if client, exists := s.nodeClients[req.TargetNode]; exists {
- resp, err := client.ForwardCommand(ctx, req)
- if err != nil {
- return &pb.ForwardCommandResponse{
- Success: false,
- ErrorMessage: fmt.Sprintf("Failed to forward to %s: %v", req.TargetNode, err),
- }, nil
- }
- return resp, nil
+ cfg, err := config.LoadConfig(configPath)
+ if err != nil {
+ log.Fatalf("failed to load config from %s: %v", configPath, err)
}
- // Node inconnu
- return &pb.ForwardCommandResponse{
- Success: false,
- ErrorMessage: fmt.Sprintf("Unknown target node: %s", req.TargetNode),
- }, nil
-}
+ // Setup HTTP Server
-func main() {
- cfg, err := config.LoadConfig("config.toml")
- if err != nil {
- log.Fatalf("failed to load config: %v", err)
+ router := mux.NewRouter()
+ log.Printf("Starting API server on %s", cfg.API.APIAddr)
+ if cfg.API.Domain != "" {
+ log.Printf(" - Configured domain: %s", cfg.API.Domain)
}
-
- // Initialiser les connexions aux autres nodes
- nodeClients := make(map[string]pb.ForwardCommandServiceClient)
- for _, addr := range cfg.Cluster.RPCServers {
- if addr != cfg.RPC.RPCAddr { // Ne pas se connecter à soi-même
- conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
- if err != nil {
- log.Printf("Failed to connect to node %s: %v", addr, err)
- continue
- }
- nodeClients[addr] = pb.NewForwardCommandServiceClient(conn)
- log.Printf("Connected to node: %s", addr)
- }
+ if len(cfg.API.CORSAllow) > 0 {
+ log.Printf(" - CORS allowed origins: %v", cfg.API.CORSAllow)
}
- lis, err := net.Listen("tcp", cfg.RPC.RPCAddr)
- if err != nil {
- log.Fatalf("failed to listen: %v", err)
+ // Appliquer le middleware CORS
+ corsConfig := &middleware.CORSConfig{
+ Domain: cfg.API.Domain,
+ AllowedOrigins: cfg.API.CORSAllow,
}
- s := grpc.NewServer()
- pb.RegisterForwardCommandServiceServer(s, &server{nodeClients: nodeClients})
- log.Printf("server listening at %v", lis.Addr())
- if err := s.Serve(lis); err != nil {
- log.Fatalf("failed to serve: %v", err)
+ router.Use(middleware.CORSMiddleware(corsConfig))
+
+ router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Sovrabase API is running"))
+ })
+
+ if err := http.ListenAndServe(cfg.API.APIAddr, router); err != nil {
+ log.Fatalf("failed to start API server: %v", err)
}
}
diff --git a/config.example.yaml b/config.example.yaml
new file mode 100644
index 0000000..a3a6dba
--- /dev/null
+++ b/config.example.yaml
@@ -0,0 +1,32 @@
+# Sovrabase Configuration Example
+# Copy this file to config.yaml and adjust the values
+
+# API Configuration
+api:
+ api_addr: "0.0.0.0:8080"
+ cors_allow:
+ - "http://localhost:3000"
+ - "https://example.com"
+ domain: "api.example.com"
+
+# Orchestrator Configuration
+orchestrator:
+ type: "docker"
+ docker_host: "unix:///var/run/docker.sock"
+ kube_api: "https://kubernetes.default.svc"
+ kube_token: "your-kubernetes-api-token"
+ namespace: "sovrabase-databases"
+
+# Internal Database Configuration
+internal_db:
+ manager: "sqlite"
+ uri: "/data/sovrabase.db"
+
+# Super User Configuration
+super_user:
+ username: "admin"
+ password: "CHANGE-THIS-TO-A-SECURE-PASSWORD"
+ email: "admin@example.com"
+
+# Region
+region: "eu-west-1"
diff --git a/config.toml b/config.toml
deleted file mode 100644
index fae791d..0000000
--- a/config.toml
+++ /dev/null
@@ -1,22 +0,0 @@
-region = "supabase"
-
-[rpc]
-rpc_secret = "random_secret_12345"
-rpc_addr = "[::]:8080"
-
-[api]
-api_addr = "[::]:3000"
-api_domain = "example.com"
-
-[internal_db]
-manager = "sqlite"
-uri = "./database/internal.db"
-
-[external_db]
-manager = "postgres"
-uri = "postgres://root:password@localhost:5432/"
-
-[cluster]
-node_id = "node-01"
-is_rpc_server = true
-rpc_servers = ["[::]:8080"]
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..a5bef10
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,33 @@
+region: supabase
+
+api:
+ api_addr: "[::]:3000"
+ domain: api.example.com
+ cors_allow:
+ - example.com
+
+internal_db:
+ manager: sqlite
+ uri: ./database/internal.db
+
+orchestrator:
+ # Type d'orchestrateur: "docker" ou "kubernetes"
+ type: docker
+
+ # Configuration Docker/Podman
+ docker_host: unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock
+ # Autres exemples:
+ # Docker standard: "unix:///var/run/docker.sock"
+ # Podman rootless: "unix:///run/user/1000/podman/podman.sock"
+ # Podman root: "unix:///run/podman/podman.sock"
+ # Hôte distant: "tcp://remote-host:2375"
+
+ # Configuration Kubernetes (si type = "kubernetes")
+ # kube_api: https://kubernetes.default.svc
+ # kube_token: your-kubernetes-api-token
+ # namespace: sovrabase-databases
+
+super_user:
+ username: "admin"
+ password: "CHANGE-THIS-TO-A-SECURE-PASSWORD"
+ email: "admin@example.com"
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..95c7ab6
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,21 @@
+version: '3.8'
+
+services:
+ sovrabase:
+ image: ghcr.io/ketsuna-org/sovrabase:latest
+ # Pour build local, décommentez :
+ # build:
+ # context: .
+ # dockerfile: Dockerfile
+ container_name: sovrabase
+ restart: unless-stopped
+ ports:
+ - "8080:8080"
+ volumes:
+ - ./config.yaml:/config.yaml:ro
+ - /var/run/docker.sock:/var/run/docker.sock
+ - sovrabase-data:/data
+
+volumes:
+ sovrabase-data:
+ driver: local
diff --git a/go.mod b/go.mod
index 3398ac7..e562326 100644
--- a/go.mod
+++ b/go.mod
@@ -2,13 +2,73 @@ module github.com/ketsuna-org/sovrabase
go 1.25.2
-require github.com/BurntSushi/toml v1.5.0
+require (
+ github.com/docker/docker v28.5.1+incompatible
+ github.com/docker/go-connections v0.6.0
+ gopkg.in/yaml.v3 v3.0.1
+ k8s.io/api v0.34.1
+ k8s.io/client-go v0.34.1
+)
require (
- golang.org/x/net v0.42.0 // indirect
- golang.org/x/sys v0.34.0 // indirect
- golang.org/x/text v0.27.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/containerd/errdefs v1.0.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/google/gnostic-models v0.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/sys/atomicwriter v0.1.0 // indirect
+ github.com/moby/term v0.5.2 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
+ go.opentelemetry.io/otel v1.38.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
+ go.opentelemetry.io/otel/metric v1.38.0 // indirect
+ go.opentelemetry.io/otel/trace v1.38.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.2 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/net v0.43.0 // indirect
+ golang.org/x/oauth2 v0.30.0 // indirect
+ golang.org/x/sys v0.35.0 // indirect
+ golang.org/x/term v0.34.0 // indirect
+ golang.org/x/text v0.28.0 // indirect
+ golang.org/x/time v0.9.0 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gotest.tools/v3 v3.5.2 // indirect
+ k8s.io/apimachinery v0.34.1 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
+ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
+ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+ sigs.k8s.io/randfill v1.0.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+ sigs.k8s.io/yaml v1.6.0 // indirect
)
diff --git a/go.sum b/go.sum
index dbdfe3f..6f0ce89 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,229 @@
-github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
-github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
-golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
-golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
+github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
+k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
+k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
+k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
+k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/internal/config/config.go b/internal/config/config.go
index c872203..ce442a1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -2,55 +2,66 @@ package config
import (
"fmt"
+ "os"
- "github.com/BurntSushi/toml"
-)
+ "gopkg.in/yaml.v3"
-// RPC holds RPC configuration
-type RPC struct {
- RPCSecret string
- RPCAddr string
-}
+ // Import des packages Docker et Kubernetes pour la gestion des conteneurs
+ _ "github.com/docker/docker/client"
+ _ "github.com/docker/go-connections/nat"
+ _ "k8s.io/api/core/v1"
+ _ "k8s.io/client-go/kubernetes"
+ _ "k8s.io/client-go/rest"
+)
// API holds API configuration
type API struct {
- APIAddr string
- APIDomain string
+ APIAddr string `yaml:"api_addr"`
+ CORSAllow []string `yaml:"cors_allow"`
+ Domain string `yaml:"domain"`
}
// InternalDB holds internal database configuration
type InternalDB struct {
- Manager string
- URI string
+ Manager string `yaml:"manager"`
+ URI string `yaml:"uri"`
}
-// ExternalDB holds external database configuration
-type ExternalDB struct {
- Manager string
- URI string
+// Orchestrator holds container orchestration configuration
+type Orchestrator struct {
+ Type string `yaml:"type"` // "docker" or "kubernetes"
+ DockerHost string `yaml:"docker_host"` // Docker/Podman socket or remote host
+ KubeAPI string `yaml:"kube_api"` // Kubernetes API endpoint
+ KubeToken string `yaml:"kube_token"` // Kubernetes API token
+ Namespace string `yaml:"namespace"` // Kubernetes namespace for database deployments
}
-// Cluster holds cluster/distributed configuration
-type Cluster struct {
- NodeID string
- IsRPCServer bool
- RPCServers []string
+// SuperUser holds super user configuration
+type SuperUser struct {
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
+ Email string `yaml:"email"`
}
// Config holds the application configuration
type Config struct {
- Region string
- RPC RPC
- API API
- InternalDB InternalDB
- ExternalDB ExternalDB
- Cluster Cluster
+ Region string
+ API API
+ InternalDB InternalDB
+ Orchestrator Orchestrator
+ SuperUser SuperUser
}
-// LoadConfig loads configuration from a TOML file
+// LoadConfig loads configuration from a YAML file
func LoadConfig(filePath string) (*Config, error) {
var config Config
- if _, err := toml.DecodeFile(filePath, &config); err != nil {
+
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read config file: %w", err)
+ }
+
+ if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to decode config file: %w", err)
}
@@ -58,6 +69,15 @@ func LoadConfig(filePath string) (*Config, error) {
if config.Region == "" {
config.Region = "supabase"
}
+ if config.Orchestrator.Type == "" {
+ config.Orchestrator.Type = "docker"
+ }
+ if config.Orchestrator.DockerHost == "" && config.Orchestrator.Type == "docker" {
+ config.Orchestrator.DockerHost = "unix:///var/run/docker.sock"
+ }
+ if config.Orchestrator.Namespace == "" && config.Orchestrator.Type == "kubernetes" {
+ config.Orchestrator.Namespace = "sovrabase-databases"
+ }
return &config, nil
}
diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go
new file mode 100644
index 0000000..182f9fc
--- /dev/null
+++ b/internal/middleware/cors.go
@@ -0,0 +1,122 @@
+package middleware
+
+import (
+ "log"
+ "net/http"
+ "strings"
+)
+
+// CORSConfig holds the configuration for CORS middleware
+type CORSConfig struct {
+ Domain string // Domain principal de l'API (si configuré)
+ AllowedOrigins []string // Origins autorisées pour CORS
+}
+
+// CORSMiddleware creates a middleware that validates the Host header
+// and sets appropriate CORS headers based on the allowed origins
+func CORSMiddleware(config *CORSConfig) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Récupérer le Host header
+ host := r.Host
+
+ // Récupérer l'Origin header (pour les requêtes CORS)
+ origin := r.Header.Get("Origin")
+
+ // Extraire le hostname du Host (sans le port)
+ hostWithoutPort := strings.Split(host, ":")[0]
+
+ // Vérification 1: Si un Domain principal est configuré, le Host doit correspondre
+ if config.Domain != "" {
+ // Autoriser localhost en développement même si un Domain est configuré
+ isLocalhost := hostWithoutPort == "localhost" || hostWithoutPort == "127.0.0.1" || strings.HasPrefix(host, "[::")
+
+ if !isLocalhost && hostWithoutPort != config.Domain && host != config.Domain {
+ log.Printf("❌ Blocked request: host '%s' does not match configured domain '%s'", host, config.Domain)
+ http.Error(w, "Forbidden: Invalid domain", http.StatusForbidden)
+ return
+ }
+
+ if isLocalhost {
+ log.Printf("⚠️ Allowing localhost/loopback address despite domain restriction: %s", host)
+ }
+ }
+
+ // Vérification 2: CORS - vérifier les origins autorisées
+ // Si aucune restriction CORS n'est configurée, on autorise tout
+ if len(config.AllowedOrigins) == 0 {
+ if origin != "" {
+ log.Printf("⚠️ No CORS origins configured, allowing all origins")
+ }
+ log.Printf("✅ Allowed request from: %s (origin: %s)", host, origin)
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ // Vérifier les origins CORS (quand il y a un header Origin)
+ allowed := false
+ matchedOrigin := ""
+
+ if origin != "" {
+ // Extraire le hostname de l'Origin (peut être avec protocole http:// ou https://)
+ originHost := strings.TrimPrefix(origin, "http://")
+ originHost = strings.TrimPrefix(originHost, "https://")
+ originHost = strings.Split(originHost, ":")[0]
+
+ for _, allowedOrigin := range config.AllowedOrigins {
+ if originHost == allowedOrigin {
+ allowed = true
+ matchedOrigin = origin
+ break
+ }
+ }
+
+ if !allowed {
+ log.Printf("❌ Blocked CORS request from unauthorized origin: %s (host: %s)", origin, host)
+ http.Error(w, "Forbidden: Invalid origin", http.StatusForbidden)
+ return
+ }
+
+ // Définir les headers CORS appropriés
+ w.Header().Set("Access-Control-Allow-Origin", matchedOrigin)
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Access-Control-Max-Age", "3600")
+
+ // Gérer les requêtes preflight OPTIONS
+ if r.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ } else {
+ // Pas de header Origin - ce n'est pas une requête CORS
+ // Si un Domain est configuré, la vérification a déjà été faite plus haut
+ // Si pas de Domain, on vérifie que le Host correspond à un des AllowedOrigins
+ if config.Domain == "" {
+ for _, allowedOrigin := range config.AllowedOrigins {
+ if hostWithoutPort == allowedOrigin || host == allowedOrigin {
+ allowed = true
+ break
+ }
+ }
+
+ // Si localhost est utilisé en développement, on peut l'autoriser
+ if !allowed && (hostWithoutPort == "localhost" || hostWithoutPort == "127.0.0.1" || strings.HasPrefix(host, "[::")) {
+ log.Printf("⚠️ Allowing localhost/loopback address: %s", host)
+ allowed = true
+ }
+
+ if !allowed {
+ log.Printf("❌ Blocked request from unauthorized host: %s", host)
+ http.Error(w, "Forbidden: Invalid host", http.StatusForbidden)
+ return
+ }
+ }
+ }
+
+ log.Printf("✅ Allowed request from: %s (origin: %s)", host, origin)
+ next.ServeHTTP(w, r)
+ })
+ }
+}
diff --git a/internal/middleware/cors_test.go b/internal/middleware/cors_test.go
new file mode 100644
index 0000000..c36b6ef
--- /dev/null
+++ b/internal/middleware/cors_test.go
@@ -0,0 +1,305 @@
+package middleware
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+// handlerMock est un handler simple pour les tests
+func handlerMock(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("OK"))
+}
+
+func TestCORSMiddleware_AllowedHost(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{"example.com", "api.example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "example.com"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
+ }
+
+ expected := "OK"
+ if rr.Body.String() != expected {
+ t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
+ }
+}
+
+func TestCORSMiddleware_UnauthorizedHost(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{"example.com", "api.example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "unauthorized.com"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusForbidden {
+ t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden)
+ }
+}
+
+func TestCORSMiddleware_HostWithPort(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "example.com:3000"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("handler returned wrong status code for host with port: got %v want %v", status, http.StatusOK)
+ }
+}
+
+func TestCORSMiddleware_LocalhostAllowed(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ testCases := []string{
+ "localhost",
+ "localhost:3000",
+ "127.0.0.1",
+ "127.0.0.1:8080",
+ "[::1]",
+ "[::1]:3000",
+ }
+
+ for _, host := range testCases {
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = host
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("localhost %s should be allowed: got %v want %v", host, status, http.StatusOK)
+ }
+ }
+}
+
+func TestCORSMiddleware_CORSHeaders(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "api.example.com"
+ req.Header.Set("Origin", "https://example.com")
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
+ }
+
+ // Vérifier les headers CORS
+ if origin := rr.Header().Get("Access-Control-Allow-Origin"); origin != "https://example.com" {
+ t.Errorf("Access-Control-Allow-Origin = %v, want %v", origin, "https://example.com")
+ }
+
+ if methods := rr.Header().Get("Access-Control-Allow-Methods"); methods == "" {
+ t.Error("Access-Control-Allow-Methods should be set")
+ }
+
+ if headers := rr.Header().Get("Access-Control-Allow-Headers"); headers == "" {
+ t.Error("Access-Control-Allow-Headers should be set")
+ }
+
+ if credentials := rr.Header().Get("Access-Control-Allow-Credentials"); credentials != "true" {
+ t.Errorf("Access-Control-Allow-Credentials = %v, want %v", credentials, "true")
+ }
+}
+
+func TestCORSMiddleware_PreflightRequest(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("OPTIONS", "/", nil)
+ req.Host = "api.example.com"
+ req.Header.Set("Origin", "https://example.com")
+ req.Header.Set("Access-Control-Request-Method", "POST")
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("preflight request returned wrong status code: got %v want %v", status, http.StatusOK)
+ }
+
+ // Le body ne devrait pas contenir "OK" car on répond directement à la preflight
+ if rr.Body.String() == "OK" {
+ t.Error("preflight request should not execute the handler")
+ }
+}
+
+func TestCORSMiddleware_NoConfig(t *testing.T) {
+ config := &CORSConfig{
+ AllowedOrigins: []string{},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "any-domain.com"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ // Sans configuration, tout devrait être autorisé
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("with no config, all should be allowed: got %v want %v", status, http.StatusOK)
+ }
+}
+
+// Tests pour la validation du Domain configuré
+
+func TestCORSMiddleware_DomainMatch(t *testing.T) {
+ config := &CORSConfig{
+ Domain: "api.example.com",
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "api.example.com"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("request to configured domain should be allowed: got %v want %v", status, http.StatusOK)
+ }
+}
+
+func TestCORSMiddleware_DomainMismatch(t *testing.T) {
+ config := &CORSConfig{
+ Domain: "api.example.com",
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "wrong.example.com"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusForbidden {
+ t.Errorf("request to wrong domain should be blocked: got %v want %v", status, http.StatusForbidden)
+ }
+}
+
+func TestCORSMiddleware_DomainWithPort(t *testing.T) {
+ config := &CORSConfig{
+ Domain: "api.example.com",
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "api.example.com:3000"
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("request to configured domain with port should be allowed: got %v want %v", status, http.StatusOK)
+ }
+}
+
+func TestCORSMiddleware_LocalhostWithDomainRestriction(t *testing.T) {
+ config := &CORSConfig{
+ Domain: "api.example.com",
+ AllowedOrigins: []string{"example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ testCases := []string{
+ "localhost",
+ "localhost:3000",
+ "127.0.0.1",
+ "[::1]",
+ }
+
+ for _, host := range testCases {
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = host
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("localhost %s should be allowed even with domain restriction: got %v want %v", host, status, http.StatusOK)
+ }
+ }
+}
+
+func TestCORSMiddleware_DomainAndCORS(t *testing.T) {
+ config := &CORSConfig{
+ Domain: "api.example.com",
+ AllowedOrigins: []string{"example.com", "app.example.com"},
+ }
+
+ middleware := CORSMiddleware(config)
+ handler := middleware(http.HandlerFunc(handlerMock))
+
+ req := httptest.NewRequest("GET", "/", nil)
+ req.Host = "api.example.com"
+ req.Header.Set("Origin", "https://example.com")
+
+ rr := httptest.NewRecorder()
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("request with valid domain and origin should be allowed: got %v want %v", status, http.StatusOK)
+ }
+
+ // Vérifier les headers CORS
+ if origin := rr.Header().Get("Access-Control-Allow-Origin"); origin != "https://example.com" {
+ t.Errorf("Access-Control-Allow-Origin = %v, want %v", origin, "https://example.com")
+ }
+}
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
new file mode 100644
index 0000000..bb72614
--- /dev/null
+++ b/internal/orchestrator/orchestrator.go
@@ -0,0 +1,551 @@
+package orchestrator
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/filters"
+ "github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/client"
+ "github.com/docker/go-connections/nat"
+ "github.com/ketsuna-org/sovrabase/internal/config"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+)
+
+// Orchestrator interface pour gérer les conteneurs de bases de données
+type Orchestrator interface {
+ // CreateDatabase crée une nouvelle instance de base de données pour un projet
+ CreateDatabase(ctx context.Context, projectID string, options *DatabaseOptions) (*DatabaseInfo, error)
+
+ // DeleteDatabase supprime une instance de base de données
+ DeleteDatabase(ctx context.Context, projectID string) error
+
+ // GetDatabaseInfo retourne les informations de connexion à la base de données
+ GetDatabaseInfo(ctx context.Context, projectID string) (*DatabaseInfo, error)
+
+ // ListDatabases liste toutes les bases de données gérées
+ ListDatabases(ctx context.Context) ([]*DatabaseInfo, error)
+
+ // DatabaseExists vérifie si une base de données existe déjà
+ DatabaseExists(ctx context.Context, projectID string) (bool, error)
+}
+
+// DatabaseOptions contient les options pour créer une base de données
+type DatabaseOptions struct {
+ PostgresVersion string // Version de PostgreSQL (défaut: "16-alpine")
+ Password string // Mot de passe (généré si vide)
+ Port int // Port hôte (auto-assigné si 0)
+ Memory string // Limite mémoire (ex: "512m")
+ CPUs string // Limite CPU (ex: "0.5")
+}
+
+// DatabaseInfo contient les informations d'une base de données
+type DatabaseInfo struct {
+ ProjectID string
+ ContainerID string
+ ContainerName string
+ Status string
+ PostgresVersion string
+ Host string
+ Port string
+ Database string
+ User string
+ Password string
+ ConnectionString string
+ CreatedAt time.Time
+}
+
+// DockerOrchestrator gère les bases de données via Docker/Podman
+type DockerOrchestrator struct {
+ client *client.Client
+ config *config.Orchestrator
+}
+
+// KubernetesOrchestrator gère les bases de données via Kubernetes
+type KubernetesOrchestrator struct {
+ client *kubernetes.Clientset
+ config *config.Orchestrator
+}
+
+// NewOrchestrator crée un orchestrateur basé sur la configuration
+func NewOrchestrator(cfg *config.Orchestrator) (Orchestrator, error) {
+ switch cfg.Type {
+ case "docker":
+ return NewDockerOrchestrator(cfg)
+ case "kubernetes":
+ return NewKubernetesOrchestrator(cfg)
+ default:
+ return nil, fmt.Errorf("type d'orchestrateur non supporté: %s", cfg.Type)
+ }
+}
+
+// NewDockerOrchestrator crée un orchestrateur Docker
+func NewDockerOrchestrator(cfg *config.Orchestrator) (*DockerOrchestrator, error) {
+ cli, err := client.NewClientWithOpts(
+ client.WithHost(cfg.DockerHost),
+ client.WithAPIVersionNegotiation(),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("échec de connexion à Docker: %w", err)
+ }
+
+ return &DockerOrchestrator{
+ client: cli,
+ config: cfg,
+ }, nil
+}
+
+// NewKubernetesOrchestrator crée un orchestrateur Kubernetes
+func NewKubernetesOrchestrator(cfg *config.Orchestrator) (*KubernetesOrchestrator, error) {
+ kubeConfig := &rest.Config{
+ Host: cfg.KubeAPI,
+ BearerToken: cfg.KubeToken,
+ TLSClientConfig: rest.TLSClientConfig{
+ Insecure: false, // À configurer selon vos besoins
+ },
+ }
+
+ clientset, err := kubernetes.NewForConfig(kubeConfig)
+ if err != nil {
+ return nil, fmt.Errorf("échec de connexion à Kubernetes: %w", err)
+ }
+
+ return &KubernetesOrchestrator{
+ client: clientset,
+ config: cfg,
+ }, nil
+}
+
+// Implémentations des méthodes pour DockerOrchestrator
+
+// CreateDatabase crée une nouvelle instance PostgreSQL dans un conteneur
+func (d *DockerOrchestrator) CreateDatabase(ctx context.Context, projectID string, options *DatabaseOptions) (*DatabaseInfo, error) {
+ // Vérifier si la base existe déjà
+ exists, err := d.DatabaseExists(ctx, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la vérification de l'existence: %w", err)
+ }
+ if exists {
+ return nil, fmt.Errorf("une base de données existe déjà pour le projet: %s", projectID)
+ }
+
+ // Définir les valeurs par défaut
+ if options == nil {
+ options = &DatabaseOptions{}
+ }
+ if options.PostgresVersion == "" {
+ options.PostgresVersion = "16-alpine"
+ }
+ if options.Password == "" {
+ options.Password = generatePassword(projectID)
+ }
+ if options.Port == 0 {
+ options.Port = findAvailablePort(ctx, d.client)
+ }
+
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+ imageName := fmt.Sprintf("docker.io/library/postgres:%s", options.PostgresVersion)
+ dbName := sanitizeDBName(projectID)
+ dbUser := sanitizeDBName(projectID)
+
+ // Pull l'image PostgreSQL
+ reader, err := d.client.ImagePull(ctx, imageName, image.PullOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors du pull de l'image: %w", err)
+ }
+ // Consommer la sortie pour attendre la fin du pull
+ _, _ = io.Copy(io.Discard, reader)
+ reader.Close()
+
+ // Configuration du conteneur
+ containerConfig := &container.Config{
+ Image: imageName,
+ Env: []string{
+ fmt.Sprintf("POSTGRES_PASSWORD=%s", options.Password),
+ fmt.Sprintf("POSTGRES_DB=%s", dbName),
+ fmt.Sprintf("POSTGRES_USER=%s", dbUser),
+ },
+ ExposedPorts: nat.PortSet{
+ "5432/tcp": struct{}{},
+ },
+ Labels: map[string]string{
+ "sovrabase.managed": "true",
+ "sovrabase.project_id": projectID,
+ "sovrabase.type": "postgres",
+ "sovrabase.version": options.PostgresVersion,
+ "sovrabase.created_at": time.Now().UTC().Format(time.RFC3339),
+ },
+ }
+
+ // Configuration de l'hôte
+ hostConfig := &container.HostConfig{
+ PortBindings: nat.PortMap{
+ "5432/tcp": []nat.PortBinding{
+ {
+ HostIP: "127.0.0.1",
+ HostPort: fmt.Sprintf("%d", options.Port),
+ },
+ },
+ },
+ AutoRemove: false,
+ RestartPolicy: container.RestartPolicy{
+ Name: "unless-stopped",
+ },
+ }
+
+ // Ajouter les limites de ressources si spécifiées
+ if options.Memory != "" {
+ hostConfig.Resources.Memory = parseMemory(options.Memory)
+ }
+ if options.CPUs != "" {
+ hostConfig.Resources.NanoCPUs = parseCPUs(options.CPUs)
+ }
+
+ // Créer le conteneur
+ resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName)
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la création du conteneur: %w", err)
+ }
+
+ // Gérer les warnings
+ if len(resp.Warnings) > 0 {
+ for _, warning := range resp.Warnings {
+ fmt.Printf("Warning: %s\n", warning)
+ }
+ }
+
+ // Démarrer le conteneur
+ if err := d.client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
+ // En cas d'erreur, nettoyer le conteneur créé
+ _ = d.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true})
+ return nil, fmt.Errorf("erreur lors du démarrage du conteneur: %w", err)
+ }
+
+ // Attendre que PostgreSQL soit prêt (max 30 secondes)
+ if err := d.waitForPostgres(ctx, resp.ID, 30*time.Second); err != nil {
+ return nil, fmt.Errorf("PostgreSQL n'a pas démarré correctement: %w", err)
+ }
+
+ // Créer les informations de la base de données
+ dbInfo := &DatabaseInfo{
+ ProjectID: projectID,
+ ContainerID: resp.ID,
+ ContainerName: containerName,
+ Status: "running",
+ PostgresVersion: options.PostgresVersion,
+ Host: "localhost",
+ Port: fmt.Sprintf("%d", options.Port),
+ Database: dbName,
+ User: dbUser,
+ Password: options.Password,
+ ConnectionString: fmt.Sprintf("postgresql://%s:%s@localhost:%d/%s?sslmode=disable", dbUser, options.Password, options.Port, dbName),
+ CreatedAt: time.Now().UTC(),
+ }
+
+ return dbInfo, nil
+}
+
+// DeleteDatabase supprime le conteneur de base de données
+func (d *DockerOrchestrator) DeleteDatabase(ctx context.Context, projectID string) error {
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+
+ // Vérifier si le conteneur existe
+ exists, err := d.DatabaseExists(ctx, projectID)
+ if err != nil {
+ return fmt.Errorf("erreur lors de la vérification: %w", err)
+ }
+ if !exists {
+ return fmt.Errorf("aucune base de données trouvée pour le projet: %s", projectID)
+ }
+
+ // Arrêter le conteneur (timeout de 10 secondes)
+ timeout := 10
+ stopOptions := container.StopOptions{
+ Timeout: &timeout,
+ }
+ if err := d.client.ContainerStop(ctx, containerName, stopOptions); err != nil {
+ // Ignorer si déjà arrêté
+ if !strings.Contains(err.Error(), "is not running") {
+ return fmt.Errorf("erreur lors de l'arrêt du conteneur: %w", err)
+ }
+ }
+
+ // Supprimer le conteneur et ses volumes
+ removeOptions := container.RemoveOptions{
+ Force: true,
+ RemoveVolumes: true,
+ }
+ if err := d.client.ContainerRemove(ctx, containerName, removeOptions); err != nil {
+ return fmt.Errorf("erreur lors de la suppression du conteneur: %w", err)
+ }
+
+ return nil
+}
+
+// GetDatabaseInfo récupère les informations d'une base de données
+func (d *DockerOrchestrator) GetDatabaseInfo(ctx context.Context, projectID string) (*DatabaseInfo, error) {
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+
+ // Inspecter le conteneur
+ containerJSON, err := d.client.ContainerInspect(ctx, containerName)
+ if err != nil {
+ if client.IsErrNotFound(err) {
+ return nil, fmt.Errorf("base de données non trouvée pour le projet: %s", projectID)
+ }
+ return nil, fmt.Errorf("erreur lors de l'inspection du conteneur: %w", err)
+ }
+
+ // Extraire les informations
+ labels := containerJSON.Config.Labels
+ env := parseEnvVars(containerJSON.Config.Env)
+
+ port := "unknown"
+ if bindings, ok := containerJSON.NetworkSettings.Ports["5432/tcp"]; ok && len(bindings) > 0 {
+ port = bindings[0].HostPort
+ }
+
+ dbName := env["POSTGRES_DB"]
+ dbUser := env["POSTGRES_USER"]
+ dbPassword := env["POSTGRES_PASSWORD"]
+
+ status := "stopped"
+ if containerJSON.State.Running {
+ status = "running"
+ }
+
+ createdAt, _ := time.Parse(time.RFC3339, labels["sovrabase.created_at"])
+
+ dbInfo := &DatabaseInfo{
+ ProjectID: projectID,
+ ContainerID: containerJSON.ID,
+ ContainerName: containerName,
+ Status: status,
+ PostgresVersion: labels["sovrabase.version"],
+ Host: "localhost",
+ Port: port,
+ Database: dbName,
+ User: dbUser,
+ Password: dbPassword,
+ ConnectionString: fmt.Sprintf("postgresql://%s:%s@localhost:%s/%s?sslmode=disable", dbUser, dbPassword, port, dbName),
+ CreatedAt: createdAt,
+ }
+
+ return dbInfo, nil
+}
+
+// ListDatabases liste toutes les bases de données gérées
+func (d *DockerOrchestrator) ListDatabases(ctx context.Context) ([]*DatabaseInfo, error) {
+ // Filtrer les conteneurs avec le label sovrabase.managed=true
+ filterArgs := filters.NewArgs()
+ filterArgs.Add("label", "sovrabase.managed=true")
+ filterArgs.Add("label", "sovrabase.type=postgres")
+
+ containers, err := d.client.ContainerList(ctx, container.ListOptions{
+ All: true,
+ Filters: filterArgs,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la liste des conteneurs: %w", err)
+ }
+
+ databases := make([]*DatabaseInfo, 0, len(containers))
+ for _, cont := range containers {
+ projectID := cont.Labels["sovrabase.project_id"]
+ if projectID == "" {
+ continue
+ }
+
+ dbInfo, err := d.GetDatabaseInfo(ctx, projectID)
+ if err != nil {
+ // Logger l'erreur mais continuer
+ fmt.Printf("Warning: impossible de récupérer les infos pour %s: %v\n", projectID, err)
+ continue
+ }
+
+ databases = append(databases, dbInfo)
+ }
+
+ return databases, nil
+}
+
+// DatabaseExists vérifie si une base de données existe
+func (d *DockerOrchestrator) DatabaseExists(ctx context.Context, projectID string) (bool, error) {
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+
+ _, err := d.client.ContainerInspect(ctx, containerName)
+ if err != nil {
+ if client.IsErrNotFound(err) {
+ return false, nil
+ }
+ return false, fmt.Errorf("erreur lors de la vérification: %w", err)
+ }
+
+ return true, nil
+}
+
+// Implémentations des méthodes pour KubernetesOrchestrator
+func (k *KubernetesOrchestrator) CreateDatabase(ctx context.Context, projectID string, options *DatabaseOptions) (*DatabaseInfo, error) {
+ // TODO: Créer un StatefulSet PostgreSQL
+ return nil, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) DeleteDatabase(ctx context.Context, projectID string) error {
+ // TODO: Supprimer le StatefulSet et PVC
+ return fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) GetDatabaseInfo(ctx context.Context, projectID string) (*DatabaseInfo, error) {
+ // TODO: Récupérer l'URL via le Service Kubernetes
+ return nil, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) ListDatabases(ctx context.Context) ([]*DatabaseInfo, error) {
+ // TODO: Lister tous les StatefulSets de bases de données
+ return nil, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) DatabaseExists(ctx context.Context, projectID string) (bool, error) {
+ // TODO: Vérifier l'existence du StatefulSet
+ return false, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+// Fonctions utilitaires
+
+// waitForPostgres attend que PostgreSQL soit prêt
+func (d *DockerOrchestrator) waitForPostgres(ctx context.Context, containerID string, timeout time.Duration) error {
+ deadline := time.Now().Add(timeout)
+
+ for time.Now().Before(deadline) {
+ // Vérifier si le conteneur est toujours en cours d'exécution
+ inspect, err := d.client.ContainerInspect(ctx, containerID)
+ if err != nil {
+ return fmt.Errorf("erreur lors de l'inspection: %w", err)
+ }
+
+ if !inspect.State.Running {
+ return fmt.Errorf("le conteneur s'est arrêté de manière inattendue")
+ }
+
+ // Tenter une connexion PostgreSQL via exec
+ execConfig := container.ExecOptions{
+ Cmd: []string{"pg_isready", "-U", "postgres"},
+ AttachStdout: true,
+ AttachStderr: true,
+ }
+
+ execResp, err := d.client.ContainerExecCreate(ctx, containerID, execConfig)
+ if err == nil {
+ attachResp, err := d.client.ContainerExecAttach(ctx, execResp.ID, container.ExecAttachOptions{})
+ if err == nil {
+ attachResp.Close()
+
+ execInspect, err := d.client.ContainerExecInspect(ctx, execResp.ID)
+ if err == nil && execInspect.ExitCode == 0 {
+ return nil // PostgreSQL est prêt
+ }
+ }
+ }
+
+ time.Sleep(1 * time.Second)
+ }
+
+ return fmt.Errorf("timeout en attendant que PostgreSQL démarre")
+}
+
+// generatePassword génère un mot de passe sécurisé
+func generatePassword(projectID string) string {
+ // Pour la production, utiliser crypto/rand
+ // Ici, génération simple pour l'exemple
+ return fmt.Sprintf("secure_%s_%d", projectID, time.Now().Unix())
+}
+
+// sanitizeDBName nettoie un nom pour l'utiliser comme nom de DB/user
+func sanitizeDBName(name string) string {
+ // Remplacer les caractères non alphanumériques par des underscores
+ result := strings.Map(func(r rune) rune {
+ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
+ return r
+ }
+ return '_'
+ }, name)
+
+ // Limiter à 63 caractères (limite PostgreSQL)
+ if len(result) > 63 {
+ result = result[:63]
+ }
+
+ return strings.ToLower(result)
+}
+
+// findAvailablePort trouve un port disponible
+func findAvailablePort(ctx context.Context, cli *client.Client) int {
+ // Liste des ports utilisés
+ usedPorts := make(map[int]bool)
+
+ containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
+ if err == nil {
+ for _, cont := range containers {
+ for _, port := range cont.Ports {
+ if port.PublicPort > 0 {
+ usedPorts[int(port.PublicPort)] = true
+ }
+ }
+ }
+ }
+
+ // Chercher un port libre à partir de 5433
+ for port := 5433; port < 6000; port++ {
+ if !usedPorts[port] {
+ return port
+ }
+ }
+
+ return 5433 // Par défaut
+}
+
+// parseMemory convertit une chaîne mémoire en bytes
+func parseMemory(mem string) int64 {
+ // Exemples: "512m", "1g", "256M"
+ mem = strings.ToLower(strings.TrimSpace(mem))
+
+ multiplier := int64(1)
+ if strings.HasSuffix(mem, "k") {
+ multiplier = 1024
+ mem = strings.TrimSuffix(mem, "k")
+ } else if strings.HasSuffix(mem, "m") {
+ multiplier = 1024 * 1024
+ mem = strings.TrimSuffix(mem, "m")
+ } else if strings.HasSuffix(mem, "g") {
+ multiplier = 1024 * 1024 * 1024
+ mem = strings.TrimSuffix(mem, "g")
+ }
+
+ var value int64
+ fmt.Sscanf(mem, "%d", &value)
+ return value * multiplier
+}
+
+// parseCPUs convertit une chaîne CPU en nanoCPUs
+func parseCPUs(cpus string) int64 {
+ // Exemple: "0.5" = 500000000 nanoCPUs
+ var value float64
+ fmt.Sscanf(cpus, "%f", &value)
+ return int64(value * 1e9)
+}
+
+// parseEnvVars convertit un tableau d'env vars en map
+func parseEnvVars(envVars []string) map[string]string {
+ result := make(map[string]string)
+ for _, env := range envVars {
+ parts := strings.SplitN(env, "=", 2)
+ if len(parts) == 2 {
+ result[parts[0]] = parts[1]
+ }
+ }
+ return result
+}
diff --git a/internal/orchestrator/test_orchestrator_api.go b/internal/orchestrator/test_orchestrator_api.go
new file mode 100644
index 0000000..d66bde5
--- /dev/null
+++ b/internal/orchestrator/test_orchestrator_api.go
@@ -0,0 +1,297 @@
+package orchestrator
+
+import (
+ "context"
+ "testing"
+
+ "github.com/ketsuna-org/sovrabase/internal/config"
+)
+
+// setupOrchestrator crée un orchestrateur pour les tests
+func setupOrchestrator(t *testing.T) Orchestrator {
+ t.Helper()
+
+ cfg, err := config.LoadConfig("../../config.yaml")
+ if err != nil {
+ t.Fatalf("Erreur de chargement de la config: %v", err)
+ }
+
+ orch, err := NewOrchestrator(&cfg.Orchestrator)
+ if err != nil {
+ t.Fatalf("Erreur de création de l'orchestrateur: %v", err)
+ }
+
+ return orch
+}
+
+// cleanupDatabase supprime la base de données de test si elle existe
+func cleanupDatabase(t *testing.T, orch Orchestrator, projectID string) {
+ t.Helper()
+ ctx := context.Background()
+
+ exists, err := orch.DatabaseExists(ctx, projectID)
+ if err != nil {
+ t.Logf("Avertissement lors de la vérification d'existence: %v", err)
+ return
+ }
+
+ if exists {
+ if err := orch.DeleteDatabase(ctx, projectID); err != nil {
+ t.Logf("Avertissement lors du nettoyage: %v", err)
+ }
+ }
+}
+
+func TestDatabaseExists(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-exists-project"
+
+ defer cleanupDatabase(t, orch, projectID)
+
+ // La base ne devrait pas exister initialement
+ exists, err := orch.DatabaseExists(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la vérification d'existence: %v", err)
+ }
+ if exists {
+ t.Error("La base de données ne devrait pas exister initialement")
+ }
+}
+
+func TestCreateDatabase(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-create-project"
+
+ defer cleanupDatabase(t, orch, projectID)
+
+ options := &DatabaseOptions{
+ PostgresVersion: "16-alpine",
+ Port: 5434,
+ Memory: "512m",
+ CPUs: "0.5",
+ }
+
+ dbInfo, err := orch.CreateDatabase(ctx, projectID, options)
+ if err != nil {
+ t.Fatalf("Erreur lors de la création de la base de données: %v", err)
+ }
+
+ if dbInfo.ProjectID != projectID {
+ t.Errorf("ProjectID attendu: %s, obtenu: %s", projectID, dbInfo.ProjectID)
+ }
+
+ if dbInfo.Status == "" {
+ t.Error("Le status ne devrait pas être vide")
+ }
+
+ if dbInfo.ConnectionString == "" {
+ t.Error("La chaîne de connexion ne devrait pas être vide")
+ }
+}
+
+func TestGetDatabaseInfo(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-getinfo-project"
+
+ defer cleanupDatabase(t, orch, projectID)
+
+ // Créer d'abord une base de données
+ options := &DatabaseOptions{
+ PostgresVersion: "16-alpine",
+ Port: 5435,
+ Memory: "512m",
+ CPUs: "0.5",
+ }
+
+ _, err := orch.CreateDatabase(ctx, projectID, options)
+ if err != nil {
+ t.Fatalf("Erreur lors de la création de la base de données: %v", err)
+ }
+
+ // Récupérer les informations
+ dbInfo, err := orch.GetDatabaseInfo(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la récupération des informations: %v", err)
+ }
+
+ if dbInfo.ProjectID != projectID {
+ t.Errorf("ProjectID attendu: %s, obtenu: %s", projectID, dbInfo.ProjectID)
+ }
+
+ if dbInfo.ContainerName == "" {
+ t.Error("Le nom du conteneur ne devrait pas être vide")
+ }
+}
+
+func TestListDatabases(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-list-project"
+
+ defer cleanupDatabase(t, orch, projectID)
+
+ // Créer une base de données
+ options := &DatabaseOptions{
+ PostgresVersion: "16-alpine",
+ Port: 5436,
+ Memory: "512m",
+ CPUs: "0.5",
+ }
+
+ _, err := orch.CreateDatabase(ctx, projectID, options)
+ if err != nil {
+ t.Fatalf("Erreur lors de la création de la base de données: %v", err)
+ }
+
+ // Lister les bases de données
+ databases, err := orch.ListDatabases(ctx)
+ if err != nil {
+ t.Fatalf("Erreur lors du listage des bases de données: %v", err)
+ }
+
+ if len(databases) == 0 {
+ t.Error("Au moins une base de données devrait être listée")
+ }
+
+ // Vérifier que notre base de données est dans la liste
+ found := false
+ for _, db := range databases {
+ if db.ProjectID == projectID {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ t.Errorf("La base de données %s devrait être dans la liste", projectID)
+ }
+}
+
+func TestCreateDatabaseConflict(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-conflict-project"
+
+ defer cleanupDatabase(t, orch, projectID)
+
+ options := &DatabaseOptions{
+ PostgresVersion: "16-alpine",
+ Port: 5437,
+ Memory: "512m",
+ CPUs: "0.5",
+ }
+
+ // Première création
+ _, err := orch.CreateDatabase(ctx, projectID, options)
+ if err != nil {
+ t.Fatalf("Erreur lors de la première création: %v", err)
+ }
+
+ // Deuxième création (devrait échouer)
+ _, err = orch.CreateDatabase(ctx, projectID, options)
+ if err == nil {
+ t.Error("La création d'une base existante devrait échouer")
+ }
+}
+
+func TestDeleteDatabase(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-delete-project"
+
+ // Créer une base de données
+ options := &DatabaseOptions{
+ PostgresVersion: "16-alpine",
+ Port: 5438,
+ Memory: "512m",
+ CPUs: "0.5",
+ }
+
+ _, err := orch.CreateDatabase(ctx, projectID, options)
+ if err != nil {
+ t.Fatalf("Erreur lors de la création de la base de données: %v", err)
+ }
+
+ // Supprimer la base de données
+ err = orch.DeleteDatabase(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la suppression: %v", err)
+ }
+
+ // Vérifier que la base n'existe plus
+ exists, err := orch.DatabaseExists(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la vérification d'existence: %v", err)
+ }
+
+ if exists {
+ t.Error("La base de données devrait avoir été supprimée")
+ }
+}
+
+func TestDatabaseLifecycle(t *testing.T) {
+ orch := setupOrchestrator(t)
+ ctx := context.Background()
+ projectID := "test-lifecycle-project"
+
+ defer cleanupDatabase(t, orch, projectID)
+
+ // 1. Vérifier que la base n'existe pas
+ exists, err := orch.DatabaseExists(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la vérification d'existence: %v", err)
+ }
+ if exists {
+ t.Error("La base ne devrait pas exister initialement")
+ }
+
+ // 2. Créer la base
+ options := &DatabaseOptions{
+ PostgresVersion: "16-alpine",
+ Port: 5439,
+ Memory: "512m",
+ CPUs: "0.5",
+ }
+
+ dbInfo, err := orch.CreateDatabase(ctx, projectID, options)
+ if err != nil {
+ t.Fatalf("Erreur lors de la création: %v", err)
+ }
+
+ // 3. Vérifier que la base existe
+ exists, err = orch.DatabaseExists(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la vérification d'existence: %v", err)
+ }
+ if !exists {
+ t.Error("La base devrait exister après création")
+ }
+
+ // 4. Récupérer les informations
+ dbInfo2, err := orch.GetDatabaseInfo(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la récupération des informations: %v", err)
+ }
+
+ if dbInfo.ProjectID != dbInfo2.ProjectID {
+ t.Error("Les informations récupérées ne correspondent pas")
+ }
+
+ // 5. Supprimer la base
+ err = orch.DeleteDatabase(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la suppression: %v", err)
+ }
+
+ // 6. Vérifier que la base n'existe plus
+ exists, err = orch.DatabaseExists(ctx, projectID)
+ if err != nil {
+ t.Fatalf("Erreur lors de la vérification finale: %v", err)
+ }
+ if exists {
+ t.Error("La base ne devrait plus exister après suppression")
+ }
+}
diff --git a/pkg/proto/sovrabase.pb.go b/pkg/proto/sovrabase.pb.go
deleted file mode 100644
index f3038e3..0000000
--- a/pkg/proto/sovrabase.pb.go
+++ /dev/null
@@ -1,215 +0,0 @@
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// protoc-gen-go v1.36.10
-// protoc v6.32.1
-// source: proto/sovrabase.proto
-
-package sovrabase
-
-import (
- protoreflect "google.golang.org/protobuf/reflect/protoreflect"
- protoimpl "google.golang.org/protobuf/runtime/protoimpl"
- reflect "reflect"
- sync "sync"
- unsafe "unsafe"
-)
-
-const (
- // Verify that this generated code is sufficiently up-to-date.
- _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
- // Verify that runtime/protoimpl is sufficiently up-to-date.
- _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
-)
-
-type ForwardCommandRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
- TargetNode string `protobuf:"bytes,2,opt,name=target_node,json=targetNode,proto3" json:"target_node,omitempty"`
- Params map[string]string `protobuf:"bytes,3,rep,name=params,proto3" json:"params,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *ForwardCommandRequest) Reset() {
- *x = ForwardCommandRequest{}
- mi := &file_proto_sovrabase_proto_msgTypes[0]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *ForwardCommandRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ForwardCommandRequest) ProtoMessage() {}
-
-func (x *ForwardCommandRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_sovrabase_proto_msgTypes[0]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use ForwardCommandRequest.ProtoReflect.Descriptor instead.
-func (*ForwardCommandRequest) Descriptor() ([]byte, []int) {
- return file_proto_sovrabase_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *ForwardCommandRequest) GetCommand() string {
- if x != nil {
- return x.Command
- }
- return ""
-}
-
-func (x *ForwardCommandRequest) GetTargetNode() string {
- if x != nil {
- return x.TargetNode
- }
- return ""
-}
-
-func (x *ForwardCommandRequest) GetParams() map[string]string {
- if x != nil {
- return x.Params
- }
- return nil
-}
-
-type ForwardCommandResponse struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
- Result string `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"`
- ErrorMessage string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *ForwardCommandResponse) Reset() {
- *x = ForwardCommandResponse{}
- mi := &file_proto_sovrabase_proto_msgTypes[1]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *ForwardCommandResponse) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ForwardCommandResponse) ProtoMessage() {}
-
-func (x *ForwardCommandResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_sovrabase_proto_msgTypes[1]
- if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use ForwardCommandResponse.ProtoReflect.Descriptor instead.
-func (*ForwardCommandResponse) Descriptor() ([]byte, []int) {
- return file_proto_sovrabase_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *ForwardCommandResponse) GetSuccess() bool {
- if x != nil {
- return x.Success
- }
- return false
-}
-
-func (x *ForwardCommandResponse) GetResult() string {
- if x != nil {
- return x.Result
- }
- return ""
-}
-
-func (x *ForwardCommandResponse) GetErrorMessage() string {
- if x != nil {
- return x.ErrorMessage
- }
- return ""
-}
-
-var File_proto_sovrabase_proto protoreflect.FileDescriptor
-
-const file_proto_sovrabase_proto_rawDesc = "" +
- "\n" +
- "\x15proto/sovrabase.proto\x12\tsovrabase\"\xd3\x01\n" +
- "\x15ForwardCommandRequest\x12\x18\n" +
- "\acommand\x18\x01 \x01(\tR\acommand\x12\x1f\n" +
- "\vtarget_node\x18\x02 \x01(\tR\n" +
- "targetNode\x12D\n" +
- "\x06params\x18\x03 \x03(\v2,.sovrabase.ForwardCommandRequest.ParamsEntryR\x06params\x1a9\n" +
- "\vParamsEntry\x12\x10\n" +
- "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"o\n" +
- "\x16ForwardCommandResponse\x12\x18\n" +
- "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x16\n" +
- "\x06result\x18\x02 \x01(\tR\x06result\x12#\n" +
- "\rerror_message\x18\x03 \x01(\tR\ferrorMessage2n\n" +
- "\x15ForwardCommandService\x12U\n" +
- "\x0eForwardCommand\x12 .sovrabase.ForwardCommandRequest\x1a!.sovrabase.ForwardCommandResponseB\"Z github.com/ketsuna-org/sovrabaseb\x06proto3"
-
-var (
- file_proto_sovrabase_proto_rawDescOnce sync.Once
- file_proto_sovrabase_proto_rawDescData []byte
-)
-
-func file_proto_sovrabase_proto_rawDescGZIP() []byte {
- file_proto_sovrabase_proto_rawDescOnce.Do(func() {
- file_proto_sovrabase_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_sovrabase_proto_rawDesc), len(file_proto_sovrabase_proto_rawDesc)))
- })
- return file_proto_sovrabase_proto_rawDescData
-}
-
-var file_proto_sovrabase_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
-var file_proto_sovrabase_proto_goTypes = []any{
- (*ForwardCommandRequest)(nil), // 0: sovrabase.ForwardCommandRequest
- (*ForwardCommandResponse)(nil), // 1: sovrabase.ForwardCommandResponse
- nil, // 2: sovrabase.ForwardCommandRequest.ParamsEntry
-}
-var file_proto_sovrabase_proto_depIdxs = []int32{
- 2, // 0: sovrabase.ForwardCommandRequest.params:type_name -> sovrabase.ForwardCommandRequest.ParamsEntry
- 0, // 1: sovrabase.ForwardCommandService.ForwardCommand:input_type -> sovrabase.ForwardCommandRequest
- 1, // 2: sovrabase.ForwardCommandService.ForwardCommand:output_type -> sovrabase.ForwardCommandResponse
- 2, // [2:3] is the sub-list for method output_type
- 1, // [1:2] is the sub-list for method input_type
- 1, // [1:1] is the sub-list for extension type_name
- 1, // [1:1] is the sub-list for extension extendee
- 0, // [0:1] is the sub-list for field type_name
-}
-
-func init() { file_proto_sovrabase_proto_init() }
-func file_proto_sovrabase_proto_init() {
- if File_proto_sovrabase_proto != nil {
- return
- }
- type x struct{}
- out := protoimpl.TypeBuilder{
- File: protoimpl.DescBuilder{
- GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
- RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_sovrabase_proto_rawDesc), len(file_proto_sovrabase_proto_rawDesc)),
- NumEnums: 0,
- NumMessages: 3,
- NumExtensions: 0,
- NumServices: 1,
- },
- GoTypes: file_proto_sovrabase_proto_goTypes,
- DependencyIndexes: file_proto_sovrabase_proto_depIdxs,
- MessageInfos: file_proto_sovrabase_proto_msgTypes,
- }.Build()
- File_proto_sovrabase_proto = out.File
- file_proto_sovrabase_proto_goTypes = nil
- file_proto_sovrabase_proto_depIdxs = nil
-}
diff --git a/pkg/proto/sovrabase_grpc.pb.go b/pkg/proto/sovrabase_grpc.pb.go
deleted file mode 100644
index a25a392..0000000
--- a/pkg/proto/sovrabase_grpc.pb.go
+++ /dev/null
@@ -1,125 +0,0 @@
-// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
-// versions:
-// - protoc-gen-go-grpc v1.5.1
-// - protoc v6.32.1
-// source: proto/sovrabase.proto
-
-package sovrabase
-
-import (
- context "context"
- grpc "google.golang.org/grpc"
- codes "google.golang.org/grpc/codes"
- status "google.golang.org/grpc/status"
-)
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the grpc package it is being compiled against.
-// Requires gRPC-Go v1.64.0 or later.
-const _ = grpc.SupportPackageIsVersion9
-
-const (
- ForwardCommandService_ForwardCommand_FullMethodName = "/sovrabase.ForwardCommandService/ForwardCommand"
-)
-
-// ForwardCommandServiceClient is the client API for ForwardCommandService service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-//
-// ForwardCommand service
-type ForwardCommandServiceClient interface {
- ForwardCommand(ctx context.Context, in *ForwardCommandRequest, opts ...grpc.CallOption) (*ForwardCommandResponse, error)
-}
-
-type forwardCommandServiceClient struct {
- cc grpc.ClientConnInterface
-}
-
-func NewForwardCommandServiceClient(cc grpc.ClientConnInterface) ForwardCommandServiceClient {
- return &forwardCommandServiceClient{cc}
-}
-
-func (c *forwardCommandServiceClient) ForwardCommand(ctx context.Context, in *ForwardCommandRequest, opts ...grpc.CallOption) (*ForwardCommandResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(ForwardCommandResponse)
- err := c.cc.Invoke(ctx, ForwardCommandService_ForwardCommand_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-// ForwardCommandServiceServer is the server API for ForwardCommandService service.
-// All implementations must embed UnimplementedForwardCommandServiceServer
-// for forward compatibility.
-//
-// ForwardCommand service
-type ForwardCommandServiceServer interface {
- ForwardCommand(context.Context, *ForwardCommandRequest) (*ForwardCommandResponse, error)
- mustEmbedUnimplementedForwardCommandServiceServer()
-}
-
-// UnimplementedForwardCommandServiceServer must be embedded to have
-// forward compatible implementations.
-//
-// NOTE: this should be embedded by value instead of pointer to avoid a nil
-// pointer dereference when methods are called.
-type UnimplementedForwardCommandServiceServer struct{}
-
-func (UnimplementedForwardCommandServiceServer) ForwardCommand(context.Context, *ForwardCommandRequest) (*ForwardCommandResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method ForwardCommand not implemented")
-}
-func (UnimplementedForwardCommandServiceServer) mustEmbedUnimplementedForwardCommandServiceServer() {}
-func (UnimplementedForwardCommandServiceServer) testEmbeddedByValue() {}
-
-// UnsafeForwardCommandServiceServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to ForwardCommandServiceServer will
-// result in compilation errors.
-type UnsafeForwardCommandServiceServer interface {
- mustEmbedUnimplementedForwardCommandServiceServer()
-}
-
-func RegisterForwardCommandServiceServer(s grpc.ServiceRegistrar, srv ForwardCommandServiceServer) {
- // If the following call pancis, it indicates UnimplementedForwardCommandServiceServer was
- // embedded by pointer and is nil. This will cause panics if an
- // unimplemented method is ever invoked, so we test this at initialization
- // time to prevent it from happening at runtime later due to I/O.
- if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
- t.testEmbeddedByValue()
- }
- s.RegisterService(&ForwardCommandService_ServiceDesc, srv)
-}
-
-func _ForwardCommandService_ForwardCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(ForwardCommandRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(ForwardCommandServiceServer).ForwardCommand(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: ForwardCommandService_ForwardCommand_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(ForwardCommandServiceServer).ForwardCommand(ctx, req.(*ForwardCommandRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
-// ForwardCommandService_ServiceDesc is the grpc.ServiceDesc for ForwardCommandService service.
-// It's only intended for direct use with grpc.RegisterService,
-// and not to be introspected or modified (even as a copy)
-var ForwardCommandService_ServiceDesc = grpc.ServiceDesc{
- ServiceName: "sovrabase.ForwardCommandService",
- HandlerType: (*ForwardCommandServiceServer)(nil),
- Methods: []grpc.MethodDesc{
- {
- MethodName: "ForwardCommand",
- Handler: _ForwardCommandService_ForwardCommand_Handler,
- },
- },
- Streams: []grpc.StreamDesc{},
- Metadata: "proto/sovrabase.proto",
-}
diff --git a/proto/sovrabase.proto b/proto/sovrabase.proto
deleted file mode 100644
index d8ef2d4..0000000
--- a/proto/sovrabase.proto
+++ /dev/null
@@ -1,22 +0,0 @@
-syntax = "proto3";
-
-package sovrabase;
-
-option go_package = "github.com/ketsuna-org/sovrabase";
-
-// ForwardCommand service
-service ForwardCommandService {
- rpc ForwardCommand(ForwardCommandRequest) returns (ForwardCommandResponse);
-}
-
-message ForwardCommandRequest {
- string command = 1;
- string target_node = 2;
- map<string, string> params = 3;
-}
-
-message ForwardCommandResponse {
- bool success = 1;
- string result = 2;
- string error_message = 3;
-}