Skip to content

Commit 0e263f2

Browse files
rnorthbsideup
andauthored
Default prefixing image substitutor (#3413)
Co-authored-by: Sergei Egorov <[email protected]>
1 parent d8df52e commit 0e263f2

File tree

9 files changed

+257
-27
lines changed

9 files changed

+257
-27
lines changed

core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,37 @@
55

66
/**
77
* Testcontainers' default implementation of {@link ImageNameSubstitutor}.
8-
* Delegates to {@link ConfigurationFileImageNameSubstitutor}.
8+
* Delegates to {@link ConfigurationFileImageNameSubstitutor} followed by {@link PrefixingImageNameSubstitutor}.
99
*/
1010
@Slf4j
1111
final class DefaultImageNameSubstitutor extends ImageNameSubstitutor {
1212

1313
private final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor;
14+
private final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor;
1415

1516
public DefaultImageNameSubstitutor() {
1617
configurationFileImageNameSubstitutor = new ConfigurationFileImageNameSubstitutor();
18+
prefixingImageNameSubstitutor = new PrefixingImageNameSubstitutor();
1719
}
1820

1921
@VisibleForTesting
2022
DefaultImageNameSubstitutor(
21-
final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor
23+
final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor,
24+
final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor
2225
) {
2326
this.configurationFileImageNameSubstitutor = configurationFileImageNameSubstitutor;
27+
this.prefixingImageNameSubstitutor = prefixingImageNameSubstitutor;
2428
}
2529

2630
@Override
2731
public DockerImageName apply(final DockerImageName original) {
28-
return configurationFileImageNameSubstitutor.apply(original);
32+
return configurationFileImageNameSubstitutor
33+
.andThen(prefixingImageNameSubstitutor)
34+
.apply(original);
2935
}
3036

3137
@Override
3238
protected String getDescription() {
33-
return "DefaultImageNameSubstitutor (" + configurationFileImageNameSubstitutor.getDescription() + ")";
39+
return "DefaultImageNameSubstitutor (composite of '" + configurationFileImageNameSubstitutor.getDescription() + "' and '" + prefixingImageNameSubstitutor.getDescription() + "')";
3440
}
3541
}

core/src/main/java/org/testcontainers/utility/DockerImageName.java

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.AccessLevel;
66
import lombok.AllArgsConstructor;
77
import lombok.EqualsAndHashCode;
8+
import lombok.Getter;
89
import lombok.With;
910
import org.jetbrains.annotations.NotNull;
1011
import org.jetbrains.annotations.Nullable;
@@ -24,8 +25,8 @@ public final class DockerImageName {
2425
private static final Pattern REPO_NAME = Pattern.compile(REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*");
2526

2627
private final String rawName;
27-
private final String registry;
28-
private final String repo;
28+
@With @Getter private final String registry;
29+
@With @Getter private final String repository;
2930
@NotNull @With(AccessLevel.PRIVATE)
3031
private final Versioning versioning;
3132
@Nullable @With(AccessLevel.PRIVATE)
@@ -68,13 +69,13 @@ public DockerImageName(String fullImageName) {
6869
}
6970

7071
if (remoteName.contains("@sha256:")) {
71-
repo = remoteName.split("@sha256:")[0];
72+
repository = remoteName.split("@sha256:")[0];
7273
versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]);
7374
} else if (remoteName.contains(":")) {
74-
repo = remoteName.split(":")[0];
75+
repository = remoteName.split(":")[0];
7576
versioning = new TagVersioning(remoteName.split(":")[1]);
7677
} else {
77-
repo = remoteName;
78+
repository = remoteName;
7879
versioning = Versioning.ANY;
7980
}
8081

@@ -110,10 +111,10 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
110111
}
111112

112113
if (version.startsWith("sha256:")) {
113-
repo = remoteName;
114+
repository = remoteName;
114115
versioning = new Sha256Versioning(version.replace("sha256:", ""));
115116
} else {
116-
repo = remoteName;
117+
repository = remoteName;
117118
versioning = new TagVersioning(version);
118119
}
119120

@@ -125,9 +126,9 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
125126
*/
126127
public String getUnversionedPart() {
127128
if (!"".equals(registry)) {
128-
return registry + "/" + repo;
129+
return registry + "/" + repository;
129130
} else {
130-
return repo;
131+
return repository;
131132
}
132133
}
133134

@@ -158,18 +159,14 @@ public String toString() {
158159
public void assertValid() {
159160
//noinspection UnstableApiUsage
160161
HostAndPort.fromString(registry); // return value ignored - this throws if registry is not a valid host:port string
161-
if (!REPO_NAME.matcher(repo).matches()) {
162-
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
162+
if (!REPO_NAME.matcher(repository).matches()) {
163+
throw new IllegalArgumentException(repository + " is not a valid Docker image name (in " + rawName + ")");
163164
}
164165
if (!versioning.isValid()) {
165166
throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")");
166167
}
167168
}
168169

169-
public String getRegistry() {
170-
return registry;
171-
}
172-
173170
/**
174171
* @param newTag version tag for the copy to use
175172
* @return an immutable copy of this {@link DockerImageName} with the new version tag
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.testcontainers.utility;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
import lombok.NoArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
7+
/**
8+
* An {@link ImageNameSubstitutor} which applies a prefix to all image names, e.g. a private registry host and path.
9+
* The prefix may be set via an environment variable (<code>TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX</code>) or an equivalent
10+
* configuration file entry (see {@link TestcontainersConfiguration}).
11+
*/
12+
@NoArgsConstructor
13+
@Slf4j
14+
final class PrefixingImageNameSubstitutor extends ImageNameSubstitutor {
15+
16+
@VisibleForTesting
17+
static final String PREFIX_PROPERTY_KEY = "hub.image.name.prefix";
18+
19+
private TestcontainersConfiguration configuration = TestcontainersConfiguration.getInstance();
20+
21+
@VisibleForTesting
22+
PrefixingImageNameSubstitutor(final TestcontainersConfiguration configuration) {
23+
this.configuration = configuration;
24+
}
25+
26+
@Override
27+
public DockerImageName apply(DockerImageName original) {
28+
final String configuredPrefix = configuration.getEnvVarOrProperty(PREFIX_PROPERTY_KEY, "");
29+
30+
if (configuredPrefix.isEmpty()) {
31+
log.debug("No prefix is configured");
32+
return original;
33+
}
34+
35+
boolean isAHubImage = original.getRegistry().isEmpty();
36+
if (!isAHubImage) {
37+
log.debug("Image {} is not a Docker Hub image - not applying registry/repository change", original);
38+
return original;
39+
}
40+
41+
log.debug(
42+
"Applying changes to image name {}: applying prefix '{}'",
43+
original,
44+
configuredPrefix
45+
);
46+
47+
DockerImageName prefixAsImage = DockerImageName.parse(configuredPrefix);
48+
49+
return original
50+
.withRegistry(prefixAsImage.getRegistry())
51+
.withRepository(prefixAsImage.getRepository() + original.getRepository());
52+
}
53+
54+
@Override
55+
protected String getDescription() {
56+
return getClass().getSimpleName();
57+
}
58+
}

core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ private String getConfigurable(@NotNull final String propertyName, @Nullable fin
212212
* @param propertyName name of configuration file property (dot-separated lower case)
213213
* @return the found value, or null if not set
214214
*/
215-
@Nullable
216215
@Contract("_, !null -> !null")
217216
public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
218217
return getConfigurable(propertyName, defaultValue, userProperties, classpathProperties);
@@ -225,7 +224,6 @@ public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable
225224
* @param propertyName name of configuration file property (dot-separated lower case)
226225
* @return the found value, or null if not set
227226
*/
228-
@Nullable
229227
@Contract("_, !null -> !null")
230228
public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
231229
return getConfigurable(propertyName, defaultValue, userProperties);
@@ -238,7 +236,6 @@ public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nulla
238236
* @param propertyName name of configuration file property (dot-separated lower case)
239237
* @return the found value, or null if not set
240238
*/
241-
@Nullable
242239
@Contract("_, !null -> !null")
243240
public String getUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) {
244241
return getConfigurable(propertyName, defaultValue);

core/src/main/java/org/testcontainers/utility/Versioning.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public int hashCode() {
4646
@EqualsAndHashCode
4747
class TagVersioning implements Versioning {
4848
public static final String TAG_REGEX = "[\\w][\\w.\\-]{0,127}";
49+
static final TagVersioning LATEST = new TagVersioning("latest");
4950
private final String tag;
5051

5152
TagVersioning(String tag) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.testcontainers.utility.DefaultImageNameSubstitutor

core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package org.testcontainers.utility;
22

3-
import org.junit.Rule;
4-
import org.junit.Test;
5-
import org.junit.rules.ExpectedException;
6-
73
import static org.hamcrest.core.StringContains.containsString;
84
import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse;
95
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
106

7+
import org.junit.Rule;
8+
import org.junit.Test;
9+
import org.junit.rules.ExpectedException;
10+
1111

1212
public class DockerImageNameCompatibilityTest {
1313

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package org.testcontainers.utility;
2+
3+
import org.junit.Before;
4+
import org.junit.Test;
5+
6+
import static org.mockito.ArgumentMatchers.any;
7+
import static org.mockito.ArgumentMatchers.eq;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.when;
10+
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
11+
import static org.testcontainers.utility.PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY;
12+
13+
public class PrefixingImageNameSubstitutorTest {
14+
15+
private TestcontainersConfiguration mockConfiguration;
16+
private PrefixingImageNameSubstitutor underTest;
17+
18+
@Before
19+
public void setUp() {
20+
mockConfiguration = mock(TestcontainersConfiguration.class);
21+
underTest = new PrefixingImageNameSubstitutor(mockConfiguration);
22+
}
23+
24+
@Test
25+
public void testHappyPath() {
26+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");
27+
28+
final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));
29+
30+
assertEquals(
31+
"The prefix is applied",
32+
"someregistry.com/our-mirror/some/image:tag",
33+
result.asCanonicalNameString()
34+
);
35+
}
36+
37+
@Test
38+
public void hubIoRegistryIsNotChanged() {
39+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");
40+
41+
final DockerImageName result = underTest.apply(DockerImageName.parse("docker.io/some/image:tag"));
42+
43+
assertEquals(
44+
"The prefix is applied",
45+
"docker.io/some/image:tag",
46+
result.asCanonicalNameString()
47+
);
48+
}
49+
50+
@Test
51+
public void hubComRegistryIsNotChanged() {
52+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");
53+
54+
final DockerImageName result = underTest.apply(DockerImageName.parse("registry.hub.docker.com/some/image:tag"));
55+
56+
assertEquals(
57+
"The prefix is applied",
58+
"registry.hub.docker.com/some/image:tag",
59+
result.asCanonicalNameString()
60+
);
61+
}
62+
63+
@Test
64+
public void thirdPartyRegistriesNotAffected() {
65+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");
66+
67+
final DockerImageName result = underTest.apply(DockerImageName.parse("gcr.io/something/image:tag"));
68+
69+
assertEquals(
70+
"The prefix is not applied if a third party registry is used",
71+
"gcr.io/something/image:tag",
72+
result.asCanonicalNameString()
73+
);
74+
}
75+
76+
@Test
77+
public void testNoDoublePrefixing() {
78+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror/");
79+
80+
final DockerImageName result = underTest.apply(DockerImageName.parse("someregistry.com/some/image:tag"));
81+
82+
assertEquals(
83+
"The prefix is not applied if already present",
84+
"someregistry.com/some/image:tag",
85+
result.asCanonicalNameString()
86+
);
87+
}
88+
89+
@Test
90+
public void testHandlesEmptyValue() {
91+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("");
92+
93+
final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));
94+
95+
assertEquals(
96+
"The prefix is not applied if the env var is not set",
97+
"some/image:tag",
98+
result.asCanonicalNameString()
99+
);
100+
}
101+
102+
@Test
103+
public void testHandlesRegistryOnlyWithTrailingSlash() {
104+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/");
105+
106+
final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));
107+
108+
assertEquals(
109+
"The prefix is applied",
110+
"someregistry.com/some/image:tag",
111+
result.asCanonicalNameString()
112+
);
113+
}
114+
115+
@Test
116+
public void testCombinesLiterallyForRegistryOnlyWithoutTrailingSlash() {
117+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com");
118+
119+
final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));
120+
121+
assertEquals(
122+
"The prefix is applied",
123+
"someregistry.comsome/image:tag", // treating the prefix literally, for predictability
124+
result.asCanonicalNameString()
125+
);
126+
}
127+
128+
@Test
129+
public void testCombinesLiterallyForBothPartsWithoutTrailingSlash() {
130+
when(mockConfiguration.getEnvVarOrProperty(eq(PREFIX_PROPERTY_KEY), any())).thenReturn("someregistry.com/our-mirror");
131+
132+
final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag"));
133+
134+
assertEquals(
135+
"The prefix is applied",
136+
"someregistry.com/our-mirrorsome/image:tag", // treating the prefix literally, for predictability
137+
result.asCanonicalNameString()
138+
);
139+
}
140+
}

0 commit comments

Comments
 (0)