Skip to content

Commit 54307ab

Browse files
rnorthdbyron0
andcommitted
Support ImageFromDockerfile authenticated image pulls (#2573)
Co-Authored-By: David Byron <[email protected]>
1 parent 4cb555a commit 54307ab

File tree

2 files changed

+112
-40
lines changed

2 files changed

+112
-40
lines changed

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

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,6 @@ protected final String resolve() {
8383

8484
DockerClient dockerClient = DockerClientFactory.instance().client();
8585

86-
dependencyImageNames.forEach(imageName -> {
87-
try {
88-
log.info("Pre-emptively checking local images for '{}', referenced via a Dockerfile. If not available, it will be pulled.", imageName);
89-
DockerClientFactory.instance().checkAndPullImage(dockerClient, imageName);
90-
} catch (Exception e) {
91-
log.warn("Unable to pre-fetch an image ({}) depended upon by Dockerfile - image build will continue but may fail. Exception message was: {}", imageName, e.getMessage());
92-
}
93-
});
94-
95-
9686
try {
9787
if (deleteOnExit) {
9888
ResourceReaper.instance().registerImageForCleanup(dockerImageName);
@@ -118,6 +108,8 @@ public void onNext(BuildResponseItem item) {
118108
BuildImageCmd buildImageCmd = dockerClient.buildImageCmd(in);
119109
configure(buildImageCmd);
120110

111+
prePullDependencyImages(dependencyImageNames);
112+
121113
BuildImageResultCallback exec = buildImageCmd.exec(resultCallback);
122114

123115
long bytesToDockerDaemon = 0;
@@ -154,11 +146,30 @@ protected void configure(BuildImageCmd buildImageCmd) {
154146
this.dockerfile.ifPresent(p -> {
155147
buildImageCmd.withDockerfile(p.toFile());
156148
dependencyImageNames = new ParsedDockerfile(p).getDependencyImageNames();
149+
150+
if (dependencyImageNames.size() > 0) {
151+
// if we'll be pre-pulling images, disable the built-in pull as it is not necessary and will fail for
152+
// authenticated registries
153+
buildImageCmd.withPull(false);
154+
}
157155
});
158156

159157
this.buildArgs.forEach(buildImageCmd::withBuildArg);
160158
}
161159

160+
private void prePullDependencyImages(Set<String> imagesToPull) {
161+
final DockerClient dockerClient = DockerClientFactory.instance().client();
162+
163+
imagesToPull.forEach(imageName -> {
164+
try {
165+
log.info("Pre-emptively checking local images for '{}', referenced via a Dockerfile. If not available, it will be pulled.", imageName);
166+
DockerClientFactory.instance().checkAndPullImage(dockerClient, imageName);
167+
} catch (Exception e) {
168+
log.warn("Unable to pre-fetch an image ({}) depended upon by Dockerfile - image build will continue but may fail. Exception message was: {}", imageName, e.getMessage());
169+
}
170+
});
171+
}
172+
162173
public ImageFromDockerfile withBuildArg(final String key, final String value) {
163174
this.buildArgs.put(key, value);
164175
return this;
@@ -171,19 +182,23 @@ public ImageFromDockerfile withBuildArgs(final Map<String, String> args) {
171182

172183
/**
173184
* Sets the Dockerfile to be used for this image.
174-
* @deprecated It is recommended to use {@link #withDockerfile} instead because it honors
175-
* .dockerignore files and therefore will be more efficient
176-
* @param relativePathFromBuildRoot
185+
*
186+
* @param relativePathFromBuildContextDirectory relative path to the Dockerfile, relative to the image build context directory
187+
* @deprecated It is recommended to use {@link #withDockerfile} instead because it honors .dockerignore files and
188+
* will therefore be more efficient. Additionally, using {@link #withDockerfile} supports Dockerfiles that depend
189+
* upon images from authenticated private registries.
177190
*/
178191
@Deprecated
179-
public ImageFromDockerfile withDockerfilePath(String relativePathFromBuildRoot) {
180-
this.dockerFilePath = Optional.of(relativePathFromBuildRoot);
192+
public ImageFromDockerfile withDockerfilePath(String relativePathFromBuildContextDirectory) {
193+
this.dockerFilePath = Optional.of(relativePathFromBuildContextDirectory);
181194
return this;
182195
}
183196

184197
/**
185-
* Sets the Dockerfile to be used for this image.
186-
* @param dockerfile
198+
* Sets the Dockerfile to be used for this image. Honors .dockerignore files for efficiency.
199+
* Additionally, supports Dockerfiles that depend upon images from authenticated private registries.
200+
*
201+
* @param dockerfile path to Dockerfile on the test host.
187202
*/
188203
public ImageFromDockerfile withDockerfile(Path dockerfile) {
189204
this.dockerfile = Optional.of(dockerfile);

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

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

33
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.async.ResultCallback;
5+
import com.github.dockerjava.api.command.PullImageResultCallback;
46
import com.github.dockerjava.api.model.AuthConfig;
5-
import com.github.dockerjava.core.command.PullImageResultCallback;
6-
import com.github.dockerjava.core.command.PushImageResultCallback;
7+
import org.intellij.lang.annotations.Language;
78
import org.junit.AfterClass;
9+
import org.junit.Before;
810
import org.junit.BeforeClass;
911
import org.junit.ClassRule;
1012
import org.junit.Test;
1113
import org.mockito.Mockito;
1214
import org.testcontainers.DockerClientFactory;
15+
import org.testcontainers.containers.ContainerState;
16+
import org.testcontainers.containers.DockerComposeContainer;
1317
import org.testcontainers.containers.GenericContainer;
1418
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
1519
import org.testcontainers.images.builder.ImageFromDockerfile;
1620

21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
1725
import java.util.concurrent.TimeUnit;
1826

1927
import static org.mockito.ArgumentMatchers.any;
@@ -25,14 +33,17 @@
2533
* This test checks the integration between Testcontainers and an authenticated registry, but uses
2634
* a mock instance of {@link RegistryAuthLocator} - the purpose of the test is solely to ensure that
2735
* the auth locator is utilised, and that the credentials it provides flow through to the registry.
28-
*
36+
* <p>
2937
* {@link RegistryAuthLocatorTest} covers actual credential scenarios at a lower level, which are
3038
* impractical to test end-to-end.
3139
*/
3240
public class AuthenticatedImagePullTest {
3341

42+
/**
43+
* Containerised docker image registry, with simple hardcoded credentials
44+
*/
3445
@ClassRule
35-
public static GenericContainer authenticatedRegistry = new GenericContainer(new ImageFromDockerfile()
46+
public static GenericContainer<?> authenticatedRegistry = new GenericContainer<>(new ImageFromDockerfile()
3647
.withDockerfileFromBuilder(builder -> {
3748
builder.from("registry:2")
3849
.run("htpasswd -Bbn testuser notasecret > /htpasswd")
@@ -46,28 +57,17 @@ public class AuthenticatedImagePullTest {
4657
private static RegistryAuthLocator originalAuthLocatorSingleton;
4758
private static DockerClient client;
4859

49-
private static String testRegistryAddress;
5060
private static String testImageName;
5161
private static String testImageNameWithTag;
5262

5363
@BeforeClass
54-
public static void setUp() {
64+
public static void setUp() throws InterruptedException {
5565
originalAuthLocatorSingleton = RegistryAuthLocator.instance();
5666
client = DockerClientFactory.instance().client();
5767

58-
testRegistryAddress = authenticatedRegistry.getContainerIpAddress() + ":" + authenticatedRegistry.getFirstMappedPort();
68+
String testRegistryAddress = authenticatedRegistry.getContainerIpAddress() + ":" + authenticatedRegistry.getFirstMappedPort();
5969
testImageName = testRegistryAddress + "/alpine";
6070
testImageNameWithTag = testImageName + ":latest";
61-
}
62-
63-
@AfterClass
64-
public static void tearDown() {
65-
RegistryAuthLocator.setInstance(originalAuthLocatorSingleton);
66-
client.removeImageCmd(testImageNameWithTag).withForce(true).exec();
67-
}
68-
69-
@Test
70-
public void testThatAuthLocatorIsUsed() throws Exception {
7171

7272
final DockerImageName expectedName = new DockerImageName(testImageNameWithTag);
7373
final AuthConfig authConfig = new AuthConfig()
@@ -83,17 +83,77 @@ public void testThatAuthLocatorIsUsed() throws Exception {
8383

8484
// a push will use the auth locator for authentication, although that isn't the goal of this test
8585
putImageInRegistry();
86+
}
87+
88+
@Before
89+
public void removeImageFromLocalDocker() {
90+
// remove the image tag from local docker so that it must be pulled before use
91+
client.removeImageCmd(testImageNameWithTag).withForce(true).exec();
92+
}
8693

94+
@AfterClass
95+
public static void tearDown() {
96+
RegistryAuthLocator.setInstance(originalAuthLocatorSingleton);
97+
}
98+
99+
@Test
100+
public void testThatAuthLocatorIsUsedForContainerCreation() {
87101
// actually start a container, which will require an authenticated pull
88-
try (final GenericContainer container = new GenericContainer<>(testImageNameWithTag)
102+
try (final GenericContainer<?> container = new GenericContainer<>(testImageNameWithTag)
89103
.withCommand("/bin/sh", "-c", "sleep 10")) {
90104
container.start();
91105

92106
assertTrue("container started following an authenticated pull", container.isRunning());
93107
}
94108
}
95109

96-
private void putImageInRegistry() throws InterruptedException {
110+
@Test
111+
public void testThatAuthLocatorIsUsedForDockerfileBuild() throws IOException {
112+
// Prepare a simple temporary Dockerfile which requires our custom private image
113+
Path tempContext = Files.createTempDirectory(Paths.get("."), this.getClass().getSimpleName() + "-test-");
114+
Path tempFile = Files.createTempFile(tempContext, "test", ".Dockerfile");
115+
String dockerFileContent = "FROM " + testImageNameWithTag;
116+
Files.write(tempFile, dockerFileContent.getBytes());
117+
118+
// Start a container built from a derived image, which will require an authenticated pull
119+
try (final GenericContainer<?> container = new GenericContainer<>(
120+
new ImageFromDockerfile()
121+
.withDockerfile(tempFile)
122+
)
123+
.withCommand("/bin/sh", "-c", "sleep 10")) {
124+
container.start();
125+
126+
assertTrue("container started following an authenticated pull", container.isRunning());
127+
}
128+
}
129+
130+
@Test
131+
public void testThatAuthLocatorIsUsedForDockerComposePull() throws IOException {
132+
// Prepare a simple temporary Docker Compose manifest which requires our custom private image
133+
Path tempContext = Files.createTempDirectory(Paths.get("."), this.getClass().getSimpleName() + "-test-");
134+
Path tempFile = Files.createTempFile(tempContext, "test", ".docker-compose.yml");
135+
@Language("yaml") String composeFileContent =
136+
"version: '2.0'\n" +
137+
"services:\n" +
138+
" privateservice:\n" +
139+
" command: /bin/sh -c 'sleep 60'\n" +
140+
" image: " + testImageNameWithTag;
141+
Files.write(tempFile, composeFileContent.getBytes());
142+
143+
// Start the docker compose project, which will require an authenticated pull
144+
try (final DockerComposeContainer<?> compose = new DockerComposeContainer<>(tempFile.toFile())) {
145+
compose.start();
146+
147+
assertTrue("container started following an authenticated pull",
148+
compose
149+
.getContainerByServiceName("privateservice_1")
150+
.map(ContainerState::isRunning)
151+
.orElse(false)
152+
);
153+
}
154+
}
155+
156+
private static void putImageInRegistry() throws InterruptedException {
97157
// It doesn't matter which image we use for this test, but use one that's likely to have been pulled already
98158
final String dummySourceImage = TestcontainersConfiguration.getInstance().getRyukImage();
99159

@@ -109,10 +169,7 @@ private void putImageInRegistry() throws InterruptedException {
109169
client.tagImageCmd(id, testImageName, "latest").exec();
110170

111171
client.pushImageCmd(testImageNameWithTag)
112-
.exec(new PushImageResultCallback())
172+
.exec(new ResultCallback.Adapter<>())
113173
.awaitCompletion(1, TimeUnit.MINUTES);
114-
115-
// remove the image tag from local docker so that it must be pulled before use
116-
client.removeImageCmd(testImageNameWithTag).withForce(true).exec();
117174
}
118175
}

0 commit comments

Comments
 (0)