diff options
| -rw-r--r-- | .dockerignore | 4 | ||||
| -rw-r--r-- | .github/workflows/build.yml | 165 | ||||
| -rw-r--r-- | .gitkeep | 0 | ||||
| -rw-r--r-- | Dockerfile | 14 | ||||
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.md | 236 | ||||
| -rw-r--r-- | cmd/server/main.go | 93 | ||||
| -rw-r--r-- | config.example.yaml | 32 | ||||
| -rw-r--r-- | config.toml | 35 | ||||
| -rw-r--r-- | config.yaml | 33 | ||||
| -rw-r--r-- | docker-compose.yml | 21 | ||||
| -rw-r--r-- | go.mod | 9 | ||||
| -rw-r--r-- | go.sum | 8 | ||||
| -rw-r--r-- | internal/config/config.go | 51 | ||||
| -rw-r--r-- | internal/middleware/cors.go | 122 | ||||
| -rw-r--r-- | internal/middleware/cors_test.go | 305 | ||||
| -rw-r--r-- | internal/orchestrator/README.md | 235 | ||||
| -rw-r--r-- | internal/orchestrator/test_orchestrator_api.go | 297 | ||||
| -rw-r--r-- | pkg/proto/sovrabase.pb.go | 215 | ||||
| -rw-r--r-- | pkg/proto/sovrabase_grpc.pb.go | 125 | ||||
| -rw-r--r-- | proto/sovrabase.proto | 22 | ||||
| -rw-r--r-- | scripts/test_orchestrator_api.go | 137 |
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 @@ -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"] @@ -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/* @@ -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://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 @@ -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 @@ -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 -} |
