Skip to content

Commit 212cf54

Browse files
authored
Add support for workdir and env var in exec command (#7816)
Currently, only `user` and `command` can be used with `execInContainer`. This commit introduces `ExecConfig`, which allows to set workdir and env vars when running an exec process. `execInContainerWithUser` is deprecated in favor of `execInContainer(ExecConfig)`
1 parent d5dc4d3 commit 212cf54

File tree

6 files changed

+212
-37
lines changed

6 files changed

+212
-37
lines changed

core/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ tasks.japicmp {
5252

5353
methodExcludes = [
5454
"org.testcontainers.containers.Container#getDockerClient()",
55-
"org.testcontainers.containers.ContainerState#getDockerClient()"
55+
"org.testcontainers.containers.ContainerState#getDockerClient()",
56+
"org.testcontainers.containers.ContainerState#execInContainer(org.testcontainers.containers.ExecConfig)",
57+
"org.testcontainers.containers.ContainerState#execInContainer(java.nio.charset.Charset,org.testcontainers.containers.ExecConfig)"
5658
]
5759

5860
fieldExcludes = []

core/src/main/java/org/testcontainers/containers/ContainerState.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,28 +266,51 @@ default Container.ExecResult execInContainer(Charset outputCharset, String... co
266266
* Run a command inside a running container as a given user, as using "docker exec -u user".
267267
* <p>
268268
* @see ExecInContainerPattern#execInContainerWithUser(DockerClient, InspectContainerResponse, String, String...)
269+
* @deprecated use {@link #execInContainer(ExecConfig)}
269270
*/
271+
@Deprecated
270272
default Container.ExecResult execInContainerWithUser(String user, String... command)
271273
throws UnsupportedOperationException, IOException, InterruptedException {
272-
return ExecInContainerPattern.execInContainerWithUser(getDockerClient(), getContainerInfo(), user, command);
274+
return ExecInContainerPattern.execInContainer(
275+
getDockerClient(),
276+
getContainerInfo(),
277+
ExecConfig.builder().user(user).command(command).build()
278+
);
273279
}
274280

275281
/**
276282
* Run a command inside a running container as a given user, as using "docker exec -u user".
277283
* <p>
278284
* @see ExecInContainerPattern#execInContainerWithUser(DockerClient, InspectContainerResponse, Charset, String, String...)
285+
* @deprecated use {@link #execInContainer(Charset, ExecConfig)}
279286
*/
287+
@Deprecated
280288
default Container.ExecResult execInContainerWithUser(Charset outputCharset, String user, String... command)
281289
throws UnsupportedOperationException, IOException, InterruptedException {
282-
return ExecInContainerPattern.execInContainerWithUser(
290+
return ExecInContainerPattern.execInContainer(
283291
getDockerClient(),
284292
getContainerInfo(),
285293
outputCharset,
286-
user,
287-
command
294+
ExecConfig.builder().user(user).command(command).build()
288295
);
289296
}
290297

298+
/**
299+
* Run a command inside a running container, as though using "docker exec".
300+
*/
301+
default Container.ExecResult execInContainer(ExecConfig execConfig)
302+
throws UnsupportedOperationException, IOException, InterruptedException {
303+
return ExecInContainerPattern.execInContainer(getDockerClient(), getContainerInfo(), execConfig);
304+
}
305+
306+
/**
307+
* Run a command inside a running container, as though using "docker exec".
308+
*/
309+
default Container.ExecResult execInContainer(Charset outputCharset, ExecConfig execConfig)
310+
throws UnsupportedOperationException, IOException, InterruptedException {
311+
return ExecInContainerPattern.execInContainer(getDockerClient(), getContainerInfo(), outputCharset, execConfig);
312+
}
313+
291314
/**
292315
*
293316
* Copies a file or directory to the container.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.testcontainers.containers;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.util.Map;
7+
8+
/**
9+
* Exec configuration.
10+
*/
11+
@Builder
12+
@Getter
13+
public class ExecConfig {
14+
15+
/**
16+
* The command to run.
17+
*/
18+
private String[] command;
19+
20+
/**
21+
* The user to run the exec process.
22+
*/
23+
private String user;
24+
25+
/**
26+
* Key-value pairs of environment variables.
27+
*/
28+
private Map<String, String> envVars;
29+
30+
/**
31+
* The working directory for the exec process.
32+
*/
33+
private String workDir;
34+
}

core/src/main/java/org/testcontainers/containers/ExecInContainerPattern.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import java.io.IOException;
1717
import java.nio.charset.Charset;
1818
import java.nio.charset.StandardCharsets;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.stream.Collectors;
1922

2023
/**
2124
* Provides utility methods for executing commands in containers
@@ -97,7 +100,9 @@ public Container.ExecResult execInContainer(
97100
* @param command the command to execute
98101
* @see #execInContainerWithUser(DockerClient, InspectContainerResponse, Charset, String,
99102
* String...)
103+
* @deprecated use {@link #execInContainer(DockerClient, InspectContainerResponse, ExecConfig)}
100104
*/
105+
@Deprecated
101106
public Container.ExecResult execInContainerWithUser(
102107
DockerClient dockerClient,
103108
InspectContainerResponse containerInfo,
@@ -121,13 +126,64 @@ public Container.ExecResult execInContainerWithUser(
121126
* @throws IOException if there's an issue communicating with Docker
122127
* @throws InterruptedException if the thread waiting for the response is interrupted
123128
* @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec".
129+
* @deprecated use {@link #execInContainer(DockerClient, InspectContainerResponse, Charset, ExecConfig)}
124130
*/
131+
@Deprecated
125132
public Container.ExecResult execInContainerWithUser(
126133
DockerClient dockerClient,
127134
InspectContainerResponse containerInfo,
128135
Charset outputCharset,
129136
String user,
130137
String... command
138+
) throws UnsupportedOperationException, IOException, InterruptedException {
139+
return execInContainer(
140+
dockerClient,
141+
containerInfo,
142+
outputCharset,
143+
ExecConfig.builder().user(user).command(command).build()
144+
);
145+
}
146+
147+
/**
148+
* Run a command inside a running container as a given user, as using "docker exec -u user".
149+
* <p>
150+
* This functionality is not available on a docker daemon running the older "lxc" execution
151+
* driver. At the time of writing, CircleCI was using this driver.
152+
* @param dockerClient the {@link DockerClient}
153+
* @param containerInfo the container info
154+
* @param execConfig the exec configuration
155+
* @return the result of execution
156+
* @throws IOException if there's an issue communicating with Docker
157+
* @throws InterruptedException if the thread waiting for the response is interrupted
158+
* @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec".
159+
*/
160+
public Container.ExecResult execInContainer(
161+
DockerClient dockerClient,
162+
InspectContainerResponse containerInfo,
163+
ExecConfig execConfig
164+
) throws UnsupportedOperationException, IOException, InterruptedException {
165+
return execInContainer(dockerClient, containerInfo, StandardCharsets.UTF_8, execConfig);
166+
}
167+
168+
/**
169+
* Run a command inside a running container as a given user, as using "docker exec -u user".
170+
* <p>
171+
* This functionality is not available on a docker daemon running the older "lxc" execution
172+
* driver. At the time of writing, CircleCI was using this driver.
173+
* @param dockerClient the {@link DockerClient}
174+
* @param containerInfo the container info
175+
* @param outputCharset the character set used to interpret the output.
176+
* @param execConfig the exec configuration
177+
* @return the result of execution
178+
* @throws IOException if there's an issue communicating with Docker
179+
* @throws InterruptedException if the thread waiting for the response is interrupted
180+
* @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec".
181+
*/
182+
public Container.ExecResult execInContainer(
183+
DockerClient dockerClient,
184+
InspectContainerResponse containerInfo,
185+
Charset outputCharset,
186+
ExecConfig execConfig
131187
) throws UnsupportedOperationException, IOException, InterruptedException {
132188
if (!TestEnvironment.dockerExecutionDriverSupportsExec()) {
133189
// at time of writing, this is the expected result in CircleCI.
@@ -143,17 +199,36 @@ public Container.ExecResult execInContainerWithUser(
143199
String containerId = containerInfo.getId();
144200
String containerName = containerInfo.getName();
145201

202+
String[] command = execConfig.getCommand();
146203
log.debug("{}: Running \"exec\" command: {}", containerName, String.join(" ", command));
147204
final ExecCreateCmd execCreateCmd = dockerClient
148205
.execCreateCmd(containerId)
149206
.withAttachStdout(true)
150207
.withAttachStderr(true)
151208
.withCmd(command);
209+
210+
String user = execConfig.getUser();
152211
if (user != null && !user.isEmpty()) {
153212
log.debug("{}: Running \"exec\" command with user: {}", containerName, user);
154213
execCreateCmd.withUser(user);
155214
}
156215

216+
String workDir = execConfig.getWorkDir();
217+
if (workDir != null && !workDir.isEmpty()) {
218+
log.debug("{}: Running \"exec\" command inside workingDir: {}", containerName, workDir);
219+
execCreateCmd.withWorkingDir(workDir);
220+
}
221+
222+
Map<String, String> envVars = execConfig.getEnvVars();
223+
if (envVars != null && !envVars.isEmpty()) {
224+
List<String> envVarList = envVars
225+
.entrySet()
226+
.stream()
227+
.map(e -> e.getKey() + "=" + e.getValue())
228+
.collect(Collectors.toList());
229+
execCreateCmd.withEnv(envVarList);
230+
}
231+
157232
final ExecCreateCmdResponse execCreateCmdResponse = execCreateCmd.exec();
158233

159234
final ToStringConsumer stdoutConsumer = new ToStringConsumer();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.testcontainers.junit;
2+
3+
import org.junit.Assume;
4+
import org.junit.ClassRule;
5+
import org.junit.Test;
6+
import org.testcontainers.TestImages;
7+
import org.testcontainers.containers.ExecConfig;
8+
import org.testcontainers.containers.GenericContainer;
9+
import org.testcontainers.utility.TestEnvironment;
10+
11+
import java.util.Collections;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
public class ExecInContainerTest {
16+
17+
@ClassRule
18+
public static GenericContainer<?> redis = new GenericContainer<>(TestImages.REDIS_IMAGE).withExposedPorts(6379);
19+
20+
@Test
21+
public void shouldExecuteCommand() throws Exception {
22+
// The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29),
23+
// that's the case for CircleCI.
24+
// Once they resolve the issue, this clause can be removed.
25+
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
26+
27+
final GenericContainer.ExecResult result = redis.execInContainer("redis-cli", "role");
28+
assertThat(result.getStdout())
29+
.as("Output for \"redis-cli role\" command should start with \"master\"")
30+
.startsWith("master");
31+
assertThat(result.getStderr()).as("Stderr for \"redis-cli role\" command should be empty").isEmpty();
32+
// We expect to reach this point for modern Docker versions.
33+
}
34+
35+
@Test
36+
public void shouldExecuteCommandWithUser() throws Exception {
37+
// The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29),
38+
// that's the case for CircleCI.
39+
// Once they resolve the issue, this clause can be removed.
40+
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
41+
42+
final GenericContainer.ExecResult result = redis.execInContainerWithUser("redis", "whoami");
43+
assertThat(result.getStdout())
44+
.as("Output for \"whoami\" command should start with \"redis\"")
45+
.startsWith("redis");
46+
assertThat(result.getStderr()).as("Stderr for \"whoami\" command should be empty").isEmpty();
47+
// We expect to reach this point for modern Docker versions.
48+
}
49+
50+
@Test
51+
public void shouldExecuteCommandWithWorkdir() throws Exception {
52+
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
53+
54+
final GenericContainer.ExecResult result = redis.execInContainer(
55+
ExecConfig.builder().workDir("/opt").command(new String[] { "pwd" }).build()
56+
);
57+
assertThat(result.getStdout()).startsWith("/opt");
58+
}
59+
60+
@Test
61+
public void shouldExecuteCommandWithEnvVars() throws Exception {
62+
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
63+
64+
final GenericContainer.ExecResult result = redis.execInContainer(
65+
ExecConfig
66+
.builder()
67+
.envVars(Collections.singletonMap("TESTCONTAINERS", "JAVA"))
68+
.command(new String[] { "env" })
69+
.build()
70+
);
71+
assertThat(result.getStdout()).contains("TESTCONTAINERS=JAVA");
72+
}
73+
}

core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import com.rabbitmq.client.Envelope;
1515
import org.apache.commons.io.FileUtils;
1616
import org.bson.Document;
17-
import org.junit.Assume;
1817
import org.junit.BeforeClass;
1918
import org.junit.ClassRule;
2019
import org.junit.Ignore;
@@ -28,7 +27,6 @@
2827
import org.testcontainers.containers.SelinuxContext;
2928
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
3029
import org.testcontainers.utility.Base58;
31-
import org.testcontainers.utility.TestEnvironment;
3230

3331
import java.io.BufferedReader;
3432
import java.io.File;
@@ -359,36 +357,6 @@ public void failFastWhenContainerHaltsImmediately() {
359357
}
360358
}
361359

362-
@Test
363-
public void testExecInContainer() throws Exception {
364-
// The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29),
365-
// that's the case for CircleCI.
366-
// Once they resolve the issue, this clause can be removed.
367-
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
368-
369-
final GenericContainer.ExecResult result = redis.execInContainer("redis-cli", "role");
370-
assertThat(result.getStdout())
371-
.as("Output for \"redis-cli role\" command should start with \"master\"")
372-
.startsWith("master");
373-
assertThat(result.getStderr()).as("Stderr for \"redis-cli role\" command should be empty").isEmpty();
374-
// We expect to reach this point for modern Docker versions.
375-
}
376-
377-
@Test
378-
public void testExecInContainerWithUser() throws Exception {
379-
// The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29),
380-
// that's the case for CircleCI.
381-
// Once they resolve the issue, this clause can be removed.
382-
Assume.assumeTrue(TestEnvironment.dockerExecutionDriverSupportsExec());
383-
384-
final GenericContainer.ExecResult result = redis.execInContainerWithUser("redis", "whoami");
385-
assertThat(result.getStdout())
386-
.as("Output for \"whoami\" command should start with \"redis\"")
387-
.startsWith("redis");
388-
assertThat(result.getStderr()).as("Stderr for \"whoami\" command should be empty").isEmpty();
389-
// We expect to reach this point for modern Docker versions.
390-
}
391-
392360
@Test
393361
public void extraHostTest() throws IOException {
394362
BufferedReader br = getReaderForContainerPort80(alpineExtrahost);

0 commit comments

Comments
 (0)