Skip to content

Commit dd5dd51

Browse files
committed
WIP blueprint for application containers
1 parent 652191c commit dd5dd51

File tree

15 files changed

+715
-0
lines changed

15 files changed

+715
-0
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ body:
1414
description: Which Testcontainers module are you using?
1515
options:
1616
- Core
17+
- Application-Platform
1718
- Azure
1819
- Cassandra
1920
- Clickhouse
@@ -29,6 +30,7 @@ body:
2930
- InfluxDB
3031
- K3S
3132
- Kafka
33+
- Liberty
3234
- LocalStack
3335
- MariaDB
3436
- MockServer

.github/ISSUE_TEMPLATE/enhancement.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ body:
1414
description: For which Testcontainers module does the enhancement proposal apply?
1515
options:
1616
- Core
17+
- Application-Platform
1718
- Azure
1819
- Cassandra
1920
- Clickhouse
@@ -29,6 +30,7 @@ body:
2930
- InfluxDB
3031
- K3S
3132
- Kafka
33+
- Liberty
3234
- LocalStack
3335
- MariaDB
3436
- MockServer

.github/ISSUE_TEMPLATE/feature.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ body:
1414
description: Is this feature related to any of the existing modules?
1515
options:
1616
- Core
17+
- Application-Platform
1718
- Azure
1819
- Cassandra
1920
- Clickhouse
@@ -29,6 +30,7 @@ body:
2930
- InfluxDB
3031
- K3S
3132
- Kafka
33+
- Liberty
3234
- LocalStack
3335
- MariaDB
3436
- MockServer

.github/dependabot.yml

+10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ updates:
2929
open-pull-requests-limit: 10
3030

3131
# Explicit entry for each module
32+
- package-ecosystem: "gradle"
33+
directory: "/modules/application-platform"
34+
schedule:
35+
interval: "monthly"
36+
open-pull-requests-limit: 10
3237
- package-ecosystem: "gradle"
3338
directory: "/modules/azure"
3439
schedule:
@@ -139,6 +144,11 @@ updates:
139144
schedule:
140145
interval: "monthly"
141146
open-pull-requests-limit: 10
147+
- package-ecosystem: "gradle"
148+
directory: "/modules/liberty"
149+
schedule:
150+
interval: "monthly"
151+
open-pull-requests-limit: 10
142152
- package-ecosystem: "gradle"
143153
directory: "/modules/localstack"
144154
schedule:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
description = "Testcontainers :: Application Platform"
2+
3+
dependencies {
4+
api project(':testcontainers')
5+
6+
compileOnly 'org.jetbrains:annotations:24.0.1'
7+
implementation 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6'
8+
testImplementation 'org.assertj:assertj-core:3.24.2'
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package org.testcontainers.containers;
2+
3+
import lombok.NonNull;
4+
import org.apache.commons.lang3.SystemUtils;
5+
import org.jboss.shrinkwrap.api.Archive;
6+
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
7+
import org.testcontainers.containers.wait.strategy.Wait;
8+
import org.testcontainers.utility.Base58;
9+
import org.testcontainers.utility.DockerImageName;
10+
import org.testcontainers.utility.MountableFile;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.nio.file.Paths;
17+
import java.time.Duration;
18+
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.concurrent.Future;
23+
import java.util.stream.Collectors;
24+
import java.util.stream.Stream;
25+
26+
/**
27+
* Represents a JavaEE, JakartaEE, or Microprofile application platform running inside
28+
* a Docker container
29+
*/
30+
public abstract class ApplicationContainer extends GenericContainer<ApplicationContainer> {
31+
32+
// List of applications to install
33+
List<MountableFile> archives = new ArrayList<>();
34+
35+
// Where to save runtime application archives
36+
private static final String TESTCONTAINERS_TMP_DIR_PREFIX = ".testcontainers-archive-";
37+
38+
private static final String OS_MAC_TMP_DIR = "/tmp";
39+
40+
private static Path tempDirectory;
41+
42+
// How to query application
43+
private Integer httpPort;
44+
45+
private String appContextRoot;
46+
47+
// How to query application for readiness
48+
private Integer readinessPort;
49+
50+
private String readinessPath;
51+
52+
private Duration readinessTimeout;
53+
54+
// Expected path for Microprofile platforms to query for readiness
55+
static final String MP_HEALTH_READINESS_PATH = "/health/ready";
56+
57+
//Constructors
58+
59+
public ApplicationContainer(@NonNull final Future<String> image) {
60+
super(image);
61+
}
62+
63+
public ApplicationContainer(final DockerImageName dockerImageName) {
64+
super(dockerImageName);
65+
}
66+
67+
//Overrides
68+
69+
@Override
70+
protected void configure() {
71+
super.configure();
72+
73+
// Setup default wait strategy
74+
waitingFor(
75+
Wait.forHttp(readinessPath != null ? readinessPath : appContextRoot)
76+
.forPort(readinessPort != null ? readinessPort : httpPort)
77+
.withStartupTimeout(readinessTimeout != null ? readinessTimeout : getDefaultWaitTimeout())
78+
);
79+
80+
// Copy applications
81+
for(MountableFile archive : archives) {
82+
//FIXME folder-like containerPath in copyFileToContainer is deprecated
83+
withCopyFileToContainer(archive, getApplicationInstallDirectory());
84+
}
85+
}
86+
87+
@Override
88+
public void setExposedPorts(List<Integer> exposedPorts) {
89+
if( Objects.isNull(this.httpPort) ) {
90+
super.setExposedPorts(exposedPorts);
91+
return;
92+
}
93+
94+
super.setExposedPorts(appendPort(exposedPorts, this.httpPort));
95+
}
96+
97+
//Configuration
98+
99+
/**
100+
* One or more archives to be deployed to the application platform
101+
*
102+
* @param archives - An archive created using shrinkwrap and test runtime
103+
* @return self
104+
*/
105+
public ApplicationContainer withApplicationArchvies(@NonNull Archive<?>... archives) {
106+
Stream.of(archives).forEach(archive -> {
107+
String name = archive.getName();
108+
Path target = Paths.get(getTempDirectory().toString(), name);
109+
archive.as(ZipExporter.class).exportTo(target.toFile(), true);
110+
this.archives.add(MountableFile.forHostPath(target));
111+
});
112+
113+
return this;
114+
}
115+
116+
/**
117+
* One or more archives to be deployed to the application platform
118+
*
119+
* @param archives - A MountableFile which represents an archive that was created prior to test runtime.
120+
* @return self
121+
*/
122+
public ApplicationContainer withApplicationArchives(@NonNull MountableFile... archives) {
123+
this.archives.addAll(Arrays.asList(archives));
124+
return this;
125+
}
126+
127+
/**
128+
* This will set the port used to construct the base application URL, as well as
129+
* the port used to determine container readiness in the case of Microprofile platforms.
130+
*
131+
* @param httpPort - The HTTP port used by the Application platform
132+
* @return self
133+
*/
134+
public ApplicationContainer withHttpPort(int httpPort) {
135+
if( Objects.nonNull(this.httpPort) ) {
136+
throw new IllegalStateException("Only one application HTTP port can be configured.");
137+
}
138+
139+
this.httpPort = httpPort;
140+
super.setExposedPorts(appendPort(getExposedPorts(), this.httpPort));
141+
142+
return this;
143+
}
144+
145+
/**
146+
* This will set the path used to construct the base application URL
147+
*
148+
* @param appContextRoot - The application path
149+
* @return self
150+
*/
151+
public ApplicationContainer withAppContextRoot(@NonNull String appContextRoot) {
152+
this.appContextRoot = normalizePath(appContextRoot);
153+
return this;
154+
}
155+
156+
/**
157+
* Sets the path to be used to determine container readiness.
158+
* This will be used to construct a default WaitStrategy using Wait.forHttp()
159+
*
160+
* @param readinessPath - The path to be polled for readiness.
161+
* @return self
162+
*/
163+
public ApplicationContainer withReadinessPath(String readinessPath) {
164+
return withReadinessPath(readinessPath, getDefaultWaitTimeout());
165+
}
166+
167+
/**
168+
* Sets the path to be used to determine container readiness.
169+
* The readiness check will timeout a user defined timeout.
170+
* This will be used to construct a default WaitStrategy using Wait.forHttp()
171+
*
172+
* @param readinessPath - The path to be polled for readiness.
173+
* @param timeout - The timeout after which the readiness check will fail.
174+
* @return self
175+
*/
176+
public ApplicationContainer withReadinessPath(String readinessPath, Duration timeout) {
177+
return withReadinessPath(readinessPath, httpPort, timeout);
178+
}
179+
180+
/**
181+
* Sets the path to be used to determine container readiness.
182+
* The readiness check will timeout a user defined timeout.
183+
* The readiness check will happen on an alternative port to the httpPort
184+
*
185+
* @param readinessPath - The path to be polled for readiness.
186+
* @param readinessPort - The port that should be used for the readiness check.
187+
* @param timeout - The timeout after which the readiness check will fail.
188+
* @return self
189+
*/
190+
public ApplicationContainer withReadinessPath(@NonNull String readinessPath, @NonNull Integer readinessPort, @NonNull Duration timeout) {
191+
this.readinessPath = normalizePath(readinessPath);
192+
this.readinessPort = readinessPort;
193+
return this;
194+
}
195+
196+
//Getters
197+
198+
/**
199+
* The URL used to determine container readiness.
200+
* If a readiness port and path were configured, those will be used to construct this URL.
201+
* Otherwise, the readiness path will default to the httpPort and application context root.
202+
*
203+
* @return - The readiness URL
204+
*/
205+
public String getReadinessURL() {
206+
if ( Objects.isNull(this.readinessPath) || Objects.isNull(this.readinessPort) ) {
207+
return getApplicationURL();
208+
}
209+
210+
return "http://" + getHost() + ':' + getMappedPort(this.readinessPort) + this.readinessPath;
211+
}
212+
213+
/**
214+
* The URL where the application is running.
215+
* The application URL is a concatenation of the baseURL {@link ApplicationContainer#getBaseURL()} with the appContextRoot.
216+
* This is the URL that the test client should use to connect to an application running on the application platform.
217+
*
218+
* @return - The application URL
219+
*/
220+
public String getApplicationURL() {
221+
Objects.requireNonNull(this.appContextRoot);
222+
return getBaseURL() + appContextRoot;
223+
}
224+
225+
/**
226+
* The base URL for the application platform.
227+
* This is the URL that the test client should use to connect to the application platform.
228+
*
229+
* @return - The base URL
230+
*/
231+
public String getBaseURL() {
232+
Objects.requireNonNull(this.httpPort);
233+
return "http://" + getHost() + ':' + getMappedPort(this.httpPort);
234+
}
235+
236+
// Abstract
237+
238+
/**
239+
* Each implementation will need to determine an appropriate amount of time
240+
* to wait for their application platform to start.
241+
*
242+
* @return - Duration to wait for startup
243+
*/
244+
abstract protected Duration getDefaultWaitTimeout();
245+
246+
/**
247+
* Each implementation will need to provide an install directory
248+
* where their platform expects applications to be copied into.
249+
*
250+
* @return - the application install directory
251+
*/
252+
abstract protected String getApplicationInstallDirectory();
253+
254+
// Helpers
255+
256+
/**
257+
* Create a temporary directory where runtime archives can be exported and copied to the application container.
258+
*
259+
* @return - The temporary directory path
260+
*/
261+
private static Path getTempDirectory() {
262+
if( Objects.nonNull(tempDirectory) ) {
263+
return tempDirectory;
264+
}
265+
266+
try {
267+
if (SystemUtils.IS_OS_MAC) {
268+
tempDirectory = Files.createTempDirectory(Paths.get(OS_MAC_TMP_DIR), TESTCONTAINERS_TMP_DIR_PREFIX);
269+
} else {
270+
tempDirectory = Files.createTempDirectory(TESTCONTAINERS_TMP_DIR_PREFIX);
271+
}
272+
} catch (IOException e) {
273+
tempDirectory = new File(TESTCONTAINERS_TMP_DIR_PREFIX + Base58.randomString(5)).toPath();
274+
}
275+
276+
return tempDirectory;
277+
}
278+
279+
/**
280+
* Appends a port to a list of ports at position 0 and ensures port list does
281+
* not contain duplicates to ensure this order is maintained.
282+
*
283+
* @param portList - List of existing ports
284+
* @param appendingPort - The port to append
285+
* @return - resulting list of ports
286+
*/
287+
protected static List<Integer> appendPort(List<Integer> portList, int appendingPort) {
288+
portList = new ArrayList<>(portList); //Ensure not immutable
289+
portList.add(0, appendingPort);
290+
return portList.stream().distinct().collect(Collectors.toList());
291+
}
292+
293+
/**
294+
* Normalizes the path provided by developer input.
295+
* - Ensures paths start with '/'
296+
* - Ensures paths are separated with exactly one '/'
297+
* - Ensures paths do not end with a '/'
298+
*
299+
* @param paths the list of paths to be normalized
300+
* @return The normalized path
301+
*/
302+
protected static String normalizePath(String... paths) {
303+
return Stream.of(paths)
304+
.flatMap(path -> Stream.of(path.split("/")))
305+
.filter(part -> !part.isEmpty())
306+
.collect(Collectors.joining("/", "/", ""));
307+
}
308+
309+
}

0 commit comments

Comments
 (0)