Skip to content

Commit b6332ec

Browse files
authored
Refactor testcontainer for easier integration tests (#3118)
1 parent a2e13a4 commit b6332ec

File tree

10 files changed

+298
-245
lines changed

10 files changed

+298
-245
lines changed

apm-agent-common/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,12 @@
3232
</plugins>
3333
</build>
3434

35+
<dependencies>
36+
<dependency>
37+
<groupId>org.testcontainers</groupId>
38+
<artifactId>testcontainers</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
</dependencies>
42+
3543
</project>
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.test;
20+
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.testcontainers.containers.GenericContainer;
24+
import org.testcontainers.containers.output.Slf4jLogConsumer;
25+
import org.testcontainers.utility.DockerImageName;
26+
import org.testcontainers.utility.MountableFile;
27+
28+
import javax.annotation.Nullable;
29+
import java.io.IOException;
30+
import java.lang.management.ManagementFactory;
31+
import java.nio.file.Files;
32+
import java.nio.file.Path;
33+
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.List;
36+
import java.util.concurrent.TimeUnit;
37+
38+
public abstract class AgentTestContainer<SELF extends GenericContainer<SELF>> extends GenericContainer<SELF> {
39+
40+
private static final Logger log = LoggerFactory.getLogger(AgentTestContainer.class);
41+
42+
/**
43+
* The port that the IDE will listen to, set to IDEA default value
44+
*/
45+
private static final int DEBUG_PORT = 5005;
46+
private static final String LOCAL_DEBUG_HOST = "localhost";
47+
private static final String REMOTE_DEBUG_HOST = "remote-localhost";
48+
49+
// agent path within container
50+
private static final String AGENT_JAR_PATH = "/agent.jar";
51+
// single-jar application path within container
52+
private static final String APP_JAR_PATH = "/app.jar";
53+
// security policy path within container
54+
public static final String SECURITY_POLICY_PATH = "/security.policy";
55+
56+
private boolean remoteDebug = false;
57+
private boolean agent = false;
58+
private boolean appJar = false;
59+
60+
private final List<String> systemProperties = new ArrayList<>();
61+
private final List<String> arguments = new ArrayList<>();
62+
63+
private String jvmEnvironmentVariable;
64+
65+
/**
66+
* Generic container subclass without any customization
67+
*/
68+
public static class Generic extends AgentTestContainer<Generic> {
69+
70+
public Generic(String dockerImageName) {
71+
super(dockerImageName);
72+
}
73+
}
74+
75+
protected AgentTestContainer(String dockerImageName) {
76+
super(DockerImageName.parse(dockerImageName));
77+
}
78+
79+
@Override
80+
public void start() {
81+
82+
ArrayList<String> args = new ArrayList<>();
83+
if (hasRemoteDebug()) {
84+
args.add(getRemoteDebugArgument());
85+
}
86+
if (hasJavaAgent()) {
87+
args.add(getJavaAgentArgument());
88+
}
89+
for (String keyValue : systemProperties) {
90+
args.add("-D" + keyValue);
91+
}
92+
93+
if (jvmEnvironmentVariable != null) {
94+
String value = String.join(" ", args);
95+
withEnv(jvmEnvironmentVariable, value);
96+
log.info("starting container with {} = {}", jvmEnvironmentVariable, value);
97+
}
98+
99+
if (appJar) {
100+
// java -jar invocation
101+
102+
args.add("-jar");
103+
args.add(APP_JAR_PATH);
104+
105+
args.addAll(arguments);
106+
107+
String command = "java " + String.join(" ", args);
108+
log.info("starting JVM with command line: {}", command);
109+
withCommand(command);
110+
}
111+
112+
try {
113+
super.start();
114+
} catch (RuntimeException e) {
115+
log.error("unable to start container, set breakpoint where this log is generated to debug", e);
116+
}
117+
118+
// send container logs to logger for easier debug by default
119+
followOutput(new Slf4jLogConsumer(log));
120+
}
121+
122+
public SELF withJavaAgent() {
123+
return withJavaAgent(AgentFileAccessor.Variant.STANDARD);
124+
}
125+
126+
public SELF withJavaAgent(AgentFileAccessor.Variant variant) {
127+
Path agentJar = AgentFileAccessor.getPathToJavaagent(variant);
128+
this.withCopyFileToContainer(MountableFile.forHostPath(agentJar), AGENT_JAR_PATH);
129+
agent = true;
130+
return self();
131+
}
132+
133+
/**
134+
* Sets the jar for 'java -jar app.jar' invocation
135+
*
136+
* @param appJar path to application jar
137+
* @return this
138+
*/
139+
public SELF withExecutableJar(Path appJar) {
140+
this.withCopyFileToContainer(MountableFile.forHostPath(appJar), APP_JAR_PATH);
141+
this.appJar = true;
142+
return self();
143+
}
144+
145+
/**
146+
* Sets the environment variable that will be used to set the '-javaagent', JVM System properties and remote debug
147+
*
148+
* @param name environment variable name
149+
* @return this
150+
*/
151+
public SELF withJvmArgumentsVariable(String name) {
152+
jvmEnvironmentVariable = name;
153+
return self();
154+
}
155+
156+
/**
157+
* Program arguments that are passed to {@code main(String[] args)} invocation, only relevant when used with {@link #withExecutableJar(Path)}
158+
*
159+
* @param arguments arguments
160+
* @return this
161+
*/
162+
public SELF withArguments(String... arguments) {
163+
this.arguments.addAll(Arrays.asList(arguments));
164+
return self();
165+
}
166+
167+
/**
168+
* Sets a system property
169+
*
170+
* @param key key
171+
* @param value value, {@literal null} indicates no value is provided.
172+
* @return this
173+
*/
174+
public SELF withSystemProperty(String key, @Nullable String value) {
175+
StringBuilder sb = new StringBuilder();
176+
sb.append(key);
177+
if (value != null) {
178+
sb.append("=");
179+
sb.append(value);
180+
}
181+
systemProperties.add(sb.toString());
182+
return self();
183+
}
184+
185+
public boolean hasJavaAgent() {
186+
return agent;
187+
}
188+
189+
public String getJavaAgentArgument() {
190+
return "-javaagent:" + AGENT_JAR_PATH;
191+
}
192+
193+
/**
194+
* Enables the JVM security manager with an optional policy
195+
*
196+
* @param policyFile path to policy file, set to {@literal null} to just enable the security manager
197+
* @return this
198+
*/
199+
public SELF withSecurityManager(@Nullable Path policyFile) {
200+
withSystemProperty("java.security.manager", null);
201+
if (policyFile != null) {
202+
withCopyFileToContainer(MountableFile.forHostPath(policyFile), SECURITY_POLICY_PATH);
203+
withSystemProperty("java.security.policy", SECURITY_POLICY_PATH);
204+
log.info("using security policy defined in {}", policyFile.toAbsolutePath());
205+
try {
206+
Files.readAllLines(policyFile).forEach(log::info);
207+
} catch (IOException e) {
208+
throw new IllegalStateException(e);
209+
}
210+
}
211+
return self();
212+
}
213+
214+
/**
215+
* Configures remote debugging automatically for the JVM running in the container.
216+
* On the IDE side, all is required is to add debugger listening for incoming connections on port 5005
217+
*/
218+
public SELF withRemoteDebug() {
219+
boolean isDebugging = false;
220+
221+
// test if the test code is currently being debugged
222+
List<String> jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
223+
for (String jvmArg : jvmArgs) {
224+
if (jvmArg.contains("-agentlib:jdwp=")) {
225+
isDebugging = true;
226+
}
227+
}
228+
if (!isDebugging) {
229+
// not debugging
230+
return self();
231+
}
232+
233+
if (!probeDebugger()) {
234+
log.error("Unable to detect debugger listening on port {}, remote debugging JVM within container will be disabled", DEBUG_PORT);
235+
return self();
236+
}
237+
238+
// make the docker host IP available for remote debug
239+
// the 'host-gateway' is automatically translated by docker for all OSes
240+
withExtraHost(REMOTE_DEBUG_HOST, "host-gateway");
241+
remoteDebug = true;
242+
return self();
243+
}
244+
245+
public boolean hasRemoteDebug() {
246+
return remoteDebug;
247+
}
248+
249+
public String getRemoteDebugArgument() {
250+
return remoteDebugArgument(REMOTE_DEBUG_HOST);
251+
}
252+
253+
private String remoteDebugArgument(String host) {
254+
return String.format("-agentlib:jdwp=transport=dt_socket,server=n,address=%s:%d,suspend=y", host, DEBUG_PORT);
255+
}
256+
257+
private boolean probeDebugger() {
258+
// the most straightforward way to probe for an active debugger listening on port is to start another JVM
259+
// with the debug options and check the process exit status. Trying to probe for open network port messes with
260+
// the debugger and makes IDEA stop it. The only downside of this is that the debugger will first attach to this
261+
// probe JVM, then the one running in a docker container we are aiming to debug.
262+
try {
263+
Process process = new ProcessBuilder()
264+
.command(JavaExecutable.getBinaryPath().toString(), remoteDebugArgument(LOCAL_DEBUG_HOST), "-version")
265+
.start();
266+
process.waitFor(5, TimeUnit.SECONDS);
267+
return process.exitValue() == 0;
268+
} catch (InterruptedException | IOException e) {
269+
return false;
270+
}
271+
}
272+
}

apm-agent-plugins/apm-jdbc-plugin/pom.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,44 +27,37 @@
2727
<dependency>
2828
<groupId>org.testcontainers</groupId>
2929
<artifactId>jdbc</artifactId>
30-
<version>${version.testcontainers}</version>
3130
<scope>test</scope>
3231
</dependency>
3332

3433
<dependency>
3534
<groupId>org.testcontainers</groupId>
3635
<artifactId>mysql</artifactId>
37-
<version>${version.testcontainers}</version>
3836
<scope>test</scope>
3937
</dependency>
4038
<dependency>
4139
<groupId>org.testcontainers</groupId>
4240
<artifactId>postgresql</artifactId>
43-
<version>${version.testcontainers}</version>
4441
<scope>test</scope>
4542
</dependency>
4643
<dependency>
4744
<groupId>org.testcontainers</groupId>
4845
<artifactId>mariadb</artifactId>
49-
<version>${version.testcontainers}</version>
5046
<scope>test</scope>
5147
</dependency>
5248
<dependency>
5349
<groupId>org.testcontainers</groupId>
5450
<artifactId>mssqlserver</artifactId>
55-
<version>${version.testcontainers}</version>
5651
<scope>test</scope>
5752
</dependency>
5853
<dependency>
5954
<groupId>org.testcontainers</groupId>
6055
<artifactId>db2</artifactId>
61-
<version>${version.testcontainers}</version>
6256
<scope>test</scope>
6357
</dependency>
6458
<dependency>
6559
<groupId>org.testcontainers</groupId>
6660
<artifactId>oracle-xe</artifactId>
67-
<version>${version.testcontainers}</version>
6861
<scope>test</scope>
6962
</dependency>
7063

apm-agent-plugins/apm-kafka-plugin/pom.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@
2020
<dependency>
2121
<groupId>org.testcontainers</groupId>
2222
<artifactId>testcontainers</artifactId>
23-
<version>${version.testcontainers}</version>
2423
<scope>test</scope>
2524
</dependency>
2625
<dependency>
2726
<groupId>org.testcontainers</groupId>
2827
<artifactId>kafka</artifactId>
29-
<version>${version.testcontainers}</version>
3028
<scope>test</scope>
3129
</dependency>
3230
</dependencies>

0 commit comments

Comments
 (0)