Skip to content

Commit

Permalink
feat(mongodb): add replica set support via opts (#2469)
Browse files Browse the repository at this point in the history
* 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 <mdelapenya@gmail.com>
  • Loading branch information
heiytor and mdelapenya authored May 8, 2024
1 parent de893e1 commit 788097d
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 17 deletions.
6 changes: 6 additions & 0 deletions docs/modules/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

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
Expand Down
35 changes: 35 additions & 0 deletions modules/mongodb/mongodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
}
}
54 changes: 37 additions & 17 deletions modules/mongodb/mongodb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
}
})
}
Expand Down

0 comments on commit 788097d

Please sign in to comment.