mirror of https://github.com/docker/compose.git
Merge pull request #5701 from docker/5574-seccomp-file
Add support for seccomp files
This commit is contained in:
commit
37a48073ed
|
@ -13,9 +13,9 @@ from docker.utils.config import home_dir
|
||||||
|
|
||||||
from ..config.environment import Environment
|
from ..config.environment import Environment
|
||||||
from ..const import HTTP_TIMEOUT
|
from ..const import HTTP_TIMEOUT
|
||||||
|
from ..utils import unquote_path
|
||||||
from .errors import UserError
|
from .errors import UserError
|
||||||
from .utils import generate_user_agent
|
from .utils import generate_user_agent
|
||||||
from .utils import unquote_path
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -131,14 +131,6 @@ def generate_user_agent():
|
||||||
return " ".join(parts)
|
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):
|
def human_readable_file_size(size):
|
||||||
suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', ]
|
suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', ]
|
||||||
order = int(math.log(size, 2) / 10) if size else 0
|
order = int(math.log(size, 2) / 10) if size else 0
|
||||||
|
|
|
@ -40,6 +40,7 @@ from .sort_services import sort_service_dicts
|
||||||
from .types import MountSpec
|
from .types import MountSpec
|
||||||
from .types import parse_extra_hosts
|
from .types import parse_extra_hosts
|
||||||
from .types import parse_restart_spec
|
from .types import parse_restart_spec
|
||||||
|
from .types import SecurityOpt
|
||||||
from .types import ServiceLink
|
from .types import ServiceLink
|
||||||
from .types import ServicePort
|
from .types import ServicePort
|
||||||
from .types import VolumeFromSpec
|
from .types import VolumeFromSpec
|
||||||
|
@ -734,9 +735,9 @@ def process_service(service_config):
|
||||||
if field in service_dict:
|
if field in service_dict:
|
||||||
service_dict[field] = to_list(service_dict[field])
|
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)
|
process_healthcheck(service_dict)
|
||||||
))
|
)))
|
||||||
|
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
||||||
|
@ -1376,6 +1377,16 @@ def split_path_mapping(volume_path):
|
||||||
return (volume_path, None)
|
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):
|
def join_path_mapping(pair):
|
||||||
(container, host) = pair
|
(container, host) = pair
|
||||||
if isinstance(host, dict):
|
if isinstance(host, dict):
|
||||||
|
|
|
@ -42,6 +42,7 @@ def serialize_string(dumper, data):
|
||||||
yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
||||||
yaml.SafeDumper.add_representer(types.VolumeSpec, 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.ServiceSecret, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.ServiceConfig, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
|
||||||
|
|
|
@ -4,6 +4,7 @@ Types for objects parsed from the configuration.
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
import ntpath
|
import ntpath
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -13,6 +14,7 @@ import six
|
||||||
from docker.utils.ports import build_port_bindings
|
from docker.utils.ports import build_port_bindings
|
||||||
|
|
||||||
from ..const import COMPOSEFILE_V1 as V1
|
from ..const import COMPOSEFILE_V1 as V1
|
||||||
|
from ..utils import unquote_path
|
||||||
from .errors import ConfigurationError
|
from .errors import ConfigurationError
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
from compose.utils import splitdrive
|
from compose.utils import splitdrive
|
||||||
|
@ -457,3 +459,30 @@ def normalize_port_dict(port):
|
||||||
external_ip=port.get('external_ip', ''),
|
external_ip=port.get('external_ip', ''),
|
||||||
has_ext_ip=(':' if port.get('external_ip') else ''),
|
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
|
||||||
|
|
|
@ -881,6 +881,10 @@ class Service(object):
|
||||||
init_path = options.get('init')
|
init_path = options.get('init')
|
||||||
options['init'] = True
|
options['init'] = True
|
||||||
|
|
||||||
|
security_opt = [
|
||||||
|
o.value for o in options.get('security_opt')
|
||||||
|
] if options.get('security_opt') else None
|
||||||
|
|
||||||
nano_cpus = None
|
nano_cpus = None
|
||||||
if 'cpus' in options:
|
if 'cpus' in options:
|
||||||
nano_cpus = int(options.get('cpus') * NANOCPUS_SCALE)
|
nano_cpus = int(options.get('cpus') * NANOCPUS_SCALE)
|
||||||
|
@ -910,7 +914,7 @@ class Service(object):
|
||||||
extra_hosts=options.get('extra_hosts'),
|
extra_hosts=options.get('extra_hosts'),
|
||||||
read_only=options.get('read_only'),
|
read_only=options.get('read_only'),
|
||||||
pid_mode=self.pid_mode.mode,
|
pid_mode=self.pid_mode.mode,
|
||||||
security_opt=options.get('security_opt'),
|
security_opt=security_opt,
|
||||||
ipc_mode=options.get('ipc'),
|
ipc_mode=options.get('ipc'),
|
||||||
cgroup_parent=options.get('cgroup_parent'),
|
cgroup_parent=options.get('cgroup_parent'),
|
||||||
cpu_quota=options.get('cpu_quota'),
|
cpu_quota=options.get('cpu_quota'),
|
||||||
|
|
|
@ -143,3 +143,11 @@ def parse_bytes(n):
|
||||||
return sdk_parse_bytes(n)
|
return sdk_parse_bytes(n)
|
||||||
except DockerException:
|
except DockerException:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def unquote_path(s):
|
||||||
|
if not s:
|
||||||
|
return s
|
||||||
|
if s[0] == '"' and s[-1] == '"':
|
||||||
|
return s[1:-1]
|
||||||
|
return s
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os.path
|
import json
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -1834,3 +1836,35 @@ class ProjectTest(DockerClientTestCase):
|
||||||
assert 'svc1' in svc2.get_dependency_names()
|
assert 'svc1' in svc2.get_dependency_names()
|
||||||
with pytest.raises(NoHealthCheckConfigured):
|
with pytest.raises(NoHealthCheckConfigured):
|
||||||
svc1.is_healthy()
|
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
|
||||||
|
|
|
@ -23,6 +23,7 @@ from .testcases import SWARM_SKIP_CONTAINERS_ALL
|
||||||
from .testcases import SWARM_SKIP_CPU_SHARES
|
from .testcases import SWARM_SKIP_CPU_SHARES
|
||||||
from compose import __version__
|
from compose import __version__
|
||||||
from compose.config.types import MountSpec
|
from compose.config.types import MountSpec
|
||||||
|
from compose.config.types import SecurityOpt
|
||||||
from compose.config.types import VolumeFromSpec
|
from compose.config.types import VolumeFromSpec
|
||||||
from compose.config.types import VolumeSpec
|
from compose.config.types import VolumeSpec
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
|
@ -238,11 +239,11 @@ class ServiceTest(DockerClientTestCase):
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def test_create_container_with_security_opt(self):
|
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)
|
service = self.create_service('db', security_opt=security_opt)
|
||||||
container = service.create_container()
|
container = service.create_container()
|
||||||
service.start_container(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')
|
@pytest.mark.xfail(True, reason='Not supported on most drivers')
|
||||||
def test_create_container_with_storage_opt(self):
|
def test_create_container_with_storage_opt(self):
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from compose.cli.utils import unquote_path
|
from compose.utils import unquote_path
|
||||||
|
|
||||||
|
|
||||||
class UnquotePathTest(unittest.TestCase):
|
class UnquotePathTest(unittest.TestCase):
|
||||||
|
|
Loading…
Reference in New Issue