1
+ # type: ignore
1
2
import subprocess
2
3
from typing import Iterable , List , Optional , Tuple , Union
3
4
4
5
import requests
5
-
6
6
from testcontainers .core .exceptions import NoSuchPortExposed
7
7
from testcontainers .core .waiting_utils import wait_container_is_ready
8
8
@@ -14,6 +14,9 @@ class DockerCompose:
14
14
Args:
15
15
filepath: Relative directory containing the docker compose configuration file.
16
16
compose_file_name: File name of the docker compose configuration file.
17
+ compose_command: The command to use for docker compose. If not specified, a call to
18
+ docker compose --help will be made to determine the correct command to use.
19
+ If docker compose is not installed, docker-compose will be used.
17
20
pull: Pull images before launching environment.
18
21
build: Build images referenced in the configuration file.
19
22
env_file: Path to an env file containing environment variables to pass to docker compose.
@@ -45,21 +48,19 @@ def __init__(
45
48
self ,
46
49
filepath : str ,
47
50
compose_file_name : Union [str , Iterable ] = "docker-compose.yml" ,
51
+ compose_command : str = None ,
48
52
pull : bool = False ,
49
53
build : bool = False ,
50
54
env_file : Optional [str ] = None ,
51
55
services : Optional [List [str ]] = None ,
52
56
) -> None :
53
57
self .filepath = filepath
54
- self .compose_file_names = (
55
- [compose_file_name ]
56
- if isinstance (compose_file_name , str )
57
- else list (compose_file_name )
58
- )
58
+ self .compose_file_names = [compose_file_name ] if isinstance (compose_file_name , str ) else list (compose_file_name )
59
59
self .pull = pull
60
60
self .build = build
61
61
self .env_file = env_file
62
62
self .services = services
63
+ self .compose_command = self ._get_compose_command (compose_command )
63
64
64
65
def __enter__ (self ) -> "DockerCompose" :
65
66
self .start ()
@@ -68,14 +69,37 @@ def __enter__(self) -> "DockerCompose":
68
69
def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
69
70
self .stop ()
70
71
72
+ def _get_compose_command (self , command ):
73
+ """
74
+ Returns the basecommand parts used for the docker compose commands
75
+ depending on the docker compose api.
76
+
77
+ Returns
78
+ -------
79
+ list[str]
80
+ The docker compose command parts
81
+ """
82
+ if command :
83
+ return command .split (" " )
84
+
85
+ if (
86
+ subprocess .run (
87
+ ["docker" , "compose" , "--help" ], stdout = subprocess .DEVNULL , stderr = subprocess .STDOUT
88
+ ).returncode
89
+ == 0
90
+ ):
91
+ return ["docker" , "compose" ]
92
+
93
+ return ["docker-compose" ]
94
+
71
95
def docker_compose_command (self ) -> List [str ]:
72
96
"""
73
97
Returns command parts used for the docker compose commands
74
98
75
99
Returns:
76
100
cmd: Docker compose command parts.
77
101
"""
78
- docker_compose_cmd = [ "docker-compose" ]
102
+ docker_compose_cmd = self . compose_command . copy ()
79
103
for file in self .compose_file_names :
80
104
docker_compose_cmd += ["-f" , file ]
81
105
if self .env_file :
@@ -95,7 +119,6 @@ def start(self) -> None:
95
119
up_cmd .append ("--build" )
96
120
if self .services :
97
121
up_cmd .extend (self .services )
98
-
99
122
self ._call_command (cmd = up_cmd )
100
123
101
124
def stop (self ) -> None :
@@ -105,7 +128,7 @@ def stop(self) -> None:
105
128
down_cmd = self .docker_compose_command () + ["down" , "-v" ]
106
129
self ._call_command (cmd = down_cmd )
107
130
108
- def get_logs (self ) -> Tuple [bytes , bytes ]:
131
+ def get_logs (self ) -> Tuple [str , str ]:
109
132
"""
110
133
Returns all log output from stdout and stderr
111
134
@@ -122,9 +145,7 @@ def get_logs(self) -> Tuple[bytes, bytes]:
122
145
)
123
146
return result .stdout , result .stderr
124
147
125
- def exec_in_container (
126
- self , service_name : str , command : List [str ]
127
- ) -> Tuple [str , str , int ]:
148
+ def exec_in_container (self , service_name : str , command : List [str ]) -> Tuple [str , str ]:
128
149
"""
129
150
Executes a command in the container of one of the services.
130
151
@@ -136,20 +157,14 @@ def exec_in_container(
136
157
stdout: Standard output stream.
137
158
stderr: Standard error stream.
138
159
"""
139
- exec_cmd = (
140
- self .docker_compose_command () + ["exec" , "-T" , service_name ] + command
141
- )
160
+ exec_cmd = self .docker_compose_command () + ["exec" , "-T" , service_name ] + command
142
161
result = subprocess .run (
143
162
exec_cmd ,
144
163
cwd = self .filepath ,
145
164
stdout = subprocess .PIPE ,
146
165
stderr = subprocess .PIPE ,
147
166
)
148
- return (
149
- result .stdout .decode ("utf-8" ),
150
- result .stderr .decode ("utf-8" ),
151
- result .returncode ,
152
- )
167
+ return result .stdout .decode ("utf-8" ), result .stderr .decode ("utf-8" ), result .returncode
153
168
154
169
def get_service_port (self , service_name : str , port : int ) -> int :
155
170
"""
@@ -179,15 +194,16 @@ def get_service_host(self, service_name: str, port: int) -> str:
179
194
180
195
def _get_service_info (self , service : str , port : int ) -> List [str ]:
181
196
port_cmd = self .docker_compose_command () + ["port" , service , str (port )]
182
- output = subprocess .check_output (port_cmd , cwd = self .filepath ).decode ("utf-8" )
197
+ try :
198
+ output = subprocess .check_output (port_cmd , cwd = self .filepath ).decode ("utf-8" )
199
+ except subprocess .CalledProcessError as e :
200
+ raise NoSuchPortExposed (str (e .stderr ))
183
201
result = str (output ).rstrip ().split (":" )
184
202
if len (result ) != 2 or not all (result ):
185
203
raise NoSuchPortExposed (f"port { port } is not exposed for service { service } " )
186
204
return result
187
205
188
- def _call_command (
189
- self , cmd : Union [str , List [str ]], filepath : Optional [str ] = None
190
- ) -> None :
206
+ def _call_command (self , cmd : Union [str , List [str ]], filepath : Optional [str ] = None ) -> None :
191
207
if filepath is None :
192
208
filepath = self .filepath
193
209
subprocess .call (cmd , cwd = filepath )
0 commit comments