Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: mongodb replicaset should work with auth #2847

Merged
merged 13 commits into from
Oct 28, 2024
Merged
32 changes: 32 additions & 0 deletions modules/mongodb/cli.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
129 changes: 98 additions & 31 deletions modules/mongodb/mongodb.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
package mongodb

import (
"bytes"
"context"
_ "embed"
"fmt"
"time"

"github.com/testcontainers/testcontainers-go"
"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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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(
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
59 changes: 56 additions & 3 deletions modules/mongodb/mongodb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions modules/mongodb/mount/entrypoint-tc.sh
Original file line number Diff line number Diff line change
@@ -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} "$@"