Skip to content

Commit 2aeecf1

Browse files
authored
Merge branch 'master' into check-transferable-names
2 parents cfb6086 + d604f3e commit 2aeecf1

File tree

9 files changed

+156
-68
lines changed

9 files changed

+156
-68
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
check:
3131
needs: find_gradle_jobs
3232
strategy:
33+
fail-fast: false
3334
matrix: ${{ fromJson(needs.find_gradle_jobs.outputs.matrix) }}
3435
runs-on: ubuntu-18.04
3536
steps:

azure-pipelines.yml

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,36 @@
11
jobs:
2-
- job: core
2+
3+
- job: azure_pipeline_tests
34
steps:
5+
6+
# Run a minimal set of tests in normal Linux pipeline builds
47
- task: Gradle@2
5-
displayName: Build & test
8+
condition: eq(variables['Agent.OS'], 'Linux')
9+
displayName: Build & test (Linux - minimal core)
610
env:
711
AWS_ACCESS_KEY_ID: $(aws.accessKeyId)
812
AWS_SECRET_ACCESS_KEY: $(aws.secretAccessKey)
913
inputs:
1014
gradleWrapperFile: 'gradlew'
1115
jdkVersionOption: '1.11'
12-
options: '--no-daemon --continue'
13-
tasks: 'testcontainers:check'
16+
options: "--no-daemon --continue"
17+
tasks: "testcontainers:test --tests GenericContainerRuleTest"
1418
publishJUnitResults: true
1519
testResultsFiles: '**/TEST-*.xml'
1620
- script: wget -q https://get.cimate.io/release/linux/cimate && chmod +x cimate && ./cimate "**/TEST-*.xml"
1721
condition: and(succeededOrFailed(),eq(variables['Agent.OS'], 'Linux'))
18-
- job: jdbc
19-
steps:
22+
23+
# Run all tests when running the Windows CI tests
2024
- task: Gradle@2
21-
displayName: Build & test
25+
condition: eq(variables['Agent.OS'], 'Windows_NT')
26+
displayName: Build & test (Windows - all modules)
2227
env:
2328
AWS_ACCESS_KEY_ID: $(aws.accessKeyId)
2429
AWS_SECRET_ACCESS_KEY: $(aws.secretAccessKey)
2530
inputs:
2631
gradleWrapperFile: 'gradlew'
2732
jdkVersionOption: '1.11'
2833
options: '--no-daemon --continue'
29-
tasks: 'jdbc-test:check'
30-
publishJUnitResults: true
31-
testResultsFiles: '**/TEST-*.xml'
32-
- script: wget -q https://get.cimate.io/release/linux/cimate && chmod +x cimate && ./cimate "**/TEST-*.xml"
33-
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Linux'))
34-
- job: modules
35-
steps:
36-
- task: Gradle@2
37-
displayName: Build & test
38-
env:
39-
AWS_ACCESS_KEY_ID: $(aws.accessKeyId)
40-
AWS_SECRET_ACCESS_KEY: $(aws.secretAccessKey)
41-
inputs:
42-
gradleWrapperFile: 'gradlew'
43-
jdkVersionOption: '1.11'
44-
options: '--continue -x testcontainers:check -x jdbc-test:check -PpostCheckCommand="docker image prune -af"'
4534
tasks: 'check'
4635
publishJUnitResults: true
4736
testResultsFiles: '**/TEST-*.xml'
48-
- script: wget -q https://get.cimate.io/release/linux/cimate && chmod +x cimate && ./cimate "**/TEST-*.xml"
49-
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Linux'))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
@Value
1212
@Builder
13-
class ImageData {
13+
public class ImageData {
1414

1515
@NonNull
1616
Instant createdAt;

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);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Useful place for running tests that need to be outside of the `org.testcontainers` package.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package alt.testcontainers.images;
2+
3+
import org.junit.Test;
4+
import org.testcontainers.containers.GenericContainer;
5+
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
6+
import org.testcontainers.images.AbstractImagePullPolicy;
7+
import org.testcontainers.images.ImageData;
8+
import org.testcontainers.utility.DockerImageName;
9+
10+
public class OutOfPackageImagePullPolicyTest {
11+
@Test
12+
public void shouldSupportCustomPoliciesOutOfTestContainersPackage() {
13+
try (
14+
GenericContainer<?> container = new GenericContainer<>()
15+
.withImagePullPolicy(new AbstractImagePullPolicy() {
16+
@Override
17+
protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) {
18+
return false;
19+
}
20+
})
21+
) {
22+
container.withStartupCheckStrategy(new OneShotStartupCheckStrategy());
23+
container.start();
24+
}
25+
}
26+
27+
}

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
}

modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class MariaDBContainer<SELF extends MariaDBContainer<SELF>> extends JdbcD
2323
private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF";
2424

2525
public MariaDBContainer() {
26-
super(IMAGE + ":" + DEFAULT_TAG);
26+
this(IMAGE + ":" + DEFAULT_TAG);
2727
}
2828

2929
public MariaDBContainer(String dockerImageName) {

modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class MySQLContainer<SELF extends MySQLContainer<SELF>> extends JdbcDatab
2626
private static final String MYSQL_ROOT_USER = "root";
2727

2828
public MySQLContainer() {
29-
super(IMAGE + ":" + DEFAULT_TAG);
29+
this(IMAGE + ":" + DEFAULT_TAG);
3030
}
3131

3232
public MySQLContainer(String dockerImageName) {

0 commit comments

Comments
 (0)