Skip to content

Commit 181b56e

Browse files
Support build args in FROM statement (#6119)
Fixes #3238 --------- Co-authored-by: Eddú Meléndez Gonzales <eddu.melendez@gmail.com>
1 parent a321cfa commit 181b56e

File tree

5 files changed

+65
-23
lines changed

5 files changed

+65
-23
lines changed

core/src/main/java/org/testcontainers/images/ParsedDockerfile.java

+13-13
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ public class ParsedDockerfile {
2929
private final Path dockerFilePath;
3030

3131
@Getter
32-
private Set<String> dependencyImageNames = Collections.emptySet();
32+
private final Set<String> dependencyImageNames;
3333

3434
public ParsedDockerfile(Path dockerFilePath) {
3535
this.dockerFilePath = dockerFilePath;
36-
parse(read());
36+
this.dependencyImageNames = parse(read());
3737
}
3838

3939
@VisibleForTesting
4040
ParsedDockerfile(List<String> lines) {
4141
this.dockerFilePath = Paths.get("dummy.Dockerfile");
42-
parse(lines);
42+
this.dependencyImageNames = parse(lines);
4343
}
4444

4545
private List<String> read() {
@@ -56,17 +56,17 @@ private List<String> read() {
5656
}
5757
}
5858

59-
private void parse(List<String> lines) {
60-
dependencyImageNames =
61-
lines
62-
.stream()
63-
.map(FROM_LINE_PATTERN::matcher)
64-
.filter(Matcher::matches)
65-
.map(matcher -> matcher.group("image"))
66-
.collect(Collectors.toSet());
59+
private Set<String> parse(List<String> lines) {
60+
Set<String> imageNames = lines
61+
.stream()
62+
.map(FROM_LINE_PATTERN::matcher)
63+
.filter(Matcher::matches)
64+
.map(matcher -> matcher.group("image"))
65+
.collect(Collectors.toSet());
6766

68-
if (!dependencyImageNames.isEmpty()) {
69-
log.debug("Found dependency images in Dockerfile {}: {}", dockerFilePath, dependencyImageNames);
67+
if (!imageNames.isEmpty()) {
68+
log.debug("Found dependency images in Dockerfile {}: {}", dockerFilePath, imageNames);
7069
}
70+
return imageNames;
7171
}
7272
}

core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java

+23-8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.Optional;
3838
import java.util.Set;
3939
import java.util.function.Consumer;
40+
import java.util.regex.Matcher;
4041
import java.util.zip.GZIPOutputStream;
4142

4243
@Slf4j
@@ -96,6 +97,7 @@ public ImageFromDockerfile withFileFromTransferable(String path, Transferable tr
9697
protected final String resolve() {
9798
Logger logger = DockerLoggerFactory.getLogger(dockerImageName);
9899

100+
//noinspection resource
99101
DockerClient dockerClient = DockerClientFactory.instance().client();
100102

101103
try {
@@ -107,12 +109,12 @@ public void onNext(BuildResponseItem item) {
107109
if (item.isErrorIndicated()) {
108110
logger.error(item.getErrorDetail().getMessage());
109111
} else {
110-
logger.debug(StringUtils.chomp(item.getStream(), "\n"));
112+
logger.debug(StringUtils.removeEnd(item.getStream(), "\n"));
111113
}
112114
}
113115
};
114116

115-
// We have to use pipes to avoid high memory consumption since users might want to build really big images
117+
// We have to use pipes to avoid high memory consumption since users might want to build huge images
116118
@Cleanup
117119
PipedInputStream in = new PipedInputStream();
118120
@Cleanup
@@ -169,7 +171,7 @@ public void onNext(BuildResponseItem item) {
169171
}
170172

171173
protected void configure(BuildImageCmd buildImageCmd) {
172-
buildImageCmd.withTag(this.getDockerImageName());
174+
buildImageCmd.withTags(Collections.singleton(getDockerImageName()));
173175
this.dockerFilePath.ifPresent(buildImageCmd::withDockerfilePath);
174176
this.dockerfile.ifPresent(p -> {
175177
buildImageCmd.withDockerfile(p.toFile());
@@ -188,27 +190,40 @@ protected void configure(BuildImageCmd buildImageCmd) {
188190
}
189191

190192
private void prePullDependencyImages(Set<String> imagesToPull) {
191-
final DockerClient dockerClient = DockerClientFactory.instance().client();
192-
193193
imagesToPull.forEach(imageName -> {
194+
String resolvedImageName = applyBuildArgsToImageName(imageName);
194195
try {
195196
log.info(
196197
"Pre-emptively checking local images for '{}', referenced via a Dockerfile. If not available, it will be pulled.",
197-
imageName
198+
resolvedImageName
198199
);
199-
new RemoteDockerImage(DockerImageName.parse(imageName))
200+
new RemoteDockerImage(DockerImageName.parse(resolvedImageName))
200201
.withImageNameSubstitutor(ImageNameSubstitutor.noop())
201202
.get();
202203
} catch (Exception e) {
203204
log.warn(
204205
"Unable to pre-fetch an image ({}) depended upon by Dockerfile - image build will continue but may fail. Exception message was: {}",
205-
imageName,
206+
resolvedImageName,
206207
e.getMessage()
207208
);
208209
}
209210
});
210211
}
211212

213+
/**
214+
* See {@code filterForEnvironmentVars()} in {@link com.github.dockerjava.core.dockerfile.DockerfileStatement}.
215+
*/
216+
private String applyBuildArgsToImageName(String imageName) {
217+
for (Map.Entry<String, String> entry : buildArgs.entrySet()) {
218+
String value = Matcher.quoteReplacement(entry.getValue());
219+
// handle: $VARIABLE case
220+
imageName = imageName.replace("$" + entry.getKey(), value);
221+
// handle ${VARIABLE} case
222+
imageName = imageName.replace("${" + entry.getKey() + "}", value);
223+
}
224+
return imageName;
225+
}
226+
212227
public ImageFromDockerfile withBuildArg(final String key, final String value) {
213228
this.buildArgs.put(key, value);
214229
return this;

core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java

+19-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import java.nio.file.Path;
1010
import java.nio.file.Paths;
11+
import java.util.HashMap;
12+
import java.util.Map;
1113

1214
import static org.assertj.core.api.Assertions.assertThat;
1315

@@ -22,6 +24,13 @@ public class DockerfileBuildTest {
2224

2325
@Parameterized.Parameters
2426
public static Object[][] parameters() {
27+
Map<String, String> buildArgs = new HashMap<>(4);
28+
buildArgs.put("BUILD_IMAGE", "alpine:3.16");
29+
buildArgs.put("BASE_IMAGE", "alpine");
30+
buildArgs.put("BASE_IMAGE_TAG", "3.12");
31+
buildArgs.put("UNUSED", "ignored");
32+
33+
//noinspection deprecation
2534
return new Object[][] {
2635
// Dockerfile build without explicit per-file inclusion
2736
new Object[] {
@@ -38,14 +47,22 @@ public static Object[][] parameters() {
3847
"test4567",
3948
new ImageFromDockerfile().withFileFromPath(".", RESOURCE_PATH).withDockerfilePath("./Dockerfile-alt"),
4049
},
41-
// Dockerfile build using build args
50+
// Dockerfile build using withBuildArg()
4251
new Object[] {
4352
"test7890",
4453
new ImageFromDockerfile()
4554
.withFileFromPath(".", RESOURCE_PATH)
4655
.withDockerfilePath("./Dockerfile-buildarg")
4756
.withBuildArg("CUSTOM_ARG", "test7890"),
4857
},
58+
// Dockerfile build using withBuildArgs() with build args in FROM statement
59+
new Object[] {
60+
"test1234",
61+
new ImageFromDockerfile()
62+
.withFileFromPath(".", RESOURCE_PATH)
63+
.withDockerfile(RESOURCE_PATH.resolve("Dockerfile-from-buildarg"))
64+
.withBuildArgs(buildArgs),
65+
},
4966
// Dockerfile build using withDockerfile(File)
5067
new Object[] {
5168
"test4567",
@@ -64,7 +81,7 @@ public DockerfileBuildTest(String expectedFileContent, ImageFromDockerfile image
6481
@Test
6582
public void performTest() {
6683
try (
67-
final GenericContainer container = new GenericContainer(image)
84+
final GenericContainer<?> container = new GenericContainer<>(image)
6885
.withStartupCheckStrategy(new OneShotStartupCheckStrategy())
6986
.withCommand("cat", "/test.txt")
7087
) {

core/src/test/java/org/testcontainers/images/builder/ImageFromDockerfileTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public void shouldNotAddSessionLabelIfDeleteOnExitIsFalse() {
3232
)
3333
.withDockerfileFromBuilder(it -> it.from("scratch"));
3434
String imageId = image.resolve();
35+
3536
DockerClient dockerClient = DockerClientFactory.instance().client();
3637

3738
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ARG BUILD_IMAGE
2+
ARG BASE_IMAGE
3+
ARG BASE_IMAGE_TAG
4+
5+
FROM ${BUILD_IMAGE} AS build
6+
COPY localfile.txt /test-build.txt
7+
8+
FROM $BASE_IMAGE:${BASE_IMAGE_TAG} AS base
9+
COPY --from=build /test-build.txt /test.txt

0 commit comments

Comments
 (0)