Skip to content
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

Default prefixing image substitutor #3413

Merged
merged 20 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,37 @@

/**
* Testcontainers' default implementation of {@link ImageNameSubstitutor}.
* Delegates to {@link ConfigurationFileImageNameSubstitutor}.
* Delegates to {@link ConfigurationFileImageNameSubstitutor} followed by {@link PrefixingImageNameSubstitutor}.
*/
@Slf4j
final class DefaultImageNameSubstitutor extends ImageNameSubstitutor {

private final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor;
private final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor;

public DefaultImageNameSubstitutor() {
configurationFileImageNameSubstitutor = new ConfigurationFileImageNameSubstitutor();
prefixingImageNameSubstitutor = new PrefixingImageNameSubstitutor();
}

@VisibleForTesting
DefaultImageNameSubstitutor(
final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor
final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor,
final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor
) {
this.configurationFileImageNameSubstitutor = configurationFileImageNameSubstitutor;
this.prefixingImageNameSubstitutor = prefixingImageNameSubstitutor;
}

@Override
public DockerImageName apply(final DockerImageName original) {
return configurationFileImageNameSubstitutor.apply(original);
return configurationFileImageNameSubstitutor
.andThen(prefixingImageNameSubstitutor)
.apply(original);
}

@Override
protected String getDescription() {
return "DefaultImageNameSubstitutor (" + configurationFileImageNameSubstitutor.getDescription() + ")";
return "DefaultImageNameSubstitutor (composite of '" + configurationFileImageNameSubstitutor.getDescription() + "' and '" + prefixingImageNameSubstitutor.getDescription() + "')";
}
}
23 changes: 12 additions & 11 deletions core/src/main/java/org/testcontainers/utility/DockerImageName.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.With;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -24,8 +25,8 @@ public final class DockerImageName {
private static final Pattern REPO_NAME = Pattern.compile(REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*");

private final String rawName;
private final String registry;
private final String repo;
@With @Getter private final String registry;
@With @Getter private final String repository;
@NotNull @With(AccessLevel.PRIVATE)
private final Versioning versioning;
@Nullable @With(AccessLevel.PRIVATE)
Expand Down Expand Up @@ -68,13 +69,13 @@ public DockerImageName(String fullImageName) {
}

if (remoteName.contains("@sha256:")) {
repo = remoteName.split("@sha256:")[0];
repository = remoteName.split("@sha256:")[0];
versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]);
} else if (remoteName.contains(":")) {
repo = remoteName.split(":")[0];
repository = remoteName.split(":")[0];
versioning = new TagVersioning(remoteName.split(":")[1]);
} else {
repo = remoteName;
repository = remoteName;
versioning = Versioning.ANY;
}

Expand Down Expand Up @@ -110,10 +111,10 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
}

if (version.startsWith("sha256:")) {
repo = remoteName;
repository = remoteName;
versioning = new Sha256Versioning(version.replace("sha256:", ""));
} else {
repo = remoteName;
repository = remoteName;
versioning = new TagVersioning(version);
}

Expand All @@ -125,9 +126,9 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
*/
public String getUnversionedPart() {
if (!"".equals(registry)) {
return registry + "/" + repo;
return registry + "/" + repository;
} else {
return repo;
return repository;
}
}

Expand Down Expand Up @@ -158,8 +159,8 @@ public String toString() {
public void assertValid() {
//noinspection UnstableApiUsage
HostAndPort.fromString(registry); // return value ignored - this throws if registry is not a valid host:port string
if (!REPO_NAME.matcher(repo).matches()) {
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
if (!REPO_NAME.matcher(repository).matches()) {
throw new IllegalArgumentException(repository + " is not a valid Docker image name (in " + rawName + ")");
}
if (!versioning.isValid()) {
throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.testcontainers.utility;

import com.google.common.annotations.VisibleForTesting;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.UnstableAPI;

/**
* An {@link ImageNameSubstitutor} which applies a prefix to all image names, e.g. a private registry host and path.
* The prefix may be set via an environment variable (<code>TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX</code>) or an equivalent
* configuration file entry (see {@link TestcontainersConfiguration}).
*/
@UnstableAPI
bsideup marked this conversation as resolved.
Show resolved Hide resolved
@NoArgsConstructor
@Slf4j
final class PrefixingImageNameSubstitutor extends ImageNameSubstitutor {

@VisibleForTesting
static final String PREFIX_PROPERTY_KEY = "hub.image.name.prefix";

private TestcontainersConfiguration configuration = TestcontainersConfiguration.getInstance();

@VisibleForTesting
PrefixingImageNameSubstitutor(final TestcontainersConfiguration configuration) {
this.configuration = configuration;
}

@Override
public DockerImageName apply(DockerImageName original) {
final String configuredPrefix = configuration.getEnvVarOrProperty(PREFIX_PROPERTY_KEY, "");

if (!!configuredPrefix.isEmpty()) {
rnorth marked this conversation as resolved.
Show resolved Hide resolved
log.debug("No prefix is configured");
return original;
}

boolean isAHubImage = original.getRegistry().isEmpty() ||
bsideup marked this conversation as resolved.
Show resolved Hide resolved
original.getRegistry().equals("docker.io") ||
original.getRegistry().equals("registry.hub.docker.com");
if (!isAHubImage) {
log.debug("Image {} is not a Docker Hub image - not applying registry/repository change", original);
return original;
}

log.debug(
"Applying changes to image name {}: applying prefix '{}'",
original,
configuredPrefix
);

DockerImageName prefixAsImage = DockerImageName.parse(configuredPrefix);
bsideup marked this conversation as resolved.
Show resolved Hide resolved

return original
.withRegistry(prefixAsImage.getRegistry())
.withRepository(prefixAsImage.getRepository() + original.getRepository());
}

@Override
protected String getDescription() {
return getClass().getSimpleName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ private String getConfigurable(@NotNull final String propertyName, @Nullable fin
* @param propertyName name of configuration file property (dot-separated lower case)
* @return the found value, or null if not set
*/
@Nullable
@Contract("_, !null -> !null")
public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
return getConfigurable(propertyName, defaultValue, userProperties, classpathProperties);
Expand All @@ -225,7 +224,6 @@ public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable
* @param propertyName name of configuration file property (dot-separated lower case)
* @return the found value, or null if not set
*/
@Nullable
@Contract("_, !null -> !null")
public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
return getConfigurable(propertyName, defaultValue, userProperties);
Expand All @@ -238,7 +236,6 @@ public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nulla
* @param propertyName name of configuration file property (dot-separated lower case)
* @return the found value, or null if not set
*/
@Nullable
@Contract("_, !null -> !null")
public String getUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
return getConfigurable(propertyName, defaultValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public int hashCode() {
@EqualsAndHashCode
class TagVersioning implements Versioning {
public static final String TAG_REGEX = "[\\w][\\w.\\-]{0,127}";
static final TagVersioning LATEST = new TagVersioning("latest");
private final String tag;

TagVersioning(String tag) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.testcontainers.utility.DefaultImageNameSubstitutor
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.testcontainers.utility;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.hamcrest.core.StringContains.containsString;
import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse;
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;


public class DockerImageNameCompatibilityTest {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.testcontainers.utility;

import org.junit.Before;
import org.junit.Test;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
import static org.testcontainers.utility.PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY;

public class PrefixingImageNameSubstitutorTest {

private TestcontainersConfiguration mockConfiguration;
private PrefixingImageNameSubstitutor underTest;

@Before
public void setUp() {
mockConfiguration = mock(TestcontainersConfiguration.class);
underTest = new PrefixingImageNameSubstitutor(mockConfiguration);
}

@Test
public void testHappyPath() {
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");

final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));
bsideup marked this conversation as resolved.
Show resolved Hide resolved

assertEquals(
"The prefix is applied",
"someregistry.com/our-mirror/some/image:tag",
result.asCanonicalNameString()
);
}

@Test
public void hubIoRegistryIsChanged() {
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");

final DockerImageName result = underTest.apply(DockerImageName.parse("docker.io/some/image:tag"));

assertEquals(
"The prefix is applied",
"someregistry.com/our-mirror/some/image:tag",
result.asCanonicalNameString()
);
}

@Test
public void hubComRegistryIsChanged() {
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");

final DockerImageName result = underTest.apply(DockerImageName.parse("registry.hub.docker.com/some/image:tag"));

assertEquals(
"The prefix is applied",
"someregistry.com/our-mirror/some/image:tag",
result.asCanonicalNameString()
);
}

@Test
public void thirdPartyRegistriesNotAffected() {
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");

final DockerImageName result = underTest.apply(DockerImageName.parse("gcr.io/something/image:tag"));

assertEquals(
"The prefix is not applied if a third party registry is used",
"gcr.io/something/image:tag",
result.asCanonicalNameString()
);
}

@Test
public void testNoDoublePrefixing() {
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");

final DockerImageName result = underTest.apply(DockerImageName.parse("someregistry.com/some/image:tag"));

assertEquals(
"The prefix is not applied if already present",
"someregistry.com/some/image:tag",
result.asCanonicalNameString()
);
}

@Test
public void testHandlesEmptyValue() {
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("");

final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));

assertEquals(
"The prefix is not applied if the env var is not set",
"some/image:tag",
result.asCanonicalNameString()
);
}
}
2 changes: 2 additions & 0 deletions docs/features/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Some companies disallow the usage of Docker Hub, but you can override `*.image`
> **pulsar.container.image = apachepulsar/pulsar:2.2.0**
> Used by Apache Pulsar

See [Image Name Substitution](./image_name_substitution.md) for other strategies for substituting image names to pull from other registries.
rnorth marked this conversation as resolved.
Show resolved Hide resolved

## Customizing Ryuk resource reaper

> **ryuk.container.image = testcontainers/ryuk:0.3.0**
Expand Down
29 changes: 28 additions & 1 deletion docs/features/image_name_substitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,34 @@ to:



## Automatically modifying Docker Hub image names

Testcontainers can be configured to modify Docker Hub image names on the fly to apply a prefix string.

Consider this if:

* Developers and CI machines need to use different image names. For example, developers are able to pull images from Docker Hub, but CI machines need to pull from a private registry
* Your private registry has copies of images from Docker Hub where the names are predictable, and just adding a prefix is enough.
For example, `registry.mycompany.com/mirror/mysql:8.0.22` can be derived from the original Docker Hub image name (`mysql:8.0.22`) with a consistent prefix string: `registry.mycompany.com/mirror/`

In this case, image name references in code are **unchanged**.
i.e. you would leave as-is:

<!--codeinclude-->
[Unchanged direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference
<!--/codeinclude-->

You can then configure Testcontainers to apply the prefix `registry.mycompany.com/mirror/` to every image that it tries to pull from Docker Hub.
This can be done in one of two ways:

* Setting environment variables `TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=registry.mycompany.com/mirror/`
* Via config file, setting `hub.image.name.prefix` in either:
* the `~/.testcontainers.properties` file in your user home directory, or
* a file named `testcontainers.properties` on the classpath

Testcontainers will automatically apply the prefix to every image that it pulls from Docker Hub - please verify that all [the required images](../supported_docker_environment/image_registry_rate_limiting.md#which-images-are-used-by-testcontainers) exist in your registry.

Testcontainers will not apply the prefix to non-Hub image names.



Expand All @@ -54,7 +81,7 @@ to:
Consider this if:

* You have complex rules about which private registry images should be used as substitutes, e.g.:
* non-deterministic mapping of names meaning that a [name prefix](#adding-a-registry-url-prefix-to-image-names-automatically) cannot be used
* non-deterministic mapping of names meaning that a [name prefix](#automatically-modifying-docker-hub-image-names) cannot be used
* rules depending upon developer identity or location
* or you wish to add audit logging of images used in the build
* or you wish to prevent accidental usage of images that are not on an approved list
Expand Down