-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathcontainers_run.py
More file actions
176 lines (152 loc) · 6.59 KB
/
Copy pathcontainers_run.py
File metadata and controls
176 lines (152 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""Drop-in replacement for `datalad run` for command execution in a container"""
__docformat__ = 'restructuredtext'
import logging
import os.path as op
from datalad.interface.base import Interface
from datalad.interface.base import build_doc
from datalad.support.param import Parameter
from datalad.distribution.dataset import datasetmethod
from datalad.distribution.dataset import require_dataset, get_dataset_root
from datalad.interface.base import eval_results
from datalad.utils import ensure_iter
from datalad.interface.results import get_status_dict
from datalad.core.local.run import (
Run,
get_command_pwds,
normalize_command,
run_command,
)
from datalad_container.find_container import find_container_
lgr = logging.getLogger("datalad.containers.containers_run")
# Environment variable to be set during execution to possibly
# inform underlying shim scripts about the original name of
# the container
CONTAINER_NAME_ENVVAR = 'DATALAD_CONTAINER_NAME'
_run_params = dict(
Run._params_,
container_name=Parameter(
args=('-n', '--container-name',),
metavar="NAME",
doc="""Specify the name of or a path to a known container to use
for execution, in case multiple containers are configured."""),
)
@build_doc
# all commands must be derived from Interface
class ContainersRun(Interface):
# first docstring line is used a short description in the cmdline help
# the rest is put in the verbose help and manpage
"""Drop-in replacement of 'run' to perform containerized command execution
Container(s) need to be configured beforehand (see containers-add). If no
container is specified and only one container is configured in the current
dataset, it will be selected automatically. If more than one container is
registered in the current dataset or to access containers from subdatasets,
the container has to be specified.
A command is generated based on the input arguments such that the
container image itself will be recorded as an input dependency of
the command execution in the `run` record in the git history.
During execution the environment variable {name_envvar} is set to the
name of the used container.
"""
_docs_ = dict(
name_envvar=CONTAINER_NAME_ENVVAR
)
_params_ = _run_params
@staticmethod
@datasetmethod(name='containers_run')
@eval_results
def __call__(cmd, container_name=None, dataset=None,
inputs=None, outputs=None, message=None, expand=None,
explicit=False, sidecar=None):
from unittest.mock import patch # delayed, since takes long (~600ms for yoh)
pwd, _ = get_command_pwds(dataset)
ds = require_dataset(dataset, check_installed=True,
purpose='run a containerized command execution')
container = None
for res in find_container_(ds, container_name):
if res.get("action") == "containers":
container = res
else:
yield res
assert container, "bug: container should always be defined here"
# container record would contain path to the (sub)dataset containing
# it. If not - take current dataset, as it must be coming from it
cont_dspath = op.relpath(container.get('parentds', ds.path), pwd)
image_path = container["path"]
# container definition might point to an image in some nested dataset.
# it might be useful to be distinguish between the two in such cases
image_dspath = op.relpath(get_dataset_root(image_path), pwd)
image_path = op.relpath(image_path, pwd)
# sure we could check whether the container image is present,
# but it might live in a subdataset that isn't even installed yet
# let's leave all this business to `get` that is called by `run`
common_kwargs = dict(
cont_dspath=cont_dspath,
img_dspath=image_dspath,
img_dirpath=op.dirname(image_path) or ".",
)
cmd = normalize_command(cmd)
# expand the command with container execution
if 'cmdexec' in container:
callspec = container['cmdexec']
# Temporary kludge to give a more helpful message
if callspec.startswith("["):
import json
try:
json.loads(callspec)
except json.JSONDecodeError:
pass # Never mind, false positive.
else:
raise ValueError(
'cmdexe {!r} is in an old, unsupported format. '
'Convert it to a plain string.'.format(callspec))
cmd_kwargs = dict(
img=image_path,
cmd=cmd,
**common_kwargs,
)
try:
cmd = callspec.format(**cmd_kwargs)
except KeyError as exc:
yield get_status_dict(
'run',
ds=ds,
status='error',
message=(
'Unrecognized cmdexec placeholder: %s. '
'See containers-add for information on known ones: %s',
exc,
", ".join(cmd_kwargs)))
return
else:
# just prepend and pray
cmd = container['path'] + ' ' + cmd
extra_inputs = []
for extra_input in ensure_iter(container.get("extra-input", []), set):
try:
extra_inputs.append(extra_input.format(**common_kwargs))
except KeyError as exc:
yield get_status_dict(
'run',
ds=ds,
status='error',
message=(
'Unrecognized extra_input placeholder: %s. '
'See containers-add for information on known ones: %s',
exc,
", ".join(common_kwargs)))
return
lgr.debug("extra_inputs = %r", extra_inputs)
with patch.dict('os.environ',
{CONTAINER_NAME_ENVVAR: container['name']}):
# fire!
for r in run_command(
cmd=cmd,
dataset=dataset or (ds if ds.path == pwd else None),
inputs=inputs,
extra_inputs=[image_path] + extra_inputs,
outputs=outputs,
message=message,
expand=expand,
explicit=explicit,
sidecar=sidecar):
yield r