diff --git a/docs/modules/mongodb-atlaslocal.md b/docs/modules/mongodb-atlaslocal.md
new file mode 100644
index 0000000000..3b52e3d6b4
--- /dev/null
+++ b/docs/modules/mongodb-atlaslocal.md
@@ -0,0 +1,188 @@
+# MongoDB Atlas Local
+
+Not available until the next release :material-tag: main
+
+## Introduction
+
+The MongoDB Atlas Local module for Testcontainers lets you spin up a local MongoDB Atlas instance in Docker using
+[mongodb/mongodb-atlas-local](https://hub.docker.com/r/mongodb/mongodb-atlas-local) for integration tests and
+development. This module supports SCRAM authentication, init scripts, and custom log file mounting.
+
+This module differs from the standard modules/mongodb Testcontainers module, allowing users to spin up a full local
+Atlas-like environment complete with Atlas Search and Atlas Vector Search.
+
+## Adding this module to your project dependencies
+
+Please run the following command to add the MongoDB Atlas Local module to your Go dependencies:
+
+```
+go get github.com/testcontainers/testcontainers-go/modules/mongodb/atlaslocal
+```
+
+## Usage example
+
+
+[Creating a MongoDB Atlas Local container](../../modules/mongodb/atlaslocal/examples_test.go) inside_block:runMongoDBAtlasLocalContainer
+
+
+## Module Reference
+
+### Run function
+
+- Not available until the next release :material-tag: main
+
+The `atlaslocal` module exposes one entrypoint function to create the MongoDB Atlas Local container, and this
+function receives three parameters:
+
+```golang
+func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
+```
+
+- `context.Context`, the Go context.
+- `string`, the Docker image to use.
+- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.
+
+#### Image
+
+Use the second argument in the `Run` function to set a valid Docker image.
+In example: `Run(context.Background(), "mongodb/mongodb-atlas-local:latest")`.
+
+### Container Options
+
+When starting the MongoDB Atlas Local container, you can pass options in a variadic way to configure it.
+
+#### WithUsername
+
+- Not available until the next release :material-tag: main
+
+This functional option sets the initial username to be created when the container starts, populating the
+`MONGODB_INITDB_ROOT_USERNAME` environment variable. You cannot mix this option with `WithUsernameFile`, as it will
+result in an error.
+
+#### WithPassword
+
+- Not available until the next release :material-tag: main
+
+This functional option sets the initial password to be created when the container starts, populating the
+`MONGODB_INITDB_ROOT_PASSWORD` environment variable. You cannot mix this option with `WithPasswordFile`, as it will
+result in an error.
+
+#### WithUsernameFile
+
+- Not available until the next release :material-tag: main
+
+This functional option mounts a local file as the MongoDB root username secret at `/run/secrets/mongo-root-username`
+and sets the `MONGODB_INITDB_ROOT_USERNAME_FILE` environment variable. The path must be absolute and exist; no-op if
+empty.
+
+#### WithPasswordFile
+
+- Not available until the next release :material-tag: main
+
+This functional option mounts a local file as the MongoDB root password secret at `/run/secrets/mongo-root-password` and
+sets the `MONGODB_INITDB_ROOT_PASSWORD_FILE` environment variable. The path must be absolute and exist; no-op if empty.
+
+#### WithNoTelemetry
+
+- Not available until the next release :material-tag: main
+
+This functional option disables the telemetry feature of MongoDB Atlas Local, setting the `DO_NOT_TRACK` environment
+variable to `1`.
+
+#### WithInitDatabase
+
+- Not available until the next release :material-tag: main
+
+This functional option allows you to specify a database name to be initialized when the container starts, populating
+the `MONGODB_INITDB_DATABASE` environment variable.
+
+#### WithInitScripts
+
+- Not available until the next release :material-tag: main
+
+Mounts a directory into `/docker-entrypoint-initdb.d`, running `.sh`/`.js` scripts on startup. Calling this function
+multiple times mounts only the latest directory.
+
+#### WithMongotLogFile
+
+- Not available until the next release :material-tag: main
+
+This functional option writes the mongot logs to `/tmp/mongot.log` inside the container. See
+`(*Container).ReadMongotLogs` to read the logs locally.
+
+#### WithMongotLogToStdout
+
+- Not available until the next release :material-tag: main
+
+This functional option writes the mongot logs to `/dev/stdout` inside the container. See
+`(*Container).ReadMongotLogs` to read the logs locally.
+
+#### WithMongotLogToStderr
+
+- Not available until the next release :material-tag: main
+
+This functional option writes the mongot logs to `/dev/stderr` inside the container. See
+`(*Container).ReadMongotLogs` to read the logs locally.
+
+#### WithRunnerLogFile
+
+- Not available until the next release :material-tag: main
+
+This functional option writes the runner logs to `/tmp/runner.log` inside the container. See
+`(*Container).ReadRunnerLogs` to read the logs locally.
+
+#### WithRunnerLogToStdout
+
+- Not available until the next release :material-tag: main
+
+This functional option writes the runner logs to `/dev/stdout` inside the container. See
+`(*Container).ReadRunnerLogs` to read the logs locally.
+
+#### WithRunnerLogToStderr
+
+- Not available until the next release :material-tag: main
+
+This functional option writes the runner logs to `/dev/stderr` inside the container. See
+`(*Container).ReadRunnerLogs` to read the logs locally.
+
+{% include "../features/common_functional_options_list.md" %}
+
+### Container Methods
+
+The MongoDB Atlas Local container exposes the following methods:
+
+
+#### ConnectionString
+
+- Not available until the next release :material-tag: main
+
+The `ConnectionString` method returns the connection string to connect to the MongoDB Atlas Local container.
+It returns a string with the format `mongodb://:[/]/?directConnection=true[&authSource=admin]`.
+
+It can be used to configure a MongoDB client (`go.mongodb.org/mongo-driver/v2/mongo`), e.g.:
+
+
+[Using ConnectionString with the MongoDB client](../../modules/mongodb/atlaslocal/examples_test.go) inside_block:connectToMongo
+
+
+#### ReadMongotLogs
+
+- Not available until the next release :material-tag: main
+
+The `ReadMongotLogs` returns a reader for the log solution specified when constructing the container.
+
+
+
+[Using ReadMongotLogs with the MongoDB client](../../modules/mongodb/atlaslocal/examples_test.go) inside_block:mongotLogsRead
+
+
+#### ReadRunnerLogs
+
+- Not available until the next release :material-tag: main
+
+The `ReadRunnerLogs` returns a reader for the log solution specified when constructing the container.
+
+
+
+[Using ReadRunnerLogs with the MongoDB client](../../modules/mongodb/atlaslocal/examples_test.go) inside_block:runnerLogsRead
+
diff --git a/mkdocs.yml b/mkdocs.yml
index 1d6f55baea..0603401fc6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -100,6 +100,7 @@ nav:
- modules/milvus.md
- modules/minio.md
- modules/mockserver.md
+ - modules/mongodb-atlaslocal.md
- modules/mongodb.md
- modules/mssql.md
- modules/mysql.md
diff --git a/modules/mongodb/atlaslocal/atlaslocal.go b/modules/mongodb/atlaslocal/atlaslocal.go
new file mode 100644
index 0000000000..91b9d811a2
--- /dev/null
+++ b/modules/mongodb/atlaslocal/atlaslocal.go
@@ -0,0 +1,144 @@
+package atlaslocal
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+const defaultPort = "27017/tcp"
+
+// Container represents the MongoDBAtlasLocal container type used in the module.
+type Container struct {
+ testcontainers.Container
+ userOpts options
+}
+
+// Run creates an instance of the MongoDBAtlasLocal container type.
+func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
+ userOpts := options{}
+ for _, opt := range opts {
+ if apply, ok := opt.(Option); ok {
+ if err := apply(&userOpts); err != nil {
+ return nil, fmt.Errorf("apply option: %w", err)
+ }
+ }
+ }
+
+ if err := userOpts.validate(); err != nil {
+ return nil, fmt.Errorf("validate options: %w", err)
+ }
+
+ moduleOpts := []testcontainers.ContainerCustomizer{ // Set the defaults
+ testcontainers.WithExposedPorts(defaultPort),
+ testcontainers.WithWaitStrategy(wait.ForAll(wait.ForListeningPort(defaultPort), wait.ForHealthCheck())),
+ testcontainers.WithEnv(userOpts.env()),
+ testcontainers.WithFiles(userOpts.files...),
+ }
+
+ moduleOpts = append(moduleOpts, opts...)
+
+ container, err := testcontainers.Run(ctx, img, moduleOpts...)
+ var c *Container
+ if container != nil {
+ c = &Container{Container: container, userOpts: userOpts}
+ }
+
+ if err != nil {
+ return c, fmt.Errorf("run container: %w", err)
+ }
+
+ return c, nil
+}
+
+// ConnectionString returns the connection string for the MongoDB Atlas Local
+// container. If you provide a username and a password, the connection string
+// will also include them.
+func (ctr *Container) ConnectionString(ctx context.Context) (string, error) {
+ endpoint, err := ctr.PortEndpoint(ctx, defaultPort, "")
+ if err != nil {
+ return "", fmt.Errorf("port endpoint: %w", err)
+ }
+
+ uri := &url.URL{
+ Scheme: "mongodb",
+ Host: endpoint,
+ Path: "/",
+ }
+
+ // If MONGODB_INITDB_DATABASE is set, use it as the default database in the
+ // connection string.
+ if db := ctr.userOpts.database; db != "" {
+ uri.Path, err = url.JoinPath("/", db)
+ if err != nil {
+ return "", fmt.Errorf("join path: %w", err)
+ }
+ }
+
+ user, err := ctr.userOpts.parseUsername()
+ if err != nil {
+ return "", fmt.Errorf("parse username: %w", err)
+ }
+
+ password, err := ctr.userOpts.parsePassword()
+ if err != nil {
+ return "", fmt.Errorf("parse password: %w", err)
+ }
+
+ if user != "" && password != "" {
+ uri.User = url.UserPassword(user, password)
+ }
+
+ q := uri.Query()
+ q.Set("directConnection", "true")
+ if user != "" && password != "" {
+ q.Set("authSource", "admin")
+ }
+
+ uri.RawQuery = q.Encode()
+
+ return uri.String(), nil
+}
+
+// ReadMongotLogs returns a reader for mongot logs in the container. Reads from
+// stdout/stderr or /tmp/mongot.log if configured.
+//
+// This method return the os.ErrNotExist sentinel error if it is called with
+// no log file configured.
+func (ctr *Container) ReadMongotLogs(ctx context.Context) (io.ReadCloser, error) {
+ path := ctr.userOpts.mongotLogPath
+ if path == "" {
+ return nil, os.ErrNotExist
+ }
+
+ switch path {
+ case "/dev/stdout", "/dev/stderr":
+ return ctr.Logs(ctx)
+ default:
+ return ctr.CopyFileFromContainer(ctx, path)
+ }
+}
+
+// ReadRunnerLogs() returns a reader for runner logs in the container. Reads
+// from stdout/stderr or /tmp/runner.log if configured.
+//
+// This method return the os.ErrNotExist sentinel error if it is called with
+// no log file configured.
+func (ctr *Container) ReadRunnerLogs(ctx context.Context) (io.ReadCloser, error) {
+ path := ctr.userOpts.runnerLogPath
+ if path == "" {
+ return nil, os.ErrNotExist
+ }
+
+ switch path {
+ case "/dev/stdout", "/dev/stderr":
+ return ctr.Logs(ctx)
+ default:
+ return ctr.CopyFileFromContainer(ctx, path)
+ }
+}
diff --git a/modules/mongodb/atlaslocal/atlaslocal_test.go b/modules/mongodb/atlaslocal/atlaslocal_test.go
new file mode 100644
index 0000000000..b46221857b
--- /dev/null
+++ b/modules/mongodb/atlaslocal/atlaslocal_test.go
@@ -0,0 +1,811 @@
+package atlaslocal_test
+
+import (
+ "context"
+ "io"
+ "math/rand/v2"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "go.mongodb.org/mongo-driver/v2/bson"
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+ "go.mongodb.org/mongo-driver/v2/x/mongo/driver/connstring"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/mongodb/atlaslocal"
+)
+
+const latestImage = "mongodb/mongodb-atlas-local:latest"
+
+func TestMongoDBAtlasLocal(t *testing.T) {
+ ctx := context.Background()
+
+ ctr, err := atlaslocal.Run(ctx, latestImage)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ client, td := newMongoClient(t, ctx, ctr)
+ defer td()
+
+ err = client.Ping(ctx, nil)
+ require.NoError(t, err)
+}
+
+func TestSCRAMAuth(t *testing.T) {
+ tmpDir, usernameFilepath, passwordFilepath := newAuthFiles(t)
+
+ cases := []struct {
+ name string
+ username string
+ password string
+ usernameFile string
+ passwordFile string
+ wantRunErr string
+ }{
+ {
+ name: "without auth",
+ username: "",
+ password: "",
+ usernameFile: "",
+ passwordFile: "",
+ wantRunErr: "",
+ },
+ {
+ name: "with auth",
+ username: "testuser",
+ password: "testpass",
+ usernameFile: "",
+ passwordFile: "",
+ wantRunErr: "",
+ },
+ {
+ name: "with auth files",
+ username: "",
+ password: "",
+ usernameFile: usernameFilepath,
+ passwordFile: passwordFilepath,
+ wantRunErr: "",
+ },
+ {
+ name: "with inline and files",
+ username: "testuser",
+ password: "testpass",
+ usernameFile: usernameFilepath,
+ passwordFile: passwordFilepath,
+ wantRunErr: "you cannot specify both inline credentials and files for credentials",
+ },
+ {
+ name: "username without password",
+ username: "testuser",
+ password: "",
+ usernameFile: "",
+ passwordFile: "",
+ wantRunErr: "if you specify username or password, you must provide both of them",
+ },
+ {
+ name: "password without username",
+ username: "",
+ password: "testpass",
+ usernameFile: "",
+ passwordFile: "",
+ wantRunErr: "if you specify username or password, you must provide both of them",
+ },
+ {
+ name: "username file without password file",
+ username: "",
+ password: "",
+ usernameFile: usernameFilepath,
+ passwordFile: "",
+ wantRunErr: "if you specify username file or password file, you must provide both of them",
+ },
+ {
+ name: "password file without username file",
+ username: "",
+ password: "",
+ usernameFile: "",
+ passwordFile: passwordFilepath,
+ wantRunErr: "if you specify username file or password file, you must provide both of them",
+ },
+ {
+ name: "username file invalid mount path",
+ username: "",
+ password: "",
+ usernameFile: "nonexistent_username.txt",
+ passwordFile: passwordFilepath,
+ wantRunErr: "mount path must be absolute",
+ },
+ {
+ name: "password file invalid mount path",
+ username: "",
+ password: "",
+ usernameFile: usernameFilepath,
+ passwordFile: "nonexistent_password.txt",
+ wantRunErr: "mount path must be absolute",
+ },
+ {
+ name: "username file is absolute but does not exist",
+ username: "",
+ password: "",
+ usernameFile: "/nonexistent/username.txt",
+ passwordFile: passwordFilepath,
+ wantRunErr: "does not exist or is not accessible",
+ },
+ {
+ name: "password file is absolute but does not exist",
+ username: "",
+ password: "",
+ usernameFile: usernameFilepath,
+ passwordFile: "/nonexistent/password.txt",
+ wantRunErr: "does not exist or is not accessible",
+ },
+ {
+ name: "username file is a directory",
+ username: "",
+ password: "",
+ usernameFile: tmpDir,
+ passwordFile: passwordFilepath,
+ wantRunErr: "must be a file",
+ },
+ {
+ name: "password file is a directory",
+ username: "",
+ password: "",
+ usernameFile: usernameFilepath,
+ passwordFile: tmpDir,
+ wantRunErr: "must be a file",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Construct the custom options for the MongoDB Atlas Local container.
+ opts := []testcontainers.ContainerCustomizer{}
+
+ if tc.username != "" {
+ opts = append(opts, atlaslocal.WithUsername(tc.username))
+ }
+
+ if tc.password != "" {
+ opts = append(opts, atlaslocal.WithPassword(tc.password))
+ }
+
+ if tc.usernameFile != "" {
+ opts = append(opts, atlaslocal.WithUsernameFile(tc.usernameFile))
+ }
+
+ if tc.passwordFile != "" {
+ opts = append(opts, atlaslocal.WithPasswordFile(tc.passwordFile))
+ }
+
+ // Randomize the order of the options
+ rand.Shuffle(len(opts), func(i, j int) {
+ opts[i], opts[j] = opts[j], opts[i]
+ })
+
+ // Create the MongoDB Atlas Local container with the specified options.
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, opts...)
+ testcontainers.CleanupContainer(t, ctr)
+
+ if tc.wantRunErr != "" {
+ require.ErrorContains(t, err, tc.wantRunErr)
+
+ return
+ }
+
+ require.NoError(t, err)
+
+ // Verify the environment variables are set correctly.
+ requireEnvVar(t, ctr, "MONGODB_INITDB_ROOT_USERNAME", tc.username)
+ requireEnvVar(t, ctr, "MONGODB_INITDB_ROOT_PASSWORD", tc.password)
+
+ if tc.usernameFile != "" {
+ requireEnvVar(t, ctr, "MONGODB_INITDB_ROOT_USERNAME_FILE", "/run/secrets/mongo-root-username")
+ }
+
+ if tc.passwordFile != "" {
+ requireEnvVar(t, ctr, "MONGODB_INITDB_ROOT_PASSWORD_FILE", "/run/secrets/mongo-root-password")
+ }
+
+ client, td := newMongoClient(t, context.Background(), ctr)
+ defer td()
+
+ // Execute an insert operation to verify the connection and
+ // authentication.
+ coll := client.Database("test").Collection("foo")
+
+ _, err = coll.InsertOne(context.Background(), bson.D{{Key: "test", Value: "value"}})
+ require.NoError(t, err, "Failed to insert document with authentication")
+ })
+ }
+}
+
+func TestWithNoTelemetry(t *testing.T) {
+ t.Run("with", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, atlaslocal.WithNoTelemetry())
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "DO_NOT_TRACK", "1")
+ })
+
+ t.Run("without", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "DO_NOT_TRACK", "")
+ })
+}
+
+func TestWithMongotLogFile(t *testing.T) {
+ t.Run("with", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, atlaslocal.WithMongotLogFile())
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "MONGOT_LOG_FILE", "/tmp/mongot.log")
+
+ executeAggregation(t, ctr)
+ requireMongotLogs(t, ctr)
+ })
+
+ t.Run("without", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "MONGOT_LOG_FILE", "")
+
+ executeAggregation(t, ctr)
+ requireNoMongotLogs(t, ctr)
+ })
+
+ t.Run("to stdout", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage,
+ atlaslocal.WithMongotLogToStdout())
+ testcontainers.CleanupContainer(t, ctr)
+
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "MONGOT_LOG_FILE", "/dev/stdout")
+
+ executeAggregation(t, ctr)
+
+ requireMongotLogs(t, ctr)
+ requireContainerLogsNotEmpty(t, ctr)
+ })
+
+ t.Run("to stderr", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage,
+ atlaslocal.WithMongotLogToStderr())
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "MONGOT_LOG_FILE", "/dev/stderr")
+
+ executeAggregation(t, ctr)
+
+ requireMongotLogs(t, ctr)
+ requireContainerLogsNotEmpty(t, ctr)
+ })
+}
+
+func TestWithRunnerLogFile(t *testing.T) {
+ const runnerLogFile = "/tmp/runner.log"
+
+ t.Run("with", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, atlaslocal.WithRunnerLogFile())
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "RUNNER_LOG_FILE", runnerLogFile)
+ requireRunnerLogs(t, ctr)
+ })
+
+ t.Run("without", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage)
+ testcontainers.CleanupContainer(t, ctr)
+
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "RUNNER_LOG_FILE", "")
+ requireNoRunnerLogs(t, ctr)
+ })
+
+ t.Run("to stdout", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage,
+ atlaslocal.WithRunnerLogToStdout())
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "RUNNER_LOG_FILE", "/dev/stdout")
+
+ executeAggregation(t, ctr)
+
+ requireRunnerLogs(t, ctr)
+ requireContainerLogsNotEmpty(t, ctr)
+ })
+
+ t.Run("to stderr", func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage,
+ atlaslocal.WithRunnerLogToStderr())
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireEnvVar(t, ctr, "RUNNER_LOG_FILE", "/dev/stderr")
+
+ executeAggregation(t, ctr)
+
+ requireRunnerLogs(t, ctr)
+ requireContainerLogsNotEmpty(t, ctr)
+ })
+}
+
+func TestWithInitDatabase(t *testing.T) {
+ initScripts := map[string]string{
+ "01-seed.js": `db.foo.insertOne({ _id: 1, seeded: true });`,
+ }
+
+ tmpDir := createInitScripts(t, initScripts)
+ opts := []testcontainers.ContainerCustomizer{
+ atlaslocal.WithInitDatabase("mydb"),
+ atlaslocal.WithInitScripts(tmpDir),
+ }
+
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, opts...)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireInitScriptsExist(t, ctr, initScripts)
+ requireEnvVar(t, ctr, "MONGODB_INITDB_DATABASE", "mydb")
+
+ client, td := newMongoClient(t, context.Background(), ctr)
+ defer td()
+
+ coll := client.Database("mydb").Collection("foo")
+
+ seed := bson.D{{Key: "_id", Value: int32(1)}, {Key: "seeded", Value: true}}
+
+ res := coll.FindOne(context.Background(), seed)
+ require.NoError(t, res.Err())
+
+ var doc bson.D
+ require.NoError(t, res.Decode(&doc), "Failed to decode seeded document")
+ require.Equal(t, seed, doc, "Seeded document does not match expected values")
+}
+
+func TestWithInitScripts(t *testing.T) {
+ seed1 := bson.D{{Key: "_id", Value: int32(1)}, {Key: "seeded", Value: true}}
+ seed2 := bson.D{{Key: "_id", Value: int32(2)}, {Key: "seeded", Value: true}}
+
+ cases := []struct {
+ name string
+ initScripts map[string]string // filename -> content
+ want []bson.D
+ }{
+ {
+ name: "no scripts",
+ initScripts: map[string]string{},
+ want: []bson.D{},
+ },
+ {
+ name: "single shell script",
+ initScripts: map[string]string{
+ "01-seed.sh": `mongosh --eval 'db.foo.insertOne({ _id: 1, seeded: true })'`,
+ },
+ want: []bson.D{seed1},
+ },
+ {
+ name: "single js script",
+ initScripts: map[string]string{
+ "01-seed.js": `db.foo.insertOne({ _id: 1, seeded: true });`,
+ },
+ want: []bson.D{seed1},
+ },
+ {
+ name: "mixed shell and js scripts",
+ initScripts: map[string]string{
+ "01-seed.sh": `mongosh --eval 'db.foo.insertOne({ _id: 1, seeded: true })'`,
+ "02-seed.js": `db.foo.insertOne({ _id: 2, seeded: true });`,
+ },
+ want: []bson.D{seed1, seed2},
+ },
+ {
+ name: "mixed ordered shell",
+ initScripts: map[string]string{
+ "01-seed.sh": `mongosh --eval 'db.foo.insertOne({ _id: 1, seeded: true })'`,
+ "02-seed.sh": `mongosh --eval 'db.foo.deleteOne({ _id: 1, seeded: true })'`,
+ },
+ want: []bson.D{},
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ tmpDir := createInitScripts(t, tc.initScripts)
+
+ // Start container with the init scripts mounted.
+ opts := []testcontainers.ContainerCustomizer{
+ atlaslocal.WithInitScripts(tmpDir),
+ }
+
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, opts...)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireInitScriptsExist(t, ctr, tc.initScripts)
+
+ // Connect to the server.
+ client, td := newMongoClient(t, context.Background(), ctr)
+ defer td()
+
+ // Fetch the seeded data.
+ coll := client.Database("test").Collection("foo")
+
+ cur, err := coll.Find(context.Background(), bson.D{})
+ require.NoError(t, err)
+
+ var results []bson.D
+ require.NoError(t, cur.All(context.Background(), &results))
+
+ require.ElementsMatch(t, results, tc.want, "Seeded documents do not match expected values")
+ })
+ }
+}
+
+// Ensure that we can chain multiple scripts.
+func TestWithInitScripts_MultipleScripts(t *testing.T) {
+ scripts1 := map[string]string{
+ "01-seed.sh": `mongosh --eval 'db.foo.insertOne({ _id: 1, seeded: true })'`,
+ "02-seed.js": `db.foo.insertOne({ _id: 2, seeded: true });`,
+ }
+
+ tmpDir1 := createInitScripts(t, scripts1)
+
+ scripts2 := map[string]string{
+ "03-seed.sh": `mongosh --eval 'db.foo.insertOne({ _id: 3, seeded: true })'`,
+ }
+
+ tmpDir2 := createInitScripts(t, scripts2)
+
+ // Start container with the init scripts mounted.
+ opts := []testcontainers.ContainerCustomizer{
+ atlaslocal.WithInitScripts(tmpDir1),
+ atlaslocal.WithInitScripts(tmpDir2),
+ }
+
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, opts...)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ requireInitScriptsDoesNotExist(t, ctr, scripts1)
+ requireInitScriptsExist(t, ctr, scripts2)
+}
+
+func TestConnectionString(t *testing.T) {
+ _, usernameFilepath, passwordFilepath := newAuthFiles(t)
+
+ testcases := []struct {
+ name string
+ opts []testcontainers.ContainerCustomizer
+ wantUsername string
+ wantPassword string
+ wantDatabase string
+ }{
+ {
+ name: "default",
+ opts: []testcontainers.ContainerCustomizer{},
+ wantUsername: "",
+ wantPassword: "",
+ wantDatabase: "",
+ },
+ {
+ name: "with auth options",
+ opts: []testcontainers.ContainerCustomizer{
+ atlaslocal.WithUsername("testuser"),
+ atlaslocal.WithPassword("testpass"),
+ atlaslocal.WithInitDatabase("testdb"),
+ },
+ wantUsername: "testuser",
+ wantPassword: "testpass",
+ wantDatabase: "testdb",
+ },
+ {
+ name: "with auth files",
+ opts: []testcontainers.ContainerCustomizer{
+ atlaslocal.WithUsernameFile(usernameFilepath),
+ atlaslocal.WithPasswordFile(passwordFilepath),
+ atlaslocal.WithInitDatabase("testdb"),
+ },
+ wantUsername: "file_testuser",
+ wantPassword: "file_testpass",
+ wantDatabase: "testdb",
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctr, err := atlaslocal.Run(context.Background(), latestImage, tc.opts...)
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ csRaw, err := ctr.ConnectionString(context.Background())
+ require.NoError(t, err)
+
+ connString, err := connstring.ParseAndValidate(csRaw)
+ require.NoError(t, err, "Failed to parse connection string")
+
+ require.Equal(t, "mongodb", connString.Scheme)
+ require.Equal(t, "localhost", connString.Hosts[0][:9])
+ require.NotEmpty(t, connString.Hosts[0][10:], "Port should be non-empty")
+ require.Equal(t, tc.wantUsername, connString.Username)
+ require.Equal(t, tc.wantPassword, connString.Password)
+ require.Equal(t, tc.wantDatabase, connString.Database)
+ require.True(t, connString.DirectConnection)
+ })
+ }
+}
+
+// Test Helper Functions
+
+func requireEnvVar(t *testing.T, ctr testcontainers.Container, envVarName, expected string) {
+ t.Helper()
+
+ exitCode, reader, err := ctr.Exec(context.Background(), []string{"sh", "-c", "echo $" + envVarName})
+ require.NoError(t, err)
+ require.Equal(t, 0, exitCode)
+
+ outBytes, err := io.ReadAll(reader)
+ require.NoError(t, err)
+
+ // testcontainers-go's Exec() returns a multiplexed stream in the same format
+ // used by the Docker API. Each frame is prefixed with an 8-byte header.
+ require.Greater(t, len(outBytes), 8, "Exec output too short to contain env var value")
+
+ out := strings.TrimSpace(string(outBytes[8:]))
+ require.Equal(t, expected, out, "DO_NOT_TRACK env var value mismatch")
+}
+
+func requireMongotLogs(t *testing.T, ctr testcontainers.Container) {
+ t.Helper()
+
+ // Pull the log file and require non-empty.
+ reader, err := ctr.(*atlaslocal.Container).ReadMongotLogs(context.Background())
+ require.NoError(t, err)
+ defer reader.Close()
+
+ buf := make([]byte, 1)
+ _, _ = reader.Read(buf) // read at least one byte to ensure non-empty
+}
+
+func requireNoMongotLogs(t *testing.T, ctr testcontainers.Container) {
+ t.Helper()
+
+ // Pull the log file and require non-empty.
+ reader, err := ctr.(*atlaslocal.Container).ReadMongotLogs(context.Background())
+ require.ErrorIs(t, err, os.ErrNotExist)
+
+ if reader != nil { // Failure case where reader is non-nil
+ _ = reader.Close()
+ }
+}
+
+func requireRunnerLogs(t *testing.T, ctr testcontainers.Container) {
+ t.Helper()
+
+ // Pull the log file and require non-empty.
+ reader, err := ctr.(*atlaslocal.Container).ReadRunnerLogs(context.Background())
+ require.NoError(t, err)
+
+ defer reader.Close()
+
+ buf := make([]byte, 1)
+ _, _ = reader.Read(buf) // Read at least one byte to ensure non-empty
+}
+
+func requireNoRunnerLogs(t *testing.T, ctr testcontainers.Container) {
+ t.Helper()
+
+ // Pull the log file and require non-empty.
+ reader, err := ctr.(*atlaslocal.Container).ReadRunnerLogs(context.Background())
+ require.ErrorIs(t, err, os.ErrNotExist)
+
+ if reader != nil { // Failure case where reader is non-nil
+ _ = reader.Close()
+ }
+}
+
+// createSearchIndex creates a search index with the given name on the provided
+// collection and waits for it to be acknowledged server-side.
+func createSearchIndex(t *testing.T, ctx context.Context, coll *mongo.Collection, indexName string) {
+ t.Helper()
+
+ // Create the default definition for search index
+ definition := bson.D{{Key: "mappings", Value: bson.D{{Key: "dynamic", Value: true}}}}
+ indexModel := mongo.SearchIndexModel{
+ Definition: definition,
+ Options: options.SearchIndexes().SetName(indexName),
+ }
+
+ _, err := coll.SearchIndexes().CreateOne(ctx, indexModel)
+ require.NoError(t, err)
+}
+
+// executeAggregation connects to the MongoDB Atlas Local instance, creates a
+// collection with a search index, inserts a document, and performs an
+// aggregation using the search index.
+func executeAggregation(t *testing.T, ctr testcontainers.Container) {
+ t.Helper()
+
+ client, td := newMongoClient(t, context.Background(), ctr)
+ defer td()
+
+ err := client.Database("test").CreateCollection(context.Background(), "search")
+ require.NoError(t, err)
+
+ coll := client.Database("test").Collection("search")
+
+ siCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ defer cancel()
+
+ // Create a search index on the collection.
+ createSearchIndex(t, siCtx, coll, "test_search_index")
+
+ // Insert a document into the collection and aggregate it using the search
+ // index which should log the operation to the mongot log file.
+ _, err = coll.InsertOne(context.Background(), bson.D{{Key: "txt", Value: "hello"}})
+ require.NoError(t, err)
+
+ pipeline := mongo.Pipeline{{
+ {Key: "$search", Value: bson.D{
+ {Key: "text", Value: bson.D{{Key: "query", Value: "hello"}, {Key: "path", Value: "txt"}}},
+ }},
+ }}
+
+ cur, err := coll.Aggregate(context.Background(), pipeline)
+ require.NoError(t, err)
+
+ err = cur.Close(context.Background())
+ require.NoError(t, err)
+}
+
+func newMongoClient(
+ t *testing.T,
+ ctx context.Context,
+ ctr testcontainers.Container,
+ opts ...*options.ClientOptions,
+) (*mongo.Client, func()) {
+ t.Helper()
+
+ connString, err := ctr.(*atlaslocal.Container).ConnectionString(ctx)
+ require.NoError(t, err)
+
+ copts := []*options.ClientOptions{
+ options.Client().ApplyURI(connString),
+ }
+
+ copts = append(copts, opts...)
+
+ client, err := mongo.Connect(copts...)
+ require.NoError(t, err)
+
+ return client, func() {
+ err := client.Disconnect(context.Background())
+ require.NoError(t, err, "Failed to disconnect MongoDB client")
+ }
+}
+
+func createInitScripts(t *testing.T, scripts map[string]string) string {
+ t.Helper()
+
+ tmpDir := t.TempDir()
+
+ for filename, content := range scripts {
+ scriptPath := filepath.Join(tmpDir, filename)
+ require.NoError(t, os.WriteFile(scriptPath, []byte(content), 0o755))
+
+ // Sanity check to verify that the script content is as expected.
+ got, err := os.ReadFile(scriptPath)
+ require.NoError(t, err, "Failed to read init script %s", filename)
+ require.Equal(t, string(got), content, "Content of init script %s does not match", filename)
+ }
+
+ return tmpDir
+}
+
+func requireInitScriptsExist(t *testing.T, ctr testcontainers.Container, expectedScripts map[string]string) {
+ t.Helper()
+
+ const dstDir = "/docker-entrypoint-initdb.d"
+
+ exit, r, err := ctr.Exec(context.Background(), []string{"sh", "-lc", "ls -l " + dstDir})
+ require.NoError(t, err)
+
+ // If the map is empty, the command returns exit code 2.
+ if len(expectedScripts) == 0 {
+ require.Equal(t, 2, exit)
+ } else {
+ require.Equal(t, 0, exit)
+ }
+
+ listingBytes, err := io.ReadAll(r)
+ require.NoError(t, err)
+
+ listing := string(listingBytes)
+
+ for name, want := range expectedScripts {
+ require.Contains(t, listing, name, "Init script %s not found in container", name)
+
+ rc, err := ctr.CopyFileFromContainer(context.Background(), filepath.Join(dstDir, name))
+ require.NoError(t, err, "Failed to copy init script %s from container", name)
+
+ got, err := io.ReadAll(rc)
+ require.NoError(t, err, "Failed to read init script %s content", name)
+
+ err = rc.Close()
+ require.NoError(t, err, "Failed to close reader for init script %s", name)
+
+ require.Equal(t, want, string(got), "Content of init script %s does not match", name)
+ }
+}
+
+func requireInitScriptsDoesNotExist(t *testing.T, ctr testcontainers.Container, expectedScripts map[string]string) {
+ t.Helper()
+
+ // Sanity check to verify that all scripts are present.
+ for filename := range expectedScripts {
+ cmd := []string{"sh", "-c", "cd docker-entrypoint-initdb.d && ls -l"}
+
+ exitCode, reader, err := ctr.Exec(context.Background(), cmd)
+ require.NoError(t, err)
+ require.Zero(t, exitCode, "Expected exit code 0 for command: %v", cmd)
+
+ content, _ := io.ReadAll(reader)
+ require.NotContains(t, string(content), filename)
+ }
+}
+
+func requireContainerLogsNotEmpty(t *testing.T, ctr testcontainers.Container) {
+ t.Helper()
+
+ logs, err := ctr.Logs(context.Background())
+ require.NoError(t, err)
+
+ defer logs.Close()
+
+ logBytes, err := io.ReadAll(logs)
+ require.NoError(t, err)
+
+ require.NotEmpty(t, logBytes, "Container logs should not be empty")
+}
+
+func newAuthFiles(t *testing.T) (string, string, string) {
+ t.Helper()
+
+ tmpDir := t.TempDir()
+
+ // Create username and password files.
+ usernameFilepath := filepath.Join(tmpDir, "username.txt")
+
+ err := os.WriteFile(usernameFilepath, []byte("file_testuser"), 0o755)
+ require.NoError(t, err)
+
+ _, err = os.Stat(usernameFilepath)
+ require.NoError(t, err, "Username file should exist")
+
+ // Create the password file.
+ passwordFilepath := filepath.Join(tmpDir, "password.txt")
+
+ err = os.WriteFile(passwordFilepath, []byte("file_testpass"), 0o755)
+ require.NoError(t, err)
+
+ _, err = os.Stat(passwordFilepath)
+ require.NoError(t, err, "Password file should exist")
+
+ return tmpDir, usernameFilepath, passwordFilepath
+}
diff --git a/modules/mongodb/atlaslocal/examples_test.go b/modules/mongodb/atlaslocal/examples_test.go
new file mode 100644
index 0000000000..0988dfffed
--- /dev/null
+++ b/modules/mongodb/atlaslocal/examples_test.go
@@ -0,0 +1,174 @@
+package atlaslocal_test
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+
+ "go.mongodb.org/mongo-driver/v2/mongo"
+ "go.mongodb.org/mongo-driver/v2/mongo/options"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/modules/mongodb/atlaslocal"
+)
+
+func ExampleRun() {
+ // runMongoDBAtlasLocalContainer {
+ ctx := context.Background()
+
+ atlaslocalContainer, err := atlaslocal.Run(ctx, "mongodb/mongodb-atlas-local:latest")
+ defer func() {
+ if err := testcontainers.TerminateContainer(atlaslocalContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ // }
+
+ state, err := atlaslocalContainer.State(ctx)
+ if err != nil {
+ log.Printf("failed to get container state: %s", err)
+ return
+ }
+
+ fmt.Println(state.Running)
+
+ // Output:
+ // true
+}
+
+func ExampleRun_connect() {
+ // connectToMongo {
+ ctx := context.Background()
+
+ atlaslocalContainer, err := atlaslocal.Run(ctx, "mongodb/mongodb-atlas-local:latest")
+ defer func() {
+ if err := testcontainers.TerminateContainer(atlaslocalContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ connString, err := atlaslocalContainer.ConnectionString(ctx)
+ if err != nil {
+ log.Printf("failed to get connection string: %s", err)
+ return
+ }
+
+ mongoClient, err := mongo.Connect(options.Client().ApplyURI(connString))
+ if err != nil {
+ log.Printf("failed to connect to MongoDB: %s", err)
+ return
+ }
+ // }
+
+ err = mongoClient.Ping(ctx, nil)
+ if err != nil {
+ log.Printf("failed to ping MongoDB: %s", err)
+ return
+ }
+
+ fmt.Println(mongoClient.Database("test").Name())
+
+ // Output:
+ // test
+}
+
+func ExampleRun_readMongotLogs() {
+ // mongotLogsRead {
+ ctx := context.Background()
+
+ atlaslocalContainer, err := atlaslocal.Run(ctx, "mongodb/mongodb-atlas-local:latest",
+ atlaslocal.WithMongotLogFile())
+
+ defer func() {
+ if err := testcontainers.TerminateContainer(atlaslocalContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ connString, err := atlaslocalContainer.ConnectionString(ctx)
+ if err != nil {
+ log.Printf("failed to get connection string: %s", err)
+ return
+ }
+
+ _, err = mongo.Connect(options.Client().ApplyURI(connString))
+ if err != nil {
+ log.Printf("failed to connect to MongoDB: %s", err)
+ return
+ }
+
+ reader, err := atlaslocalContainer.ReadMongotLogs(ctx)
+ if err != nil {
+ log.Printf("failed to read mongot logs: %s", err)
+ return
+ }
+ defer reader.Close()
+
+ if _, err := io.Copy(io.Discard, reader); err != nil {
+ log.Printf("failed to write mongot logs: %s", err)
+ return
+ }
+ // }
+
+ // Output:
+}
+
+func ExampleRun_readRunnerLogs() {
+ // runnerLogsRead {
+ ctx := context.Background()
+
+ atlaslocalContainer, err := atlaslocal.Run(ctx, "mongodb/mongodb-atlas-local:latest",
+ atlaslocal.WithRunnerLogFile())
+
+ defer func() {
+ if err := testcontainers.TerminateContainer(atlaslocalContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ connString, err := atlaslocalContainer.ConnectionString(ctx)
+ if err != nil {
+ log.Printf("failed to get connection string: %s", err)
+ return
+ }
+
+ _, err = mongo.Connect(options.Client().ApplyURI(connString))
+ if err != nil {
+ log.Printf("failed to connect to MongoDB: %s", err)
+ return
+ }
+
+ reader, err := atlaslocalContainer.ReadRunnerLogs(ctx)
+ if err != nil {
+ log.Printf("failed to read runner logs: %s", err)
+ return
+ }
+ defer reader.Close()
+
+ if _, err := io.Copy(io.Discard, reader); err != nil {
+ log.Printf("failed to write runner logs: %s", err)
+ return
+ }
+ // }
+
+ // Output:
+}
diff --git a/modules/mongodb/atlaslocal/options.go b/modules/mongodb/atlaslocal/options.go
new file mode 100644
index 0000000000..0dc5734b92
--- /dev/null
+++ b/modules/mongodb/atlaslocal/options.go
@@ -0,0 +1,399 @@
+package atlaslocal
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/testcontainers/testcontainers-go"
+)
+
+const (
+ passwordContainerPath = "/run/secrets/mongo-root-password"
+ usernameContainerPath = "/run/secrets/mongo-root-username"
+ envMongotLogFile = "MONGOT_LOG_FILE"
+ envRunnerLogFile = "RUNNER_LOG_FILE"
+ envMongoDBInitDatabase = "MONGODB_INITDB_DATABASE"
+ envMongoDBInitUsername = "MONGODB_INITDB_ROOT_USERNAME"
+ envMongoDBInitPassword = "MONGODB_INITDB_ROOT_PASSWORD"
+ envMongoDBInitUsernameFile = "MONGODB_INITDB_ROOT_USERNAME_FILE"
+ envMongoDBInitPasswordFile = "MONGODB_INITDB_ROOT_PASSWORD_FILE"
+ envDoNotTrack = "DO_NOT_TRACK"
+)
+
+// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface.
+var _ testcontainers.ContainerCustomizer = (Option)(nil)
+
+type options struct {
+ username string
+ password string
+ localUsernameFile string
+ localPasswordFile string
+ noTelemetry bool
+ database string
+ mongotLogPath string
+ runnerLogPath string
+
+ files []testcontainers.ContainerFile
+}
+
+func (opts options) env() map[string]string {
+ env := map[string]string{}
+
+ if opts.username != "" {
+ env[envMongoDBInitUsername] = opts.username
+ }
+
+ if opts.password != "" {
+ env[envMongoDBInitPassword] = opts.password
+ }
+
+ if opts.localUsernameFile != "" {
+ env[envMongoDBInitUsernameFile] = usernameContainerPath
+ }
+
+ if opts.localPasswordFile != "" {
+ env[envMongoDBInitPasswordFile] = passwordContainerPath
+ }
+
+ if opts.noTelemetry {
+ env[envDoNotTrack] = "1"
+ }
+
+ if opts.database != "" {
+ env[envMongoDBInitDatabase] = opts.database
+ }
+
+ if opts.mongotLogPath != "" {
+ env[envMongotLogFile] = opts.mongotLogPath
+ }
+
+ if opts.runnerLogPath != "" {
+ env[envRunnerLogFile] = opts.runnerLogPath
+ }
+
+ return env
+}
+
+func (opts options) validate() error {
+ username := opts.username
+ password := opts.password
+
+ // If username or password is specified, both must be provided.
+ if username != "" && password == "" || username == "" && password != "" {
+ return errors.New("if you specify username or password, you must provide both of them")
+ }
+
+ usernameFile := opts.localUsernameFile
+ passwordFile := opts.localPasswordFile
+
+ // If username file or password file is specified, both must be provided.
+ if usernameFile != "" && passwordFile == "" || usernameFile == "" && passwordFile != "" {
+ return errors.New("if you specify username file or password file, you must provide both of them")
+ }
+
+ // Setting credentials both inline and using files will result in a panic
+ // from the container, so we short circuit here.
+ if (username != "" || password != "") && (usernameFile != "" || passwordFile != "") {
+ return errors.New("you cannot specify both inline credentials and files for credentials")
+ }
+
+ return nil
+}
+
+// parseUsername will return either the username provided by WithUsername or
+// from the local file specified by WithUsernameFile. If both are provided, this
+// function will return an error. If neither is provided, an empty string is
+// returned.
+func (opts options) parseUsername() (string, error) {
+ if opts.username == "" && opts.localUsernameFile == "" {
+ return "", nil
+ }
+
+ if opts.username != "" && opts.localUsernameFile != "" {
+ return "", errors.New("cannot specify both inline credentials and files for credentials")
+ }
+
+ if opts.username != "" {
+ return opts.username, nil
+ }
+
+ r, err := os.ReadFile(opts.localUsernameFile)
+ return strings.TrimSpace(string(r)), err
+}
+
+// parsePassword will return either the password provided by WithPassword or
+// from the local file specified by WithPasswordFile. If both are provided, this
+// function will return an error. If neither is provided, an empty string is
+// returned.
+func (opts options) parsePassword() (string, error) {
+ if opts.password == "" && opts.localPasswordFile == "" {
+ return "", nil
+ }
+
+ if opts.password != "" && opts.localPasswordFile != "" {
+ return "", errors.New("cannot specify both inline credentials and files for credentials")
+ }
+
+ if opts.password != "" {
+ return opts.password, nil
+ }
+
+ r, err := os.ReadFile(opts.localPasswordFile)
+ return strings.TrimSpace(string(r)), err
+}
+
+// Option is an option for the Redpanda container.
+type Option func(*options) error
+
+// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
+func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
+ // NOOP to satisfy interface.
+ return nil
+}
+
+// WithUsername sets the MongoDB root username by setting the
+// MONGODB_INITDB_ROOT_USERNAME environment variable.
+func WithUsername(username string) Option {
+ return func(opts *options) error {
+ if username != "" {
+ opts.username = username
+ }
+
+ return nil
+ }
+}
+
+// WithPassword sets the MongoDB root password by setting the the
+// MONGODB_INITDB_ROOT_PASSWORD environment variable.
+func WithPassword(password string) Option {
+ return func(opts *options) error {
+ if password != "" {
+ opts.password = password
+ }
+
+ return nil
+ }
+}
+
+// WithUsernameFile mounts a local file as the MongoDB root username secret at
+// /run/secrets/mongo-root-username and sets MONGODB_INITDB_ROOT_USERNAME_FILE.
+// The path must be absolute and exist; no-op if empty.
+func WithUsernameFile(usernameFile string) Option {
+ return func(opts *options) error {
+ if usernameFile == "" {
+ return nil
+ }
+
+ // Must be an absolute path.
+ if !filepath.IsAbs(usernameFile) {
+ return fmt.Errorf("username file mount path must be absolute, got: %s", usernameFile)
+ }
+
+ // Must exist and be a file.
+ info, err := os.Stat(usernameFile)
+ if err != nil {
+ return fmt.Errorf("username file does not exist or is not accessible: %w", err)
+ }
+
+ if info.IsDir() {
+ return fmt.Errorf("username file must be a file, got a directory: %s", usernameFile)
+ }
+
+ opts.localUsernameFile = usernameFile
+
+ opts.files = append(opts.files, testcontainers.ContainerFile{
+ HostFilePath: usernameFile,
+ ContainerFilePath: usernameContainerPath,
+ FileMode: 0o444,
+ })
+
+ return nil
+ }
+}
+
+// WithPasswordFile mounts a local file as the MongoDB root password secret at
+// /run/secrets/mongo-root-password and sets MONGODB_INITDB_ROOT_PASSWORD_FILE.
+// Path must be absolute and an existing file; no-op if empty.
+func WithPasswordFile(passwordFile string) Option {
+ return func(opts *options) error {
+ if passwordFile == "" {
+ return nil
+ }
+
+ // Must be an absolute path.
+ if !filepath.IsAbs(passwordFile) {
+ return fmt.Errorf("password file mount path must be absolute, got: %s", passwordFile)
+ }
+
+ // Must exist and be a file.
+ info, err := os.Stat(passwordFile)
+ if err != nil {
+ return fmt.Errorf("password file does not exist or is not accessible: %w", err)
+ }
+
+ if info.IsDir() {
+ return fmt.Errorf("password file must be a file, got a directory: %s", passwordFile)
+ }
+
+ opts.localPasswordFile = passwordFile
+
+ opts.files = append(opts.files, testcontainers.ContainerFile{
+ HostFilePath: passwordFile,
+ ContainerFilePath: passwordContainerPath,
+ FileMode: 0o444,
+ })
+
+ return nil
+ }
+}
+
+// WithNoTelemetry opts out of telemetry for the MongoDB Atlas Local
+// container by setting the DO_NOT_TRACK environment variable to 1.
+func WithNoTelemetry() Option {
+ return func(opts *options) error {
+ opts.noTelemetry = true
+
+ return nil
+ }
+}
+
+// WithInitDatabase sets MONGODB_INITDB_DATABASE environment variable so the
+// init scripts and the default connection string target the specified database
+// instead of the default "test" database.
+func WithInitDatabase(database string) Option {
+ return func(opts *options) error {
+ opts.database = database
+
+ return nil
+ }
+}
+
+// WithInitScripts mounts a directory containing .sh/.js init scripts into
+// /docker-entrypoint-initdb.d so they run in alphabetical order on startup. If
+// called multiple times, this function removes any prior init-scripts bind and
+// uses only the latest on specified.
+func WithInitScripts(scriptsDir string) Option {
+ return func(opts *options) error {
+ if scriptsDir == "" {
+ return nil
+ }
+
+ abs, err := filepath.Abs(scriptsDir)
+ if err != nil {
+ return fmt.Errorf("get absolute path of init scripts dir: %w", err)
+ }
+
+ st, err := os.Stat(abs)
+ if err != nil {
+ return fmt.Errorf("stat init scripts dir: %w", err)
+ }
+
+ if !st.IsDir() {
+ return fmt.Errorf("init scripts path is not a directory: %s", abs)
+ }
+
+ const dstDir = "/docker-entrypoint-initdb.d/"
+
+ filtered := opts.files[:0]
+ for _, file := range opts.files {
+ if !strings.HasPrefix(file.ContainerFilePath, dstDir) {
+ filtered = append(filtered, file)
+ }
+ }
+
+ opts.files = filtered
+
+ entries, err := os.ReadDir(abs)
+ if err != nil {
+ return fmt.Errorf("read init scripts dir: %w", err)
+ }
+
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+
+ name := entry.Name()
+ if !strings.HasSuffix(name, ".sh") && !strings.HasSuffix(name, ".js") {
+ continue
+ }
+
+ f := testcontainers.ContainerFile{
+ HostFilePath: filepath.Join(abs, name),
+ ContainerFilePath: filepath.Join(dstDir, name),
+ FileMode: 0o644,
+ }
+
+ if strings.HasSuffix(name, ".sh") {
+ f.FileMode = 0o755 // Make shell scripts executable.
+ }
+
+ opts.files = append(opts.files, f)
+ }
+
+ return nil
+ }
+}
+
+// WithMongotLogStdout writes to /dev/stdout inside the container. See
+// (*Container).ReadMongotLogs to read the logs locally.
+func WithMongotLogToStdout() Option {
+ return func(opts *options) error {
+ opts.mongotLogPath = "/dev/stdout"
+
+ return nil
+ }
+}
+
+// WithMongotLogToStderr writes to /dev/stderr inside the container. See
+// (*Container).ReadMongotLogs to read the logs locally.
+func WithMongotLogToStderr() Option {
+ return func(opts *options) error {
+ opts.mongotLogPath = "/dev/stderr"
+
+ return nil
+ }
+}
+
+// WithMongotLogFile writes the mongot logs to /tmp/mongot.log inside the
+// container. See (*Container).ReadMongotLogs to read the logs locally.
+func WithMongotLogFile() Option {
+ return func(opts *options) error {
+ opts.mongotLogPath = "/tmp/mongot.log"
+
+ return nil
+ }
+}
+
+// WithRunnerLogToStdout writes to /dev/stdout inside the container. See
+// (*Container).ReadRunnerLogs to read the logs locally.
+func WithRunnerLogToStdout() Option {
+ return func(opts *options) error {
+ opts.runnerLogPath = "/dev/stdout"
+
+ return nil
+ }
+}
+
+// WithRunnerLogToStderr writes to /dev/stderr inside the container. See
+// (*Container).ReadRunnerLogs to read the logs locally.
+func WithRunnerLogToStderr() Option {
+ return func(opts *options) error {
+ opts.runnerLogPath = "/dev/stderr"
+
+ return nil
+ }
+}
+
+// WithRunnerLogFile writes the runner logs to /tmp/runner.log inside the
+// container. See (*Container).ReadRunnerLogs to read the logs locally.
+func WithRunnerLogFile() Option {
+ return func(opts *options) error {
+ opts.runnerLogPath = "/tmp/runner.log"
+
+ return nil
+ }
+}
diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go
index 76a3c57125..844c525e11 100644
--- a/modules/mongodb/mongodb.go
+++ b/modules/mongodb/mongodb.go
@@ -17,6 +17,7 @@ import (
var entrypointContent []byte
const (
+ defaultPort = "27017/tcp"
entrypointPath = "/tmp/entrypoint-tc.sh"
keyFilePath = "/tmp/mongo_keyfile"
replicaSetOptEnvKey = "testcontainers.mongodb.replicaset_name"
@@ -40,10 +41,10 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*MongoDBContainer, error) {
req := testcontainers.ContainerRequest{
Image: img,
- ExposedPorts: []string{"27017/tcp"},
+ ExposedPorts: []string{defaultPort},
WaitingFor: wait.ForAll(
wait.ForLog("Waiting for connections"),
- wait.ForListeningPort("27017/tcp"),
+ wait.ForListeningPort(defaultPort),
),
Env: map[string]string{},
}
@@ -118,7 +119,7 @@ func WithReplicaSet(replSetName string) testcontainers.CustomizeRequestOption {
// ConnectionString returns the connection string for the MongoDB container.
// If you provide a username and a password, the connection string will also include them.
func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) {
- endpoint, err := c.PortEndpoint(ctx, "27017/tcp", "")
+ endpoint, err := c.PortEndpoint(ctx, defaultPort, "")
if err != nil {
return "", err
}