Skip to content

Commit

Permalink
Add interface to customize CreateContainerCmd (#7421)
Browse files Browse the repository at this point in the history
Add `CreateContainerCmdModifier` interface to customize
`CreateContainerCmd`. Customization is applied after Testcontainers
configuration is set.
  • Loading branch information
eddumelendez authored Aug 21, 2023
1 parent 6e81732 commit d9519f9
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -203,7 +205,6 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>

/**
* Set during container startup
*
*/
@Setter(AccessLevel.NONE)
@VisibleForTesting
Expand All @@ -220,8 +221,6 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>

private List<Consumer<OutputFrame>> logConsumers = new ArrayList<>();

private final Set<Consumer<CreateContainerCmd>> createContainerCmdModifiers = new LinkedHashSet<>();

private static final Set<String> AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>();

private static final RateLimiter DOCKER_CLIENT_RATE_LIMITER = RateLimiterBuilder
Expand All @@ -238,6 +237,19 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>

private boolean hostAccessible = false;

private final Set<CreateContainerCmdModifier> createContainerCmdModifiers = loadCreateContainerCmdCustomizers();

private Set<CreateContainerCmdModifier> loadCreateContainerCmdCustomizers() {
ServiceLoader<CreateContainerCmdModifier> containerCmdCustomizers = ServiceLoader.load(
CreateContainerCmdModifier.class
);
Set<CreateContainerCmdModifier> loadedCustomizers = new LinkedHashSet<>();
for (CreateContainerCmdModifier customizer : containerCmdCustomizers) {
loadedCustomizers.add(customizer);
}
return loadedCustomizers;
}

public GenericContainer(@NonNull final DockerImageName dockerImageName) {
this.image = new RemoteDockerImage(dockerImageName);
}
Expand Down Expand Up @@ -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<String, String> combinedLabels = new HashMap<>();
combinedLabels.putAll(labels);
Expand Down Expand Up @@ -1491,12 +1505,16 @@ public SELF withStartupAttempts(int attempts) {
* @return this
*/
public SELF withCreateContainerCmdModifier(Consumer<CreateContainerCmd> 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
*/
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<String, String> labels = new HashMap<>();
labels.put("project", "testcontainers-java");
labels.put("scope", "global");
return createContainerCmd.withLabels(labels);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.testcontainers.custom.TestCreateContainerCmdModifier
16 changes: 16 additions & 0 deletions docs/features/advanced_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

<!--codeinclude-->
[CreateContainerCmd example implementation](../../core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java)
<!--/codeinclude-->

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.
Expand Down

0 comments on commit d9519f9

Please sign in to comment.