summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorexatombe <jeremy27.clara22@gmail.com>2025-10-30 00:16:17 +0100
committerexatombe <jeremy27.clara22@gmail.com>2025-10-30 00:16:17 +0100
commit0068551700163729c1c42b4435b51064e28461a2 (patch)
treec1b3407d052642a97285154797976433b157aa22
parent7604e249e332a872ae2e19f9826b56e3bd9313aa (diff)
feat: Implement orchestrator for PostgreSQL management via Docker and Kubernetes
- Added configuration options for orchestrator in config.go, including Docker and Kubernetes settings. - Created orchestrator package to manage PostgreSQL instances, supporting automatic container creation, conflict resolution, and resource limits. - Implemented Docker orchestrator with methods for creating, deleting, and retrieving database information. - Added Kubernetes orchestrator skeleton with TODOs for future implementation. - Developed comprehensive README documentation for orchestrator usage and configuration. - Created test script for orchestrator API to validate functionality and ensure reliability.
-rw-r--r--config.toml19
-rw-r--r--go.mod75
-rw-r--r--go.sum235
-rw-r--r--internal/config/config.go57
-rw-r--r--internal/orchestrator/README.md235
-rw-r--r--internal/orchestrator/orchestrator.go551
-rw-r--r--scripts/test_orchestrator_api.go137
7 files changed, 1272 insertions, 37 deletions
diff --git a/config.toml b/config.toml
index fae791d..81564fe 100644
--- a/config.toml
+++ b/config.toml
@@ -12,9 +12,22 @@ api_domain = "example.com"
manager = "sqlite"
uri = "./database/internal.db"
-[external_db]
-manager = "postgres"
-uri = "postgres://root:password@localhost:5432/"
+[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"
diff --git a/go.mod b/go.mod
index 3398ac7..b34dcb2 100644
--- a/go.mod
+++ b/go.mod
@@ -2,13 +2,74 @@ module github.com/ketsuna-org/sovrabase
go 1.25.2
-require github.com/BurntSushi/toml v1.5.0
+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
+ k8s.io/api v0.34.1
+ k8s.io/client-go v0.34.1
+)
require (
- golang.org/x/net v0.42.0 // indirect
- golang.org/x/sys v0.34.0 // indirect
- golang.org/x/text v0.27.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
- google.golang.org/grpc v1.76.0 // indirect
- google.golang.org/protobuf v1.36.10 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/containerd/errdefs v1.0.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/google/gnostic-models v0.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/sys/atomicwriter v0.1.0 // indirect
+ github.com/moby/term v0.5.2 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
+ go.opentelemetry.io/otel v1.38.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
+ go.opentelemetry.io/otel/metric v1.38.0 // indirect
+ go.opentelemetry.io/otel/trace v1.38.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.2 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/net v0.43.0 // indirect
+ golang.org/x/oauth2 v0.30.0 // indirect
+ golang.org/x/sys v0.35.0 // indirect
+ golang.org/x/term v0.34.0 // indirect
+ golang.org/x/text v0.28.0 // indirect
+ golang.org/x/time v0.9.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // 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
+ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
+ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
+ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+ sigs.k8s.io/randfill v1.0.0 // indirect
+ sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+ sigs.k8s.io/yaml v1.6.0 // indirect
)
diff --git a/go.sum b/go.sum
index dbdfe3f..29316b1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,233 @@
+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=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
-golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
-golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
+github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/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=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
+go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
+go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+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=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
+k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
+k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
+k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
+k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/internal/config/config.go b/internal/config/config.go
index c872203..ba616f9 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -4,47 +4,57 @@ import (
"fmt"
"github.com/BurntSushi/toml"
+
+ // Import des packages Docker et Kubernetes pour la gestion des conteneurs
+ _ "github.com/docker/docker/client"
+ _ "github.com/docker/go-connections/nat"
+ _ "k8s.io/api/core/v1"
+ _ "k8s.io/client-go/kubernetes"
+ _ "k8s.io/client-go/rest"
)
// RPC holds RPC configuration
type RPC struct {
- RPCSecret string
- RPCAddr string
+ RPCSecret string `toml:"rpc_secret"`
+ RPCAddr string `toml:"rpc_addr"`
}
// API holds API configuration
type API struct {
- APIAddr string
- APIDomain string
+ APIAddr string `toml:"api_addr"`
+ APIDomain string `toml:"api_domain"`
}
// InternalDB holds internal database configuration
type InternalDB struct {
- Manager string
- URI string
+ Manager string `toml:"manager"`
+ URI string `toml:"uri"`
}
-// ExternalDB holds external database configuration
-type ExternalDB struct {
- Manager string
- URI string
+// 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
}
// Cluster holds cluster/distributed configuration
type Cluster struct {
- NodeID string
- IsRPCServer bool
- RPCServers []string
+ NodeID string `toml:"node_id"`
+ IsRPCServer bool `toml:"is_rpc_server"`
+ RPCServers []string `toml:"rpc_servers"`
}
// Config holds the application configuration
type Config struct {
- Region string
- RPC RPC
- API API
- InternalDB InternalDB
- ExternalDB ExternalDB
- Cluster Cluster
+ Region string
+ RPC RPC
+ API API
+ InternalDB InternalDB
+ Orchestrator Orchestrator
+ Cluster Cluster
}
// LoadConfig loads configuration from a TOML file
@@ -58,6 +68,15 @@ func LoadConfig(filePath string) (*Config, error) {
if config.Region == "" {
config.Region = "supabase"
}
+ if config.Orchestrator.Type == "" {
+ config.Orchestrator.Type = "docker"
+ }
+ if config.Orchestrator.DockerHost == "" && config.Orchestrator.Type == "docker" {
+ config.Orchestrator.DockerHost = "unix:///var/run/docker.sock"
+ }
+ if config.Orchestrator.Namespace == "" && config.Orchestrator.Type == "kubernetes" {
+ config.Orchestrator.Namespace = "sovrabase-databases"
+ }
return &config, nil
}
diff --git a/internal/orchestrator/README.md b/internal/orchestrator/README.md
new file mode 100644
index 0000000..d681bd1
--- /dev/null
+++ b/internal/orchestrator/README.md
@@ -0,0 +1,235 @@
+# 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/orchestrator.go b/internal/orchestrator/orchestrator.go
new file mode 100644
index 0000000..bb72614
--- /dev/null
+++ b/internal/orchestrator/orchestrator.go
@@ -0,0 +1,551 @@
+package orchestrator
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/api/types/filters"
+ "github.com/docker/docker/api/types/image"
+ "github.com/docker/docker/client"
+ "github.com/docker/go-connections/nat"
+ "github.com/ketsuna-org/sovrabase/internal/config"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+)
+
+// Orchestrator interface pour gérer les conteneurs de bases de données
+type Orchestrator interface {
+ // CreateDatabase crée une nouvelle instance de base de données pour un projet
+ CreateDatabase(ctx context.Context, projectID string, options *DatabaseOptions) (*DatabaseInfo, error)
+
+ // DeleteDatabase supprime une instance de base de données
+ DeleteDatabase(ctx context.Context, projectID string) error
+
+ // GetDatabaseInfo retourne les informations de connexion à la base de données
+ GetDatabaseInfo(ctx context.Context, projectID string) (*DatabaseInfo, error)
+
+ // ListDatabases liste toutes les bases de données gérées
+ ListDatabases(ctx context.Context) ([]*DatabaseInfo, error)
+
+ // DatabaseExists vérifie si une base de données existe déjà
+ DatabaseExists(ctx context.Context, projectID string) (bool, error)
+}
+
+// DatabaseOptions contient les options pour créer une base de données
+type DatabaseOptions struct {
+ PostgresVersion string // Version de PostgreSQL (défaut: "16-alpine")
+ Password string // Mot de passe (généré si vide)
+ Port int // Port hôte (auto-assigné si 0)
+ Memory string // Limite mémoire (ex: "512m")
+ CPUs string // Limite CPU (ex: "0.5")
+}
+
+// DatabaseInfo contient les informations d'une base de données
+type DatabaseInfo struct {
+ ProjectID string
+ ContainerID string
+ ContainerName string
+ Status string
+ PostgresVersion string
+ Host string
+ Port string
+ Database string
+ User string
+ Password string
+ ConnectionString string
+ CreatedAt time.Time
+}
+
+// DockerOrchestrator gère les bases de données via Docker/Podman
+type DockerOrchestrator struct {
+ client *client.Client
+ config *config.Orchestrator
+}
+
+// KubernetesOrchestrator gère les bases de données via Kubernetes
+type KubernetesOrchestrator struct {
+ client *kubernetes.Clientset
+ config *config.Orchestrator
+}
+
+// NewOrchestrator crée un orchestrateur basé sur la configuration
+func NewOrchestrator(cfg *config.Orchestrator) (Orchestrator, error) {
+ switch cfg.Type {
+ case "docker":
+ return NewDockerOrchestrator(cfg)
+ case "kubernetes":
+ return NewKubernetesOrchestrator(cfg)
+ default:
+ return nil, fmt.Errorf("type d'orchestrateur non supporté: %s", cfg.Type)
+ }
+}
+
+// NewDockerOrchestrator crée un orchestrateur Docker
+func NewDockerOrchestrator(cfg *config.Orchestrator) (*DockerOrchestrator, error) {
+ cli, err := client.NewClientWithOpts(
+ client.WithHost(cfg.DockerHost),
+ client.WithAPIVersionNegotiation(),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("échec de connexion à Docker: %w", err)
+ }
+
+ return &DockerOrchestrator{
+ client: cli,
+ config: cfg,
+ }, nil
+}
+
+// NewKubernetesOrchestrator crée un orchestrateur Kubernetes
+func NewKubernetesOrchestrator(cfg *config.Orchestrator) (*KubernetesOrchestrator, error) {
+ kubeConfig := &rest.Config{
+ Host: cfg.KubeAPI,
+ BearerToken: cfg.KubeToken,
+ TLSClientConfig: rest.TLSClientConfig{
+ Insecure: false, // À configurer selon vos besoins
+ },
+ }
+
+ clientset, err := kubernetes.NewForConfig(kubeConfig)
+ if err != nil {
+ return nil, fmt.Errorf("échec de connexion à Kubernetes: %w", err)
+ }
+
+ return &KubernetesOrchestrator{
+ client: clientset,
+ config: cfg,
+ }, nil
+}
+
+// Implémentations des méthodes pour DockerOrchestrator
+
+// CreateDatabase crée une nouvelle instance PostgreSQL dans un conteneur
+func (d *DockerOrchestrator) CreateDatabase(ctx context.Context, projectID string, options *DatabaseOptions) (*DatabaseInfo, error) {
+ // Vérifier si la base existe déjà
+ exists, err := d.DatabaseExists(ctx, projectID)
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la vérification de l'existence: %w", err)
+ }
+ if exists {
+ return nil, fmt.Errorf("une base de données existe déjà pour le projet: %s", projectID)
+ }
+
+ // Définir les valeurs par défaut
+ if options == nil {
+ options = &DatabaseOptions{}
+ }
+ if options.PostgresVersion == "" {
+ options.PostgresVersion = "16-alpine"
+ }
+ if options.Password == "" {
+ options.Password = generatePassword(projectID)
+ }
+ if options.Port == 0 {
+ options.Port = findAvailablePort(ctx, d.client)
+ }
+
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+ imageName := fmt.Sprintf("docker.io/library/postgres:%s", options.PostgresVersion)
+ dbName := sanitizeDBName(projectID)
+ dbUser := sanitizeDBName(projectID)
+
+ // Pull l'image PostgreSQL
+ reader, err := d.client.ImagePull(ctx, imageName, image.PullOptions{})
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors du pull de l'image: %w", err)
+ }
+ // Consommer la sortie pour attendre la fin du pull
+ _, _ = io.Copy(io.Discard, reader)
+ reader.Close()
+
+ // Configuration du conteneur
+ containerConfig := &container.Config{
+ Image: imageName,
+ Env: []string{
+ fmt.Sprintf("POSTGRES_PASSWORD=%s", options.Password),
+ fmt.Sprintf("POSTGRES_DB=%s", dbName),
+ fmt.Sprintf("POSTGRES_USER=%s", dbUser),
+ },
+ ExposedPorts: nat.PortSet{
+ "5432/tcp": struct{}{},
+ },
+ Labels: map[string]string{
+ "sovrabase.managed": "true",
+ "sovrabase.project_id": projectID,
+ "sovrabase.type": "postgres",
+ "sovrabase.version": options.PostgresVersion,
+ "sovrabase.created_at": time.Now().UTC().Format(time.RFC3339),
+ },
+ }
+
+ // Configuration de l'hôte
+ hostConfig := &container.HostConfig{
+ PortBindings: nat.PortMap{
+ "5432/tcp": []nat.PortBinding{
+ {
+ HostIP: "127.0.0.1",
+ HostPort: fmt.Sprintf("%d", options.Port),
+ },
+ },
+ },
+ AutoRemove: false,
+ RestartPolicy: container.RestartPolicy{
+ Name: "unless-stopped",
+ },
+ }
+
+ // Ajouter les limites de ressources si spécifiées
+ if options.Memory != "" {
+ hostConfig.Resources.Memory = parseMemory(options.Memory)
+ }
+ if options.CPUs != "" {
+ hostConfig.Resources.NanoCPUs = parseCPUs(options.CPUs)
+ }
+
+ // Créer le conteneur
+ resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, containerName)
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la création du conteneur: %w", err)
+ }
+
+ // Gérer les warnings
+ if len(resp.Warnings) > 0 {
+ for _, warning := range resp.Warnings {
+ fmt.Printf("Warning: %s\n", warning)
+ }
+ }
+
+ // Démarrer le conteneur
+ if err := d.client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
+ // En cas d'erreur, nettoyer le conteneur créé
+ _ = d.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true})
+ return nil, fmt.Errorf("erreur lors du démarrage du conteneur: %w", err)
+ }
+
+ // Attendre que PostgreSQL soit prêt (max 30 secondes)
+ if err := d.waitForPostgres(ctx, resp.ID, 30*time.Second); err != nil {
+ return nil, fmt.Errorf("PostgreSQL n'a pas démarré correctement: %w", err)
+ }
+
+ // Créer les informations de la base de données
+ dbInfo := &DatabaseInfo{
+ ProjectID: projectID,
+ ContainerID: resp.ID,
+ ContainerName: containerName,
+ Status: "running",
+ PostgresVersion: options.PostgresVersion,
+ Host: "localhost",
+ Port: fmt.Sprintf("%d", options.Port),
+ Database: dbName,
+ User: dbUser,
+ Password: options.Password,
+ ConnectionString: fmt.Sprintf("postgresql://%s:%s@localhost:%d/%s?sslmode=disable", dbUser, options.Password, options.Port, dbName),
+ CreatedAt: time.Now().UTC(),
+ }
+
+ return dbInfo, nil
+}
+
+// DeleteDatabase supprime le conteneur de base de données
+func (d *DockerOrchestrator) DeleteDatabase(ctx context.Context, projectID string) error {
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+
+ // Vérifier si le conteneur existe
+ exists, err := d.DatabaseExists(ctx, projectID)
+ if err != nil {
+ return fmt.Errorf("erreur lors de la vérification: %w", err)
+ }
+ if !exists {
+ return fmt.Errorf("aucune base de données trouvée pour le projet: %s", projectID)
+ }
+
+ // Arrêter le conteneur (timeout de 10 secondes)
+ timeout := 10
+ stopOptions := container.StopOptions{
+ Timeout: &timeout,
+ }
+ if err := d.client.ContainerStop(ctx, containerName, stopOptions); err != nil {
+ // Ignorer si déjà arrêté
+ if !strings.Contains(err.Error(), "is not running") {
+ return fmt.Errorf("erreur lors de l'arrêt du conteneur: %w", err)
+ }
+ }
+
+ // Supprimer le conteneur et ses volumes
+ removeOptions := container.RemoveOptions{
+ Force: true,
+ RemoveVolumes: true,
+ }
+ if err := d.client.ContainerRemove(ctx, containerName, removeOptions); err != nil {
+ return fmt.Errorf("erreur lors de la suppression du conteneur: %w", err)
+ }
+
+ return nil
+}
+
+// GetDatabaseInfo récupère les informations d'une base de données
+func (d *DockerOrchestrator) GetDatabaseInfo(ctx context.Context, projectID string) (*DatabaseInfo, error) {
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+
+ // Inspecter le conteneur
+ containerJSON, err := d.client.ContainerInspect(ctx, containerName)
+ if err != nil {
+ if client.IsErrNotFound(err) {
+ return nil, fmt.Errorf("base de données non trouvée pour le projet: %s", projectID)
+ }
+ return nil, fmt.Errorf("erreur lors de l'inspection du conteneur: %w", err)
+ }
+
+ // Extraire les informations
+ labels := containerJSON.Config.Labels
+ env := parseEnvVars(containerJSON.Config.Env)
+
+ port := "unknown"
+ if bindings, ok := containerJSON.NetworkSettings.Ports["5432/tcp"]; ok && len(bindings) > 0 {
+ port = bindings[0].HostPort
+ }
+
+ dbName := env["POSTGRES_DB"]
+ dbUser := env["POSTGRES_USER"]
+ dbPassword := env["POSTGRES_PASSWORD"]
+
+ status := "stopped"
+ if containerJSON.State.Running {
+ status = "running"
+ }
+
+ createdAt, _ := time.Parse(time.RFC3339, labels["sovrabase.created_at"])
+
+ dbInfo := &DatabaseInfo{
+ ProjectID: projectID,
+ ContainerID: containerJSON.ID,
+ ContainerName: containerName,
+ Status: status,
+ PostgresVersion: labels["sovrabase.version"],
+ Host: "localhost",
+ Port: port,
+ Database: dbName,
+ User: dbUser,
+ Password: dbPassword,
+ ConnectionString: fmt.Sprintf("postgresql://%s:%s@localhost:%s/%s?sslmode=disable", dbUser, dbPassword, port, dbName),
+ CreatedAt: createdAt,
+ }
+
+ return dbInfo, nil
+}
+
+// ListDatabases liste toutes les bases de données gérées
+func (d *DockerOrchestrator) ListDatabases(ctx context.Context) ([]*DatabaseInfo, error) {
+ // Filtrer les conteneurs avec le label sovrabase.managed=true
+ filterArgs := filters.NewArgs()
+ filterArgs.Add("label", "sovrabase.managed=true")
+ filterArgs.Add("label", "sovrabase.type=postgres")
+
+ containers, err := d.client.ContainerList(ctx, container.ListOptions{
+ All: true,
+ Filters: filterArgs,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("erreur lors de la liste des conteneurs: %w", err)
+ }
+
+ databases := make([]*DatabaseInfo, 0, len(containers))
+ for _, cont := range containers {
+ projectID := cont.Labels["sovrabase.project_id"]
+ if projectID == "" {
+ continue
+ }
+
+ dbInfo, err := d.GetDatabaseInfo(ctx, projectID)
+ if err != nil {
+ // Logger l'erreur mais continuer
+ fmt.Printf("Warning: impossible de récupérer les infos pour %s: %v\n", projectID, err)
+ continue
+ }
+
+ databases = append(databases, dbInfo)
+ }
+
+ return databases, nil
+}
+
+// DatabaseExists vérifie si une base de données existe
+func (d *DockerOrchestrator) DatabaseExists(ctx context.Context, projectID string) (bool, error) {
+ containerName := fmt.Sprintf("sovrabase-db-%s", projectID)
+
+ _, err := d.client.ContainerInspect(ctx, containerName)
+ if err != nil {
+ if client.IsErrNotFound(err) {
+ return false, nil
+ }
+ return false, fmt.Errorf("erreur lors de la vérification: %w", err)
+ }
+
+ return true, nil
+}
+
+// Implémentations des méthodes pour KubernetesOrchestrator
+func (k *KubernetesOrchestrator) CreateDatabase(ctx context.Context, projectID string, options *DatabaseOptions) (*DatabaseInfo, error) {
+ // TODO: Créer un StatefulSet PostgreSQL
+ return nil, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) DeleteDatabase(ctx context.Context, projectID string) error {
+ // TODO: Supprimer le StatefulSet et PVC
+ return fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) GetDatabaseInfo(ctx context.Context, projectID string) (*DatabaseInfo, error) {
+ // TODO: Récupérer l'URL via le Service Kubernetes
+ return nil, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) ListDatabases(ctx context.Context) ([]*DatabaseInfo, error) {
+ // TODO: Lister tous les StatefulSets de bases de données
+ return nil, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+func (k *KubernetesOrchestrator) DatabaseExists(ctx context.Context, projectID string) (bool, error) {
+ // TODO: Vérifier l'existence du StatefulSet
+ return false, fmt.Errorf("non implémenté pour Kubernetes")
+}
+
+// Fonctions utilitaires
+
+// waitForPostgres attend que PostgreSQL soit prêt
+func (d *DockerOrchestrator) waitForPostgres(ctx context.Context, containerID string, timeout time.Duration) error {
+ deadline := time.Now().Add(timeout)
+
+ for time.Now().Before(deadline) {
+ // Vérifier si le conteneur est toujours en cours d'exécution
+ inspect, err := d.client.ContainerInspect(ctx, containerID)
+ if err != nil {
+ return fmt.Errorf("erreur lors de l'inspection: %w", err)
+ }
+
+ if !inspect.State.Running {
+ return fmt.Errorf("le conteneur s'est arrêté de manière inattendue")
+ }
+
+ // Tenter une connexion PostgreSQL via exec
+ execConfig := container.ExecOptions{
+ Cmd: []string{"pg_isready", "-U", "postgres"},
+ AttachStdout: true,
+ AttachStderr: true,
+ }
+
+ execResp, err := d.client.ContainerExecCreate(ctx, containerID, execConfig)
+ if err == nil {
+ attachResp, err := d.client.ContainerExecAttach(ctx, execResp.ID, container.ExecAttachOptions{})
+ if err == nil {
+ attachResp.Close()
+
+ execInspect, err := d.client.ContainerExecInspect(ctx, execResp.ID)
+ if err == nil && execInspect.ExitCode == 0 {
+ return nil // PostgreSQL est prêt
+ }
+ }
+ }
+
+ time.Sleep(1 * time.Second)
+ }
+
+ return fmt.Errorf("timeout en attendant que PostgreSQL démarre")
+}
+
+// generatePassword génère un mot de passe sécurisé
+func generatePassword(projectID string) string {
+ // Pour la production, utiliser crypto/rand
+ // Ici, génération simple pour l'exemple
+ return fmt.Sprintf("secure_%s_%d", projectID, time.Now().Unix())
+}
+
+// sanitizeDBName nettoie un nom pour l'utiliser comme nom de DB/user
+func sanitizeDBName(name string) string {
+ // Remplacer les caractères non alphanumériques par des underscores
+ result := strings.Map(func(r rune) rune {
+ if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
+ return r
+ }
+ return '_'
+ }, name)
+
+ // Limiter à 63 caractères (limite PostgreSQL)
+ if len(result) > 63 {
+ result = result[:63]
+ }
+
+ return strings.ToLower(result)
+}
+
+// findAvailablePort trouve un port disponible
+func findAvailablePort(ctx context.Context, cli *client.Client) int {
+ // Liste des ports utilisés
+ usedPorts := make(map[int]bool)
+
+ containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
+ if err == nil {
+ for _, cont := range containers {
+ for _, port := range cont.Ports {
+ if port.PublicPort > 0 {
+ usedPorts[int(port.PublicPort)] = true
+ }
+ }
+ }
+ }
+
+ // Chercher un port libre à partir de 5433
+ for port := 5433; port < 6000; port++ {
+ if !usedPorts[port] {
+ return port
+ }
+ }
+
+ return 5433 // Par défaut
+}
+
+// parseMemory convertit une chaîne mémoire en bytes
+func parseMemory(mem string) int64 {
+ // Exemples: "512m", "1g", "256M"
+ mem = strings.ToLower(strings.TrimSpace(mem))
+
+ multiplier := int64(1)
+ if strings.HasSuffix(mem, "k") {
+ multiplier = 1024
+ mem = strings.TrimSuffix(mem, "k")
+ } else if strings.HasSuffix(mem, "m") {
+ multiplier = 1024 * 1024
+ mem = strings.TrimSuffix(mem, "m")
+ } else if strings.HasSuffix(mem, "g") {
+ multiplier = 1024 * 1024 * 1024
+ mem = strings.TrimSuffix(mem, "g")
+ }
+
+ var value int64
+ fmt.Sscanf(mem, "%d", &value)
+ return value * multiplier
+}
+
+// parseCPUs convertit une chaîne CPU en nanoCPUs
+func parseCPUs(cpus string) int64 {
+ // Exemple: "0.5" = 500000000 nanoCPUs
+ var value float64
+ fmt.Sscanf(cpus, "%f", &value)
+ return int64(value * 1e9)
+}
+
+// parseEnvVars convertit un tableau d'env vars en map
+func parseEnvVars(envVars []string) map[string]string {
+ result := make(map[string]string)
+ for _, env := range envVars {
+ parts := strings.SplitN(env, "=", 2)
+ if len(parts) == 2 {
+ result[parts[0]] = parts[1]
+ }
+ }
+ return result
+}
diff --git a/scripts/test_orchestrator_api.go b/scripts/test_orchestrator_api.go
new file mode 100644
index 0000000..ee926ee
--- /dev/null
+++ b/scripts/test_orchestrator_api.go
@@ -0,0 +1,137 @@
+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
+}