22import subprocess
33import logging
44from bd2k .util .exceptions import panic
5+ from toil_scripts .lib import require
56
67_log = logging .getLogger (__name__ )
78
@@ -15,7 +16,8 @@ def mock_mode():
1516 return True if int (os .environ .get ('TOIL_SCRIPTS_MOCK_MODE' , '0' )) else False
1617
1718
18- def docker_call (tool ,
19+ def docker_call (tool = None ,
20+ tools = None ,
1921 parameters = None ,
2022 work_dir = '.' ,
2123 rm = True ,
@@ -25,11 +27,15 @@ def docker_call(tool,
2527 outputs = None ,
2628 docker_parameters = None ,
2729 check_output = False ,
30+ return_stderr = False ,
2831 mock = None ):
2932 """
3033 Calls Docker, passing along parameters and tool.
3134
32- :param str tool: Name of the Docker image to be used (e.g. quay.io/ucsc_cgl/samtools)
35+ :param (str tool | list[str] tools): Name of the Docker image to be used (e.g. quay.io/ucsc_cgl/samtools)
36+ OR str list of names of the Docker images and order to be used when piping commands to
37+ Docker. (e.g. ['quay.io/ucsc_cgl/samtools', 'ubuntu']). Both tool and tools are mutually
38+ exclusive parameters to docker_call.
3339 :param list[str] parameters: Command line arguments to be passed to the tool
3440 :param str work_dir: Directory to mount into the container via `-v`. Destination convention is /data
3541 :param bool rm: Set to True to pass `--rm` flag.
@@ -41,8 +47,33 @@ def docker_call(tool,
4147 or a url. The value is only used if mock=True
4248 :param dict[str,str] docker_parameters: Parameters to pass to docker
4349 :param bool check_output: When True, this function returns docker's output
50+ :param bool return_stderr: When True, this function includes stderr in docker's output
4451 :param bool mock: Whether to run in mock mode. If this variable is unset, its value will be determined by
4552 the environment variable.
53+
54+ Piping docker commands can be done in one of two ways depending on use case:
55+ Running a pipe in docker in 'pipe-in-single-container' mode produces command structure
56+ docker '... | ... | ...' where each '...' command corresponds to each element in the 'parameters'
57+ argument that uses a docker container. This is the most efficient method if you want to run a pipe of
58+ commands where each command uses the same docker container.
59+
60+ Running a pipe in docker in 'pipe-of-containers' mode produces command structure
61+ docker '...' | docker '...' | docker '...' where each '...' command corresponds to each element in
62+ the 'parameters' argument that uses a docker container and each 'docker' tool in the pipe
63+ corresponds to each element in the 'tool' argument
64+
65+ Examples for running command 'head -c 1M /dev/urandom | gzip | gunzip | md5sum 1>&2':
66+ Running 'pipe-in-single-container' mode:
67+ command= ['head -c 1M /dev/urandom', 'gzip', 'gunzip', 'md5sum 1>&2']
68+ docker_work_dir=curr_work_dir
69+ docker_tools=['ubuntu']
70+ stdout = docker_call(work_dir=docker_work_dir, parameters=command, tools=docker_tools, check_output=True)
71+
72+ Running 'pipe-of-containers' mode:
73+ command= ['head -c 1M /dev/urandom', 'gzip', 'gunzip', 'md5sum 1>&2']
74+ docker_work_dir=curr_work_dir
75+ docker_tools=['ubuntu', 'ubuntu', 'ubuntu', 'ubuntu']
76+ stdout = docker_call(work_dir=docker_work_dir, parameters=command, tools=docker_tools, check_output=True)
4677 """
4778 from toil_scripts .lib .urls import download_url
4879
@@ -83,37 +114,69 @@ def docker_call(tool,
83114 if env :
84115 for e , v in env .iteritems ():
85116 base_docker_call .extend (['-e' , '{}={}' .format (e , v )])
117+
86118 if docker_parameters :
87119 base_docker_call += docker_parameters
120+
121+ docker_call = []
122+
123+ require (bool (tools ) != bool (tool ), 'Either "tool" or "tools" must contain a value, but not both' )
124+
125+ # Pipe functionality
126+ # each element in the parameters list must represent a sub-pipe command
127+ if bool (tools ):
128+ if len (tools ) > 1 :
129+ require (len (tools ) == len (parameters ), "Both 'tools'({}) and 'parameters'({}) arguments must\
130+ contain the same number of elements" .format (len (tools ), len (parameters )))
131+ # If tool is a list containing multiple docker container name strings
132+ # then format the docker call in the 'pipe-of-containers' mode
133+ docker_call .extend (base_docker_call + ['--entrypoint /bin/bash' , tools [0 ], '-c \' {}\' ' .format (parameters [0 ])])
134+ for i in xrange (1 , len (tools )):
135+ docker_call .extend (['|' ] + base_docker_call + ['-i --entrypoint /bin/bash' , tools [i ], '-c \' {}\' ' .format (parameters [i ])])
136+ docker_call = " " .join (docker_call )
137+ _log .debug ("Calling docker with %s." % docker_call )
138+
139+ elif len (tools ) == 1 :
140+ # If tool is a list containing a single docker container name string
141+ # then format the docker call in the 'pipe-in-single-container' mode
142+ docker_call .extend (base_docker_call + ['--entrypoint /bin/bash' , tools [0 ], '-c \' {}\' ' .format (" | " .join (parameters ))])
143+ docker_call = " " .join (docker_call )
144+ _log .debug ("Calling docker with %s." % docker_call )
145+
146+ else :
147+ assert False
148+ else :
149+ docker_call = " " .join (base_docker_call + [tool ] + parameters )
150+ _log .debug ("Calling docker with %s." % docker_call )
88151
89- _log .debug ("Calling docker with %s." % " " .join (base_docker_call + [tool ] + parameters ))
90-
91- docker_call = base_docker_call + [tool ] + parameters
92-
152+
93153 try :
94154 if outfile :
95- subprocess .check_call (docker_call , stdout = outfile )
155+ subprocess .check_call (docker_call , stdout = outfile , shell = True )
96156 else :
97157 if check_output :
98- return subprocess .check_output (docker_call )
158+ if return_stderr :
159+ return subprocess .check_output (docker_call , shell = True , stderr = subprocess .STDOUT )
160+ else :
161+ return subprocess .check_output (docker_call , shell = True )
99162 else :
100- subprocess .check_call (docker_call )
163+ subprocess .check_call (docker_call , shell = True )
101164 # Fix root ownership of output files
102165 except :
103166 # Panic avoids hiding the exception raised in the try block
104167 with panic ():
105- _fix_permissions (base_docker_call , tool , work_dir )
168+ _fix_permissions (base_docker_call , tool , tools , work_dir )
106169 else :
107- _fix_permissions (base_docker_call , tool , work_dir )
170+ _fix_permissions (base_docker_call , tool , tools , work_dir )
108171
109172 for filename in outputs .keys ():
110173 if not os .path .isabs (filename ):
111174 filename = os .path .join (work_dir , filename )
112175 assert (os .path .isfile (filename ))
113176
114177
115- def _fix_permissions (base_docker_call , tool , work_dir ):
116- """
178+ def _fix_permissions (base_docker_call , tool , tools , work_dir ):
179+ """
117180 Fix permission of a mounted Docker directory by reusing the tool
118181
119182 :param list base_docker_call: Docker run parameters
@@ -122,5 +185,18 @@ def _fix_permissions(base_docker_call, tool, work_dir):
122185 """
123186 base_docker_call .append ('--entrypoint=chown' )
124187 stat = os .stat (work_dir )
125- command = base_docker_call + [tool ] + ['-R' , '{}:{}' .format (stat .st_uid , stat .st_gid ), '/data' ]
126- subprocess .check_call (command )
188+ if tools :
189+ command_list = []
190+ for tool in tools :
191+ command = base_docker_call + [tool ] + ['-R' , '{}:{}' .format (stat .st_uid , stat .st_gid ), '/data' ]
192+ command_list .append (command )
193+
194+ for command in command_list :
195+ subprocess .check_call (command )
196+ else :
197+ command = base_docker_call + [tool ] + ['-R' , '{}:{}' .format (stat .st_uid , stat .st_gid ), '/data' ]
198+ subprocess .check_call (command )
199+
200+
201+
202+
0 commit comments