@@ -15,7 +15,8 @@ def mock_mode():
1515 return True if int (os .environ .get ('TOIL_SCRIPTS_MOCK_MODE' , '0' )) else False
1616
1717
18- def docker_call (tool ,
18+ def docker_call (tool = '' ,
19+ tools = [],
1920 parameters = None ,
2021 work_dir = '.' ,
2122 rm = True ,
@@ -25,11 +26,14 @@ def docker_call(tool,
2526 outputs = None ,
2627 docker_parameters = None ,
2728 check_output = False ,
29+ return_stderr = False ,
2830 mock = None ):
2931 """
3032 Calls Docker, passing along parameters and tool.
3133
32- :param str tool: Name of the Docker image to be used (e.g. quay.io/ucsc_cgl/samtools)
34+ :param str tool: Name of the Docker image to be used (e.g. quay.io/ucsc_cgl/samtools)
35+ :param list[str] tools: str list of names of the Docker images and order to be used in
36+ adding piped commands to docker. (e.g. ['quay.io/ucsc_cgl/samtools', 'ubuntu'])
3337 :param list[str] parameters: Command line arguments to be passed to the tool
3438 :param str work_dir: Directory to mount into the container via `-v`. Destination convention is /data
3539 :param bool rm: Set to True to pass `--rm` flag.
@@ -41,8 +45,33 @@ def docker_call(tool,
4145 or a url. The value is only used if mock=True
4246 :param dict[str,str] docker_parameters: Parameters to pass to docker
4347 :param bool check_output: When True, this function returns docker's output
48+ :param bool return_stderr: When True, this function includes stderr in docker's output
4449 :param bool mock: Whether to run in mock mode. If this variable is unset, its value will be determined by
4550 the environment variable.
51+
52+ Piping docker commands can be done in one of two ways depending on use case:
53+ Running a pipe in docker in 'pipe-in-single-container' mode produces command structure
54+ docker '... | ... | ...' where each '...' command corresponds to each element in the 'parameters'
55+ argument that uses a docker container. This is the most efficient method if you want to run a pipe of
56+ commands where each command uses the same docker container.
57+
58+ Running a pipe in docker in 'pipe-of-containers' mode produces command structure
59+ docker '...' | docker '...' | docker '...' where each '...' command corresponds to each element in
60+ the 'parameters' argument that uses a docker container and each 'docker' tool in the pipe
61+ corresponds to each element in the 'tool' argument
62+
63+ Examples for running command 'head -c 1M </dev/urandom | tee >(md5sum 1>&2) | gzip | gunzip | md5sum 1>&2':
64+ Running 'pipe-in-single-container' mode:
65+ command= ['head -c 1M /dev/urandom | tee >(md5sum 1>&2)', 'gzip', 'gunzip', 'md5sum 1>&2']
66+ work_dir=curr_work_dir
67+ docker_tools=['ubuntu']
68+ stdout = docker_call(work_dir=docker_work_dir, parameters=command, tool=docker_tools, check_output=True)
69+
70+ Running 'pipe-of-containers' mode:
71+ command= ['head -c 1M /dev/urandom | tee >(md5sum 1>&2)', 'gzip', 'gunzip', 'md5sum 1>&2']
72+ work_dir=curr_work_dir
73+ docker_tools=['ubuntu', 'ubuntu', 'ubuntu', 'ubuntu']
74+ stdout = docker_call(work_dir=docker_work_dir, parameters=command, tool=docker_tools, check_output=True)
4675 """
4776 from toil_scripts .lib .urls import download_url
4877
@@ -83,36 +112,73 @@ def docker_call(tool,
83112 if env :
84113 for e , v in env .iteritems ():
85114 base_docker_call .extend (['-e' , '{}={}' .format (e , v )])
115+
86116 if docker_parameters :
87117 base_docker_call += docker_parameters
118+
119+ docker_call = []
120+
121+ run_pipe = False
88122
89- _log .debug ("Calling docker with %s." % " " .join (base_docker_call + [tool ] + parameters ))
90-
91- docker_call = base_docker_call + [tool ] + parameters
123+ if bool (tools ) == bool (tool ):
124+ raise Exception ('Either "tool" or "tools" must contain a value, but not both.' )
125+ if not tools :
126+ tools = [ tool ]
127+ else :
128+ run_pipe = True
129+
130+ # Pipe functionality
131+ # each element in the parameters list must represent a sub-pipe command
132+ shell_flag = True # Flag for running subprocess with string command or list command
133+ if run_pipe :
134+ command_list = []
135+ if len (tools ) > 1 :
136+ # If tool is a list containing multiple docker container name strings
137+ # then format the docker call in the 'pipe-of-containers' mode
138+ docker_call .extend (base_docker_call + ['--entrypoint /bin/bash' , tools [0 ], '-c \' {}\' ' .format (parameters [0 ])])
139+ for i in xrange (1 , len (tools )):
140+ docker_call .extend (['|' ] + base_docker_call + ['-i --entrypoint /bin/bash' , tools [i ], '-c \' {}\' ' .format (parameters [i ])])
141+ docker_call = " " .join (docker_call )
142+ _log .debug ("Calling docker with %s." % docker_call )
143+
144+ elif len (tools ) == 1 :
145+ # If tool is a list containing a single docker container name string
146+ # then format the docker call in the 'pipe-in-single-container' mode
147+ docker_call .extend (base_docker_call + ['--entrypoint /bin/bash' , tools [0 ], '-c \' {}\' ' .format (" | " .join (parameters ))])
148+ docker_call = " " .join (docker_call )
149+ _log .debug ("Calling docker with %s." % docker_call )
150+
151+ else :
152+ docker_call = " " .join (base_docker_call + tools + parameters )
153+ _log .debug ("Calling docker with %s." % docker_call )
92154
155+
93156 try :
94157 if outfile :
95- subprocess .check_call (docker_call , stdout = outfile )
158+ subprocess .check_call (docker_call , stdout = outfile , shell = shell_flag )
159+ elif check_output and return_stderr :
160+ return subprocess .check_output (docker_call , shell = shell_flag , stderr = subprocess .STDOUT )
161+ elif check_output :
162+ return subprocess .check_output (docker_call , shell = shell_flag )
163+ elif return_stderr :
164+ return subprocess .check_call (docker_call , stderr = subprocess .STDOUT , shell = shell_flag )
96165 else :
97- if check_output :
98- return subprocess .check_output (docker_call )
99- else :
100- subprocess .check_call (docker_call )
166+ subprocess .check_call (docker_call , shell = shell_flag )
101167 # Fix root ownership of output files
102168 except :
103169 # Panic avoids hiding the exception raised in the try block
104170 with panic ():
105- _fix_permissions (base_docker_call , tool , work_dir )
171+ _fix_permissions (base_docker_call , tools , work_dir )
106172 else :
107- _fix_permissions (base_docker_call , tool , work_dir )
173+ _fix_permissions (base_docker_call , tools , work_dir )
108174
109175 for filename in outputs .keys ():
110176 if not os .path .isabs (filename ):
111177 filename = os .path .join (work_dir , filename )
112178 assert (os .path .isfile (filename ))
113179
114180
115- def _fix_permissions (base_docker_call , tool , work_dir ):
181+ def _fix_permissions (base_docker_call , tools , work_dir ):
116182 """
117183 Fix permission of a mounted Docker directory by reusing the tool
118184
@@ -122,5 +188,15 @@ def _fix_permissions(base_docker_call, tool, work_dir):
122188 """
123189 base_docker_call .append ('--entrypoint=chown' )
124190 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 )
191+ command = []
192+ command_list = []
193+ for tool in tools :
194+ command = base_docker_call + [tool ] + ['-R' , '{}:{}' .format (stat .st_uid , stat .st_gid ), '/data' ]
195+ command_list .append (command )
196+
197+ for command in command_list :
198+ subprocess .check_call (command )
199+
200+
201+
202+
0 commit comments