You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: extended configuration options for container.exec method (#1050)
The branch adds the `ExecConfig` struct (patterned on the [Java
implementation of the same
feature](https://github.com/testcontainers/testcontainers-java/blob/main/core/src/main/java/org/testcontainers/containers/ExecConfig.java))
and includes it as a valid input for `DockerContainer.exec()`. This
allows setting the running user, working directory, environment
variables, and privileged mode. Other features become easy extensions of
the interface.
Also included in the branch:
- Extensions to the core test suite, pinning existing behavior of
`.exec()` that wasn't previously tested, notably the fact that
`str`-typed commands _are not_ executed in shell mode
- Rewrite of `docs/features/executing_commands.md`, which incorrect
prior to this branch. It now aligns with the actual code
Copy file name to clipboardExpand all lines: docs/features/executing_commands.md
+57-78Lines changed: 57 additions & 78 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,104 +4,80 @@ Testcontainers-Python provides several ways to execute commands inside container
4
4
5
5
## Basic Command Execution
6
6
7
-
The simplest way to execute a command is using the `exec` method:
7
+
The simplest way to execute a command is using the `exec` method, passing either an argv list or a string:
8
8
9
9
```python
10
-
from testcontainers.community.genericimportGenericContainer
10
+
from testcontainers.core.containerimportDockerContainer
11
11
12
-
with GenericContainer("alpine:latest") as container:
13
-
# Execute a simple command
14
-
exit_code, output = container.exec(["ls", "-la"])
15
-
print(output) # Command output as string
16
-
```
12
+
with DockerContainer("alpine:latest") as container:
13
+
# Execute a simple command (argv form)
14
+
result = container.exec(["ls", "-la"])
15
+
print(result.exit_code) # 0
16
+
print(result.output) # command output as bytes
17
+
print(result.output.decode()) # ...decoded to str
17
18
18
-
## Command Execution with Options
19
+
# A string is also accepted
20
+
result = container.exec("ls -la")
21
+
```
19
22
20
-
You can customize command execution with various options:
23
+
`exec` returns a named `(exit_code, output)` tuple, so you can also unpack it directly:
21
24
22
25
```python
23
-
with GenericContainer("alpine:latest") as container:
24
-
# Execute command with user
25
-
exit_code, output = container.exec(
26
-
["whoami"],
27
-
user="nobody"
28
-
)
29
-
30
-
# Execute command with environment variables
31
-
exit_code, output = container.exec(
32
-
["echo", "$TEST_VAR"],
33
-
environment={"TEST_VAR": "test_value"}
34
-
)
35
-
36
-
# Execute command with working directory
37
-
exit_code, output = container.exec(
38
-
["pwd"],
39
-
workdir="/tmp"
40
-
)
26
+
exit_code, output = container.exec(["ls", "-la"])
41
27
```
42
28
43
-
## Interactive Commands
29
+
> **A string command is *not* run through a shell.**`docker-py` tokenizes it with `shlex.split`, so shell features such as pipes, redirections, and variable expansion are passed through literally — `container.exec("echo $HOME")` prints the text `$HOME`, not your home directory. When you need shell behavior, invoke a shell explicitly: `container.exec(["sh", "-c", "echo $HOME"])`.
44
30
45
-
For interactive commands, you can use the `exec_interactive` method:
31
+
## Command Execution with Options
32
+
33
+
To customize how a command runs — the user, environment, or working directory — pass an `ExecConfig`:
46
34
47
35
```python
48
-
with GenericContainer("alpine:latest") as container:
49
-
# Start an interactive shell
50
-
container.exec_interactive(["sh"])
51
-
```
36
+
from testcontainers.core.container import DockerContainer, ExecConfig
52
37
53
-
## Command Execution with Timeout
38
+
with DockerContainer("alpine:latest") as container:
39
+
# Execute command as a specific user
40
+
result = container.exec(ExecConfig(command=["whoami"], user="nobody"))
54
41
55
-
You can set a timeout for command execution:
42
+
# Execute command with environment variables
43
+
# (use a command that reads the environment, e.g. printenv -- a bare
44
+
# argv command is not shell-expanded, see the note above)
45
+
result = container.exec(ExecConfig(command=["printenv", "TEST_VAR"], environment={"TEST_VAR": "test_value"}))
56
46
57
-
```python
58
-
with GenericContainer("alpine:latest") as container:
59
-
# Execute command with timeout
60
-
try:
61
-
exit_code, output = container.exec(
62
-
["sleep", "10"],
63
-
timeout=5# Timeout in seconds
64
-
)
65
-
exceptTimeoutError:
66
-
print("Command timed out")
47
+
# Execute command in a working directory (str or pathlib.Path)
48
+
result = container.exec(ExecConfig(command=["pwd"], workdir="/tmp"))
67
49
```
68
50
69
-
## Command Execution with Privileges
70
-
71
-
For commands that require elevated privileges:
51
+
`ExecConfig` is a frozen dataclass: only `command` is required, and `user`, `environment`, `workdir`, and `privileged` are optional. Because it is immutable, the idiomatic way to derive a variant is `dataclasses.replace`:
72
52
73
53
```python
74
-
with GenericContainer("alpine:latest") as container:
75
-
# Execute command with privileges
76
-
exit_code, output = container.exec(
77
-
["mount"],
78
-
privileged=True
79
-
)
54
+
from dataclasses import replace
55
+
56
+
base = ExecConfig(command=["pwd"], workdir="/tmp")
57
+
in_var = replace(base, workdir="/var")
80
58
```
81
59
82
-
## Command Execution with TTY
60
+
## Command Execution with Privileges
83
61
84
-
For commands that require a TTY:
62
+
For commands that require elevated privileges, set `privileged=True`:
85
63
86
64
```python
87
-
with GenericContainer("alpine:latest") as container:
88
-
# Execute command with TTY
89
-
exit_code, output = container.exec(
90
-
["top"],
91
-
tty=True
92
-
)
65
+
from testcontainers.core.container import DockerContainer, ExecConfig
66
+
67
+
with DockerContainer("alpine:latest") as container:
68
+
# Execute command with privileges
69
+
result = container.exec(ExecConfig(command=["mount"], privileged=True))
93
70
```
94
71
95
72
## Best Practices
96
73
97
-
1. Use appropriate timeouts for long-running commands
98
-
2. Handle command failures gracefully
99
-
3. Use environment variables for configuration
100
-
4. Consider security implications of privileged commands
101
-
5. Clean up after command execution
102
-
6. Use appropriate user permissions
103
-
7. Handle command output appropriately
104
-
8. Consider using shell scripts for complex commands
74
+
1. Handle command failures gracefully — check `exit_code` rather than assuming success
75
+
2. Use environment variables for configuration
76
+
3. Consider security implications of privileged commands
77
+
4. Clean up after command execution
78
+
5. Use appropriate user permissions
79
+
6. Decode `output` from bytes when you need text
80
+
7. Use an explicit `["sh", "-c", ...]` invocation for commands that rely on shell features
105
81
106
82
## Common Use Cases
107
83
@@ -121,23 +97,27 @@ with PostgresContainer() as postgres:
121
97
### File Operations
122
98
123
99
```python
124
-
with GenericContainer("alpine:latest") as container:
100
+
from testcontainers.core.container import DockerContainer
101
+
102
+
with DockerContainer("alpine:latest") as container:
0 commit comments