88This module is designed to simulate the actions of the Data Manager
99and Job Operator that are running in the DM kubernetes deployment.
1010"""
11+
12+ import contextlib
1113import copy
1214import os
1315import shutil
1416import subprocess
17+ import sys
1518import time
16- from typing import Any , Dict , Optional , Tuple
19+ from typing import Any , Dict , List , Optional , Tuple
1720
1821# The 'simulated' instance directory,
1922# created by the Data Manager prior to launching the corresponding Job.
5962"""
6063
6164
65+ def _get_docker_compose_command () -> str :
66+ # Try 'docker compose' (v2) and then 'docker-compose' (v1)
67+ # we need one or the other.
68+ dc_command : str = ""
69+ try :
70+ _ = subprocess .run (
71+ ["docker" , "compose" , "version" ],
72+ capture_output = True ,
73+ check = False ,
74+ timeout = 4 ,
75+ )
76+ dc_command = "docker compose"
77+ except FileNotFoundError :
78+ with contextlib .suppress (FileNotFoundError ):
79+ _ = subprocess .run (
80+ ["docker-compose" , "version" ],
81+ capture_output = True ,
82+ check = False ,
83+ timeout = 4 ,
84+ )
85+ dc_command = "docker-compose"
86+ if not dc_command :
87+ print ("ERROR: Neither 'docker compose' nor 'docker-compose' has been found" )
88+ print ("One of these is required." )
89+ print ("Please install one of them." )
90+ sys .exit (1 )
91+
92+ assert dc_command
93+ return dc_command
94+
95+
6296def _get_docker_compose_version () -> str :
63- result = subprocess . run (
64- [ "docker-compose" , " version" ], capture_output = True , check = False , timeout = 4
65- )
97+ dc_command = _get_docker_compose_command ()
98+ version_cmd : List [ str ] = dc_command . split () + [ " version" ]
99+ result = subprocess . run ( version_cmd , capture_output = True , check = False , timeout = 4 )
66100
67101 # stdout will contain the version on the first line: -
68- # "docker-compose version 1 .29.2, build unknown"
102+ # "docker-compose version v1 .29.2, build unknown"
69103 # Ignore the first 23 characters of the first line...
70104 return str (result .stdout .decode ("utf-8" ).split ("\n " )[0 ][23 :])
71105
@@ -77,10 +111,12 @@ def get_test_root() -> str:
77111
78112
79113class Compose :
80- """A class handling the execution of 'docker- compose'
114+ """A class handling the execution of 'docker compose'
81115 for an individual test.
82116 """
83117
118+ # The docker-compose command (for the first test)
119+ _COMPOSE_COMMAND : Optional [str ] = None
84120 # The docker-compose version (for the first test)
85121 _COMPOSE_VERSION : Optional [str ] = None
86122
@@ -144,10 +180,14 @@ def create(self) -> str:
144180 if os .path .exists (test_path ):
145181 shutil .rmtree (test_path )
146182
183+ # Do we have the command?
184+ if not Compose ._COMPOSE_COMMAND :
185+ Compose ._COMPOSE_COMMAND = _get_docker_compose_command ()
186+ print (f"# Compose command: { Compose ._COMPOSE_COMMAND } " )
147187 # Do we have the docker-compose version the user's installed?
148188 if not Compose ._COMPOSE_VERSION :
149189 Compose ._COMPOSE_VERSION = _get_docker_compose_version ()
150- print (f"# Compose: docker-compose ( { Compose ._COMPOSE_VERSION } ) " )
190+ print (f"# Compose version: { Compose ._COMPOSE_VERSION } " )
151191
152192 # Make the test directory
153193 # (where the test is launched from)
@@ -159,15 +199,8 @@ def create(self) -> str:
159199 os .makedirs (inst_path )
160200
161201 # Run as a specific user/group ID?
162- if self ._user_id is not None :
163- user_id = self ._user_id
164- else :
165- user_id = os .getuid ()
166- if self ._group_id is not None :
167- group_id = self ._group_id
168- else :
169- group_id = os .getgid ()
170-
202+ user_id = self ._user_id if self ._user_id is not None else os .getuid ()
203+ group_id = self ._group_id if self ._group_id is not None else os .getgid ()
171204 # Write the Docker compose content to a file in the test directory
172205 additional_environment : str = ""
173206 if self ._test_environment :
@@ -214,15 +247,17 @@ def run(
214247 caller along with the stdout and stderr content.
215248 A non-zero exit code does not necessarily mean the test has failed.
216249 """
250+ assert Compose ._COMPOSE_COMMAND
217251
218252 execution_directory : str = self .get_test_path ()
219253
220- print ('# Compose: Executing the test ("docker-compose up")...' )
254+ print (f '# Compose: Executing the test ("{ Compose . _COMPOSE_COMMAND } up")...' )
221255 print (f'# Compose: Execution directory is "{ execution_directory } "' )
222256
223257 cwd = os .getcwd ()
224258 os .chdir (execution_directory )
225259
260+ timeout : bool = False
226261 try :
227262 # Run the container, and then cleanup.
228263 # If a test environment is set then we pass in these values to the
@@ -237,33 +272,45 @@ def run(
237272 # we set the prefix for the network name and can use compose files
238273 # from different directories. Without this the network name
239274 # is prefixed by the directory the compose file is in.
275+ up_cmd : List [str ] = Compose ._COMPOSE_COMMAND .split () + [
276+ "-p" ,
277+ "data-manager" ,
278+ "up" ,
279+ "--exit-code-from" ,
280+ "job" ,
281+ "--abort-on-container-exit" ,
282+ ]
240283 test = subprocess .run (
241- [
242- "docker-compose" ,
243- "-p" ,
244- "data-manager" ,
245- "up" ,
246- "--exit-code-from" ,
247- "job" ,
248- "--abort-on-container-exit" ,
249- ],
284+ up_cmd ,
250285 capture_output = True ,
251286 timeout = timeout_minutes * 60 ,
252287 check = False ,
253288 env = env ,
254289 )
290+ down_cmd : List [str ] = Compose ._COMPOSE_COMMAND .split () + ["down" ]
255291 _ = subprocess .run (
256- [ "docker-compose" , "down" ] ,
292+ down_cmd ,
257293 capture_output = True ,
258294 timeout = 240 ,
259295 check = False ,
260296 )
297+ except : # pylint: disable=bare-except
298+ timeout = True
261299 finally :
262300 os .chdir (cwd )
263301
264- print (f"# Compose: Executed (exit code { test .returncode } )" )
302+ if timeout :
303+ print ("# Compose: ERROR - Test timeout" )
304+ return_code : int = - 911
305+ test_stdout : str = ""
306+ test_stderr : str = ""
307+ else :
308+ print (f"# Compose: Executed (exit code { test .returncode } )" )
309+ return_code = test .returncode
310+ test_stdout = test .stdout .decode ("utf-8" )
311+ test_stderr = test .stderr .decode ("utf-8" )
265312
266- return test . returncode , test . stdout . decode ( "utf-8" ), test . stderr . decode ( "utf-8" )
313+ return return_code , test_stdout , test_stderr
267314
268315 def delete (self ) -> None :
269316 """Deletes a test directory created by 'create()'."""
@@ -279,9 +326,10 @@ def delete(self) -> None:
279326 def run_group_compose_file (compose_file : str , delay_seconds : int = 0 ) -> bool :
280327 """Starts a group compose file in a detached state.
281328 The file is expected to be a compose file in the 'data-manager' directory.
282- We pull the continer imag to reduce the 'docker-compose up' time
329+ We pull the container image to reduce the 'docker-compose up' time
283330 and then optionally wait for a period of seconds.
284331 """
332+ assert Compose ._COMPOSE_COMMAND
285333
286334 print ("# Compose: Starting test group containers..." )
287335
@@ -290,13 +338,13 @@ def run_group_compose_file(compose_file: str, delay_seconds: int = 0) -> bool:
290338 try :
291339 # Pre-pull the docker-compose images.
292340 # This saves start-up execution time.
341+ pull_cmd : List [str ] = Compose ._COMPOSE_COMMAND .split () + [
342+ "-f" ,
343+ os .path .join ("data-manager" , compose_file ),
344+ "pull" ,
345+ ]
293346 _ = subprocess .run (
294- [
295- "docker-compose" ,
296- "-f" ,
297- os .path .join ("data-manager" , compose_file ),
298- "pull" ,
299- ],
347+ pull_cmd ,
300348 capture_output = False ,
301349 check = False ,
302350 )
@@ -306,16 +354,16 @@ def run_group_compose_file(compose_file: str, delay_seconds: int = 0) -> bool:
306354 # we set the prefix for the network name and services from this container
307355 # are visible to the test container. Without this the network name
308356 # is prefixed by the directory the compose file is in.
357+ up_cmd : List [str ] = Compose ._COMPOSE_COMMAND .split () + [
358+ "-f" ,
359+ os .path .join ("data-manager" , compose_file ),
360+ "-p" ,
361+ "data-manager" ,
362+ "up" ,
363+ "-d" ,
364+ ]
309365 _ = subprocess .run (
310- [
311- "docker-compose" ,
312- "-f" ,
313- os .path .join ("data-manager" , compose_file ),
314- "-p" ,
315- "data-manager" ,
316- "up" ,
317- "-d" ,
318- ],
366+ up_cmd ,
319367 capture_output = False ,
320368 check = False ,
321369 )
@@ -335,19 +383,20 @@ def stop_group_compose_file(compose_file: str) -> bool:
335383 """Stops a group compose file.
336384 The file is expected to be a compose file in the 'data-manager' directory.
337385 """
386+ assert Compose ._COMPOSE_COMMAND
338387
339388 print ("# Compose: Stopping test group containers..." )
340389
341390 try :
342391 # Bring the compose file down...
392+ down_cmd : List [str ] = Compose ._COMPOSE_COMMAND .split () + [
393+ "-f" ,
394+ os .path .join ("data-manager" , compose_file ),
395+ "down" ,
396+ "--remove-orphans" ,
397+ ]
343398 _ = subprocess .run (
344- [
345- "docker-compose" ,
346- "-f" ,
347- os .path .join ("data-manager" , compose_file ),
348- "down" ,
349- "--remove-orphans" ,
350- ],
399+ down_cmd ,
351400 capture_output = False ,
352401 timeout = 240 ,
353402 check = False ,
0 commit comments