Add support for seccomp files

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2018-02-21 18:32:45 -08:00
parent ec0de7eb68
commit a6c31b80fe
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 ..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__)

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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