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
31 changes: 31 additions & 0 deletions modules/mongodb/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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,
}
}
125 changes: 95 additions & 30 deletions modules/mongodb/mongodb.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package mongodb

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

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

var (
//go:embed mount/entrypoint.sh
entrypointContent []byte
)

const (
entrypointPath = "/tmp/entrypoint.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 @@ -49,11 +63,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
if username != "" && password == "" || username == "" && password != "" {
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 @@ -89,25 +109,7 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption {
// It will wait until the replica set is ready.
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
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 +131,77 @@ 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 withAuthReplicaset(replicaSet, username, password)(genericContainerReq)
}

return []string{
"sh",
"-c",
// In previous versions, the binary "mongosh" was named "mongo".
"mongosh --quiet --eval " + command + " || mongo --quiet --eval " + command,
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
}
}
53 changes: 53 additions & 0 deletions modules/mongodb/mongodb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,59 @@ func TestMongoDB(t *testing.T) {
mongodb.WithReplicaSet("rs"),
},
},
{
name: "With Auth, Replica set and mongo:7",
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
img: "mongo:7",
opts: []testcontainers.ContainerCustomizer{
mongodb.WithReplicaSet("rs"),
mongodb.WithUsername("tester"),
mongodb.WithPassword("testerpass"),
},
},
{
name: "With Auth, Replica set and mongo:6",
img: "mongo:6",
opts: []testcontainers.ContainerCustomizer{
mongodb.WithReplicaSet("rs"),
mongodb.WithUsername("tester"),
mongodb.WithPassword("testerpass"),
},
},
{
name: "With Auth only and mongo:6",
img: "mongo:6",
opts: []testcontainers.ContainerCustomizer{
mongodb.WithUsername("tester"),
mongodb.WithPassword("testerpass"),
},
},
{
name: "Enterprise Server with Auth, Replica set",
img: "mongodb/mongodb-enterprise-server:7.0.0-ubi8",
opts: []testcontainers.ContainerCustomizer{
mongodb.WithReplicaSet("rs"),
mongodb.WithUsername("tester"),
mongodb.WithPassword("testerpass"),
},
},
{
name: "Community Server with Auth, Replica set",
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 set and 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.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved

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} "$@"