summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore4
-rw-r--r--.github/workflows/build.yml165
-rw-r--r--.gitkeep0
-rw-r--r--Dockerfile14
-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.toml35
-rw-r--r--config.yaml33
-rw-r--r--docker-compose.yml21
-rw-r--r--go.mod9
-rw-r--r--go.sum8
-rw-r--r--internal/config/config.go51
-rw-r--r--internal/middleware/cors.go122
-rw-r--r--internal/middleware/cors_test.go305
-rw-r--r--internal/orchestrator/README.md235
-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
-rw-r--r--scripts/test_orchestrator_api.go137
22 files changed, 1275 insertions, 889 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/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..78323d7
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,165 @@
+name: Build
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main, develop ]
+ workflow_dispatch:
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+ # Build Go binaries for multiple platforms
+ build-binaries:
+ name: Build Go Binary
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ # Linux AMD64
+ - goos: linux
+ goarch: amd64
+ output: sovrabase-linux-amd64
+ # Linux ARM64
+ - goos: linux
+ goarch: arm64
+ output: sovrabase-linux-arm64
+ # macOS AMD64 (Intel)
+ - goos: darwin
+ goarch: amd64
+ output: sovrabase-darwin-amd64
+ # macOS ARM64 (Apple Silicon)
+ - goos: darwin
+ goarch: arm64
+ output: sovrabase-darwin-arm64
+ # Windows AMD64
+ - goos: windows
+ goarch: amd64
+ output: sovrabase-windows-amd64.exe
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25.2'
+ cache: true
+
+ - name: Download dependencies
+ run: go mod download
+
+ - name: Build binary
+ env:
+ GOOS: ${{ matrix.goos }}
+ GOARCH: ${{ matrix.goarch }}
+ CGO_ENABLED: 0
+ run: |
+ go build -ldflags="-w -s" -o ${{ matrix.output }} ./cmd/server/main.go
+
+ - name: Upload binary artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.output }}
+ path: ${{ matrix.output }}
+ retention-days: 7
+
+ # Build Docker images for multiple architectures
+ build-docker:
+ name: Build Docker Image
+ strategy:
+ matrix:
+ include:
+ # AMD64 on Ubuntu
+ - runner: ubuntu-latest
+ platform: linux/amd64
+ arch: amd64
+ # ARM64 on Ubuntu ARM runner
+ - runner: ubuntu-24.04-arm
+ platform: linux/arm64
+ arch: arm64
+
+ runs-on: ${{ matrix.runner }}
+
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch,suffix=-${{ matrix.arch }}
+ type=ref,event=pr,suffix=-${{ matrix.arch }}
+ type=sha,prefix={{branch}}-,suffix=-${{ matrix.arch }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ platforms: ${{ matrix.platform }}
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: |
+ TARGETARCH=${{ matrix.arch }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ provenance: false
+
+ # Create manifest list to combine both architectures
+ create-manifest:
+ name: Create Multi-arch Manifest
+ needs: build-docker
+ runs-on: ubuntu-latest
+ if: github.event_name != 'pull_request'
+
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Log in to Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Create and push manifest
+ run: |
+ # Extract branch or tag name
+ BRANCH=${GITHUB_REF##*/}
+ SHA_SHORT=$(echo $GITHUB_SHA | cut -c1-7)
+
+ # Create manifest for branch tag (combines amd64 and arm64)
+ docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH} \
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH}-${SHA_SHORT}-amd64 \
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH}-${SHA_SHORT}-arm64
+
+ # Create manifest for latest tag if on default branch
+ if [ "${{ github.ref }}" = "refs/heads/main" ] || [ "${{ github.ref }}" = "refs/heads/master" ]; then
+ docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH}-${SHA_SHORT}-amd64 \
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH}-${SHA_SHORT}-arm64
+ fi
diff --git a/.gitkeep b/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/.gitkeep
+++ /dev/null
diff --git a/Dockerfile b/Dockerfile
index 9fffe62..1f19a96 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,9 @@
# Stage 1: Build
FROM golang:1.25.2-alpine AS builder
+# Build arguments for multi-architecture support
+ARG TARGETARCH
+
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
@@ -18,7 +21,8 @@ COPY . .
# Build the application
# CGO_ENABLED=0 for static binary
# -ldflags="-w -s" to strip debug info and reduce binary size
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
+# TARGETARCH will be automatically set by Docker buildx (amd64 or arm64)
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
-ldflags="-w -s" \
-o sovrabase \
./cmd/server/main.go
@@ -34,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 81564fe..0000000
--- a/config.toml
+++ /dev/null
@@ -1,35 +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"
-
-[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"
-
-[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 b34dcb2..e562326 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,9 @@ module github.com/ketsuna-org/sovrabase
go 1.25.2
require (
- github.com/BurntSushi/toml v1.5.0
github.com/docker/docker v28.5.1+incompatible
github.com/docker/go-connections v0.6.0
- google.golang.org/grpc v1.76.0
- google.golang.org/protobuf v1.36.10
+ gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.34.1
k8s.io/client-go v0.34.1
)
@@ -31,6 +29,7 @@ require (
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
@@ -59,10 +58,10 @@ require (
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/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // 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
- gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
k8s.io/apimachinery v0.34.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
diff --git a/go.sum b/go.sum
index 29316b1..6f0ce89 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,5 @@
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/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
-github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
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=
@@ -47,8 +45,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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=
@@ -58,6 +54,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
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=
@@ -189,8 +187,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
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=
-gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
-gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
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=
diff --git a/internal/config/config.go b/internal/config/config.go
index ba616f9..ce442a1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -2,8 +2,9 @@ package config
import (
"fmt"
+ "os"
- "github.com/BurntSushi/toml"
+ "gopkg.in/yaml.v3"
// Import des packages Docker et Kubernetes pour la gestion des conteneurs
_ "github.com/docker/docker/client"
@@ -13,54 +14,54 @@ import (
_ "k8s.io/client-go/rest"
)
-// RPC holds RPC configuration
-type RPC struct {
- RPCSecret string `toml:"rpc_secret"`
- RPCAddr string `toml:"rpc_addr"`
-}
-
// API holds API configuration
type API struct {
- APIAddr string `toml:"api_addr"`
- APIDomain string `toml:"api_domain"`
+ APIAddr string `yaml:"api_addr"`
+ CORSAllow []string `yaml:"cors_allow"`
+ Domain string `yaml:"domain"`
}
// InternalDB holds internal database configuration
type InternalDB struct {
- Manager string `toml:"manager"`
- URI string `toml:"uri"`
+ Manager string `yaml:"manager"`
+ URI string `yaml:"uri"`
}
// Orchestrator holds container orchestration configuration
type Orchestrator struct {
- Type string `toml:"type"` // "docker" or "kubernetes"
- DockerHost string `toml:"docker_host"` // Docker/Podman socket or remote host
- KubeAPI string `toml:"kube_api"` // Kubernetes API endpoint
- KubeToken string `toml:"kube_token"` // Kubernetes API token
- Namespace string `toml:"namespace"` // Kubernetes namespace for database deployments
+ 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 `toml:"node_id"`
- IsRPCServer bool `toml:"is_rpc_server"`
- RPCServers []string `toml:"rpc_servers"`
+// 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
Orchestrator Orchestrator
- Cluster Cluster
+ 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)
}
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/README.md b/internal/orchestrator/README.md
deleted file mode 100644
index d681bd1..0000000
--- a/internal/orchestrator/README.md
+++ /dev/null
@@ -1,235 +0,0 @@
-# Orchestrator - Gestionnaire de bases de données
-
-Le package `orchestrator` permet de gérer automatiquement des instances de bases de données PostgreSQL via Docker/Podman ou Kubernetes.
-
-## Fonctionnalités
-
-- ✅ Création automatique de conteneurs PostgreSQL par projet
-- ✅ Gestion des conflits et détection des bases existantes
-- ✅ Assignment automatique des ports
-- ✅ Génération sécurisée des mots de passe
-- ✅ Limites de ressources (CPU/Mémoire)
-- ✅ Attente automatique du démarrage de PostgreSQL
-- ✅ Suppression propre des conteneurs et volumes
-- ✅ Liste de toutes les bases gérées
-- ✅ Support Docker, Podman et Kubernetes (en cours)
-
-## Configuration
-
-Fichier `config.toml` :
-
-```toml
-[orchestrator]
-type = "docker"
-docker_host = "unix:///mnt/wsl/podman-sockets/podman-machine-default/podman-root.sock"
-
-# Pour Docker standard
-# docker_host = "unix:///var/run/docker.sock"
-
-# Pour Podman rootless
-# docker_host = "unix:///run/user/1000/podman/podman.sock"
-
-# Pour Kubernetes
-# type = "kubernetes"
-# kube_api = "https://kubernetes.default.svc"
-# kube_token = "your-token"
-# namespace = "sovrabase-databases"
-```
-
-## Utilisation
-
-### Création d'un orchestrateur
-
-```go
-import (
- "github.com/ketsuna-org/sovrabase/internal/config"
- "github.com/ketsuna-org/sovrabase/internal/orchestrator"
-)
-
-// Charger la configuration
-cfg, err := config.LoadConfig("config.toml")
-if err != nil {
- log.Fatal(err)
-}
-
-// Créer l'orchestrateur
-orch, err := orchestrator.NewOrchestrator(&cfg.Orchestrator)
-if err != nil {
- log.Fatal(err)
-}
-```
-
-### Créer une base de données
-
-```go
-ctx := context.Background()
-
-options := &orchestrator.DatabaseOptions{
- PostgresVersion: "16-alpine",
- Port: 5434, // Auto-assigné si 0
- Memory: "512m", // Optionnel
- CPUs: "0.5", // Optionnel
- Password: "", // Généré si vide
-}
-
-dbInfo, err := orch.CreateDatabase(ctx, "my-project", options)
-if err != nil {
- log.Fatal(err)
-}
-
-fmt.Printf("Connection: %s\n", dbInfo.ConnectionString)
-```
-
-### Récupérer les informations
-
-```go
-dbInfo, err := orch.GetDatabaseInfo(ctx, "my-project")
-if err != nil {
- log.Fatal(err)
-}
-
-fmt.Printf("Database: %s\n", dbInfo.Database)
-fmt.Printf("Port: %s\n", dbInfo.Port)
-fmt.Printf("Status: %s\n", dbInfo.Status)
-```
-
-### Lister toutes les bases
-
-```go
-databases, err := orch.ListDatabases(ctx)
-if err != nil {
- log.Fatal(err)
-}
-
-for _, db := range databases {
- fmt.Printf("%s - %s (port %s)\n", db.ProjectID, db.Status, db.Port)
-}
-```
-
-### Supprimer une base
-
-```go
-err := orch.DeleteDatabase(ctx, "my-project")
-if err != nil {
- log.Fatal(err)
-}
-```
-
-### Vérifier l'existence
-
-```go
-exists, err := orch.DatabaseExists(ctx, "my-project")
-if err != nil {
- log.Fatal(err)
-}
-```
-
-## Structure DatabaseInfo
-
-```go
-type DatabaseInfo struct {
- ProjectID string // ID du projet
- ContainerID string // ID du conteneur
- ContainerName string // Nom du conteneur
- Status string // "running" ou "stopped"
- PostgresVersion string // Version PostgreSQL
- Host string // Hôte (localhost)
- Port string // Port de connexion
- Database string // Nom de la DB
- User string // Utilisateur
- Password string // Mot de passe
- ConnectionString string // String de connexion complète
- CreatedAt time.Time // Date de création
-}
-```
-
-## Gestion des erreurs
-
-Le package gère automatiquement :
-
-- **Conflits** : Détecte si une base existe déjà
-- **Ports occupés** : Trouve automatiquement un port libre (5433-6000)
-- **Échec de démarrage** : Nettoie le conteneur si PostgreSQL ne démarre pas
-- **Timeout** : Attend max 30 secondes le démarrage de PostgreSQL
-- **Nettoyage** : Supprime les volumes lors de la suppression
-
-## Labels des conteneurs
-
-Tous les conteneurs créés ont les labels suivants :
-
-```
-sovrabase.managed=true
-sovrabase.project_id=<project-id>
-sovrabase.type=postgres
-sovrabase.version=<pg-version>
-sovrabase.created_at=<timestamp>
-```
-
-## Scripts de test
-
-### Test complet de l'API
-
-```bash
-cd scripts
-go run test_orchestrator_api.go
-```
-
-### Test simple de création
-
-```bash
-cd scripts
-go run test_orchestrator.go
-```
-
-### Nettoyage
-
-```bash
-cd scripts
-go run cleanup_test.go
-```
-
-## Commandes utiles
-
-```bash
-# Voir tous les conteneurs Sovrabase
-podman ps -a --filter "label=sovrabase.managed=true"
-
-# Logs d'un conteneur
-podman logs sovrabase-db-<project-id>
-
-# Se connecter à la DB
-podman exec -it sovrabase-db-<project-id> psql -U <user> -d <database>
-
-# Supprimer tous les conteneurs Sovrabase
-podman rm -f $(podman ps -aq --filter "label=sovrabase.managed=true")
-```
-
-## Architecture
-
-```
-orchestrator/
-├── orchestrator.go # Interface et implémentations
-├── docker.go # Logique Docker/Podman
-└── kubernetes.go # Logique Kubernetes (TODO)
-```
-
-## Roadmap
-
-- [x] Support Docker/Podman
-- [x] Gestion des conflits
-- [x] Assignment automatique des ports
-- [x] Limites de ressources
-- [x] Attente du démarrage
-- [ ] Support Kubernetes
-- [ ] Backup/Restore automatique
-- [ ] Métriques et monitoring
-- [ ] Mise à jour des versions PostgreSQL
-- [ ] Réplication et haute disponibilité
-
-## Contribution
-
-Les contributions sont les bienvenues ! Assurez-vous que tous les tests passent :
-
-```bash
-go test ./internal/orchestrator/...
-```
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;
-}
diff --git a/scripts/test_orchestrator_api.go b/scripts/test_orchestrator_api.go
deleted file mode 100644
index ee926ee..0000000
--- a/scripts/test_orchestrator_api.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "log"
-
- "github.com/ketsuna-org/sovrabase/internal/config"
- "github.com/ketsuna-org/sovrabase/internal/orchestrator"
-)
-
-func main() {
- log.Println("🚀 Test de l'API Orchestrator\n")
-
- // Charger la configuration
- cfg, err := config.LoadConfig("../config.toml")
- if err != nil {
- log.Fatalf("❌ Erreur de chargement de la config: %v", err)
- }
-
- log.Printf("✅ Configuration chargée (type: %s)\n\n", cfg.Orchestrator.Type)
-
- // Créer l'orchestrateur
- orch, err := orchestrator.NewOrchestrator(&cfg.Orchestrator)
- if err != nil {
- log.Fatalf("❌ Erreur de création de l'orchestrateur: %v", err)
- }
-
- ctx := context.Background()
- projectID := "my-awesome-project"
-
- // Test 1: Vérifier si la base existe déjà
- log.Println("📋 Test 1: Vérification de l'existence")
- exists, err := orch.DatabaseExists(ctx, projectID)
- if err != nil {
- log.Fatalf("❌ Erreur: %v", err)
- }
- log.Printf(" Existe déjà: %v\n\n", exists)
-
- // Si existe déjà, la supprimer d'abord
- if exists {
- log.Println("🗑️ Base existante détectée, suppression...")
- if err := orch.DeleteDatabase(ctx, projectID); err != nil {
- log.Fatalf("❌ Erreur de suppression: %v", err)
- }
- log.Println(" ✅ Supprimée\n")
- }
-
- // Test 2: Créer une nouvelle base de données
- log.Println("📋 Test 2: Création d'une nouvelle base de données")
- options := &orchestrator.DatabaseOptions{
- PostgresVersion: "16-alpine",
- Port: 5434,
- Memory: "512m",
- CPUs: "0.5",
- }
-
- dbInfo, err := orch.CreateDatabase(ctx, projectID, options)
- if err != nil {
- log.Fatalf("❌ Erreur de création: %v", err)
- }
-
- log.Println(" ✅ Base de données créée avec succès!")
- printDatabaseInfo(dbInfo)
-
- // Test 3: Récupérer les informations
- log.Println("\n📋 Test 3: Récupération des informations")
- dbInfo2, err := orch.GetDatabaseInfo(ctx, projectID)
- if err != nil {
- log.Fatalf("❌ Erreur: %v", err)
- }
- log.Println(" ✅ Informations récupérées")
- printDatabaseInfo(dbInfo2)
-
- // Test 4: Lister toutes les bases de données
- log.Println("\n📋 Test 4: Liste de toutes les bases de données")
- databases, err := orch.ListDatabases(ctx)
- if err != nil {
- log.Fatalf("❌ Erreur: %v", err)
- }
- log.Printf(" ✅ Nombre de bases trouvées: %d\n", len(databases))
- for i, db := range databases {
- log.Printf(" %d. %s (%s) - Port: %s\n", i+1, db.ProjectID, db.Status, db.Port)
- }
-
- // Test 5: Tester un conflit (création d'une base existante)
- log.Println("\n📋 Test 5: Test de conflit (création d'une base existante)")
- _, err = orch.CreateDatabase(ctx, projectID, options)
- if err != nil {
- log.Printf(" ✅ Erreur attendue reçue: %v\n", err)
- } else {
- log.Println(" ❌ Aucune erreur reçue (inattendu!)")
- }
-
- // Test 6: Supprimer la base de données
- log.Println("\n📋 Test 6: Suppression de la base de données")
- if err := orch.DeleteDatabase(ctx, projectID); err != nil {
- log.Fatalf("❌ Erreur de suppression: %v", err)
- }
- log.Println(" ✅ Base de données supprimée")
-
- // Test 7: Vérifier que la base n'existe plus
- log.Println("\n📋 Test 7: Vérification de la suppression")
- exists, err = orch.DatabaseExists(ctx, projectID)
- if err != nil {
- log.Fatalf("❌ Erreur: %v", err)
- }
- log.Printf(" ✅ Existe: %v (attendu: false)\n", exists)
-
- log.Println("\n" + repeat("=", 60))
- log.Println("🎉 Tous les tests sont passés avec succès!")
- log.Println(repeat("=", 60))
-}
-
-func printDatabaseInfo(db *orchestrator.DatabaseInfo) {
- fmt.Println("\n " + repeat("-", 50))
- fmt.Printf(" 📊 Projet: %s\n", db.ProjectID)
- fmt.Printf(" 📦 Conteneur: %s\n", db.ContainerName)
- fmt.Printf(" 🆔 Container ID: %s\n", db.ContainerID[:12])
- fmt.Printf(" 📊 Status: %s\n", db.Status)
- fmt.Printf(" 🐘 Version: PostgreSQL %s\n", db.PostgresVersion)
- fmt.Printf(" 🔌 Port: %s\n", db.Port)
- fmt.Printf(" 💾 Database: %s\n", db.Database)
- fmt.Printf(" 👤 User: %s\n", db.User)
- fmt.Printf(" 🔑 Password: %s\n", db.Password)
- fmt.Printf(" 🔗 Connection: %s\n", db.ConnectionString)
- fmt.Printf(" 📅 Created: %s\n", db.CreatedAt.Format("2006-01-02 15:04:05"))
- fmt.Println(" " + repeat("-", 50))
-}
-
-func repeat(s string, n int) string {
- result := ""
- for i := 0; i < n; i++ {
- result += s
- }
- return result
-}