Skip to content

Commit 1a7ff13

Browse files
author
Vasileios Karakasis
authored
Merge pull request #787 from rsarm/containerplatform
[feat] Add high-level API for container platforms
2 parents d8517e5 + 1521c31 commit 1a7ff13

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed

reframe/core/containers.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import abc
2+
3+
import reframe.core.fields as fields
4+
import reframe.utility.typecheck as typ
5+
from reframe.core.exceptions import ContainerError
6+
7+
8+
class ContainerPlatform(abc.ABC):
9+
"""The abstract base class of any container platform.
10+
11+
Concrete container platforms inherit from this class and must override the
12+
:func:`emit_prepare_cmds` and :func:`emit_launch_cmds` abstract functions.
13+
"""
14+
15+
registry = fields.TypedField('registry', str, type(None))
16+
image = fields.TypedField('image', str, type(None))
17+
requires_mpi = fields.TypedField('requires_mpi', bool)
18+
commands = fields.TypedField('commands', typ.List[str])
19+
mount_points = fields.TypedField('mount_points',
20+
typ.List[typ.Tuple[str, str]])
21+
workdir = fields.TypedField('workdir', str, type(None))
22+
23+
def __init__(self):
24+
self.registry = None
25+
self.image = None
26+
self.requires_mpi = False
27+
self.commands = []
28+
self.mount_points = []
29+
self.workdir = None
30+
31+
@abc.abstractmethod
32+
def emit_prepare_cmds(self):
33+
"""Returns commands that are necessary before running with this
34+
container platform.
35+
36+
:raises: `ContainerError` in case of errors.
37+
38+
.. note:
39+
This method is relevant only to developers of new container
40+
platforms.
41+
"""
42+
43+
@abc.abstractmethod
44+
def emit_launch_cmds(self):
45+
"""Returns the command for running with this container platform.
46+
47+
:raises: `ContainerError` in case of errors.
48+
49+
.. note:
50+
This method is relevant only to developers of new container
51+
platforms.
52+
"""
53+
if self.registry:
54+
self.image = '/'.join([self.registry, self.image])
55+
56+
@abc.abstractmethod
57+
def validate(self):
58+
"""Validates this container platform.
59+
60+
:raises: `ContainerError` in case of errors.
61+
62+
.. note:
63+
This method is relevant only to developers of new container
64+
platforms.
65+
"""
66+
if self.image is None:
67+
raise ContainerError('no image specified')
68+
69+
if not self.commands:
70+
raise ContainerError('no commands specified')
71+
72+
73+
class Docker(ContainerPlatform):
74+
"""An implementation of ContainerPlatform to run containers with Docker."""
75+
76+
def emit_prepare_cmds(self):
77+
pass
78+
79+
def emit_launch_cmds(self):
80+
super().emit_launch_cmds()
81+
docker_opts = ['-v "%s":"%s"' % mp for mp in self.mount_points]
82+
run_cmd = 'docker run %s %s bash -c ' % (' '.join(docker_opts),
83+
self.image)
84+
return run_cmd + "'" + '; '.join(
85+
['cd ' + self.workdir] + self.commands) + "'"
86+
87+
def validate(self):
88+
super().validate()
89+
90+
91+
class ContainerPlatformField(fields.TypedField):
92+
"""A field representing a container platforms.
93+
94+
You may either assign an instance of :class:`ContainerPlatform:` or a
95+
string representing the name of the concrete class of a container platform.
96+
"""
97+
98+
def __init__(self, fieldname, *other_types):
99+
super().__init__(fieldname, ContainerPlatform, *other_types)
100+
101+
def __set__(self, obj, value):
102+
if isinstance(value, str):
103+
try:
104+
value = globals()[value]()
105+
except KeyError:
106+
raise ValueError(
107+
'unknown container platform: %s' % value) from None
108+
109+
super().__set__(obj, value)

reframe/core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ class BuildSystemError(ReframeError):
108108
"""Raised when a build system is not configured properly."""
109109

110110

111+
class ContainerError(ReframeError):
112+
"""Raised when a container platform is not configured properly."""
113+
114+
111115
class BuildError(ReframeError):
112116
"""Raised when a build fails."""
113117

unittests/test_containers.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import abc
2+
import unittest
3+
4+
import pytest
5+
import reframe.core.containers as containers
6+
from reframe.core.exceptions import ContainerError
7+
8+
9+
class _ContainerPlatformTest(abc.ABC):
10+
@abc.abstractmethod
11+
def create_container_platform(self):
12+
pass
13+
14+
@property
15+
@abc.abstractmethod
16+
def exp_cmd_mount_points(self):
17+
pass
18+
19+
@property
20+
@abc.abstractmethod
21+
def exp_cmd_custom_registry(self):
22+
pass
23+
24+
def setUp(self):
25+
self.container_platform = self.create_container_platform()
26+
27+
def test_mount_points(self):
28+
self.container_platform.image = 'name:tag'
29+
self.container_platform.mount_points = [('/path/one', '/one'),
30+
('/path/two', '/two')]
31+
self.container_platform.commands = ['cmd1', 'cmd2']
32+
self.container_platform.workdir = '/stagedir'
33+
assert (self.exp_cmd_mount_points ==
34+
self.container_platform.emit_launch_cmds())
35+
36+
def test_missing_image(self):
37+
self.container_platform.commands = ['cmd']
38+
with pytest.raises(ContainerError):
39+
self.container_platform.validate()
40+
41+
def test_missing_commands(self):
42+
self.container_platform.image = 'name:tag'
43+
with pytest.raises(ContainerError):
44+
self.container_platform.validate()
45+
46+
def test_custom_registry(self):
47+
self.container_platform.registry = 'registry/custom'
48+
self.container_platform.image = 'name:tag'
49+
self.container_platform.commands = ['cmd']
50+
self.container_platform.mount_points = [('/path/one', '/one')]
51+
self.container_platform.workdir = '/stagedir'
52+
assert (self.exp_cmd_custom_registry ==
53+
self.container_platform.emit_launch_cmds())
54+
55+
56+
class TestDocker(_ContainerPlatformTest, unittest.TestCase):
57+
def create_container_platform(self):
58+
return containers.Docker()
59+
60+
@property
61+
def exp_cmd_mount_points(self):
62+
return ('docker run -v "/path/one":"/one" -v "/path/two":"/two" '
63+
"name:tag bash -c 'cd /stagedir; cmd1; cmd2'")
64+
65+
@property
66+
def exp_cmd_custom_registry(self):
67+
return ('docker run -v "/path/one":"/one" registry/custom/name:tag '
68+
"bash -c 'cd /stagedir; cmd'")

0 commit comments

Comments
 (0)