From d29289aaf6f13b0ff44ce71c782f9901bdebb209 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Fri, 14 Feb 2025 20:17:58 -0500 Subject: [PATCH 01/21] [MongoDB] Improve Replica Set Initialization & Connection Handling --- docker.go | 44 ++++++++++++++++++++++++++++- modules/mongodb/mongodb.go | 37 ++++++++++++++++++++----- modules/mongodb/mongodb_test.go | 49 +++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/docker.go b/docker.go index 58428ad409..5c88cd9bb1 100644 --- a/docker.go +++ b/docker.go @@ -1510,7 +1510,12 @@ func (p *DockerProvider) daemonHostLocked(ctx context.Context) (string, error) { } p.hostCache = ip } else { - p.hostCache = "localhost" + ip, err := getLocalNonLoopbackIP() + if err != nil { + p.hostCache = "localhost" + } else { + p.hostCache = ip + } } default: return "", errors.New("could not determine host through env or docker host") @@ -1800,3 +1805,40 @@ func tryClose(r io.Reader) { _ = rc.Close() } } + +// getLocalNonLoopbackIP returns the first non-loopback IPv4 address found. +func getLocalNonLoopbackIP() (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", err + } + for _, iface := range interfaces { + // Skip down or loopback interfaces. + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue // try next interface + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + // Check if it's a valid IPv4 and not loopback. + if ip == nil || ip.IsLoopback() { + continue + } + ip = ip.To4() + if ip == nil { + continue // not IPv4 + } + return ip.String(), nil + } + } + return "", errors.New("no non-loopback IP address found") +} diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index b2fa8bb023..9214945548 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -125,10 +125,19 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) if err != nil { return "", err } + + var base string if c.username != "" && c.password != "" { - return fmt.Sprintf("mongodb://%s:%s@%s:%s", c.username, c.password, host, port.Port()), nil + base = fmt.Sprintf("mongodb://%s:%s@%s:%s", c.username, c.password, host, port.Port()) + } else { + base = fmt.Sprintf("mongodb://%s:%s", host, port.Port()) + } + + if c.replicaSet != "" { + base = fmt.Sprintf("%s/?replicaSet=%s", base, c.replicaSet) } - return c.Endpoint(ctx, "mongodb") + + return base, nil } func setupEntrypointForAuth(req *testcontainers.GenericContainerRequest) { @@ -176,17 +185,27 @@ func initiateReplicaSet(req *testcontainers.GenericContainerRequest, cli mongoCl req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ PostStarts: []testcontainers.ContainerHook{ func(ctx context.Context, c testcontainers.Container) error { - ip, err := c.ContainerIP(ctx) + // Wait for MongoDB to be ready + if err := waitForMongoReady(ctx, c, cli); err != nil { + return fmt.Errorf("failed to wait for MongoDB ready: %w", err) + } + + // Initiate replica set + host, err := c.Host(ctx) if err != nil { - return fmt.Errorf("container ip: %w", err) + return fmt.Errorf("failed to get host: %w", err) + } + mappedPort, err := c.MappedPort(ctx, "27017/tcp") + if err != nil { + return fmt.Errorf("failed to get mapped port: %w", err) } cmd := cli.eval( - "rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:27017' } ] })", + "rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:%s' } ] })", replSetName, - ip, + host, + mappedPort.Port(), ) - return wait.ForExec(cmd).WaitUntilReady(ctx, c) }, }, @@ -208,3 +227,7 @@ func withAuthReplicaset( return nil } } + +func waitForMongoReady(ctx context.Context, c testcontainers.Container, cli mongoCli) error { + return wait.ForExec(cli.eval("db.runCommand({ ping: 1 })")).WaitUntilReady(ctx, c) +} diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 8cc49e629c..d4ddb31a5a 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -140,3 +140,52 @@ func TestMongoDB(t *testing.T) { }) } } + +func TestMongoDBChangeStream(t *testing.T) { + ctx := context.Background() + + // Start MongoDB with replica set (required for change streams) + mongodbContainer, err := mongodb.Run(ctx, "mongo:7", + mongodb.WithReplicaSet("rs0"), + ) + require.NoError(t, err) + testcontainers.CleanupContainer(t, mongodbContainer) + + endpoint, err := mongodbContainer.ConnectionString(ctx) + require.NoError(t, err) + + endpoint = endpoint + "/?replicaSet=rs0" + // Connect to MongoDB + mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint)) + require.NoError(t, err) + defer mongoClient.Disconnect(ctx) + + // Create a collection + coll := mongoClient.Database("test").Collection("changes") + + // Start change stream + stream, err := coll.Watch(ctx, mongo.Pipeline{}) + require.NoError(t, err) + defer stream.Close(ctx) + + // Insert a document + doc := bson.M{"message": "hello change streams"} + _, err = coll.InsertOne(ctx, doc) + require.NoError(t, err) + + // Wait for the change event + require.True(t, stream.Next(ctx)) + + var changeEvent bson.M + err = stream.Decode(&changeEvent) + require.NoError(t, err) + + // Verify the change event + operationType, ok := changeEvent["operationType"].(string) + require.True(t, ok) + require.Equal(t, "insert", operationType) + + fullDocument, ok := changeEvent["fullDocument"].(bson.M) + require.True(t, ok) + require.Equal(t, "hello change streams", fullDocument["message"]) +} From 605e85919a4adc4b6792b1bd8b6279c8f8f62175 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Fri, 14 Feb 2025 20:29:01 -0500 Subject: [PATCH 02/21] Test helper function --- docker_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docker_test.go b/docker_test.go index 4e90f5ddb2..8cb0594c31 100644 --- a/docker_test.go +++ b/docker_test.go @@ -8,6 +8,7 @@ import ( "io" "log" "math/rand" + "net" "net/http" "os" "path/filepath" @@ -2269,3 +2270,26 @@ func Test_Provider_DaemonHost_Issue2897(t *testing.T) { require.NoError(t, err) } } + +func TestGetLocalNonLoopbackIP(t *testing.T) { + ip, err := getLocalNonLoopbackIP() + if err != nil { + // If no non-loopback IP is found, skip the test. + t.Skip("No non-loopback IP address found; skipping test:", err) + } + + if ip == "" { + t.Error("Expected a non-empty IP string, got empty string") + } + + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + t.Errorf("Returned IP %q is not a valid IP address", ip) + } + + if parsedIP.IsLoopback() { + t.Errorf("Expected a non-loopback IP, but got loopback IP: %q", ip) + } + + t.Logf("Found non-loopback IP: %s", ip) +} From 22743236964adfa89bee7a0b85c1dfcde25b27eb Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Fri, 14 Feb 2025 20:52:30 -0500 Subject: [PATCH 03/21] Remove helper --- docker.go | 44 +------------------------------------------- docker_test.go | 24 ------------------------ 2 files changed, 1 insertion(+), 67 deletions(-) diff --git a/docker.go b/docker.go index 5c88cd9bb1..58428ad409 100644 --- a/docker.go +++ b/docker.go @@ -1510,12 +1510,7 @@ func (p *DockerProvider) daemonHostLocked(ctx context.Context) (string, error) { } p.hostCache = ip } else { - ip, err := getLocalNonLoopbackIP() - if err != nil { - p.hostCache = "localhost" - } else { - p.hostCache = ip - } + p.hostCache = "localhost" } default: return "", errors.New("could not determine host through env or docker host") @@ -1805,40 +1800,3 @@ func tryClose(r io.Reader) { _ = rc.Close() } } - -// getLocalNonLoopbackIP returns the first non-loopback IPv4 address found. -func getLocalNonLoopbackIP() (string, error) { - interfaces, err := net.Interfaces() - if err != nil { - return "", err - } - for _, iface := range interfaces { - // Skip down or loopback interfaces. - if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { - continue - } - addrs, err := iface.Addrs() - if err != nil { - continue // try next interface - } - for _, addr := range addrs { - var ip net.IP - switch v := addr.(type) { - case *net.IPNet: - ip = v.IP - case *net.IPAddr: - ip = v.IP - } - // Check if it's a valid IPv4 and not loopback. - if ip == nil || ip.IsLoopback() { - continue - } - ip = ip.To4() - if ip == nil { - continue // not IPv4 - } - return ip.String(), nil - } - } - return "", errors.New("no non-loopback IP address found") -} diff --git a/docker_test.go b/docker_test.go index 8cb0594c31..4e90f5ddb2 100644 --- a/docker_test.go +++ b/docker_test.go @@ -8,7 +8,6 @@ import ( "io" "log" "math/rand" - "net" "net/http" "os" "path/filepath" @@ -2270,26 +2269,3 @@ func Test_Provider_DaemonHost_Issue2897(t *testing.T) { require.NoError(t, err) } } - -func TestGetLocalNonLoopbackIP(t *testing.T) { - ip, err := getLocalNonLoopbackIP() - if err != nil { - // If no non-loopback IP is found, skip the test. - t.Skip("No non-loopback IP address found; skipping test:", err) - } - - if ip == "" { - t.Error("Expected a non-empty IP string, got empty string") - } - - parsedIP := net.ParseIP(ip) - if parsedIP == nil { - t.Errorf("Returned IP %q is not a valid IP address", ip) - } - - if parsedIP.IsLoopback() { - t.Errorf("Expected a non-loopback IP, but got loopback IP: %q", ip) - } - - t.Logf("Found non-loopback IP: %s", ip) -} From 7646ab0bfc5a6892b7195eff8fd3fa9f3f639a86 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Fri, 14 Feb 2025 20:58:27 -0500 Subject: [PATCH 04/21] Fix tests --- modules/mongodb/mongodb_test.go | 50 ++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index d4ddb31a5a..7ecd41018b 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -2,6 +2,9 @@ package mongodb_test import ( "context" + "errors" + "net" + "os" "testing" "github.com/stretchr/testify/require" @@ -13,7 +16,47 @@ import ( "github.com/testcontainers/testcontainers-go/modules/mongodb" ) +func getLocalNonLoopbackIP() (string, error) { + interfaces, err := net.Interfaces() + if err != nil { + return "", err + } + for _, iface := range interfaces { + // Skip down or loopback interfaces. + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue // try next interface + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + // Check if it's a valid IPv4 and not loopback. + if ip == nil || ip.IsLoopback() { + continue + } + ip = ip.To4() + if ip == nil { + continue // not IPv4 + } + return ip.String(), nil + } + } + return "", errors.New("no non-loopback IP address found") +} func TestMongoDB(t *testing.T) { + host, err := getLocalNonLoopbackIP() + if err != nil { + host = "host.docker.internal" + } + os.Setenv("TESTCONTAINERS_HOST_OVERRIDE", host) type tests struct { name string img string @@ -142,6 +185,12 @@ func TestMongoDB(t *testing.T) { } func TestMongoDBChangeStream(t *testing.T) { + host, err := getLocalNonLoopbackIP() + if err != nil { + host = "host.docker.internal" + } + os.Setenv("TESTCONTAINERS_HOST_OVERRIDE", host) + ctx := context.Background() // Start MongoDB with replica set (required for change streams) @@ -154,7 +203,6 @@ func TestMongoDBChangeStream(t *testing.T) { endpoint, err := mongodbContainer.ConnectionString(ctx) require.NoError(t, err) - endpoint = endpoint + "/?replicaSet=rs0" // Connect to MongoDB mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint)) require.NoError(t, err) From 40fb27e5db5522ade1d590ca0e22abc1975c35bc Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Fri, 14 Feb 2025 21:05:08 -0500 Subject: [PATCH 05/21] Correct test --- modules/mongodb/mongodb_test.go | 93 ++++++++++++++------------------- 1 file changed, 38 insertions(+), 55 deletions(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 7ecd41018b..b03f93bdd3 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net" + "net/url" "os" "testing" @@ -51,6 +52,7 @@ func getLocalNonLoopbackIP() (string, error) { } return "", errors.New("no non-loopback IP address found") } + func TestMongoDB(t *testing.T) { host, err := getLocalNonLoopbackIP() if err != nil { @@ -168,72 +170,53 @@ func TestMongoDB(t *testing.T) { endpoint, err := mongodbContainer.ConnectionString(ctx) require.NoError(tt, err) - // Force direct connection to the container to avoid the replica set - // connection string that is returned by the container itself when - // using the replica set option. + // Force direct connection to the container. mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint).SetDirect(true)) require.NoError(tt, err) err = mongoClient.Ping(ctx, nil) require.NoError(tt, err) - require.Equal(t, "test", mongoClient.Database("test").Name()) + require.Equal(tt, "test", mongoClient.Database("test").Name()) - _, err = mongoClient.Database("testcontainer").Collection("test").InsertOne(context.Background(), bson.M{}) + // Basic insert test. + _, err = mongoClient.Database("testcontainer").Collection("test").InsertOne(ctx, bson.M{}) require.NoError(tt, err) + + // If the container is configured with a replica set, run the change stream test. + if hasReplica, _ := hasReplicaSet(endpoint); hasReplica { + coll := mongoClient.Database("test").Collection("changes") + stream, err := coll.Watch(ctx, mongo.Pipeline{}) + require.NoError(tt, err) + defer stream.Close(ctx) + + doc := bson.M{"message": "hello change streams"} + _, err = coll.InsertOne(ctx, doc) + require.NoError(tt, err) + + require.True(tt, stream.Next(ctx), "Expected to receive a change stream event") + var changeEvent bson.M + err = stream.Decode(&changeEvent) + require.NoError(tt, err) + + opType, ok := changeEvent["operationType"].(string) + require.True(tt, ok, "Expected operationType field") + require.Equal(tt, "insert", opType, "Expected operationType to be 'insert'") + + fullDoc, ok := changeEvent["fullDocument"].(bson.M) + require.True(tt, ok, "Expected fullDocument field") + require.Equal(tt, "hello change streams", fullDoc["message"]) + } }) } } -func TestMongoDBChangeStream(t *testing.T) { - host, err := getLocalNonLoopbackIP() +// hasReplicaSet checks if the connection string includes a replicaSet query parameter. +func hasReplicaSet(connStr string) (bool, error) { + u, err := url.Parse(connStr) if err != nil { - host = "host.docker.internal" + return false, err } - os.Setenv("TESTCONTAINERS_HOST_OVERRIDE", host) - - ctx := context.Background() - - // Start MongoDB with replica set (required for change streams) - mongodbContainer, err := mongodb.Run(ctx, "mongo:7", - mongodb.WithReplicaSet("rs0"), - ) - require.NoError(t, err) - testcontainers.CleanupContainer(t, mongodbContainer) - - endpoint, err := mongodbContainer.ConnectionString(ctx) - require.NoError(t, err) - - // Connect to MongoDB - mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(endpoint)) - require.NoError(t, err) - defer mongoClient.Disconnect(ctx) - - // Create a collection - coll := mongoClient.Database("test").Collection("changes") - - // Start change stream - stream, err := coll.Watch(ctx, mongo.Pipeline{}) - require.NoError(t, err) - defer stream.Close(ctx) - - // Insert a document - doc := bson.M{"message": "hello change streams"} - _, err = coll.InsertOne(ctx, doc) - require.NoError(t, err) - - // Wait for the change event - require.True(t, stream.Next(ctx)) - - var changeEvent bson.M - err = stream.Decode(&changeEvent) - require.NoError(t, err) - - // Verify the change event - operationType, ok := changeEvent["operationType"].(string) - require.True(t, ok) - require.Equal(t, "insert", operationType) - - fullDocument, ok := changeEvent["fullDocument"].(bson.M) - require.True(t, ok) - require.Equal(t, "hello change streams", fullDocument["message"]) + q := u.Query() + _, ok := q["replicaSet"] + return ok, nil } From 9f812313ca6591881996dc26bc4a9ae515f08f7c Mon Sep 17 00:00:00 2001 From: Thanh Truong Date: Sun, 16 Feb 2025 15:22:48 -0500 Subject: [PATCH 06/21] Address PR comments --- modules/mongodb/mongodb.go | 19 ++++++++++++------- modules/mongodb/mongodb_test.go | 23 +++++++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 9214945548..14d001cf2e 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -6,6 +6,8 @@ import ( _ "embed" "errors" "fmt" + "net" + "net/url" "time" "github.com/testcontainers/testcontainers-go" @@ -125,19 +127,22 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) if err != nil { return "", err } + u := url.URL{ + Scheme: "mongodb", + Host: net.JoinHostPort(host, port.Port()), + } - var base string if c.username != "" && c.password != "" { - base = fmt.Sprintf("mongodb://%s:%s@%s:%s", c.username, c.password, host, port.Port()) - } else { - base = fmt.Sprintf("mongodb://%s:%s", host, port.Port()) + u.User = url.UserPassword(c.username, c.password) } if c.replicaSet != "" { - base = fmt.Sprintf("%s/?replicaSet=%s", base, c.replicaSet) + q := url.Values{} + q.Add("replicaSet", c.replicaSet) + u.RawQuery = q.Encode() } - return base, nil + return u.String(), nil } func setupEntrypointForAuth(req *testcontainers.GenericContainerRequest) { @@ -187,7 +192,7 @@ func initiateReplicaSet(req *testcontainers.GenericContainerRequest, cli mongoCl func(ctx context.Context, c testcontainers.Container) error { // Wait for MongoDB to be ready if err := waitForMongoReady(ctx, c, cli); err != nil { - return fmt.Errorf("failed to wait for MongoDB ready: %w", err) + return fmt.Errorf("wait for mongo: %w", err) } // Initiate replica set diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index b03f93bdd3..4e984cd7da 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -3,6 +3,7 @@ package mongodb_test import ( "context" "errors" + "fmt" "net" "net/url" "os" @@ -17,10 +18,10 @@ import ( "github.com/testcontainers/testcontainers-go/modules/mongodb" ) -func getLocalNonLoopbackIP() (string, error) { +func localNonLoopbackIP() (string, error) { interfaces, err := net.Interfaces() if err != nil { - return "", err + return "", fmt.Errorf("list network interfaces: %w", err) } for _, iface := range interfaces { // Skip down or loopback interfaces. @@ -54,7 +55,7 @@ func getLocalNonLoopbackIP() (string, error) { } func TestMongoDB(t *testing.T) { - host, err := getLocalNonLoopbackIP() + host, err := localNonLoopbackIP() if err != nil { host = "host.docker.internal" } @@ -210,13 +211,23 @@ func TestMongoDB(t *testing.T) { } } +// hasReplicaSet checks if the connection string includes a replicaSet query parameter. // hasReplicaSet checks if the connection string includes a replicaSet query parameter. func hasReplicaSet(connStr string) (bool, error) { u, err := url.Parse(connStr) if err != nil { - return false, err + return false, fmt.Errorf("parse connection string: %w", err) } q := u.Query() - _, ok := q["replicaSet"] - return ok, nil + replicaSetValues, ok := q["replicaSet"] + if !ok { + return false, nil + } + // Check if any of the replica set values are non-empty. + for _, value := range replicaSetValues { + if value != "" { + return true, nil // Found a non-empty replicaSet value + } + } + return false, nil // replicaSet parameter exists, but all values are empty } From 9490718433f860354d79517f4c9d7d2f694a3f62 Mon Sep 17 00:00:00 2001 From: Thanh Truong Date: Sun, 16 Feb 2025 15:29:14 -0500 Subject: [PATCH 07/21] Fix bugs --- modules/mongodb/mongodb.go | 1 + modules/mongodb/mongodb_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 14d001cf2e..c43ddbac0e 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -140,6 +140,7 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) q := url.Values{} q.Add("replicaSet", c.replicaSet) u.RawQuery = q.Encode() + u.Path = "/" } return u.String(), nil diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 4e984cd7da..2729fe367a 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -39,6 +39,8 @@ func localNonLoopbackIP() (string, error) { ip = v.IP case *net.IPAddr: ip = v.IP + default: + continue } // Check if it's a valid IPv4 and not loopback. if ip == nil || ip.IsLoopback() { From 868260aa1c31970fb7e60cabf66f7a1a5e2d648f Mon Sep 17 00:00:00 2001 From: Thanh Truong Date: Sun, 16 Feb 2025 15:31:06 -0500 Subject: [PATCH 08/21] Remove "failed" prefix --- modules/mongodb/mongodb.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index c43ddbac0e..2a56ad808e 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -199,11 +199,11 @@ func initiateReplicaSet(req *testcontainers.GenericContainerRequest, cli mongoCl // Initiate replica set host, err := c.Host(ctx) if err != nil { - return fmt.Errorf("failed to get host: %w", err) + return fmt.Errorf("get host: %w", err) } mappedPort, err := c.MappedPort(ctx, "27017/tcp") if err != nil { - return fmt.Errorf("failed to get mapped port: %w", err) + return fmt.Errorf("get mapped port: %w", err) } cmd := cli.eval( From 209e8aabe0f9569ad3093ff5ebaad394bb15d5b5 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Sun, 16 Feb 2025 21:04:13 -0500 Subject: [PATCH 09/21] Apply suggestion. Co-authored-by: Steven Hartland --- modules/mongodb/mongodb_test.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 2729fe367a..88d2503d0b 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -220,16 +220,5 @@ func hasReplicaSet(connStr string) (bool, error) { if err != nil { return false, fmt.Errorf("parse connection string: %w", err) } - q := u.Query() - replicaSetValues, ok := q["replicaSet"] - if !ok { - return false, nil - } - // Check if any of the replica set values are non-empty. - for _, value := range replicaSetValues { - if value != "" { - return true, nil // Found a non-empty replicaSet value - } - } - return false, nil // replicaSet parameter exists, but all values are empty + return q.Get("replicaSet") == "" } From 6fec97699d68f66e1416909ce85c1e9615a7bb31 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Sun, 16 Feb 2025 21:04:51 -0500 Subject: [PATCH 10/21] Apply suggestion Co-authored-by: Steven Hartland --- modules/mongodb/mongodb_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 88d2503d0b..53f06add9c 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -43,7 +43,7 @@ func localNonLoopbackIP() (string, error) { continue } // Check if it's a valid IPv4 and not loopback. - if ip == nil || ip.IsLoopback() { + if ip.IsLoopback() { continue } ip = ip.To4() From cc222bd69e2942183cd10a3015fd93f6367e39b0 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Sun, 16 Feb 2025 21:05:33 -0500 Subject: [PATCH 11/21] Remove duplicate comments Co-authored-by: Steven Hartland --- modules/mongodb/mongodb_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 53f06add9c..2513b8bea7 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -213,7 +213,6 @@ func TestMongoDB(t *testing.T) { } } -// hasReplicaSet checks if the connection string includes a replicaSet query parameter. // hasReplicaSet checks if the connection string includes a replicaSet query parameter. func hasReplicaSet(connStr string) (bool, error) { u, err := url.Parse(connStr) From 69e3a25374395819d58016f7a1856229399089f3 Mon Sep 17 00:00:00 2001 From: Thanh Truong Date: Sun, 16 Feb 2025 21:27:36 -0500 Subject: [PATCH 12/21] address comments --- modules/mongodb/mongodb.go | 3 ++- modules/mongodb/mongodb_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 2a56ad808e..f8c3a71269 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -136,11 +136,12 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) u.User = url.UserPassword(c.username, c.password) } + u.Path = "/" + if c.replicaSet != "" { q := url.Values{} q.Add("replicaSet", c.replicaSet) u.RawQuery = q.Encode() - u.Path = "/" } return u.String(), nil diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 2513b8bea7..caa26b7b36 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -6,7 +6,6 @@ import ( "fmt" "net" "net/url" - "os" "testing" "github.com/stretchr/testify/require" @@ -53,7 +52,7 @@ func localNonLoopbackIP() (string, error) { return ip.String(), nil } } - return "", errors.New("no non-loopback IP address found") + return "", errors.New("no non-loopback IPv4 address found") } func TestMongoDB(t *testing.T) { @@ -61,7 +60,7 @@ func TestMongoDB(t *testing.T) { if err != nil { host = "host.docker.internal" } - os.Setenv("TESTCONTAINERS_HOST_OVERRIDE", host) + t.Setenv("TESTCONTAINERS_HOST_OVERRIDE", host) type tests struct { name string img string @@ -219,5 +218,6 @@ func hasReplicaSet(connStr string) (bool, error) { if err != nil { return false, fmt.Errorf("parse connection string: %w", err) } - return q.Get("replicaSet") == "" + q := u.Query() + return q.Get("replicaSet") != "", nil } From 09610f38eb18d6c37f3fd1991bfa2bc94e617816 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Mon, 17 Feb 2025 09:15:17 -0500 Subject: [PATCH 13/21] Apply suggestion to remove hard code message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel de la Peña --- modules/mongodb/mongodb_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index caa26b7b36..7338887b15 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -195,7 +195,7 @@ func TestMongoDB(t *testing.T) { _, err = coll.InsertOne(ctx, doc) require.NoError(tt, err) - require.True(tt, stream.Next(ctx), "Expected to receive a change stream event") + require.True(tt, stream.Next(ctx)) var changeEvent bson.M err = stream.Decode(&changeEvent) require.NoError(tt, err) From 7618320af4fef2aebbfb78a3bc143a3841023283 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Mon, 17 Feb 2025 09:15:45 -0500 Subject: [PATCH 14/21] Apply coding style suggestion. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel de la Peña --- modules/mongodb/mongodb_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 7338887b15..d01fffc062 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -27,6 +27,7 @@ func localNonLoopbackIP() (string, error) { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } + addrs, err := iface.Addrs() if err != nil { continue // try next interface From 06754a9a99d946f519c02a611cd9a91fd023e464 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Mon, 17 Feb 2025 09:16:04 -0500 Subject: [PATCH 15/21] Apply coding style suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel de la Peña --- modules/mongodb/mongodb_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index d01fffc062..dbaab41705 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -19,6 +19,7 @@ import ( func localNonLoopbackIP() (string, error) { interfaces, err := net.Interfaces() + if err != nil { return "", fmt.Errorf("list network interfaces: %w", err) } From 337972559d9e7485014ef1894cba7bbf6aa2158e Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Mon, 17 Feb 2025 09:16:26 -0500 Subject: [PATCH 16/21] Apply coding style suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel de la Peña --- modules/mongodb/mongodb_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index dbaab41705..d0d6b23e32 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -23,6 +23,7 @@ func localNonLoopbackIP() (string, error) { if err != nil { return "", fmt.Errorf("list network interfaces: %w", err) } + for _, iface := range interfaces { // Skip down or loopback interfaces. if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { From 62d640d034ad36f44be688fd1b9d49559a602fbf Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Mon, 17 Feb 2025 11:33:03 -0500 Subject: [PATCH 17/21] Apply suggestion Co-authored-by: Steven Hartland --- modules/mongodb/mongodb.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index f8c3a71269..888241bf47 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -130,6 +130,7 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) u := url.URL{ Scheme: "mongodb", Host: net.JoinHostPort(host, port.Port()), + Path: "/", } if c.username != "" && c.password != "" { From 37cb2c57480ee0b7388013524f9f386f5f260eb9 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Mon, 17 Feb 2025 11:33:12 -0500 Subject: [PATCH 18/21] Apply suggestion Co-authored-by: Steven Hartland --- modules/mongodb/mongodb.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 888241bf47..9b2557056c 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -137,8 +137,6 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) u.User = url.UserPassword(c.username, c.password) } - u.Path = "/" - if c.replicaSet != "" { q := url.Values{} q.Add("replicaSet", c.replicaSet) From deb1fbd63b0c23d27cc4aaa3a8ce4facf62bf709 Mon Sep 17 00:00:00 2001 From: Thanh Truong Date: Mon, 17 Feb 2025 17:39:40 -0500 Subject: [PATCH 19/21] Fix lint --- modules/mongodb/mongodb.go | 2 +- modules/mongodb/mongodb_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 9b2557056c..be737e93d4 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -130,7 +130,7 @@ func (c *MongoDBContainer) ConnectionString(ctx context.Context) (string, error) u := url.URL{ Scheme: "mongodb", Host: net.JoinHostPort(host, port.Port()), - Path: "/", + Path: "/", } if c.username != "" && c.password != "" { diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index d0d6b23e32..0bf1b51edd 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -19,7 +19,6 @@ import ( func localNonLoopbackIP() (string, error) { interfaces, err := net.Interfaces() - if err != nil { return "", fmt.Errorf("list network interfaces: %w", err) } From 6e93a1dc33ead797de827c64690a1dbfb34aaa58 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Sat, 29 Mar 2025 08:10:29 -0400 Subject: [PATCH 20/21] Refactor MongoDB replica set initiation and remove unused localNonLoopbackIP function --- modules/mongodb/mongodb.go | 19 +++---------- modules/mongodb/mongodb_test.go | 47 --------------------------------- 2 files changed, 4 insertions(+), 62 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index be737e93d4..5ba23d7c97 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -191,26 +191,15 @@ func initiateReplicaSet(req *testcontainers.GenericContainerRequest, cli mongoCl req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{ PostStarts: []testcontainers.ContainerHook{ func(ctx context.Context, c testcontainers.Container) error { - // Wait for MongoDB to be ready - if err := waitForMongoReady(ctx, c, cli); err != nil { - return fmt.Errorf("wait for mongo: %w", err) - } - - // Initiate replica set - host, err := c.Host(ctx) - if err != nil { - return fmt.Errorf("get host: %w", err) - } - mappedPort, err := c.MappedPort(ctx, "27017/tcp") + ip, err := c.ContainerIP(ctx) if err != nil { - return fmt.Errorf("get mapped port: %w", err) + return fmt.Errorf("container ip: %w", err) } cmd := cli.eval( - "rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:%s' } ] })", + "rs.initiate({ _id: '%s', members: [ { _id: 0, host: '%s:27017' } ] })", replSetName, - host, - mappedPort.Port(), + ip, ) return wait.ForExec(cmd).WaitUntilReady(ctx, c) }, diff --git a/modules/mongodb/mongodb_test.go b/modules/mongodb/mongodb_test.go index 0bf1b51edd..4f0699d5d5 100644 --- a/modules/mongodb/mongodb_test.go +++ b/modules/mongodb/mongodb_test.go @@ -2,9 +2,7 @@ package mongodb_test import ( "context" - "errors" "fmt" - "net" "net/url" "testing" @@ -17,52 +15,7 @@ import ( "github.com/testcontainers/testcontainers-go/modules/mongodb" ) -func localNonLoopbackIP() (string, error) { - interfaces, err := net.Interfaces() - if err != nil { - return "", fmt.Errorf("list network interfaces: %w", err) - } - - for _, iface := range interfaces { - // Skip down or loopback interfaces. - if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { - continue - } - - addrs, err := iface.Addrs() - if err != nil { - continue // try next interface - } - for _, addr := range addrs { - var ip net.IP - switch v := addr.(type) { - case *net.IPNet: - ip = v.IP - case *net.IPAddr: - ip = v.IP - default: - continue - } - // Check if it's a valid IPv4 and not loopback. - if ip.IsLoopback() { - continue - } - ip = ip.To4() - if ip == nil { - continue // not IPv4 - } - return ip.String(), nil - } - } - return "", errors.New("no non-loopback IPv4 address found") -} - func TestMongoDB(t *testing.T) { - host, err := localNonLoopbackIP() - if err != nil { - host = "host.docker.internal" - } - t.Setenv("TESTCONTAINERS_HOST_OVERRIDE", host) type tests struct { name string img string From d4adb4d7c3ed2ca9e65d1b4cb7cfb2cae2221928 Mon Sep 17 00:00:00 2001 From: ttruongatl Date: Wed, 2 Apr 2025 07:32:01 -0400 Subject: [PATCH 21/21] refactor(mongodb): remove unused waitForMongoReady function --- modules/mongodb/mongodb.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/mongodb/mongodb.go b/modules/mongodb/mongodb.go index 5ba23d7c97..f187dbf977 100644 --- a/modules/mongodb/mongodb.go +++ b/modules/mongodb/mongodb.go @@ -222,7 +222,3 @@ func withAuthReplicaset( return nil } } - -func waitForMongoReady(ctx context.Context, c testcontainers.Container, cli mongoCli) error { - return wait.ForExec(cli.eval("db.runCommand({ ping: 1 })")).WaitUntilReady(ctx, c) -}