Skip to content

Commit

Permalink
Load ImageNameSubstitutor from Service Loaders mechanism (#8866)
Browse files Browse the repository at this point in the history
Add additional mechanism to load `ImageNameSubstitutor`.
  • Loading branch information
eddumelendez authored Jul 11, 2024
1 parent 90b098b commit 18a7c27
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.UnstableAPI;

import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.stream.StreamSupport;

/**
* An image name substitutor converts a Docker image name, as may be specified in code, to an alternative name.
Expand All @@ -25,26 +27,19 @@ public abstract class ImageNameSubstitutor implements Function<DockerImageName,
static ImageNameSubstitutor defaultImplementation = new DefaultImageNameSubstitutor();

public static synchronized ImageNameSubstitutor instance() {
return instance(Thread.currentThread().getContextClassLoader());
}

@VisibleForTesting
static synchronized ImageNameSubstitutor instance(ClassLoader classLoader) {
if (instance == null) {
final String configuredClassName = TestcontainersConfiguration.getInstance().getImageSubstitutorClassName();

if (configuredClassName != null) {
log.debug("Attempting to instantiate an ImageNameSubstitutor with class: {}", configuredClassName);
ImageNameSubstitutor configuredInstance;
try {
configuredInstance =
(ImageNameSubstitutor) Thread
.currentThread()
.getContextClassLoader()
.loadClass(configuredClassName)
.getConstructor()
.newInstance();
} catch (Exception e) {
throw new IllegalArgumentException(
"Configured Image Substitutor could not be loaded: " + configuredClassName,
e
);
}
ImageNameSubstitutor configuredInstance = getImageNameSubstitutor(classLoader);

if (configuredInstance != null) {
log.debug(
"Attempting to instantiate an ImageNameSubstitutor with class: {}",
configuredInstance.getClass().getCanonicalName()
);

log.info("Found configured ImageNameSubstitutor: {}", configuredInstance.getDescription());

Expand All @@ -63,6 +58,26 @@ public static synchronized ImageNameSubstitutor instance() {
return instance;
}

private static ImageNameSubstitutor getImageNameSubstitutor(ClassLoader classLoader) {
final String configuredClassName = TestcontainersConfiguration.getInstance().getImageSubstitutorClassName();

if (configuredClassName != null) {
try {
return (ImageNameSubstitutor) classLoader.loadClass(configuredClassName).getConstructor().newInstance();
} catch (Exception e) {
throw new IllegalArgumentException(
"Configured Image Substitutor could not be loaded: " + configuredClassName,
e
);
}
}

return StreamSupport
.stream(ServiceLoader.load(ImageNameSubstitutor.class, classLoader).spliterator(), false)
.findFirst()
.orElse(null);
}

public static ImageNameSubstitutor noop() {
return new NoopImageNameSubstitutor();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.testcontainers.containers.GenericContainer;

import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;

public class ImageNameSubstitutorTest {

@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();

@Rule
public MockTestcontainersConfigurationRule config = new MockTestcontainersConfigurationRule();

Expand Down Expand Up @@ -81,4 +93,47 @@ public void testImageNameSubstitutorToString() {
);
}
}

@Test
public void testImageNameSubstitutorFromServiceLoader() throws IOException {
Path tempDir = this.tempFolder.newFolder("image-name-substitutor-test").toPath();
Path metaInfDir = Paths.get(tempDir.toString(), "META-INF", "services");
Files.createDirectories(metaInfDir);

createClassFile(tempDir, "org/testcontainers/utility/ImageNameSubstitutor.class", ImageNameSubstitutor.class);
createClassFile(tempDir, "org/testcontainers/utility/FakeImageSubstitutor.class", FakeImageSubstitutor.class);

// Create service provider configuration file
createServiceProviderFile(
metaInfDir,
"org.testcontainers.utility.ImageNameSubstitutor",
"org.testcontainers.utility.FakeImageSubstitutor"
);

URL[] urls = { tempDir.toUri().toURL() };
URLClassLoader classLoader = new URLClassLoader(urls, ImageNameSubstitutorTest.class.getClassLoader());

final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(classLoader);

DockerImageName result = imageNameSubstitutor.apply(DockerImageName.parse("original"));
assertThat(result.asCanonicalNameString())
.as("the image has been substituted by default then configured implementations")
.isEqualTo("transformed-substituted-image:latest");
}

private void createClassFile(Path tempDir, String classFilePath, Class<?> clazz) throws IOException {
Path classFile = Paths.get(tempDir.toString(), classFilePath);
Files.createDirectories(classFile.getParent());
Files.copy(clazz.getResourceAsStream("/" + classFilePath), classFile);
}

private void createServiceProviderFile(Path metaInfDir, String serviceInterface, String... implementations)
throws IOException {
Path serviceFile = Paths.get(metaInfDir.toString(), serviceInterface);
try (FileWriter writer = new FileWriter(serviceFile.toFile())) {
for (String impl : implementations) {
writer.write(impl + "\n");
}
}
}
}
7 changes: 7 additions & 0 deletions docs/features/image_name_substitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ Note that it is also possible to provide this same configuration property:

Please see [the documentation on configuration mechanisms](./configuration.md) for more information.

Also, you can use the `ServiceLoader` mechanism to provide the fully qualified class name of the `ImageNameSubstitutor` implementation:

=== "`src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor`"
```text
com.mycompany.testcontainers.ExampleImageNameSubstitutor
```


## Overriding image names individually in configuration

Expand Down

0 comments on commit 18a7c27

Please sign in to comment.