diff --git a/modules/mongodb/cli.go b/modules/mongodb/cli.go new file mode 100644 index 0000000000..f990bf17c8 --- /dev/null +++ b/modules/mongodb/cli.go @@ -0,0 +1,32 @@ +package mongodb + +import "fmt" + +// mongoCli is cli to interact with MongoDB. If username and password are provided +// it will use credentials to authenticate. +type mongoCli struct { + mongoshBaseCmd string + mongoBaseCmd string +} + +func newMongoCli(username string, password string) mongoCli { + authArgs := "" + if username != "" && password != "" { + authArgs = fmt.Sprintf("--username %s --password %s", username, password) + } + + return mongoCli{ + mongoshBaseCmd: fmt.Sprintf("mongosh %s --quiet", authArgs), + mongoBaseCmd: fmt.Sprintf("mongo %s --quiet", authArgs), + } +} + +func (m mongoCli) eval(command string, args ...any) []string { + command = "\"" + fmt.Sprintf(command, args...) + "\"" + + return []string{ + "sh", + "-c", + m.mongoshBaseCmd + " --eval " + command + " || " + m.mongoBaseCmd + " --eval " + command, + } +} diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 3f73e5dc70..8acbf98ee2 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -1,7 +1,9 @@ package mongodb import ( + "bytes" "context" + _ "embed" "fmt" "time" @@ -9,11 +11,21 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +//go:embed mount/entrypoint-tc.sh +var entrypointContent []byte + +const ( + entrypointPath = "/tmp/entrypoint-tc.sh" + keyFilePath = "/tmp/mongo_keyfile" + replicaSetOptEnvKey = "testcontainers.mongodb.replicaset_name" +) + // MongoDBContainer represents the MongoDB container type used in the module type MongoDBContainer struct { testcontainers.Container - username string - password string + username string + password string + replicaSet string } // Deprecated: use Run instead @@ -50,10 +62,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom return nil, fmt.Errorf("if you specify username or password, you must provide both of them") } + replicaSet := req.Env[replicaSetOptEnvKey] + if replicaSet != "" { + if err := configureRequestForReplicaset(username, password, replicaSet, &genericContainerReq); err != nil { + return nil, err + } + } + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) var c *MongoDBContainer if container != nil { - c = &MongoDBContainer{Container: container, username: username, password: password} + c = &MongoDBContainer{Container: container, username: username, password: password, replicaSet: replicaSet} } if err != nil { @@ -85,28 +104,10 @@ 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. +// WithReplicaSet sets the replica set name for Single node MongoDB replica set. func WithReplicaSet(replSetName string) testcontainers.CustomizeRequestOption { return func(req *testcontainers.GenericContainerRequest) error { - req.Cmd = append(req.Cmd, "--replSet", replSetName) - req.WaitingFor = wait.ForAll( - req.WaitingFor, - wait.ForExec(eval("rs.status().ok")), - ).WithDeadline(60 * time.Second) - 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 fmt.Errorf("container ip: %w", err) - } - - cmd := eval("rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:27017' } ] })", replSetName, ip) - return wait.ForExec(cmd).WaitUntilReady(ctx, c) - }, - }, - }) + req.Env[replicaSetOptEnvKey] = replSetName return nil } @@ -129,14 +130,80 @@ 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...) + "\"" +func setupEntrypointForAuth(req *testcontainers.GenericContainerRequest) { + req.Files = append( + req.Files, testcontainers.ContainerFile{ + Reader: bytes.NewReader(entrypointContent), + ContainerFilePath: entrypointPath, + FileMode: 0o755, + }, + ) + req.Entrypoint = []string{entrypointPath} + req.Env["MONGO_KEYFILE"] = keyFilePath +} + +func configureRequestForReplicaset( + username string, + password string, + replicaSet string, + genericContainerReq *testcontainers.GenericContainerRequest, +) error { + if !(username != "" && password != "") { + return noAuthReplicaSet(replicaSet)(genericContainerReq) + } - return []string{ - "sh", - "-c", - // In previous versions, the binary "mongosh" was named "mongo". - "mongosh --quiet --eval " + command + " || mongo --quiet --eval " + command, + return withAuthReplicaset(replicaSet, username, password)(genericContainerReq) +} + +func noAuthReplicaSet(replSetName string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + cli := newMongoCli("", "") + req.Cmd = append(req.Cmd, "--replSet", replSetName) + initiateReplicaSet(req, cli, replSetName) + + return nil + } +} + +func initiateReplicaSet(req *testcontainers.GenericContainerRequest, cli mongoCli, replSetName string) { + req.WaitingFor = wait.ForAll( + req.WaitingFor, + wait.ForExec(cli.eval("rs.status().ok")), + ).WithDeadline(60 * time.Second) + + 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 fmt.Errorf("container ip: %w", err) + } + + cmd := cli.eval( + "rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:27017' } ] })", + replSetName, + ip, + ) + + return wait.ForExec(cmd).WaitUntilReady(ctx, c) + }, + }, + }, + ) +} + +func withAuthReplicaset( + replSetName string, + username string, + password string, +) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + setupEntrypointForAuth(req) + cli := newMongoCli(username, password) + req.Cmd = append(req.Cmd, "--replSet", replSetName, "--keyFile", keyFilePath) + initiateReplicaSet(req, cli, replSetName) + + return nil } } diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 03d669bb7e..8cc49e629c 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -36,26 +36,79 @@ func TestMongoDB(t *testing.T) { opts: []testcontainers.ContainerCustomizer{}, }, { - name: "With Replica set and mongo:4", + name: "with-replica/mongo:4", img: "mongo:4", opts: []testcontainers.ContainerCustomizer{ mongodb.WithReplicaSet("rs"), }, }, { - name: "With Replica set and mongo:6", + name: "with-replica/mongo:6", img: "mongo:6", opts: []testcontainers.ContainerCustomizer{ mongodb.WithReplicaSet("rs"), }, }, { - name: "With Replica set and mongo:7", + name: "with-replica/mongo:7", img: "mongo:7", opts: []testcontainers.ContainerCustomizer{ mongodb.WithReplicaSet("rs"), }, }, + { + name: "with-auth/replica/mongo:7", + img: "mongo:7", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongo:6", + img: "mongo:6", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/mongo:6", + img: "mongo:6", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongodb-enterprise-server:7.0.0-ubi8", + img: "mongodb/mongodb-enterprise-server:7.0.0-ubi8", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongodb-community-server:7.0.2-ubi8", + img: "mongodb/mongodb-community-server:7.0.2-ubi8", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, + { + name: "with-auth/replica/mongo:4", + img: "mongo:4", + opts: []testcontainers.ContainerCustomizer{ + mongodb.WithReplicaSet("rs"), + mongodb.WithUsername("tester"), + mongodb.WithPassword("testerpass"), + }, + }, } for _, tc := range testCases { diff --git a/modules/mongodb/mount/entrypoint-tc.sh b/modules/mongodb/mount/entrypoint-tc.sh new file mode 100644 index 0000000000..1561415aad --- /dev/null +++ b/modules/mongodb/mount/entrypoint-tc.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -Eeuo pipefail + +# detect mongo user and group +function get_user_group() { + user_group=$(cut -d: -f1,5 /etc/passwd | grep mongo) + echo "${user_group}" +} + +# detect the entrypoint +function get_entrypoint() { + entrypoint=$(find /usr/local/bin -name 'docker-entrypoint.*') + if [[ "${entrypoint}" == *.py ]]; then + entrypoint="python3 ${entrypoint}" + else + entrypoint="exec ${entrypoint}" + fi + echo "${entrypoint}" +} + +ENTRYPOINT=$(get_entrypoint) +MONGO_USER_GROUP=$(get_user_group) + +# Create the keyfile +openssl rand -base64 756 > "${MONGO_KEYFILE}" + +# Set the permissions and ownership of the keyfile +chown "${MONGO_USER_GROUP}" "${MONGO_KEYFILE}" +chmod 400 "${MONGO_KEYFILE}" + +${ENTRYPOINT} "$@"