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

[Enhancement]: Add one node replica set support #1154

Closed
diegosasw opened this issue Apr 13, 2024 · 5 comments · Fixed by #1196
Closed

[Enhancement]: Add one node replica set support #1154

diegosasw opened this issue Apr 13, 2024 · 5 comments · Fixed by #1196
Labels
enhancement New feature or request

Comments

@diegosasw
Copy link

diegosasw commented Apr 13, 2024

Problem

There is no one-node replica set support for testcontainers-dotnet but the java version seems to have something. However, by looking at the java code I have some doubts whether that's properly initializing the replica set.

Solution

Add a WithReplicaSet(string replicaSetName = "rs0") or similar builder method.
Add another method to initialize the replica set.
Add another method to obtain the ReplicaSet connection string which should be something similar to mongodb://{hostname}:{mapped_port}/?replicaSet=rs0

Benefit

It would allow testing mongoDb replica set and things such as change streams, only available on replica sets. Also, it would get closer to the Java version in functionality.

Alternatives

Currently, mongoDb containers with one node replica set are possible to be created with Ductus.FluentDocker.
Also with testcontainers, but the random port is a challenge.

Would you like to help contributing this enhancement?

Yes

@diegosasw diegosasw added the enhancement New feature or request label Apr 13, 2024
@diegosasw
Copy link
Author

diegosasw commented Apr 13, 2024

I have tried to add this functionality on this branch https://github.com/testcontainers/testcontainers-dotnet/tree/57bf8adf56e3ae701b8b488113a1e8db01d41a4a
but I wasn't successful.

Please view https://github.com/testcontainers/testcontainers-dotnet/blob/57bf8adf56e3ae701b8b488113a1e8db01d41a4a/tests/Testcontainers.MongoDb.Tests/MongoDbReplicaSetContainerTest.cs as it's the closest I got.

The biggest challenge I am finding is due to the random port. Not really a problem with testcontainers but with a lack of knowledge about mongodb and docker networking.

The mentioned test works well, and it demonstrates how a replica set can be created and initiated and available through a connection string such as mongodb://127.0.0.1:27017/?replicaSet=rs0

But it works because I am disabling the random port, which is not Ok.

If I specify a random port I could get the port number being used but, when initiating the replica set, I would get an error

MongoServerError[InvalidReplicaSetConfig]: No host described in new configuration with {version: 1, term: 0} for replica set rs0 maps to this node

I have manually tried to start the container without executing the rs.initiate(); part, but I wasn't successful. This is the process.

  1. I execute the mongodb container in debug mode and set a breakpoint to leave it running and to explore the port being used
  2. I check the mongodb container Id with
$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED         STATUS         PORTS
      NAMES
4149a2252b5c   mongo:7.0.7-jammy           "docker-entrypoint.s…"   5 seconds ago   Up 4 seconds   0.0.0.0:27017->27017/tcp   inspiring_hopper
49c9c66dab55   testcontainers/ryuk:0.6.0   "/bin/ryuk"              6 seconds ago   Up 5 seconds   0.0.0.0:51409->8080/tcp    testcontainers-ryuk-9647d181-f95c-480c-8903-ade881c200d3
  1. I access the container's mongosh
$ docker exec -it 4149a2252b5c mongosh
Current Mongosh Log ID: 661aa220025bffaaf5db83af
Connecting to:          mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.0
Using MongoDB:          7.0.7
Using Mongosh:          2.2.0

For mongosh info see: https://docs.mongodb.com/mongodb-shell/


To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
You can opt-out by running the disableTelemetry() command.

test>
  1. I try to initiate the replica set
test> rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongo:27017'}]});
{ ok: 1 }

and this works well for the port 27017, but if I am using a random port and I try to initiate it with that other port (e.g: 51523)

test> rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongo:51523'}]});
MongoServerError[InvalidReplicaSetConfig]: No host described in new configuration with {version: 1, term: 0} for replica set rs0 maps to this node

Useful links:

@diegosasw diegosasw changed the title [Enhancement]: Add replica set support [Enhancement]: Add one node replica set support Apr 13, 2024
@eddumelendez
Copy link
Member

Hi @diegosasw, just cross-posting my question here

@diegosasw
Copy link
Author

Hi @diegosasw, just cross-posting my question here

I've had a look at your Go PR.
I can see you're also using custom object with members when initializing (Java mongo replica set does not have that). Did you make it work with random ports and with connection string with mongodb://host:port/?replica set=rs?

I'm wondering whether you know why this is not working for me.

@kieronlanning
Copy link

kieronlanning commented May 16, 2024

I've been trying to do this as well, the closest I've gotten is this:

public static IContainer CreateMongoDBWithReplicaSet(Action<ContainerBuilder>? config = null)
{
	var builder = new ContainerBuilder()
		.WithImage("mongo:7.0")
		.WithCommand("--replSet rs0 --bind_ip_all")
		.WithPortBinding(MongoDbBuilder.MongoDbPort, true)
		.WithEnvironment("MONGO_INITDB_ROOT_USERNAME", MongoDbBuilder.DefaultUsername)
		.WithEnvironment("MONGO_INITDB_ROOT_PASSWORD", MongoDbBuilder.DefaultPassword)
		.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(MongoDbBuilder.MongoDbPort))
		.WithStartupCallback(async (container, cancellationToken) =>
		{
			await container.ExecAsync(["bash", "-c", $"echo 'disableTelemetry()' | mongosh -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD"], isSuccess: result =>
			{
				result.Stderr.Should().BeNullOrEmpty();

				if (!string.IsNullOrEmpty(result.Stdout))
				{
					Console.WriteLine("stdout:" + result.Stdout);
				}

				return true;

			}, cancellationToken: cancellationToken);

			await container.ExecAsync(["bash", "-c", $"echo 'try {{ rs.status() }} catch (err) {{ rs.initiate({{_id: \"rs0\", members: [{{ _id: 0, host: \"localhost:{MongoDbBuilder.MongoDbPort}\" }}]}}) }};' | mongosh -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD"], e =>
			{
				return false;
			}, cancellationToken: cancellationToken);

			return;

			await container.ExecAsync(["bash", "-c", $"echo 'disableTelemetry(); rs.initiate({{_id: \"rs0\", members: [{{ _id: 0, host: \"localhost:{MongoDbBuilder.MongoDbPort}\" }}]}});' | mongosh -u $MONGO_INITDB_ROOT_USERNAME -p $MONGO_INITDB_ROOT_PASSWORD"], e =>
			{
				return false;
			}, cancellationToken: cancellationToken);
		});

	config?.Invoke(builder);

	return builder.Build();
}

static async Task<bool> ExecAsync(this IContainer container, List<string> commands, Func<ExecResult, bool>? isSuccess = null, bool throwOnFailure = true, int attempts = 10, int delayInMS = 100, CancellationToken cancellationToken = default)
{
	var attemptCount = 0;
	while (attemptCount < attempts)
	{
		try
		{
			var result = await container.ExecAsync(commands, cancellationToken);
			if (result.ExitCode == 0)
			{
				return isSuccess is not null && !isSuccess(result)
					? throw new Exception($"Command failed. Stdout: {result.Stdout}, Stderr: {result.Stderr}")
					: true;
			}

			result.ExitCode.Should().Be(0,
				because: $"MongoDB replica set initialization failed. Attempt {attemptCount + 1} of 10. Stdout: {result.Stdout}, Stderr: {result.Stderr}");
		}
		catch
		{
			await Task.Delay(delayInMS, cancellationToken);
			attemptCount++;
		}
	}

	return throwOnFailure
		? throw new Exception("Failed to execute command.")
		: false;
}

It's failing as it requires a key, which I've been trying to use mount paths to generate and executing commands but so far failing.

@artiomchi
Copy link
Contributor

Hey all, I've been looking for the same functionality myself and have implemented it locally before I saw this issue. I've just created a PR that provides a working configuration for a single node replica set - I've tested it locally on our integration tests, and it's been working perfectly so far

Let me know what you think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants