diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index 2a4e260509c..da42a6f8036 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -204,6 +204,22 @@ default SELF withEnv(String key, Function, String> mapper) { */ SELF withEnv(Map env); + /** + * Add a label to the container. + * + * @param key label key + * @param value label value + * @return this + */ + SELF withLabel(String key, String value); + + /** + * Add labels to the container. + * @param labels map of labels + * @return this + */ + SELF withLabels(Map labels); + /** * Set the command that should be run in the container * diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 527e391718d..9ec38a2f2bf 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -110,6 +110,9 @@ public class GenericContainer> @NonNull private Map env = new HashMap<>(); + @NonNull + private Map labels = new HashMap<>(); + @NonNull private String[] commandParts = new String[0]; @@ -470,10 +473,14 @@ private void applyConfiguration(CreateContainerCmd createCommand) { createContainerCmdModifiers.forEach(hook -> hook.accept(createCommand)); - Map labels = createCommand.getLabels(); - labels = new HashMap<>(labels != null ? labels : Collections.emptyMap()); - labels.putAll(DockerClientFactory.DEFAULT_LABELS); - createCommand.withLabels(labels); + Map combinedLabels = new HashMap<>(); + combinedLabels.putAll(labels); + if (createCommand.getLabels() != null) { + combinedLabels.putAll(createCommand.getLabels()); + } + combinedLabels.putAll(DockerClientFactory.DEFAULT_LABELS); + + createCommand.withLabels(combinedLabels); } private Set findLinksFromThisContainer(String alias, LinkableContainer linkableContainer) { @@ -700,6 +707,27 @@ public SELF withEnv(Map env) { return self(); } + /** + * {@inheritDoc} + */ + @Override + public SELF withLabel(String key, String value) { + if (key.startsWith("org.testcontainers")) { + throw new IllegalArgumentException("The org.testcontainers namespace is reserved for interal use"); + } + labels.put(key, value); + return self(); + } + + /** + * {@inheritDoc} + */ + @Override + public SELF withLabels(Map labels) { + labels.forEach(this::withLabel); + return self(); + } + /** * {@inheritDoc} */ diff --git a/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java b/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java index 677f97d67d9..7325cb809fb 100644 --- a/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java +++ b/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java @@ -22,6 +22,7 @@ import java.net.Socket; import java.time.Duration; import java.util.Arrays; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; @@ -220,6 +221,32 @@ public void environmentFromMapTest() throws IOException { assertEquals("Environment variables can be passed into a command from a map", "42 and 50", line); } + @Test + public void customLabelTest() { + try (final GenericContainer alpineCustomLabel = new GenericContainer("alpine:3.2") + .withLabel("our.custom", "label") + .withCommand("top")) { + + alpineCustomLabel.start(); + + Map labels = alpineCustomLabel.getCurrentContainerInfo().getConfig().getLabels(); + assertTrue("org.testcontainers label is present", labels.containsKey("org.testcontainers")); + assertTrue("our.custom label is present", labels.containsKey("our.custom")); + assertEquals("our.custom label value is label", labels.get("our.custom"), "label"); + } + } + + @Test + public void exceptionThrownWhenTryingToOverrideTestcontainersLabels() { + assertThrows("When trying to overwrite an 'org.testcontainers' label, withLabel() throws an exception", + IllegalArgumentException.class, + () -> { + new GenericContainer("alpine:3.2") + .withLabel("org.testcontainers.foo", "false"); + } + ); + } + @Test public void customClasspathResourceMappingTest() throws IOException { // Note: This functionality doesn't work if you are running your build inside a Docker container; @@ -341,7 +368,7 @@ public void copyToContainerTest() throws Exception { try (final GenericContainer alpineCopyToContainer = new GenericContainer("alpine:3.2") .withCommand("top")){ - + alpineCopyToContainer.start(); final MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); alpineCopyToContainer.copyFileToContainer(mountableFile, "/home/"); diff --git a/docs/usage/generic_containers.md b/docs/usage/generic_containers.md index f5748284586..f848abb1414 100644 --- a/docs/usage/generic_containers.md +++ b/docs/usage/generic_containers.md @@ -46,4 +46,4 @@ The class rule provides methods for discovering how your tests can interact with For example, with the Redis example above, the following will allow your tests to access the Redis service: ```java String redisUrl = redis.getContainerIpAddress() + ":" + redis.getMappedPort(6379); -``` \ No newline at end of file +``` diff --git a/docs/usage/options.md b/docs/usage/options.md index dbd45804c7c..deaf429816d 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -29,6 +29,14 @@ new GenericContainer(...) .withEnv("API_TOKEN", "foo") ``` +### Labels + +To add a custom label to the container, use `withLabel`: +```java +new GenericContainer(...) + .withLabel("your.custom", "label") +``` + ### Command By default the container will execute whatever command is specified in the image's Dockerfile. To override this, and specify a different command, use `withCommand`: