Skip to content

Commit de509ba

Browse files
authored
Merge pull request #835 from common-workflow-language/strict-memory-limit
add --strict-memory-limit
2 parents 4617415 + cca93ba commit de509ba

File tree

3 files changed

+101
-68
lines changed

3 files changed

+101
-68
lines changed

cwltool/argparser.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
179179
exgroup.add_argument("--quiet", action="store_true", help="Only print warnings and errors.")
180180
exgroup.add_argument("--debug", action="store_true", help="Print even more logging")
181181

182+
parser.add_argument(
183+
"--strict-memory-limit", action="store_true", help="When running with "
184+
"software containers and the Docker engine, pass either the "
185+
"calculated memory allocation from ResourceRequirements or the "
186+
"default of 1 gigabyte to Docker's --memory option.")
187+
182188
parser.add_argument("--timestamps", action="store_true", help="Add "
183189
"timestamps to the errors, warnings, and "
184190
"notifications.")

cwltool/context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def make_tool_notimpl(toolpath_object, # type: Dict[Text, Any]
3333
): # type: (...) -> Process
3434
raise NotImplementedError()
3535

36+
3637
default_make_tool = make_tool_notimpl # type: Callable[[Dict[Text, Any], LoadingContext], Process]
3738

3839
class LoadingContext(ContextBase):
@@ -46,7 +47,7 @@ def __init__(self, kwargs=None):
4647
self.overrides_list = [] # type: List[Dict[Text, Any]]
4748
self.loader = None # type: Optional[Loader]
4849
self.avsc_names = None # type: Optional[schema.Names]
49-
self.disable_js_validation = False # type: bool
50+
self.disable_js_validation = False # type: bool
5051
self.js_hint_options_file = None
5152
self.do_validate = True # type: bool
5253
self.enable_dev = False # type: bool
@@ -72,7 +73,7 @@ def __init__(self, kwargs=None):
7273
# type: (Optional[Dict[str, Any]]) -> None
7374
select_resources_callable = Callable[ # pylint: disable=unused-variable
7475
[Dict[str, int], RuntimeContext], Dict[str, int]]
75-
self.user_space_docker_cmd = "" # type: Text
76+
self.user_space_docker_cmd = "" # type: Text
7677
self.secret_store = None # type: Optional[SecretStore]
7778
self.no_read_only = False # type: bool
7879
self.custom_net = "" # type: Text
@@ -115,6 +116,7 @@ def __init__(self, kwargs=None):
115116
self.eval_timeout = 20 # type: float
116117
self.postScatterEval = None # type: Optional[Callable[[Dict[Text, Any]], Dict[Text, Any]]]
117118
self.on_error = "stop" # type: Text
119+
self.strict_memory_limit = False # type: bool
118120

119121
self.record_container_id = None
120122
self.cidfile_dir = None

cwltool/docker.py

Lines changed: 91 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -72,78 +72,86 @@ def _check_docker_machine_path(path): # type: (Optional[Text]) -> None
7272

7373

7474
class DockerCommandLineJob(ContainerCommandLineJob):
75+
"""Runs a CommandLineJob in a sofware container using the Docker engine."""
7576

7677
@staticmethod
77-
def get_image(dockerRequirement, # type: Dict[Text, Text]
78+
def get_image(docker_requirement, # type: Dict[Text, Text]
7879
pull_image, # type: bool
7980
force_pull=False, # type: bool
8081
tmp_outdir_prefix=DEFAULT_TMP_PREFIX # type: Text
8182
): # type: (...) -> bool
83+
"""
84+
Retrieve the relevant Docker container image.
85+
86+
Returns True upon success
87+
"""
8288
found = False
8389

84-
if "dockerImageId" not in dockerRequirement and "dockerPull" in dockerRequirement:
85-
dockerRequirement["dockerImageId"] = dockerRequirement["dockerPull"]
90+
if "dockerImageId" not in docker_requirement \
91+
and "dockerPull" in docker_requirement:
92+
docker_requirement["dockerImageId"] = docker_requirement["dockerPull"]
8693

8794
with found_images_lock:
88-
if dockerRequirement["dockerImageId"] in found_images:
95+
if docker_requirement["dockerImageId"] in found_images:
8996
return True
9097

91-
for ln in subprocess.check_output(
98+
for line in subprocess.check_output(
9299
["docker", "images", "--no-trunc", "--all"]).decode('utf-8').splitlines():
93100
try:
94-
m = re.match(r"^([^ ]+)\s+([^ ]+)\s+([^ ]+)", ln)
95-
sp = dockerRequirement["dockerImageId"].split(":")
96-
if len(sp) == 1:
97-
sp.append("latest")
98-
elif len(sp) == 2:
99-
# if sp[1] doesn't match valid tag names, it is a part of repository
100-
if not re.match(r'[\w][\w.-]{0,127}', sp[1]):
101-
sp[0] = sp[0] + ":" + sp[1]
102-
sp[1] = "latest"
103-
elif len(sp) == 3:
104-
if re.match(r'[\w][\w.-]{0,127}', sp[2]):
105-
sp[0] = sp[0] + ":" + sp[1]
106-
sp[1] = sp[2]
107-
del sp[2]
101+
match = re.match(r"^([^ ]+)\s+([^ ]+)\s+([^ ]+)", line)
102+
split = docker_requirement["dockerImageId"].split(":")
103+
if len(split) == 1:
104+
split.append("latest")
105+
elif len(split) == 2:
106+
# if split[1] doesn't match valid tag names, it is a part of repository
107+
if not re.match(r'[\w][\w.-]{0,127}', split[1]):
108+
split[0] = split[0] + ":" + split[1]
109+
split[1] = "latest"
110+
elif len(split) == 3:
111+
if re.match(r'[\w][\w.-]{0,127}', split[2]):
112+
split[0] = split[0] + ":" + split[1]
113+
split[1] = split[2]
114+
del split[2]
108115

109116
# check for repository:tag match or image id match
110-
if (m and
111-
((sp[0] == m.group(1) and sp[1] == m.group(2)) or
112-
dockerRequirement["dockerImageId"] == m.group(3))):
117+
if (match and
118+
((split[0] == match.group(1) and split[1] == match.group(2)) or
119+
docker_requirement["dockerImageId"] == match.group(3))):
113120
found = True
114121
break
115122
except ValueError:
116123
pass
117124

118125
if (force_pull or not found) and pull_image:
119126
cmd = [] # type: List[Text]
120-
if "dockerPull" in dockerRequirement:
121-
cmd = ["docker", "pull", str(dockerRequirement["dockerPull"])]
127+
if "dockerPull" in docker_requirement:
128+
cmd = ["docker", "pull", str(docker_requirement["dockerPull"])]
122129
_logger.info(Text(cmd))
123130
subprocess.check_call(cmd, stdout=sys.stderr)
124131
found = True
125-
elif "dockerFile" in dockerRequirement:
132+
elif "dockerFile" in docker_requirement:
126133
dockerfile_dir = str(tempfile.mkdtemp(prefix=tmp_outdir_prefix))
127-
with open(os.path.join(dockerfile_dir, "Dockerfile"), "wb") as df:
128-
df.write(dockerRequirement["dockerFile"].encode('utf-8'))
134+
with open(os.path.join(
135+
dockerfile_dir, "Dockerfile"), "wb") as dfile:
136+
dfile.write(docker_requirement["dockerFile"].encode('utf-8'))
129137
cmd = ["docker", "build", "--tag=%s" %
130-
str(dockerRequirement["dockerImageId"]), dockerfile_dir]
138+
str(docker_requirement["dockerImageId"]), dockerfile_dir]
131139
_logger.info(Text(cmd))
132140
subprocess.check_call(cmd, stdout=sys.stderr)
133141
found = True
134-
elif "dockerLoad" in dockerRequirement:
142+
elif "dockerLoad" in docker_requirement:
135143
cmd = ["docker", "load"]
136144
_logger.info(Text(cmd))
137-
if os.path.exists(dockerRequirement["dockerLoad"]):
138-
_logger.info(u"Loading docker image from %s", dockerRequirement["dockerLoad"])
139-
with open(dockerRequirement["dockerLoad"], "rb") as f:
140-
loadproc = subprocess.Popen(cmd, stdin=f, stdout=sys.stderr)
145+
if os.path.exists(docker_requirement["dockerLoad"]):
146+
_logger.info(u"Loading docker image from %s", docker_requirement["dockerLoad"])
147+
with open(docker_requirement["dockerLoad"], "rb") as dload:
148+
loadproc = subprocess.Popen(cmd, stdin=dload, stdout=sys.stderr)
141149
else:
142150
loadproc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
143151
stdout=sys.stderr)
144152
assert loadproc.stdin is not None
145-
_logger.info(u"Sending GET request to %s", dockerRequirement["dockerLoad"])
146-
req = requests.get(dockerRequirement["dockerLoad"], stream=True)
153+
_logger.info(u"Sending GET request to %s", docker_requirement["dockerLoad"])
154+
req = requests.get(docker_requirement["dockerLoad"], stream=True)
147155
size = 0
148156
for chunk in req.iter_content(1024 * 1024):
149157
size += len(chunk)
@@ -152,18 +160,19 @@ def get_image(dockerRequirement, # type: Dict[Text, Text]
152160
loadproc.stdin.close()
153161
rcode = loadproc.wait()
154162
if rcode != 0:
155-
raise WorkflowException("Docker load returned non-zero exit status %i" % (rcode))
163+
raise WorkflowException(
164+
"Docker load returned non-zero exit status %i" % (rcode))
156165
found = True
157-
elif "dockerImport" in dockerRequirement:
158-
cmd = ["docker", "import", str(dockerRequirement["dockerImport"]),
159-
str(dockerRequirement["dockerImageId"])]
166+
elif "dockerImport" in docker_requirement:
167+
cmd = ["docker", "import", str(docker_requirement["dockerImport"]),
168+
str(docker_requirement["dockerImageId"])]
160169
_logger.info(Text(cmd))
161170
subprocess.check_call(cmd, stdout=sys.stderr)
162171
found = True
163172

164173
if found:
165174
with found_images_lock:
166-
found_images.add(dockerRequirement["dockerImageId"])
175+
found_images.add(docker_requirement["dockerImageId"])
167176

168177
return found
169178

@@ -178,10 +187,10 @@ def get_from_requirements(self,
178187
errmsg = None
179188
try:
180189
subprocess.check_output(["docker", "version"])
181-
except subprocess.CalledProcessError as e:
182-
errmsg = "Cannot communicate with docker daemon: " + Text(e)
183-
except OSError as e:
184-
errmsg = "'docker' executable not found: " + Text(e)
190+
except subprocess.CalledProcessError as err:
191+
errmsg = "Cannot communicate with docker daemon: " + Text(err)
192+
except OSError as err:
193+
errmsg = "'docker' executable not found: " + Text(err)
185194

186195
if errmsg:
187196
if req:
@@ -191,18 +200,18 @@ def get_from_requirements(self,
191200

192201
if self.get_image(r, pull_image, force_pull, tmp_outdir_prefix):
193202
return r["dockerImageId"]
194-
else:
195-
if req:
196-
raise WorkflowException(u"Docker image %s not found" % r["dockerImageId"])
203+
if req:
204+
raise WorkflowException(u"Docker image %s not found" % r["dockerImageId"])
197205

198206
return None
199207

200208
def add_volumes(self, pathmapper, runtime, secret_store=None):
201209
# type: (PathMapper, List[Text], SecretStore) -> None
210+
"""Append volume mappings to the runtime option list."""
202211

203212
host_outdir = self.outdir
204213
container_outdir = self.builder.outdir
205-
for src, vol in pathmapper.items():
214+
for _, vol in pathmapper.items():
206215
if not vol.staged:
207216
continue
208217
host_outdir_tgt = None # type: Optional[Text]
@@ -256,12 +265,12 @@ def add_volumes(self, pathmapper, runtime, secret_store=None):
256265
else:
257266
contents = vol.resolved
258267
if host_outdir_tgt:
259-
with open(host_outdir_tgt, "wb") as f:
260-
f.write(contents.encode("utf-8"))
268+
with open(host_outdir_tgt, "wb") as file_literal:
269+
file_literal.write(contents.encode("utf-8"))
261270
else:
262-
fd, createtmp = tempfile.mkstemp(dir=self.tmpdir)
263-
with os.fdopen(fd, "wb") as f:
264-
f.write(contents.encode("utf-8"))
271+
tmp_fd, createtmp = tempfile.mkstemp(dir=self.tmpdir)
272+
with os.fdopen(tmp_fd, "wb") as file_literal:
273+
file_literal.write(contents.encode("utf-8"))
265274
runtime.append(u"--volume=%s:%s:rw" % (
266275
docker_windows_path_adjust(os.path.realpath(createtmp)),
267276
vol.target))
@@ -270,7 +279,12 @@ def create_runtime(self, env, runtimeContext):
270279
# type: (MutableMapping[Text, Text], RuntimeContext) -> List
271280
user_space_docker_cmd = runtimeContext.user_space_docker_cmd
272281
if user_space_docker_cmd:
273-
runtime = [user_space_docker_cmd, u"run"]
282+
if 'udocker' in user_space_docker_cmd and not runtimeContext.debug:
283+
runtime = [user_space_docker_cmd, u"--quiet", u"run"]
284+
# udocker 1.1.1 will output diagnostic messages to stdout
285+
# without this
286+
else:
287+
runtime = [user_space_docker_cmd, u"run"]
274288
else:
275289
runtime = [u"docker", u"run", u"-i"]
276290

@@ -325,27 +339,38 @@ def create_runtime(self, env, runtimeContext):
325339

326340
# add parameters to docker to write a container ID file
327341
if runtimeContext.record_container_id:
328-
if runtimeContext.cidfile_dir != "":
329-
if not os.path.isdir(runtimeContext.cidfile_dir):
330-
_logger.error("--cidfile-dir %s error:\n%s", runtimeContext.cidfile_dir,
331-
runtimeContext.cidfile_dir + " is not a directory or "
332-
"directory doesn't exist, please check it first")
342+
cidfile_dir = runtimeContext.cidfile_
343+
if cidfile_dir != "":
344+
if not os.path.isdir(cidfile_dir):
345+
_logger.error("--cidfile-dir %s error:\n%s", cidfile_dir,
346+
cidfile_dir + "%s is not a directory or "
347+
"directory doesn't exist, please check it first")
333348
exit(2)
334-
if not os.path.exists(runtimeContext.cidfile_dir):
335-
_logger.error("--cidfile-dir %s error:\n%s", runtimeContext.cidfile_dir,
349+
if not os.path.exists(cidfile_dir):
350+
_logger.error("--cidfile-dir %s error:\n%s", cidfile_dir,
336351
"directory doesn't exist, please create it first")
337352
exit(2)
338353
else:
339354
cidfile_dir = os.getcwd()
340355
cidfile_name = datetime.datetime.now().strftime("%Y%m%d%H%M%S-%f") + ".cid"
341356
if runtimeContext.cidfile_prefix != "":
342357
cidfile_name = str(runtimeContext.cidfile_prefix + "-" + cidfile_name)
343-
cidfile_path = os.path.join(runtimeContext.cidfile_dir, cidfile_name)
358+
cidfile_path = os.path.join(cidfile_dir, cidfile_name)
344359
runtime.append(u"--cidfile=%s" % cidfile_path)
345360

346-
for t, v in self.environment.items():
347-
runtime.append(u"--env=%s=%s" % (t, v))
348-
349-
runtime.append("--memory=%dm" % self.builder.resources["ram"])
361+
for key, value in self.environment.items():
362+
runtime.append(u"--env=%s=%s" % (key, value))
363+
364+
if runtimeContext.strict_memory_limit and not user_space_docker_cmd:
365+
runtime.append("--memory=%dm" % self.builder.resources["ram"])
366+
elif not user_space_docker_cmd:
367+
res_req = self.builder.get_requirement("ResourceRequirement")[0]
368+
if res_req and ("ramMin" in res_req or "ramMax" is res_req):
369+
_logger.warning(
370+
u"[job %s] Skipping Docker software container '--memory' limit "
371+
"despite presence of ResourceRequirement with ramMin "
372+
"and/or ramMax setting. Consider running with "
373+
"--strict-memory-limit for increased portability "
374+
"assurance.", self.name)
350375

351376
return runtime

0 commit comments

Comments
 (0)