Skip to content

Commit 576042b

Browse files
committed
Support setting the mapping for build source path between container and host mechine
* Set the path mapping with global config *.yml file * If the mapping is not set on global config file, try to read the mapping from environment variable.
1 parent 43e2f77 commit 576042b

6 files changed

Lines changed: 164 additions & 19 deletions

File tree

apluslms_roman/backends/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1+
import logging
12
from collections import namedtuple
23
from collections.abc import Mapping
34

45
from ..observer import BuildObserver
5-
6+
from ..utils.path_mapping import get_host_path
67

78
BACKENDS = {
89
'docker': 'apluslms_roman.backends.docker.DockerBackend',
910
}
1011

11-
1212
BuildTask = namedtuple('BuildTask', [
1313
'path',
1414
'steps',
1515
])
1616

17+
logger = logging.getLogger(__name__)
18+
1719

1820
def clean_image_name(image):
1921
if ':' not in image:
@@ -112,3 +114,9 @@ def verify(self):
112114

113115
def version_info(self):
114116
pass
117+
118+
def remap_path(self, path):
119+
map_ = self.environment.environ.get('directory_map', {})
120+
logger.debug("get mapping from environment:%s", map_)
121+
map_ = dict(map_) if len(map_) == 0 else map_
122+
return get_host_path(path, map_)

apluslms_roman/backends/docker.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import docker
2-
from os.path import join
1+
import logging
2+
from os.path import join, expanduser
33

4+
import docker
45
from apluslms_yamlidator.utils.decorator import cached_property
56

67
from ..utils.translation import _
@@ -12,6 +13,8 @@
1213

1314
Mount = docker.types.Mount
1415

16+
logger = logging.getLogger(__name__)
17+
1518

1619
class DockerBackend(Backend):
1720
name = 'docker'
@@ -22,14 +25,28 @@ class DockerBackend(Backend):
2225
@cached_property
2326
def _client(self):
2427
env = self.environment.environ
25-
kwargs = {}
26-
version = env.get('DOCKER_VERSION', None)
27-
if version:
28-
kwargs['version'] = version
29-
timeout = env.get('DOCKER_TIMEOUT', None)
30-
if timeout:
31-
kwargs['timeout'] = timeout
32-
return docker.from_env(environment=env, **kwargs)
28+
params = {
29+
'base_url': env.get('host'),
30+
'version': env.get('version'),
31+
}
32+
if 'timeout' in env:
33+
params['timeout'] = env['timeout']
34+
35+
# false values: 0, false, '', unset
36+
# true values: 1, true, "yes"
37+
tls_verify = bool(env.get('tls_verify', False))
38+
cert_path = env.get('cert_path') or None
39+
if tls_verify or cert_path:
40+
if not cert_path:
41+
cert_path = join(expanduser('~'), '.docker')
42+
params['tls'] = docker.tls.TLSConfig(
43+
client_cert=(join(cert_path, 'cert.pem'), join(cert_path, 'key.pem')),
44+
ca_cert=join(cert_path, 'ca.pem'),
45+
verify=tls_verify,
46+
ssl_version=env.get('tls_ssl_version'),
47+
assert_hostname=tls_verify and env.get('tls_assert_hostname'),
48+
)
49+
return docker.DockerClient(**params)
3350

3451
def _run_opts(self, task, step):
3552
env = self.environment
@@ -41,9 +58,12 @@ def _run_opts(self, task, step):
4158
user='{}:{}'.format(env.uid, env.gid),
4259
)
4360

61+
path = self.remap_path(task.path)
62+
63+
logger.debug("Final path is:%s", path)
4464
# mounts and workdir
4565
if step.mnt:
46-
opts['mounts'] = [Mount(step.mnt, task.path, type='bind', read_only=False)]
66+
opts['mounts'] = [Mount(step.mnt, path, type='bind', read_only=False)]
4767
opts['working_dir'] = step.mnt
4868
else:
4969
wpath = self.WORK_PATH

apluslms_roman/builder.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .utils.importing import import_string
1010
from .utils.translation import _
1111

12+
1213
class Builder:
1314
def __init__(self, engine, config, observer=None):
1415
if not isdir(config.dir):
@@ -18,7 +19,6 @@ def __init__(self, engine, config, observer=None):
1819
self._engine = engine
1920
self._observer = observer or StreamObserver()
2021

21-
2222
def get_steps(self, refs: list = None):
2323
steps = [BuildStep.from_config(i, step)
2424
for i, step in enumerate(self.config.steps)]
@@ -33,7 +33,7 @@ def get_steps(self, refs: list = None):
3333
def build(self, step_refs: list = None):
3434
backend = self._engine.backend
3535
observer = self._observer
36-
steps = self.get_steps(step_refs) # NOTE: may raise KeyError or IndexError
36+
steps = self.get_steps(step_refs) # NOTE: may raise KeyError or IndexError
3737

3838
task = BuildTask(self.path, steps)
3939
observer.enter_prepare()
@@ -64,11 +64,9 @@ def __init__(self, backend_class=None, settings=None):
6464

6565
name = getattr(backend_class, 'name', None) or backend_class.__name__.lower()
6666
env_prefix = name.upper() + '_'
67-
env = {k: v for k, v in environ.items() if k.startswith(env_prefix)}
67+
env = {key: value for key, value in environ.items() if key.startswith(env_prefix)}
6868
if settings:
69-
for k, v in settings.get(name, {}).items():
70-
if v is not None and v != '':
71-
env[env_prefix + k.replace('-', '_').upper()] = v
69+
env.update(settings.get(name, {}))
7270
self._environment = Environment(getuid(), getegid(), env)
7371

7472
@cached_property

apluslms_roman/schemas/roman_settings-v1.0.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ properties:
2424
type: object
2525
additionalProperties: false
2626
properties:
27+
directory_map:
28+
title: docker container-host machine path mapping
29+
description: The dictornary mapping between docker and it's host
30+
type: object
2731
host:
2832
title: docker host
2933
description: the URL to the Docker host
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import json
2+
import logging
3+
import re
4+
from os import environ
5+
from pathlib import PurePosixPath
6+
7+
logger = logging.getLogger(__name__)
8+
json_re = re.compile(r'^(?:["[{]|(?:-?[1-9]\d*(?:\.\d+)?|null|true|false)$)')
9+
10+
11+
def get_host_path(original, mapping):
12+
ret = original
13+
orig_path = PurePosixPath(original)
14+
for k, v in mapping.items():
15+
try:
16+
logger.debug("Mapping:%s:%s", k, v)
17+
relative_path = orig_path.relative_to(k)
18+
ret = PurePosixPath(v).joinpath(relative_path)
19+
return str(ret)
20+
except ValueError:
21+
logger.warning("Error when composing new path!")
22+
pass
23+
return str(ret)
24+
25+
26+
def nest_dict(flat_dict, sep):
27+
ret = {}
28+
for key, value in flat_dict.items():
29+
key_list = key.split(sep, 1)
30+
if len(key_list) == 2:
31+
root = key_list[0]
32+
if root not in ret:
33+
ret[root] = {}
34+
ret[root][key_list[1]] = value
35+
else:
36+
ret[key] = value
37+
return ret
38+
39+
40+
def load_from_env(env_prefix=None, sep=None, decode_json=True):
41+
if decode_json:
42+
decode = lambda s: json.loads(s) if json_re.match(s) is not None else s
43+
else:
44+
decode = lambda s: s
45+
env = {key[len(env_prefix):].lower(): decode(value) for key, value in environ.items() if key.startswith(env_prefix)}
46+
if sep is not None:
47+
env = nest_dict(env, sep)
48+
return env

tests/utils/test_path_mapping.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import os
2+
import unittest
3+
from json import loads
4+
5+
from apluslms_roman.utils.path_mapping import json_re, load_from_env
6+
7+
test_case_loadable = (
8+
'true',
9+
'false',
10+
'null',
11+
'123',
12+
'-123',
13+
'3.14',
14+
'-3.14',
15+
'{"foo": "bar"}',
16+
'[1, 2, 3]',
17+
'"foo bar"'
18+
)
19+
20+
test_case_not_loadable = (
21+
"/foobar.py",
22+
"text",
23+
"yes",
24+
"0123123",
25+
)
26+
27+
28+
class TestJsonLoadable(unittest.TestCase):
29+
30+
def test_loadable_not_raise(self):
31+
for case in test_case_loadable:
32+
with self.subTest(non_json=case):
33+
loads(case)
34+
35+
def test_not_loadable_raise(self):
36+
for case in test_case_not_loadable:
37+
with self.subTest(non_json=case):
38+
with self.assertRaises(ValueError, msg="Testing:{}".format(case)):
39+
loads(case)
40+
41+
42+
class TestJsonRegex(unittest.TestCase):
43+
44+
def test_loadable_match(self):
45+
for case in test_case_loadable:
46+
with self.subTest(non_json=case):
47+
self.assertTrue(json_re.match(case)is not None, msg="Testing:{}".format(case))
48+
49+
def test_not_loadable_not_match(self):
50+
for case in test_case_not_loadable:
51+
with self.subTest(non_json=case):
52+
self.assertFalse(json_re.match(case) is not None, msg="Testing:{}".format(case))
53+
54+
55+
class TestLoadFromEnv(unittest.TestCase):
56+
57+
def test_with_decode_json(self):
58+
os.environ['DOCKER_FOO_BAR'] = '123'
59+
self.assertEqual({'foo': {'bar': '123'}}, load_from_env('DOCKER_', '_', False))
60+
61+
def test_without_decode_json(self):
62+
os.environ['DOCKER_FOO_BAR'] = '123'
63+
self.assertEqual({'foo': {'bar': 123}}, load_from_env('DOCKER_', '_', True))
64+
65+
def test_without_separation_char(self):
66+
os.environ['DOCKER_FOO_BAR'] = '123'
67+
self.assertEqual({'foo_bar': 123}, load_from_env('DOCKER_'))

0 commit comments

Comments
 (0)