diff --git a/docs/modules/nebulagraph.md b/docs/modules/nebulagraph.md
new file mode 100644
index 0000000000..99d6886d3a
--- /dev/null
+++ b/docs/modules/nebulagraph.md
@@ -0,0 +1,111 @@
+# NebulaGraph
+
+Not available until the next release :material-tag: main
+
+## Introduction
+
+The Testcontainers module for [NebulaGraph](https://nebula-graph.io/), a distributed, scalable, and lightning-fast graph database. This module manages a complete NebulaGraph cluster including Meta Service, Storage Service, and Graph Service components.
+
+## Adding this module to your project dependencies
+
+Add the NebulaGraph module to your Go dependencies:
+
+```go
+go get github.com/testcontainers/testcontainers-go/modules/nebulagraph
+```
+
+## Usage example
+
+
+[Creating a NebulaGraph container](../../modules/nebulagraph/nebulagraph_test.go) inside_block:TestNebulaGraphContainer
+
+
+## Module Reference
+
+### RunCluster function
+
+- Not available until the next release :material-tag: main
+
+The NebulaGraph module provides a function to create a complete NebulaGraph cluster within a Docker network:
+
+```golang
+func RunCluster(ctx context.Context,
+ graphdImg string, graphdCustomizers []testcontainers.ContainerCustomizer,
+ storagedImg string, storagedCustomizers []testcontainers.ContainerCustomizer,
+ metadImg string, metadCustomizers []testcontainers.ContainerCustomizer,
+) (*Cluster, error)
+```
+
+This function creates a complete NebulaGraph cluster with customizable settings. It returns a `Cluster` struct that contains references to all four components:
+- Meta Service (metad)
+- Storage Service (storaged)
+- Graph Service (graphd)
+
+### Default Configuration
+
+The module uses the following default configurations:
+
+#### Default Images
+ - Graph Service: `vesoft/nebula-graphd:v3.8.0`
+ - Meta Service: `vesoft/nebula-metad:v3.8.0`
+ - Storage Service: `vesoft/nebula-storaged:v3.8.0`
+
+#### Exposed Ports
+ - Graph Service: 9669 (TCP), 19669 (HTTP)
+ - Meta Service: 9559 (TCP), 19559 (HTTP)
+ - Storage Service: 9779 (TCP), 19779 (HTTP)
+
+#### Health Checks
+
+The module implements health checks for all services:
+
+- Meta Service: HTTP health check on `/status` endpoint (port 19559)
+- Graph Service: HTTP health check on `/status` endpoint (port 19669)
+- Storage Service: Log-based health check for initialization
+- Activator Service: Log-based health check and exit status for storage registration
+
+A cluster is considered ready when:
+
+1. Meta service is healthy and accessible
+2. Graph service is healthy and accessible
+3. Storage service is initialized and running
+4. Storage service is successfully registered with the meta service via the activator
+
+### Container Options
+
+When starting the NebulaGraph container, you can pass options in a variadic way to configure it.
+
+The module supports customization for each service container (Meta, Storage, Graph, and Activator) through ContainerCustomizer options. Common customizations include:
+
+- Custom images for each service
+- Environment variables
+- Resource limits
+- Network settings
+- Volume mounts
+- Wait strategies
+
+{% include "../features/common_functional_options_list.md" %}
+
+### Container Methods
+
+The `Cluster` struct provides the following methods:
+
+#### ConnectionString
+
+- Not available until the next release :material-tag: main
+
+Returns the host:port string for connecting to the NebulaGraph graph service (graphd).
+
+```golang
+func (c *Cluster) ConnectionString(ctx context.Context) (string, error)
+```
+
+#### Terminate
+
+- Not available until the next release :material-tag: main
+
+Stops and removes all containers in the NebulaGraph cluster (Meta, Storage, Graph, and Activator services) and cleans up the associated Docker network.
+
+```golang
+func (c *Cluster) Terminate(ctx context.Context) error
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 625acbf673..1d6f55baea 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -104,6 +104,7 @@ nav:
- modules/mssql.md
- modules/mysql.md
- modules/nats.md
+ - modules/nebulagraph.md
- modules/neo4j.md
- modules/ollama.md
- modules/openfga.md
diff --git a/modules/nebulagraph/Makefile b/modules/nebulagraph/Makefile
new file mode 100644
index 0000000000..8185444293
--- /dev/null
+++ b/modules/nebulagraph/Makefile
@@ -0,0 +1,5 @@
+include ../../commons-test.mk
+
+.PHONY: test
+test:
+ $(MAKE) test-nebulagraph
diff --git a/modules/nebulagraph/activator.sh b/modules/nebulagraph/activator.sh
new file mode 100644
index 0000000000..e4dd096315
--- /dev/null
+++ b/modules/nebulagraph/activator.sh
@@ -0,0 +1,24 @@
+for i in $(seq 1 ${ACTIVATOR_RETRY}); do
+ echo "nebula" | nebula-console -addr graphd0 -port 9669 -u root -e 'ADD HOSTS "storaged0":9779' 1>/dev/null 2>/dev/null
+ if [ $? -eq 0 ]; then
+ echo "✔️ Storage activated successfully."
+ exit 0
+ else
+ output=$(echo "nebula" | nebula-console -addr graphd0 -port 9669 -u root -e 'ADD HOSTS "storaged0":9779' 2>&1)
+ if echo "$output" | grep -q "Existed"; then
+ echo "✔️ Storage activated already , Exiting..."
+ exit 0
+ fi
+ fi
+ if [ $i -lt ${ACTIVATOR_RETRY} ]; then
+ echo "⏳ Attempting to activate storaged, attempt $i/${ACTIVATOR_RETRY}... It's normal to take some attempts before storaged is ready. Please wait."
+ else
+ echo "❌ Failed to activate storaged after ${ACTIVATOR_RETRY} attempts. Please check MetaD, StorageD logs."
+ echo "ℹ️ Error during storage activation:"
+ echo "=============================================================="
+ echo "$output"
+ echo "=============================================================="
+ exit 1
+ fi
+ sleep 5
+ done && tail -f /dev/null
\ No newline at end of file
diff --git a/modules/nebulagraph/go.mod b/modules/nebulagraph/go.mod
new file mode 100644
index 0000000000..8ba2104ec2
--- /dev/null
+++ b/modules/nebulagraph/go.mod
@@ -0,0 +1,70 @@
+module github.com/testcontainers/testcontainers-go/modules/nebulagraph
+
+go 1.23.6
+
+require (
+ github.com/jolestar/go-commons-pool v2.0.0+incompatible
+ github.com/nebula-contrib/nebula-sirius v1.0.0-rc2
+ github.com/stretchr/testify v1.10.0
+ github.com/testcontainers/testcontainers-go v0.38.0
+)
+
+require (
+ dario.cat/mergo v1.0.1 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/apache/thrift v0.21.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // 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/containerd/platforms v0.2.1 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/docker v28.2.2+incompatible // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/ebitengine/purego v0.8.4 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.1.0 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/morikuni/aec v1.0.0 // 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/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/shirou/gopsutil/v4 v4.25.5 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.37.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+ go.opentelemetry.io/otel/metric v1.37.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+ go.opentelemetry.io/otel/trace v1.37.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.7.1 // indirect
+ golang.org/x/crypto v0.40.0 // indirect
+ 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/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/modules/nebulagraph/go.sum b/modules/nebulagraph/go.sum
new file mode 100644
index 0000000000..77dd158160
--- /dev/null
+++ b/modules/nebulagraph/go.sum
@@ -0,0 +1,200 @@
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
+github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+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/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+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.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
+github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+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/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
+github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+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/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.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
+github.com/jolestar/go-commons-pool v2.0.0+incompatible h1:uHn5uRKsLLQSf9f1J5QPY2xREWx/YH+e4bIIXcAuAaE=
+github.com/jolestar/go-commons-pool v2.0.0+incompatible/go.mod h1:ChJYIbIch0DMCSU6VU0t0xhPoWDR2mMFIQek3XWU0s8=
+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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+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/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+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/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/nebula-contrib/nebula-sirius v1.0.0-rc2 h1:fHoW6ELbJJOaNRGcYkjzGA9gJYmECGXAXjHMXNTrWQQ=
+github.com/nebula-contrib/nebula-sirius v1.0.0-rc2/go.mod h1:ScJ/CkDeN5LhdvUfooNVB2/xBtr88tBxCHswW00m5Ew=
+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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+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/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
+github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
+github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+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.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
+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/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
+golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
+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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+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/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=
diff --git a/modules/nebulagraph/nebulagraph.go b/modules/nebulagraph/nebulagraph.go
new file mode 100644
index 0000000000..04e713406c
--- /dev/null
+++ b/modules/nebulagraph/nebulagraph.go
@@ -0,0 +1,121 @@
+package nebulagraph
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/network"
+)
+
+// Cluster represents a running NebulaGraph cluster for testing
+type Cluster struct {
+ graphd testcontainers.Container
+ metad testcontainers.Container
+ storaged testcontainers.Container
+ network *testcontainers.DockerNetwork
+}
+
+// RunCluster starts a NebulaGraph cluster (metad, storaged, graphd and activator) containers within a Docker network
+func RunCluster(ctx context.Context,
+ graphdImg string, graphdCustomizers []testcontainers.ContainerCustomizer,
+ storagedImg string, storagedCustomizers []testcontainers.ContainerCustomizer,
+ metadImg string, metadCustomizers []testcontainers.ContainerCustomizer,
+) (*Cluster, error) {
+ // 1. Create a custom network
+ netRes, err := network.New(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("new nebulagraph network: %w", err)
+ }
+
+ // 2. Start metad
+ aggMetadCustomizers := append(defaultMetadContainerCustomizers(netRes), metadCustomizers...)
+ metad, err := testcontainers.Run(ctx, metadImg, aggMetadCustomizers...)
+ if err != nil {
+ errs := []error{fmt.Errorf("run metad container: %w", err)}
+ errs2 := terminateContainersAndRemoveNetwork(ctx, netRes)
+ errs = append(errs, errs2...)
+ return nil, errors.Join(errs...)
+ }
+
+ // 3. Start graphd (needed for storage registration)
+ aggGraphdCustomizers := append(defaultGraphdContainerCustomizers(netRes), graphdCustomizers...)
+ graphd, err := testcontainers.Run(ctx, graphdImg, aggGraphdCustomizers...)
+ if err != nil {
+ errs := []error{fmt.Errorf("run graphd container: %w", err)}
+ errs2 := terminateContainersAndRemoveNetwork(ctx, netRes, metad)
+ errs = append(errs, errs2...)
+ return nil, errors.Join(errs...)
+ }
+
+ // 4. Start storaged
+ aggStoragedCustomizers := append(defaultStoragedContainerCustomizers(netRes), storagedCustomizers...)
+ storaged, err := testcontainers.Run(ctx, storagedImg, aggStoragedCustomizers...)
+ if err != nil {
+ errs := []error{fmt.Errorf("run storaged container: %w", err)}
+ fmt.Println("error starting storaged: ", err)
+ errs2 := terminateContainersAndRemoveNetwork(ctx, netRes, graphd, metad)
+ errs = append(errs, errs2...)
+ return nil, errors.Join(errs...)
+ }
+
+ // 5. Run storage registration command with retry logic
+ activator, err := testcontainers.Run(ctx, defaultNebulaConsoleImage, defaultActivatorContainerCustomizers(netRes)...)
+ if err != nil {
+ errs := []error{fmt.Errorf("run activator container: %w", err)}
+ errs2 := terminateContainersAndRemoveNetwork(ctx, netRes, storaged, graphd, metad)
+ errs = append(errs, errs2...)
+ return nil, errors.Join(errs...)
+ }
+
+ activatorState, err := activator.State(ctx)
+ if err != nil {
+ errs := []error{fmt.Errorf("get activator container state: %w", err)}
+ errs2 := terminateContainersAndRemoveNetwork(ctx, netRes, storaged, graphd, metad, activator)
+ errs = append(errs, errs2...)
+ return nil, errors.Join(errs...)
+ }
+
+ if !activatorState.Running && activatorState.ExitCode != 0 {
+ errs := []error{fmt.Errorf("activator container not running or exited with code %d", activatorState.ExitCode)}
+ errs2 := terminateContainersAndRemoveNetwork(ctx, netRes, storaged, graphd, metad)
+ errs = append(errs, errs2...)
+ return nil, errors.Join(errs...)
+ }
+
+ return &Cluster{
+ graphd: graphd,
+ metad: metad,
+ storaged: storaged,
+ network: netRes,
+ }, nil
+}
+
+// ConnectionString returns the host:port for connecting to NebulaGraph graphd
+func (c *Cluster) ConnectionString(ctx context.Context) (string, error) {
+ return c.graphd.PortEndpoint(ctx, graphdPort, "")
+}
+
+// Terminate stops all NebulaGraph containers
+func (c *Cluster) Terminate(ctx context.Context) error {
+ errs := terminateContainersAndRemoveNetwork(ctx, c.network, c.graphd, c.metad, c.storaged)
+ return errors.Join(errs...)
+}
+
+func terminateContainersAndRemoveNetwork(ctx context.Context, netRes *testcontainers.DockerNetwork, containers ...testcontainers.Container) []error {
+ var errs []error
+ for _, ctr := range containers {
+ if ctr != nil {
+ if err := ctr.Terminate(ctx); err != nil {
+ errs = append(errs, fmt.Errorf("terminate container: %w", err))
+ }
+ }
+ }
+
+ if err := netRes.Remove(ctx); err != nil {
+ errs = append(errs, fmt.Errorf("network remove: %w", err))
+ }
+
+ return errs
+}
diff --git a/modules/nebulagraph/nebulagraph_test.go b/modules/nebulagraph/nebulagraph_test.go
new file mode 100644
index 0000000000..89186a5acc
--- /dev/null
+++ b/modules/nebulagraph/nebulagraph_test.go
@@ -0,0 +1,112 @@
+package nebulagraph_test
+
+import (
+ "context"
+ "net"
+ "strconv"
+ "testing"
+ "time"
+
+ pool "github.com/jolestar/go-commons-pool"
+ nebula_sirius "github.com/nebula-contrib/nebula-sirius"
+ "github.com/nebula-contrib/nebula-sirius/nebula"
+ "github.com/stretchr/testify/require"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/nebulagraph"
+)
+
+const (
+ defaultGraphdImage = "vesoft/nebula-graphd:v3.8.0"
+ defaultMetadImage = "vesoft/nebula-metad:v3.8.0"
+ defaultStoragedImage = "vesoft/nebula-storaged:v3.8.0"
+)
+
+func TestNebulaGraphContainer(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+ defer cancel()
+
+ container, err := nebulagraph.RunCluster(ctx,
+ defaultGraphdImage, []testcontainers.ContainerCustomizer{},
+ defaultStoragedImage, []testcontainers.ContainerCustomizer{},
+ defaultMetadImage, []testcontainers.ContainerCustomizer{},
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = container.Terminate(ctx) })
+
+ conn, err := container.ConnectionString(ctx)
+ require.NoError(t, err)
+ require.NotEmpty(t, conn)
+
+ // Parse the connection string to get host and port
+ host, portt, err := net.SplitHostPort(conn)
+ require.NoError(t, err)
+
+ portInt, err := strconv.Atoi(portt)
+ require.NoError(t, err)
+
+ // Create client factory
+ clientFactory := nebula_sirius.NewNebulaClientFactory(
+ &nebula_sirius.NebulaClientConfig{
+ HostAddress: nebula_sirius.HostAddress{
+ Host: host,
+ Port: portInt,
+ },
+ },
+ nebula_sirius.DefaultLogger{},
+ nebula_sirius.DefaultClientNameGenerator,
+ )
+
+ // Create client pool
+ nebulaClientPool := pool.NewObjectPool(
+ ctx,
+ clientFactory,
+ &pool.ObjectPoolConfig{
+ MaxIdle: 5,
+ MaxTotal: 10,
+ },
+ )
+
+ // Test client connection and basic queries
+ t.Run("basic-operations", func(t *testing.T) {
+ // Get a client from the pool
+ clientObj, err := nebulaClientPool.BorrowObject(ctx)
+ require.NoError(t, err)
+ defer func() {
+ err := nebulaClientPool.ReturnObject(ctx, clientObj)
+ require.NoError(t, err)
+ }()
+
+ client := clientObj.(*nebula_sirius.WrappedNebulaClient)
+ require.NotNil(t, client)
+
+ // Get graph client
+ g, err := client.GraphClient()
+ require.NoError(t, err)
+
+ // Authenticate
+ auth, err := g.Authenticate(ctx, []byte("root"), []byte("nebula"))
+ require.NoError(t, err)
+ require.Equal(t, nebula.ErrorCode_SUCCEEDED, auth.GetErrorCode(), "Auth error: %s", auth.GetErrorMsg())
+
+ // Test YIELD query
+ result, err := g.Execute(ctx, *auth.SessionID, []byte("YIELD 1;"))
+ require.NoError(t, err)
+ require.Equal(t, nebula.ErrorCode_SUCCEEDED, result.GetErrorCode(), "Query error: %s", result.GetErrorMsg())
+
+ // Validate result contains our storage node
+ resultSet, err := nebula_sirius.GenResultSet(result)
+ require.NoError(t, err)
+
+ // Convert result to string for validation
+ rows := resultSet.GetRows()
+ require.NotEmpty(t, rows, "Expected at least one row in YIELD output")
+
+ row := rows[0]
+ require.NotNil(t, row, "Row should not be nil")
+
+ vals := row.GetValues()
+ require.NotEmpty(t, vals, "Row values should not be empty")
+ require.Equal(t, int64(1), vals[0].GetIVal())
+ })
+}
diff --git a/modules/nebulagraph/options.go b/modules/nebulagraph/options.go
new file mode 100644
index 0000000000..f04e3a8376
--- /dev/null
+++ b/modules/nebulagraph/options.go
@@ -0,0 +1,134 @@
+package nebulagraph
+
+import (
+ _ "embed"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/network"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+//go:embed activator.sh
+var activatorScript string
+
+const user = "root"
+
+const (
+ defaultNebulaConsoleImage = "vesoft/nebula-console:v3.8.0"
+)
+
+const (
+ metadNetworkAlias = "metad0"
+ graphdNetworkAlias = "graphd0"
+ storagedNetworkAlias = "storaged0"
+)
+
+const (
+ graphdPort = "9669"
+ metadPort = "9559"
+ storagedPort = "9779"
+
+ graphdPortHTTP = "19669"
+ metadPortHTTP = "19559"
+ storagedPortHTTP = "19779"
+)
+
+func defaultGraphdContainerCustomizers(nw *testcontainers.DockerNetwork) []testcontainers.ContainerCustomizer {
+ customizers := []testcontainers.ContainerCustomizer{
+ testcontainers.WithExposedPorts(graphdPort+"/tcp", graphdPortHTTP+"/tcp"),
+ testcontainers.WithCmdArgs([]string{
+ "--meta_server_addrs=" + metadNetworkAlias + ":" + metadPort,
+ "--port=" + graphdPort,
+ "--local_ip=" + graphdNetworkAlias,
+ "--ws_ip=" + graphdNetworkAlias,
+ "--ws_http_port=" + graphdPortHTTP,
+ "--logtostderr=true",
+ "--redirect_stdout=false",
+ "--v=0",
+ "--minloglevel=0",
+ }...),
+ testcontainers.WithEnv(map[string]string{"USER": user}),
+ testcontainers.WithWaitStrategy(wait.ForHTTP("/status").WithPort(graphdPortHTTP + "/tcp").
+ WithStatusCodeMatcher(
+ func(status int) bool {
+ return status == http.StatusOK
+ },
+ )),
+ network.WithNetwork([]string{graphdNetworkAlias}, nw),
+ }
+ return customizers
+}
+
+func defaultMetadContainerCustomizers(nw *testcontainers.DockerNetwork) []testcontainers.ContainerCustomizer {
+ customizers := []testcontainers.ContainerCustomizer{
+ testcontainers.WithExposedPorts(metadPort+"/tcp", metadPortHTTP+"/tcp"),
+ testcontainers.WithCmdArgs([]string{
+ "--meta_server_addrs=" + metadNetworkAlias + ":" + metadPort,
+ "--local_ip=" + metadNetworkAlias,
+ "--ws_ip=" + metadNetworkAlias,
+ "--port=" + metadPort,
+ "--ws_http_port=" + metadPortHTTP,
+ "--data_path=/data/meta",
+ "--logtostderr=true",
+ "--redirect_stdout=false",
+ "--v=0",
+ "--minloglevel=0",
+ }...),
+ testcontainers.WithEnv(map[string]string{"USER": user}),
+ testcontainers.WithWaitStrategy(wait.ForHTTP("/status").WithPort(metadPortHTTP + "/tcp").
+ WithStatusCodeMatcher(
+ func(status int) bool {
+ return status == http.StatusOK
+ },
+ )),
+ network.WithNetwork([]string{metadNetworkAlias}, nw),
+ }
+ return customizers
+}
+
+func defaultStoragedContainerCustomizers(nw *testcontainers.DockerNetwork) []testcontainers.ContainerCustomizer {
+ customizers := []testcontainers.ContainerCustomizer{
+ testcontainers.WithExposedPorts(storagedPort+"/tcp", storagedPortHTTP+"/tcp"),
+ testcontainers.WithCmdArgs([]string{
+ "--meta_server_addrs=" + metadNetworkAlias + ":" + metadPort,
+ "--local_ip=" + storagedNetworkAlias,
+ "--ws_ip=" + storagedNetworkAlias,
+ "--port=" + storagedPort,
+ "--ws_http_port=" + storagedPortHTTP,
+ "--data_path=/data/storage",
+ "--logtostderr=true",
+ "--redirect_stdout=false",
+ "--v=0",
+ "--minloglevel=0",
+ }...),
+ testcontainers.WithEnv(map[string]string{"USER": user}),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog(fmt.Sprintf(`localhost = "%s":%s`, storagedNetworkAlias, storagedPort)).WithStartupTimeout(30 * time.Second),
+ ),
+ network.WithNetwork([]string{storagedNetworkAlias}, nw),
+ }
+ return customizers
+}
+
+func defaultActivatorContainerCustomizers(nw *testcontainers.DockerNetwork) []testcontainers.ContainerCustomizer {
+ customizers := []testcontainers.ContainerCustomizer{
+ testcontainers.WithEntrypoint([]string{}...),
+ testcontainers.WithCmd([]string{
+ "sh", "-c",
+ activatorScript,
+ }...),
+ testcontainers.WithExposedPorts(),
+ testcontainers.WithEnv(map[string]string{
+ "USER": user,
+ "ACTIVATOR_RETRY": "30",
+ }),
+ testcontainers.WithWaitStrategy(
+ wait.ForLog(`✔️ Storage activated`).WithStartupTimeout(60 * time.Second),
+ ),
+ network.WithNetwork([]string{}, nw),
+ }
+ return customizers
+}