Merge pull request #5701 from docker/5574-seccomp-file

Add support for seccomp files
This commit is contained in:
Joffrey F 2018-02-26 11:22:01 -08:00 committed by GitHub
commit 37a48073ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 96 additions and 16 deletions

View File

@ -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__)

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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'),

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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):