Skip to content

Commit 3d8ee40

Browse files
tetronAnton Khodak
authored and
Anton Khodak
committed
Implement targeted "overrides" of requirements on specific tools (#440)
* Apply requirement overrides at load time * Override identifiers are relative to workflow, other identifiers are relative to job document. * Update documentation
1 parent b3ee659 commit 3d8ee40

18 files changed

+386
-83
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pydocstyle_report.txt: $(PYSOURCES)
8989
pydocstyle setup.py $^ > pydocstyle_report.txt 2>&1 || true
9090

9191
diff_pydocstyle_report: pydocstyle_report.txt
92-
diff-quality --violations=pep8 $^
92+
diff-quality --violations=pycodestyle $^
9393

9494
## autopep8 : fix most Python code indentation and formatting
9595
autopep8: $(PYSOURCES)

README.rst

+59-15
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ and ``--tmp-outdir-prefix`` to somewhere under ``/Users``::
106106
.. |Build Status| image:: https://ci.commonwl.org/buildStatus/icon?job=cwltool-conformance
107107
:target: https://ci.commonwl.org/job/cwltool-conformance/
108108

109+
Running user-space implementations of Docker
110+
--------------------------------------------
111+
112+
Some compute environments disallow user-space installation of Docker due to incompatiblities in libraries or to meet security requirements. The CWL reference supports using a user space implementation with the `--user-space-docker-cmd` option.
113+
114+
Example using `dx-docker` (https://wiki.dnanexus.com/Developer-Tutorials/Using-Docker-Images):
115+
116+
For use on Linux, install the DNAnexus toolkit (see https://wiki.dnanexus.com/Downloads for instructions).
117+
118+
Run `cwltool` just as you normally would, but with the new option, e.g. from the conformance tests:
119+
120+
.. code:: bash
121+
122+
cwltool --user-space-docker-cmd=dx-docker --outdir=/tmp/tmpidytmp v1.0/test-cwl-out2.cwl v1.0/empty.json
123+
109124
Tool or workflow loading from remote or local locations
110125
-------------------------------------------------------
111126

@@ -369,6 +384,50 @@ at the following links:
369384
- `Specifications - Implementation <https://github.com/galaxyproject/galaxy/commit/81d71d2e740ee07754785306e4448f8425f890bc>`__
370385
- `Initial cwltool Integration Pull Request <https://github.com/common-workflow-language/cwltool/pull/214>`__
371386

387+
Overriding workflow requirements at load time
388+
---------------------------------------------
389+
390+
Sometimes a workflow needs additional requirements to run in a particular
391+
environment or with a particular dataset. To avoid the need to modify the
392+
underlying workflow, cwltool supports requirement "overrides".
393+
394+
The format of the "overrides" object is a mapping of item identifier (workflow,
395+
workflow step, or command line tool) followed by a list of ProcessRequirements
396+
that should be applied.
397+
398+
.. code:: yaml
399+
400+
cwltool:overrides:
401+
echo.cwl:
402+
- class: EnvVarRequirement
403+
envDef:
404+
MESSAGE: override_value
405+
406+
407+
Overrides can be specified either on the command line, or as part of the job
408+
input document. Workflow steps are identified using the name of the workflow
409+
file followed by the step name as a document fragment identifier "#id".
410+
Override identifiers are relative to the toplevel workflow document.
411+
412+
.. code:: bash
413+
414+
cwltool --overrides overrides.yml my-tool.cwl my-job.yml
415+
416+
.. code:: yaml
417+
418+
input_parameter1: value1
419+
input_parameter2: value2
420+
cwltool:overrides:
421+
workflow.cwl#step1:
422+
- class: EnvVarRequirement
423+
envDef:
424+
MESSAGE: override_value
425+
426+
.. code:: bash
427+
428+
cwltool my-tool.cwl my-job-with-overrides.yml
429+
430+
372431
CWL Tool Control Flow
373432
---------------------
374433

@@ -500,18 +559,3 @@ logger_handler
500559
logging.Handler
501560

502561
Handler object for logging.
503-
504-
Running user-space implementations of Docker
505-
--------------------------------------------
506-
507-
Some compute environments disallow user-space installation of Docker due to incompatiblities in libraries or to meet security requirements. The CWL reference supports using a user space implementation with the `--user-space-docker-cmd` option.
508-
509-
Example using `dx-docker` (https://wiki.dnanexus.com/Developer-Tutorials/Using-Docker-Images):
510-
511-
For use on Linux, install the DNAnexus toolkit (see https://wiki.dnanexus.com/Downloads for instructions).
512-
513-
Run `cwltool` just as you normally would, but with the new option, e.g. from the conformance tests:
514-
515-
```
516-
cwltool --user-space-docker-cmd=dx-docker --outdir=/tmp/tmpidytmp v1.0/test-cwl-out2.cwl v1.0/empty.json
517-
```

cwltool/load_tool.py

+79-22
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import uuid
99
import hashlib
1010
import json
11-
from typing import Any, Callable, Dict, List, Text, Tuple, Union, cast
11+
import copy
12+
from typing import Any, Callable, Dict, List, Text, Tuple, Union, cast, Iterable
1213

1314
import requests.sessions
1415
from six import itervalues, string_types
@@ -23,19 +24,65 @@
2324

2425
from . import process, update
2526
from .errors import WorkflowException
26-
from .process import Process, shortname
27+
from .process import Process, shortname, get_schema
2728
from .update import ALLUPDATES
2829

2930
_logger = logging.getLogger("cwltool")
3031

3132
jobloaderctx = {
3233
u"cwl": "https://w3id.org/cwl/cwl#",
34+
u"cwltool": "http://commonwl.org/cwltool#",
3335
u"path": {u"@type": u"@id"},
3436
u"location": {u"@type": u"@id"},
3537
u"format": {u"@type": u"@id"},
3638
u"id": u"@id"
3739
}
3840

41+
42+
overrides_ctx = {
43+
u"overrideTarget": {u"@type": u"@id"},
44+
u"cwltool": "http://commonwl.org/cwltool#",
45+
u"overrides": {
46+
"@id": "cwltool:overrides",
47+
"mapSubject": "overrideTarget",
48+
"mapPredicate": "override"
49+
},
50+
u"override": {
51+
"@id": "cwltool:override",
52+
"mapSubject": "class"
53+
}
54+
} # type: Dict[Text, Union[Dict[Any, Any], Text, Iterable[Text]]]
55+
56+
def resolve_tool_uri(argsworkflow, # type: Text
57+
resolver=None, # type: Callable[[Loader, Union[Text, Dict[Text, Any]]], Text]
58+
fetcher_constructor=None,
59+
# type: Callable[[Dict[Text, Text], requests.sessions.Session], Fetcher]
60+
document_loader=None # type: Loader
61+
):
62+
# type: (...) -> Tuple[Text, Text]
63+
64+
uri = None # type: Text
65+
split = urllib.parse.urlsplit(argsworkflow)
66+
# In case of Windows path, urlsplit misjudge Drive letters as scheme, here we are skipping that
67+
if split.scheme and split.scheme in [u'http',u'https',u'file']:
68+
uri = argsworkflow
69+
elif os.path.exists(os.path.abspath(argsworkflow)):
70+
uri = file_uri(str(os.path.abspath(argsworkflow)))
71+
elif resolver:
72+
if document_loader is None:
73+
document_loader = Loader(jobloaderctx, fetcher_constructor=fetcher_constructor) # type: ignore
74+
uri = resolver(document_loader, argsworkflow)
75+
76+
if uri is None:
77+
raise ValidationException("Not found: '%s'" % argsworkflow)
78+
79+
if argsworkflow != uri:
80+
_logger.info("Resolved '%s' to '%s'", argsworkflow, uri)
81+
82+
fileuri = urllib.parse.urldefrag(uri)[0]
83+
return uri, fileuri
84+
85+
3986
def fetch_document(argsworkflow, # type: Union[Text, Dict[Text, Any]]
4087
resolver=None, # type: Callable[[Loader, Union[Text, Dict[Text, Any]]], Text]
4188
fetcher_constructor=None
@@ -49,22 +96,7 @@ def fetch_document(argsworkflow, # type: Union[Text, Dict[Text, Any]]
4996
uri = None # type: Text
5097
workflowobj = None # type: CommentedMap
5198
if isinstance(argsworkflow, string_types):
52-
split = urllib.parse.urlsplit(argsworkflow)
53-
# In case of Windows path, urlsplit misjudge Drive letters as scheme, here we are skipping that
54-
if split.scheme and split.scheme in [u'http',u'https',u'file']:
55-
uri = argsworkflow
56-
elif os.path.exists(os.path.abspath(argsworkflow)):
57-
uri = file_uri(str(os.path.abspath(argsworkflow)))
58-
elif resolver:
59-
uri = resolver(document_loader, argsworkflow)
60-
61-
if uri is None:
62-
raise ValidationException("Not found: '%s'" % argsworkflow)
63-
64-
if argsworkflow != uri:
65-
_logger.info("Resolved '%s' to '%s'", argsworkflow, uri)
66-
67-
fileuri = urllib.parse.urldefrag(uri)[0]
99+
uri, fileuri = resolve_tool_uri(argsworkflow, resolver=resolver, document_loader=document_loader)
68100
workflowobj = document_loader.fetch(fileuri)
69101
elif isinstance(argsworkflow, dict):
70102
uri = "#" + Text(id(argsworkflow))
@@ -139,8 +171,9 @@ def validate_document(document_loader, # type: Loader
139171
strict=True, # type: bool
140172
preprocess_only=False, # type: bool
141173
fetcher_constructor=None,
142-
skip_schemas=None
174+
skip_schemas=None,
143175
# type: Callable[[Dict[Text, Text], requests.sessions.Session], Fetcher]
176+
overrides=None # type: List[Dict]
144177
):
145178
# type: (...) -> Tuple[Loader, Names, Union[Dict[Text, Any], List[Dict[Text, Any]]], Dict[Text, Any], Text]
146179
"""Validate a CWL document."""
@@ -155,9 +188,15 @@ def validate_document(document_loader, # type: Loader
155188

156189
jobobj = None
157190
if "cwl:tool" in workflowobj:
158-
jobobj, _ = document_loader.resolve_all(workflowobj, uri)
191+
job_loader = Loader(jobloaderctx, fetcher_constructor=fetcher_constructor) # type: ignore
192+
jobobj, _ = job_loader.resolve_all(workflowobj, uri)
159193
uri = urllib.parse.urljoin(uri, workflowobj["https://w3id.org/cwl/cwl#tool"])
160194
del cast(dict, jobobj)["https://w3id.org/cwl/cwl#tool"]
195+
196+
if "http://commonwl.org/cwltool#overrides" in jobobj:
197+
overrides.extend(resolve_overrides(jobobj, uri, uri))
198+
del jobobj["http://commonwl.org/cwltool#overrides"]
199+
161200
workflowobj = fetch_document(uri, fetcher_constructor=fetcher_constructor)[1]
162201

163202
fileuri = urllib.parse.urldefrag(uri)[0]
@@ -225,6 +264,9 @@ def validate_document(document_loader, # type: Loader
225264
if jobobj:
226265
metadata[u"cwl:defaults"] = jobobj
227266

267+
if overrides:
268+
metadata[u"cwltool:overrides"] = overrides
269+
228270
return document_loader, avsc_names, processobj, metadata, uri
229271

230272

@@ -277,14 +319,29 @@ def load_tool(argsworkflow, # type: Union[Text, Dict[Text, Any]]
277319
enable_dev=False, # type: bool
278320
strict=True, # type: bool
279321
resolver=None, # type: Callable[[Loader, Union[Text, Dict[Text, Any]]], Text]
280-
fetcher_constructor=None # type: Callable[[Dict[Text, Text], requests.sessions.Session], Fetcher]
322+
fetcher_constructor=None, # type: Callable[[Dict[Text, Text], requests.sessions.Session], Fetcher]
323+
overrides=None
281324
):
282325
# type: (...) -> Process
283326

284327
document_loader, workflowobj, uri = fetch_document(argsworkflow, resolver=resolver,
285328
fetcher_constructor=fetcher_constructor)
286329
document_loader, avsc_names, processobj, metadata, uri = validate_document(
287330
document_loader, workflowobj, uri, enable_dev=enable_dev,
288-
strict=strict, fetcher_constructor=fetcher_constructor)
331+
strict=strict, fetcher_constructor=fetcher_constructor,
332+
overrides=overrides)
289333
return make_tool(document_loader, avsc_names, metadata, uri,
290334
makeTool, kwargs if kwargs else {})
335+
336+
def resolve_overrides(ov, ov_uri, baseurl): # type: (CommentedMap, Text, Text) -> List[Dict[Text, Any]]
337+
ovloader = Loader(overrides_ctx)
338+
ret, _ = ovloader.resolve_all(ov, baseurl)
339+
if not isinstance(ret, CommentedMap):
340+
raise Exception("Expected CommentedMap, got %s" % type(ret))
341+
cwl_docloader = get_schema("v1.0")[0]
342+
cwl_docloader.resolve_all(ret, ov_uri)
343+
return ret["overrides"]
344+
345+
def load_overrides(ov, base_url): # type: (Text, Text) -> List[Dict[Text, Any]]
346+
ovloader = Loader(overrides_ctx)
347+
return resolve_overrides(ovloader.fetch(ov), ov, base_url)

0 commit comments

Comments
 (0)