-
Notifications
You must be signed in to change notification settings - Fork 30
feat(container): Add Mongo DB container #251
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
Merged
jarlah
merged 4 commits into
testcontainers:main
from
PedroI920224:feature/add_mongodb_container
Apr 17, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
054b5fb
feat(container): Add Mongo DB container
PedroI920224 79653de
Remove original and modify by in MongoTestContainers
PedroI920224 f9d3858
test(mongo): use mongodb_driver for reachability roundtrip tests
PedroI920224 66cbc31
chore(mongo): Upgrade MongoDriverVersion to 1.6.2
PedroI920224 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| # SPDX-License-Identifier: MIT | ||
| defmodule Testcontainers.MongoContainer do | ||
| @behaviour Testcontainers.DatabaseBehaviour | ||
| @moduledoc """ | ||
| Provides functionality for creating and managing Mongo container configurations. | ||
| """ | ||
|
|
||
| alias Testcontainers.CommandWaitStrategy | ||
| alias Testcontainers.Container | ||
| alias Testcontainers.ContainerBuilder | ||
| alias Testcontainers.MongoContainer | ||
|
|
||
| import Testcontainers.Container, only: [is_valid_image: 1] | ||
|
|
||
| @default_image "mongo" | ||
| @default_tag "latest" | ||
| @default_image_with_tag "#{@default_image}:#{@default_tag}" | ||
| @default_user "test" | ||
| @default_password "test" | ||
| @default_database "test" | ||
| @default_port 27017 | ||
| @default_wait_timeout 180_000 | ||
|
|
||
| @enforce_keys [:image, :user, :password, :database, :port, :wait_timeout, :persistent_volume] | ||
| defstruct [ | ||
| :image, | ||
| :user, | ||
| :password, | ||
| :database, | ||
| :port, | ||
| :wait_timeout, | ||
| :persistent_volume, | ||
| check_image: @default_image, | ||
| reuse: false | ||
| ] | ||
|
|
||
| @doc """ | ||
| Creates a new `MongoContainer` struct with default configurations. | ||
| """ | ||
| def new, | ||
| do: %__MODULE__{ | ||
| image: @default_image_with_tag, | ||
| user: @default_user, | ||
| password: @default_password, | ||
| database: @default_database, | ||
| port: @default_port, | ||
| wait_timeout: @default_wait_timeout, | ||
| persistent_volume: nil | ||
| } | ||
|
|
||
| @doc """ | ||
| Overrides the default image used for the Mongo container. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> new_config = MongoContainer.with_image(config, "mongo:5") | ||
| iex> new_config.image | ||
| "mongo:5" | ||
| """ | ||
| def with_image(%__MODULE__{} = config, image) when is_binary(image) do | ||
| %{config | image: image} | ||
| end | ||
|
|
||
| @doc """ | ||
| Alias for `with_user/2`, matching Mongo naming in other implementations. | ||
| """ | ||
| def with_username(%__MODULE__{} = config, username) when is_binary(username) do | ||
| with_user(config, username) | ||
| end | ||
|
|
||
| @doc """ | ||
| Overrides the default user used for the Mongo container. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> new_config = MongoContainer.with_user(config, "another-user") | ||
| iex> new_config.user | ||
| "another-user" | ||
| """ | ||
| def with_user(%__MODULE__{} = config, user) when is_binary(user) do | ||
| %{config | user: user} | ||
| end | ||
|
|
||
| @doc """ | ||
| Overrides the default password used for the Mongo container. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> new_config = MongoContainer.with_password(config, "another-password") | ||
| iex> new_config.password | ||
| "another-password" | ||
| """ | ||
| def with_password(%__MODULE__{} = config, password) when is_binary(password) do | ||
| %{config | password: password} | ||
| end | ||
|
|
||
| @doc """ | ||
| Overrides the default database used for the Mongo container. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> new_config = MongoContainer.with_database(config, "another-database") | ||
| iex> new_config.database | ||
| "another-database" | ||
| """ | ||
| def with_database(%__MODULE__{} = config, database) when is_binary(database) do | ||
| %{config | database: database} | ||
| end | ||
|
|
||
| @doc """ | ||
| Overrides the default port used for the Mongo container. | ||
|
|
||
| Note: this will not change what port the docker container is listening to internally. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> new_config = MongoContainer.with_port(config, 27018) | ||
| iex> new_config.port | ||
| 27018 | ||
| """ | ||
| def with_port(%__MODULE__{} = config, port) when is_integer(port) or is_tuple(port) do | ||
| %{config | port: port} | ||
| end | ||
|
|
||
| @doc """ | ||
| mounts persistent volume in Mongo data path used for the Mongo container. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> config = MongoContainer.with_persistent_volume(config, "data_volume") | ||
| iex> config.persistent_volume | ||
| "data_volume" | ||
| """ | ||
| def with_persistent_volume(%__MODULE__{} = config, persistent_volume) | ||
| when is_binary(persistent_volume) do | ||
| %{config | persistent_volume: persistent_volume} | ||
| end | ||
|
|
||
| @doc """ | ||
| Mounts the default wait timeout used for the Mongo container. | ||
|
|
||
| Note: this timeout will be used for each individual wait strategy. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> config = MongoContainer.new() | ||
| iex> new_config = MongoContainer.with_wait_timeout(config, 8000) | ||
| iex> new_config.wait_timeout | ||
| 8000 | ||
| """ | ||
| def with_wait_timeout(%__MODULE__{} = config, wait_timeout) when is_integer(wait_timeout) do | ||
| %{config | wait_timeout: wait_timeout} | ||
| end | ||
|
|
||
| @doc """ | ||
| Set the regular expression to check the image validity. | ||
| """ | ||
| def with_check_image(%__MODULE__{} = config, check_image) when is_valid_image(check_image) do | ||
| %__MODULE__{config | check_image: check_image} | ||
| end | ||
|
|
||
| @doc """ | ||
| Set the reuse flag to reuse the container if it is already running. | ||
| """ | ||
| def with_reuse(%__MODULE__{} = config, reuse) when is_boolean(reuse) do | ||
| %__MODULE__{config | reuse: reuse} | ||
| end | ||
|
|
||
| @doc """ | ||
| Retrieves the default exposed port for the Mongo container. | ||
| """ | ||
| def default_port, do: @default_port | ||
|
|
||
| @doc """ | ||
| Retrieves the default Docker image for the Mongo container. | ||
| """ | ||
| def default_image, do: @default_image | ||
|
|
||
| @doc """ | ||
| Retrieves the default Docker image including tag for the Mongo container. | ||
| """ | ||
| def default_image_with_tag, do: @default_image <> ":" <> @default_tag | ||
|
|
||
| @doc """ | ||
| Returns the port on the _host machine_ where the Mongo container is listening. | ||
| """ | ||
| def port(%Container{} = container), do: Testcontainers.get_port(container, @default_port) | ||
|
|
||
| @doc """ | ||
| Returns the connection parameters to connect to the database from the _host machine_. | ||
| """ | ||
| def connection_parameters(%Container{} = container) do | ||
| [ | ||
| hostname: Testcontainers.get_host(container), | ||
| port: port(container), | ||
| username: container.environment[:MONGO_INITDB_ROOT_USERNAME], | ||
| password: container.environment[:MONGO_INITDB_ROOT_PASSWORD], | ||
| database: container.environment[:MONGO_INITDB_DATABASE] | ||
| ] | ||
| end | ||
|
|
||
| @doc """ | ||
| Generates the MongoDB connection URL. | ||
|
|
||
| ## Options | ||
| * `:protocol` - URL scheme, defaults to `"mongodb"`. | ||
| * `:username` - Overrides username from container env. | ||
| * `:password` - Overrides password from container env. | ||
| * `:database` - Overrides database from container env. | ||
| * `:options` - Query options as map/keyword list. | ||
|
PedroI920224 marked this conversation as resolved.
|
||
| """ | ||
| def mongo_url(%Container{} = container, opts \\ []) when is_list(opts) do | ||
| protocol = Keyword.get(opts, :protocol, "mongodb") | ||
| username = Keyword.get(opts, :username, container.environment[:MONGO_INITDB_ROOT_USERNAME]) | ||
| password = Keyword.get(opts, :password, container.environment[:MONGO_INITDB_ROOT_PASSWORD]) | ||
| database = Keyword.get(opts, :database, container.environment[:MONGO_INITDB_DATABASE]) | ||
| query_string = opts |> Keyword.get(:options, []) |> encode_query_string() | ||
|
|
||
| "#{protocol}://#{username}:#{password}@#{Testcontainers.get_host(container)}:#{port(container)}/#{database}#{query_string}" | ||
| end | ||
|
|
||
| @doc """ | ||
| Alias for `mongo_url/2`. | ||
| """ | ||
| def database_url(%Container{} = container, opts \\ []), do: mongo_url(container, opts) | ||
|
|
||
| defimpl ContainerBuilder do | ||
| import Container | ||
|
|
||
| @doc """ | ||
| Implementation of the `ContainerBuilder` protocol specific to `MongoContainer`. | ||
|
|
||
| This function builds a new container configuration, ensuring the Mongo image is compatible, setting environment variables, and applying a waiting strategy for the container to be ready. | ||
|
|
||
| The build process raises an `ArgumentError` if the specified container image is not compatible with the expected Mongo image. | ||
|
|
||
| ## Examples | ||
|
|
||
| # Assuming `ContainerBuilder.build/2` is called from somewhere in the application with a `MongoContainer` configuration: | ||
| iex> config = MongoContainer.new() | ||
| iex> built_container = ContainerBuilder.build(config, []) | ||
| # `built_container` is now a ready-to-use `%Container{}` configured specifically for Mongo. | ||
|
|
||
| ## Errors | ||
|
|
||
| - Raises `ArgumentError` if the provided image is not compatible with the default Mongo image. | ||
| """ | ||
| @spec build(%MongoContainer{}) :: %Container{} | ||
| @impl true | ||
| def build(%MongoContainer{} = config) do | ||
| new(config.image) | ||
| |> then(MongoContainer.container_port_fun(config.port)) | ||
| |> with_environment(:MONGO_INITDB_ROOT_USERNAME, config.user) | ||
| |> with_environment(:MONGO_INITDB_ROOT_PASSWORD, config.password) | ||
| |> with_environment(:MONGO_INITDB_DATABASE, config.database) | ||
| |> then(MongoContainer.container_volume_fun(config.persistent_volume)) | ||
| |> with_waiting_strategy( | ||
| CommandWaitStrategy.new( | ||
| [ | ||
| "sh", | ||
| "-c", | ||
| "mongosh --eval \"db.adminCommand('ping')\" || mongo --eval \"db.adminCommand('ping')\"" | ||
| ], | ||
| config.wait_timeout | ||
| ) | ||
| ) | ||
| |> with_check_image(config.check_image) | ||
| |> with_reuse(config.reuse) | ||
| |> valid_image!() | ||
| end | ||
|
|
||
| @impl true | ||
| def after_start(_config, _container, _conn), do: :ok | ||
| end | ||
|
|
||
| @doc false | ||
| def container_port_fun(nil), do: &Function.identity/1 | ||
|
|
||
| def container_port_fun({exposed_port, host_port}) do | ||
| fn container -> Container.with_fixed_port(container, exposed_port, host_port) end | ||
| end | ||
|
|
||
| def container_port_fun(port) do | ||
| fn container -> Container.with_exposed_port(container, port) end | ||
| end | ||
|
|
||
| @doc false | ||
| def container_volume_fun(nil), do: &Function.identity/1 | ||
|
|
||
| def container_volume_fun(volume) when is_binary(volume) do | ||
| fn container -> Container.with_bind_volume(container, volume, "/data/db") end | ||
| end | ||
|
|
||
|
PedroI920224 marked this conversation as resolved.
|
||
| defp encode_query_string(options) when options in [nil, [], %{}], do: "" | ||
|
|
||
| defp encode_query_string(options) when is_map(options) or is_list(options) do | ||
| case URI.encode_query(options) do | ||
| "" -> "" | ||
| query -> "?" <> query | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.