diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index f5652938a47..b02b5274c89 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -48,6 +48,7 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; +import org.testcontainers.core.CreateContainerCmdModifier; import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.builder.Transferable; @@ -88,6 +89,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.ServiceLoader; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -203,7 +205,6 @@ public class GenericContainer> /** * Set during container startup - * */ @Setter(AccessLevel.NONE) @VisibleForTesting @@ -220,8 +221,6 @@ public class GenericContainer> private List> logConsumers = new ArrayList<>(); - private final Set> createContainerCmdModifiers = new LinkedHashSet<>(); - private static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); private static final RateLimiter DOCKER_CLIENT_RATE_LIMITER = RateLimiterBuilder @@ -238,6 +237,19 @@ public class GenericContainer> private boolean hostAccessible = false; + private final Set createContainerCmdModifiers = loadCreateContainerCmdCustomizers(); + + private Set loadCreateContainerCmdCustomizers() { + ServiceLoader containerCmdCustomizers = ServiceLoader.load( + CreateContainerCmdModifier.class + ); + Set loadedCustomizers = new LinkedHashSet<>(); + for (CreateContainerCmdModifier customizer : containerCmdCustomizers) { + loadedCustomizers.add(customizer); + } + return loadedCustomizers; + } + public GenericContainer(@NonNull final DockerImageName dockerImageName) { this.image = new RemoteDockerImage(dockerImageName); } @@ -890,7 +902,9 @@ private void applyConfiguration(CreateContainerCmd createCommand) { createCommand.withPrivileged(privilegedMode); } - createContainerCmdModifiers.forEach(hook -> hook.accept(createCommand)); + for (CreateContainerCmdModifier createContainerCmdModifier : this.createContainerCmdModifiers) { + createCommand = createContainerCmdModifier.modify(createCommand); + } Map combinedLabels = new HashMap<>(); combinedLabels.putAll(labels); @@ -1491,12 +1505,16 @@ public SELF withStartupAttempts(int attempts) { * @return this */ public SELF withCreateContainerCmdModifier(Consumer modifier) { - createContainerCmdModifiers.add(modifier); + this.createContainerCmdModifiers.add(cmd -> { + modifier.accept(cmd); + return cmd; + }); return self(); } /** * Size of /dev/shm + * * @param bytes The number of bytes to assign the shared memory. If null, it will apply the Docker default which is 64 MB. * @return this */ @@ -1507,6 +1525,7 @@ public SELF withSharedMemorySize(Long bytes) { /** * First class support for configuring tmpfs + * * @param mapping path and params of tmpfs/mount flag for container * @return this */ diff --git a/core/src/main/java/org/testcontainers/core/CreateContainerCmdModifier.java b/core/src/main/java/org/testcontainers/core/CreateContainerCmdModifier.java new file mode 100644 index 00000000000..fb45da4bc30 --- /dev/null +++ b/core/src/main/java/org/testcontainers/core/CreateContainerCmdModifier.java @@ -0,0 +1,13 @@ +package org.testcontainers.core; + +import com.github.dockerjava.api.command.CreateContainerCmd; + +/** + * Callback interface that can be used to customize a {@link CreateContainerCmd}. + */ +public interface CreateContainerCmdModifier { + /** + * Callback to modify a {@link CreateContainerCmd} instance. + */ + CreateContainerCmd modify(CreateContainerCmd createContainerCmd); +} diff --git a/core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java b/core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java new file mode 100644 index 00000000000..9657e57bd32 --- /dev/null +++ b/core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java @@ -0,0 +1,18 @@ +package org.testcontainers.custom; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import org.testcontainers.core.CreateContainerCmdModifier; + +import java.util.HashMap; +import java.util.Map; + +public class TestCreateContainerCmdModifier implements CreateContainerCmdModifier { + + @Override + public CreateContainerCmd modify(CreateContainerCmd createContainerCmd) { + Map labels = new HashMap<>(); + labels.put("project", "testcontainers-java"); + labels.put("scope", "global"); + return createContainerCmd.withLabels(labels); + } +} diff --git a/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java b/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java index a9a3e966543..79465f192c4 100644 --- a/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java +++ b/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java @@ -264,6 +264,7 @@ public void customLabelTest() { final GenericContainer alpineCustomLabel = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withLabel("our.custom", "label") .withCommand("top") + .withCreateContainerCmdModifier(cmd -> cmd.getLabels().put("scope", "local")) ) { alpineCustomLabel.start(); @@ -278,6 +279,10 @@ public void customLabelTest() { .containsKey("org.testcontainers.version"); assertThat(labels).as("our.custom label is present").containsKey("our.custom"); assertThat(labels).as("our.custom label value is label").containsEntry("our.custom", "label"); + assertThat(labels) + .as("project label value is testcontainers-java") + .containsEntry("project", "testcontainers-java"); + assertThat(labels).as("scope label value is local").containsEntry("scope", "local"); } } diff --git a/core/src/test/resources/META-INF/services/org.testcontainers.core.CreateContainerCmdModifier b/core/src/test/resources/META-INF/services/org.testcontainers.core.CreateContainerCmdModifier new file mode 100644 index 00000000000..c4eb86e2cbc --- /dev/null +++ b/core/src/test/resources/META-INF/services/org.testcontainers.core.CreateContainerCmdModifier @@ -0,0 +1 @@ +org.testcontainers.custom.TestCreateContainerCmdModifier diff --git a/docs/features/advanced_options.md b/docs/features/advanced_options.md index 61b9112a03c..cd9c57f99eb 100644 --- a/docs/features/advanced_options.md +++ b/docs/features/advanced_options.md @@ -33,6 +33,8 @@ It is possible to specify an Image Pull Policy to determine at runtime whether a ## Customizing the container +### Using docker-java + It is possible to use the [`docker-java`](https://github.com/docker-java/docker-java) API directly to customize containers before creation. This is useful if there is a need to use advanced Docker features that are not exposed by the Testcontainers API. Any customizations you make using `withCreateContainerCmdModifier` will be applied _on top_ of the container definition that Testcontainers creates, but before it is created. For example, this can be used to change the container hostname: @@ -53,6 +55,20 @@ For example, this can be used to change the container hostname: For what is possible, consult the [`docker-java CreateContainerCmd` source code](https://github.com/docker-java/docker-java/blob/3.2.1/docker-java-api/src/main/java/com/github/dockerjava/api/command/CreateContainerCmd.java). +### Using CreateContainerCmdModifier + +Testcontainers provides a `CreateContainerCmdModifier` to customize [`docker-java CreateContainerCmd`](https://github.com/docker-java/docker-java/blob/3.2.1/docker-java-api/src/main/java/com/github/dockerjava/api/command/CreateContainerCmd.java) +via Service Provider Interface (SPI) mechanism. + + +[CreateContainerCmd example implementation](../../core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java) + + +The previous implementation should be registered in `META-INF/services/org.testcontainers.core.CreateContainerCmdModifier` file. + +!!! warning + `CreateContainerCmdModifier` implementation will apply to all containers created by Testcontainers. + ## Parallel Container Startup Usually, containers are started sequentially when more than one container is used.