Selaa lähdekoodia

Add support for seccomp files

Signed-off-by: Joffrey F <[email protected]>
Joffrey F 7 vuotta sitten
vanhempi
sitoutus
a6c31b80fe

+ 1 - 1
compose/cli/docker_client.py

@@ -13,9 +13,9 @@ from docker.utils.config import home_dir
 
 from ..config.environment import Environment
 from ..const import HTTP_TIMEOUT
+from ..utils import unquote_path
 from .errors import UserError
 from .utils import generate_user_agent
-from .utils import unquote_path
 
 log = logging.getLogger(__name__)
 

+ 0 - 8
compose/cli/utils.py

@@ -131,14 +131,6 @@ def generate_user_agent():
     return " ".join(parts)
 
 
-def unquote_path(s):
-    if not s:
-        return s
-    if s[0] == '"' and s[-1] == '"':
-        return s[1:-1]
-    return s
-
-
 def human_readable_file_size(size):
     suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', ]
     order = int(math.log(size, 2) / 10) if size else 0

+ 13 - 2
compose/config/config.py

@@ -40,6 +40,7 @@ from .sort_services import sort_service_dicts
 from .types import MountSpec
 from .types import parse_extra_hosts
 from .types import parse_restart_spec
+from .types import SecurityOpt
 from .types import ServiceLink
 from .types import ServicePort
 from .types import VolumeFromSpec
@@ -734,9 +735,9 @@ def process_service(service_config):
         if field in service_dict:
             service_dict[field] = to_list(service_dict[field])
 
-    service_dict = process_blkio_config(process_ports(
+    service_dict = process_security_opt(process_blkio_config(process_ports(
         process_healthcheck(service_dict)
-    ))
+    )))
 
     return service_dict
 
@@ -1376,6 +1377,16 @@ def split_path_mapping(volume_path):
         return (volume_path, None)
 
 
+def process_security_opt(service_dict):
+    security_opts = service_dict.get('security_opt', [])
+    result = []
+    for value in security_opts:
+        result.append(SecurityOpt.parse(value))
+    if result:
+        service_dict['security_opt'] = result
+    return service_dict
+
+
 def join_path_mapping(pair):
     (container, host) = pair
     if isinstance(host, dict):

+ 1 - 0
compose/config/serialize.py

@@ -42,6 +42,7 @@ def serialize_string(dumper, data):
 yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
 yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
+yaml.SafeDumper.add_representer(types.SecurityOpt, serialize_config_type)
 yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)

+ 29 - 0
compose/config/types.py

@@ -4,6 +4,7 @@ Types for objects parsed from the configuration.
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+import json
 import ntpath
 import os
 import re
@@ -13,6 +14,7 @@ import six
 from docker.utils.ports import build_port_bindings
 
 from ..const import COMPOSEFILE_V1 as V1
+from ..utils import unquote_path
 from .errors import ConfigurationError
 from compose.const import IS_WINDOWS_PLATFORM
 from compose.utils import splitdrive
@@ -457,3 +459,30 @@ def normalize_port_dict(port):
         external_ip=port.get('external_ip', ''),
         has_ext_ip=(':' if port.get('external_ip') else ''),
     )
+
+
+class SecurityOpt(namedtuple('_SecurityOpt', 'value src_file')):
+    @classmethod
+    def parse(cls, value):
+        # based on https://github.com/docker/cli/blob/9de1b162f/cli/command/container/opts.go#L673-L697
+        con = value.split('=', 2)
+        if len(con) == 1 and con[0] != 'no-new-privileges':
+            if ':' not in value:
+                raise ConfigurationError('Invalid security_opt: {}'.format(value))
+            con = value.split(':', 2)
+
+        if con[0] == 'seccomp' and con[1] != 'unconfined':
+            try:
+                with open(unquote_path(con[1]), 'r') as f:
+                    seccomp_data = json.load(f)
+            except (IOError, ValueError) as e:
+                raise ConfigurationError('Error reading seccomp profile: {}'.format(e))
+            return cls(
+                'seccomp={}'.format(json.dumps(seccomp_data)), con[1]
+            )
+        return cls(value, None)
+
+    def repr(self):
+        if self.src_file is not None:
+            return 'seccomp:{}'.format(self.src_file)
+        return self.value

+ 5 - 1
compose/service.py

@@ -881,6 +881,10 @@ class Service(object):
             init_path = options.get('init')
             options['init'] = True
 
+        security_opt = [
+            o.value for o in options.get('security_opt')
+        ] if options.get('security_opt') else None
+
         nano_cpus = None
         if 'cpus' in options:
             nano_cpus = int(options.get('cpus') * NANOCPUS_SCALE)
@@ -910,7 +914,7 @@ class Service(object):
             extra_hosts=options.get('extra_hosts'),
             read_only=options.get('read_only'),
             pid_mode=self.pid_mode.mode,
-            security_opt=options.get('security_opt'),
+            security_opt=security_opt,
             ipc_mode=options.get('ipc'),
             cgroup_parent=options.get('cgroup_parent'),
             cpu_quota=options.get('cpu_quota'),

+ 8 - 0
compose/utils.py

@@ -143,3 +143,11 @@ def parse_bytes(n):
         return sdk_parse_bytes(n)
     except DockerException:
         return None
+
+
+def unquote_path(s):
+    if not s:
+        return s
+    if s[0] == '"' and s[-1] == '"':
+        return s[1:-1]
+    return s

+ 35 - 1
tests/integration/project_test.py

@@ -1,8 +1,10 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-import os.path
+import json
+import os
 import random
+import tempfile
 
 import py
 import pytest
@@ -1834,3 +1836,35 @@ class ProjectTest(DockerClientTestCase):
         assert 'svc1' in svc2.get_dependency_names()
         with pytest.raises(NoHealthCheckConfigured):
             svc1.is_healthy()
+
+    def test_project_up_seccomp_profile(self):
+        seccomp_data = {
+            'defaultAction': 'SCMP_ACT_ALLOW',
+            'syscalls': []
+        }
+        fd, profile_path = tempfile.mkstemp('_seccomp.json')
+        self.addCleanup(os.remove, profile_path)
+        with os.fdopen(fd, 'w') as f:
+            json.dump(seccomp_data, f)
+
+        config_dict = {
+            'version': '2.3',
+            'services': {
+                'svc1': {
+                    'image': 'busybox:latest',
+                    'command': 'top',
+                    'security_opt': ['seccomp:"{}"'.format(profile_path)]
+                }
+            }
+        }
+
+        config_data = load_config(config_dict)
+        project = Project.from_config(name='composetest', config_data=config_data, client=self.client)
+        project.up()
+        containers = project.containers()
+        assert len(containers) == 1
+
+        remote_secopts = containers[0].get('HostConfig.SecurityOpt')
+        assert len(remote_secopts) == 1
+        assert remote_secopts[0].startswith('seccomp=')
+        assert json.loads(remote_secopts[0].lstrip('seccomp=')) == seccomp_data

+ 3 - 2
tests/integration/service_test.py

@@ -23,6 +23,7 @@ from .testcases import SWARM_SKIP_CONTAINERS_ALL
 from .testcases import SWARM_SKIP_CPU_SHARES
 from compose import __version__
 from compose.config.types import MountSpec
+from compose.config.types import SecurityOpt
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeSpec
 from compose.const import IS_WINDOWS_PLATFORM
@@ -238,11 +239,11 @@ class ServiceTest(DockerClientTestCase):
         }]
 
     def test_create_container_with_security_opt(self):
-        security_opt = ['label:disable']
+        security_opt = [SecurityOpt.parse('label:disable')]
         service = self.create_service('db', security_opt=security_opt)
         container = service.create_container()
         service.start_container(container)
-        assert set(container.get('HostConfig.SecurityOpt')) == set(security_opt)
+        assert set(container.get('HostConfig.SecurityOpt')) == set([o.repr() for o in security_opt])
 
     @pytest.mark.xfail(True, reason='Not supported on most drivers')
     def test_create_container_with_storage_opt(self):

+ 1 - 1
tests/unit/cli/utils_test.py

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
 
 import unittest
 
-from compose.cli.utils import unquote_path
+from compose.utils import unquote_path
 
 
 class UnquotePathTest(unittest.TestCase):