From 788097d50f6cb16a6d8d325a44796fd25e40c0a6 Mon Sep 17 00:00:00 2001 From: Heitor Danilo Date: Wed, 8 May 2024 18:17:09 -0300 Subject: [PATCH] feat(mongodb): add replica set support via opts (#2469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mongodb): add WithReplicaSet option The `WithReplicaSet` option configures the MongoDB container to run a single-node replica set named "rs". The container will wait until the replica set is ready. * docs: document the new option * fix: proper indent --------- Co-authored-by: Manuel de la Peña --- docs/modules/mongodb.md | 6 ++++ modules/mongodb/mongodb.go | 35 +++++++++++++++++++++ modules/mongodb/mongodb_test.go | 54 ++++++++++++++++++++++----------- 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md index 31cb5dc38e..3562aada1f 100644 --- a/docs/modules/mongodb.md +++ b/docs/modules/mongodb.md @@ -58,6 +58,12 @@ It is used in conjunction with `WithUsername` to set a username and its password E.g. `testcontainers.WithPassword("mymongopwd")`. +#### WithReplicaSet + +- Not available until the next release of testcontainers-go :material-tag: main + +The `WithReplicaSet` functional option configures the container to run a single-node MongoDB replica set named `rs`. The MongoDB container will wait until the replica set is ready. + {% include "../features/common_functional_options.md" %} ### Container Methods diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 565e8bc466..ac29414825 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -79,6 +79,29 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption { } } +// WithReplicaSet configures the container to run a single-node MongoDB replica set named "rs". +// It will wait until the replica set is ready. +func WithReplicaSet() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Cmd = append(req.Cmd, "--replSet", "rs") + req.LifecycleHooks = append(req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ + PostStarts: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { + ip, err := c.ContainerIP(ctx) + if err != nil { + return err + } + + cmd := eval("rs.initiate({ _id: 'rs', members: [ { _id: 0, host: '%s:27017' } ] })", ip) + return wait.ForExec(cmd).WaitUntilReady(ctx, c) + }, + }, + }) + + return nil + } +} + // 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) { @@ -95,3 +118,15 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) } return c.Endpoint(ctx, "mongodb") } + +// eval builds an mongosh|mongo eval command. +func eval(command string, args ...any) []string { + command = "\"" + fmt.Sprintf(command, args...) + "\"" + + return []string{ + "sh", + "-c", + // In previous versions, the binary "mongosh" was named "mongo". + "mongosh --quiet --eval " + command + " || mongo --quiet --eval " + command, + } +} diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 994b4c448c..0012b97624 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -14,50 +14,70 @@ import ( func TestMongoDB(t *testing.T) { type tests struct { - name string - image string + name string + opts []testcontainers.ContainerCustomizer } testCases := []tests{ { - name: "From Docker Hub", - image: "mongo:6", + name: "From Docker Hub", + opts: []testcontainers.ContainerCustomizer{ + testcontainers.WithImage("mongo:6"), + }, }, { - name: "Community Server", - image: "mongodb/mongodb-community-server:7.0.2-ubi8", + name: "Community Server", + opts: []testcontainers.ContainerCustomizer{ + testcontainers.WithImage("mongodb/mongodb-community-server:7.0.2-ubi8"), + }, }, { - name: "Enterprise Server", - image: "mongodb/mongodb-enterprise-server:7.0.0-ubi8", + name: "Enterprise Server", + opts: []testcontainers.ContainerCustomizer{ + testcontainers.WithImage("mongodb/mongodb-enterprise-server:7.0.0-ubi8"), + }, + }, + { + name: "With Replica set and mongo:4", + opts: []testcontainers.ContainerCustomizer{ + testcontainers.WithImage("mongo:4"), + mongodb.WithReplicaSet(), + }, + }, + { + name: "With Replica set and mongo:6", + opts: []testcontainers.ContainerCustomizer{ + testcontainers.WithImage("mongo:6"), + mongodb.WithReplicaSet(), + }, }, } for _, tc := range testCases { - image := tc.image - t.Run(image, func(t *testing.T) { - t.Parallel() + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() ctx := context.Background() - mongodbContainer, err := mongodb.RunContainer(ctx, testcontainers.WithImage(image)) + mongodbContainer, err := mongodb.RunContainer(ctx, tc.opts...) if err != nil { - t.Fatalf("failed to start container: %s", err) + tt.Fatalf("failed to start container: %s", err) } defer func() { if err := mongodbContainer.Terminate(ctx); err != nil { - t.Fatalf("failed to terminate container: %s", err) + tt.Fatalf("failed to terminate container: %s", err) } }() endpoint, err := mongodbContainer.ConnectionString(ctx) if err != nil { - t.Fatalf("failed to get connection string: %s", err) + tt.Fatalf("failed to get connection string: %s", err) } mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint)) if err != nil { - t.Fatalf("failed to connect to MongoDB: %s", err) + tt.Fatalf("failed to connect to MongoDB: %s", err) } err = mongoClient.Ping(ctx, nil) @@ -66,7 +86,7 @@ func TestMongoDB(t *testing.T) { } if mongoClient.Database("test").Name() != "test" { - t.Fatalf("failed to connect to the correct database") + tt.Fatalf("failed to connect to the correct database") } }) }