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 19 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() + "')";
}
}
27 changes: 12 additions & 15 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,18 +159,14 @@ 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 + ")");
}
}

public String getRegistry() {
return registry;
}

/**
* @param newTag version tag for the copy to use
* @return an immutable copy of this {@link DockerImageName} with the new version tag
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.testcontainers.utility;

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

/**
* 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}).
*/
@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()) {
log.debug("No prefix is configured");
return original;
}

boolean isAHubImage = original.getRegistry().isEmpty();
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,140 @@
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 hubIoRegistryIsNotChanged() {
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",
"docker.io/some/image:tag",
result.asCanonicalNameString()
);
}

@Test
public void hubComRegistryIsNotChanged() {
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",
"registry.hub.docker.com/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()
);
}

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

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

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

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

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

assertEquals(
"The prefix is applied",
"someregistry.comsome/image:tag", // treating the prefix literally, for predictability
result.asCanonicalNameString()
);
}

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

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

assertEquals(
"The prefix is applied",
"someregistry.com/our-mirrorsome/image:tag", // treating the prefix literally, for predictability
result.asCanonicalNameString()
);
}
}
Loading