diff options
author | Joffrey F <joffrey@docker.com> | 2017-10-25 19:03:12 -0700 |
---|---|---|
committer | Joffrey F <joffrey@docker.com> | 2017-10-26 14:13:04 -0700 |
commit | 9eff7a63f7212bd827d3ff24cb800caea13cc22f (patch) | |
tree | 84ab28b5002ebe049c752fdf850a6c5264e3a454 | |
parent | 10ea65f5ab34cc9924e09ee9ed54d7005d997c40 (diff) | |
download | docker-py-9eff7a63f7212bd827d3ff24cb800caea13cc22f.tar.gz |
Add support for new ContainerSpec parameters
Signed-off-by: Joffrey F <joffrey@docker.com>
-rw-r--r-- | docker/api/build.py | 7 | ||||
-rw-r--r-- | docker/api/service.py | 65 | ||||
-rw-r--r-- | docker/models/services.py | 37 | ||||
-rw-r--r-- | docker/types/__init__.py | 5 | ||||
-rw-r--r-- | docker/types/containers.py | 9 | ||||
-rw-r--r-- | docker/types/healthcheck.py | 24 | ||||
-rw-r--r-- | docker/types/services.py | 152 | ||||
-rw-r--r-- | docker/utils/__init__.py | 2 | ||||
-rw-r--r-- | docker/utils/utils.py | 6 | ||||
-rw-r--r-- | docs/api.rst | 8 |
10 files changed, 265 insertions, 50 deletions
diff --git a/docker/api/build.py b/docker/api/build.py index 42a1a29..25f271a 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -237,10 +237,9 @@ class BuildApiMixin(object): 'extra_hosts was only introduced in API version 1.27' ) - encoded_extra_hosts = [ - '{}:{}'.format(k, v) for k, v in extra_hosts.items() - ] - params.update({'extrahosts': encoded_extra_hosts}) + if isinstance(extra_hosts, dict): + extra_hosts = utils.format_extra_hosts(extra_hosts) + params.update({'extrahosts': extra_hosts}) if context is not None: headers = {'Content-Type': 'application/tar'} diff --git a/docker/api/service.py b/docker/api/service.py index 4b555a5..9ce830c 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -4,45 +4,62 @@ from ..types import ServiceMode def _check_api_features(version, task_template, update_config): + + def raise_version_error(param, min_version): + raise errors.InvalidVersion( + '{} is not supported in API version < {}'.format( + param, min_version + ) + ) + if update_config is not None: if utils.version_lt(version, '1.25'): if 'MaxFailureRatio' in update_config: - raise errors.InvalidVersion( - 'UpdateConfig.max_failure_ratio is not supported in' - ' API version < 1.25' - ) + raise_version_error('UpdateConfig.max_failure_ratio', '1.25') if 'Monitor' in update_config: - raise errors.InvalidVersion( - 'UpdateConfig.monitor is not supported in' - ' API version < 1.25' - ) + raise_version_error('UpdateConfig.monitor', '1.25') if task_template is not None: if 'ForceUpdate' in task_template and utils.version_lt( version, '1.25'): - raise errors.InvalidVersion( - 'force_update is not supported in API version < 1.25' - ) + raise_version_error('force_update', '1.25') if task_template.get('Placement'): if utils.version_lt(version, '1.30'): if task_template['Placement'].get('Platforms'): - raise errors.InvalidVersion( - 'Placement.platforms is not supported in' - ' API version < 1.30' - ) - + raise_version_error('Placement.platforms', '1.30') if utils.version_lt(version, '1.27'): if task_template['Placement'].get('Preferences'): - raise errors.InvalidVersion( - 'Placement.preferences is not supported in' - ' API version < 1.27' - ) - if task_template.get('ContainerSpec', {}).get('TTY'): + raise_version_error('Placement.preferences', '1.27') + + if task_template.get('ContainerSpec'): + container_spec = task_template.get('ContainerSpec') + if utils.version_lt(version, '1.25'): - raise errors.InvalidVersion( - 'ContainerSpec.TTY is not supported in API version < 1.25' - ) + if container_spec.get('TTY'): + raise_version_error('ContainerSpec.tty', '1.25') + if container_spec.get('Hostname') is not None: + raise_version_error('ContainerSpec.hostname', '1.25') + if container_spec.get('Hosts') is not None: + raise_version_error('ContainerSpec.hosts', '1.25') + if container_spec.get('Groups') is not None: + raise_version_error('ContainerSpec.groups', '1.25') + if container_spec.get('DNSConfig') is not None: + raise_version_error('ContainerSpec.dns_config', '1.25') + if container_spec.get('Healthcheck') is not None: + raise_version_error('ContainerSpec.healthcheck', '1.25') + + if utils.version_lt(version, '1.28'): + if container_spec.get('ReadOnly') is not None: + raise_version_error('ContainerSpec.dns_config', '1.28') + if container_spec.get('StopSignal') is not None: + raise_version_error('ContainerSpec.stop_signal', '1.28') + + if utils.version_lt(version, '1.30'): + if container_spec.get('Configs') is not None: + raise_version_error('ContainerSpec.configs', '1.30') + if container_spec.get('Privileges') is not None: + raise_version_error('ContainerSpec.privileges', '1.30') class ServiceApiMixin(object): diff --git a/docker/models/services.py b/docker/models/services.py index e1e2ea6..d45621b 100644 --- a/docker/models/services.py +++ b/docker/models/services.py @@ -147,6 +147,22 @@ class ServiceCollection(Collection): user (str): User to run commands as. workdir (str): Working directory for commands to run. tty (boolean): Whether a pseudo-TTY should be allocated. + groups (:py:class:`list`): A list of additional groups that the + container process will run as. + open_stdin (boolean): Open ``stdin`` + read_only (boolean): Mount the container's root filesystem as read + only. + stop_signal (string): Set signal to stop the service's containers + healthcheck (Healthcheck): Healthcheck + configuration for this service. + hosts (:py:class:`dict`): A set of host to IP mappings to add to + the container's `hosts` file. + dns_config (DNSConfig): Specification for DNS + related configurations in resolver configuration file. + configs (:py:class:`list`): List of :py:class:`ConfigReference` + that will be exposed to the service. + privileges (Privileges): Security options for the service's + containers. Returns: (:py:class:`Service`) The created service. @@ -202,18 +218,27 @@ class ServiceCollection(Collection): # kwargs to copy straight over to ContainerSpec CONTAINER_SPEC_KWARGS = [ - 'image', - 'command', 'args', + 'command', + 'configs', + 'dns_config', 'env', + 'groups', + 'healthcheck', 'hostname', - 'workdir', - 'user', + 'hosts', + 'image', 'labels', 'mounts', - 'stop_grace_period', + 'open_stdin', + 'privileges' + 'read_only', 'secrets', - 'tty' + 'stop_grace_period', + 'stop_signal', + 'tty', + 'user', + 'workdir', ] # kwargs to copy straight over to TaskTemplate diff --git a/docker/types/__init__.py b/docker/types/__init__.py index edc919d..39c93e3 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -3,7 +3,8 @@ from .containers import ContainerConfig, HostConfig, LogConfig, Ulimit from .healthcheck import Healthcheck from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig from .services import ( - ContainerSpec, DriverConfig, EndpointSpec, Mount, Placement, Resources, - RestartPolicy, SecretReference, ServiceMode, TaskTemplate, UpdateConfig + ConfigReference, ContainerSpec, DNSConfig, DriverConfig, EndpointSpec, + Mount, Placement, Privileges, Resources, RestartPolicy, SecretReference, + ServiceMode, TaskTemplate, UpdateConfig ) from .swarm import SwarmSpec, SwarmExternalCA diff --git a/docker/types/containers.py b/docker/types/containers.py index 3fc13d9..13bea71 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -4,8 +4,8 @@ import warnings from .. import errors from ..utils.utils import ( convert_port_bindings, convert_tmpfs_mounts, convert_volume_binds, - format_environment, normalize_links, parse_bytes, parse_devices, - split_command, version_gte, version_lt, + format_environment, format_extra_hosts, normalize_links, parse_bytes, + parse_devices, split_command, version_gte, version_lt, ) from .base import DictType from .healthcheck import Healthcheck @@ -257,10 +257,7 @@ class HostConfig(dict): if extra_hosts is not None: if isinstance(extra_hosts, dict): - extra_hosts = [ - '{0}:{1}'.format(k, v) - for k, v in sorted(six.iteritems(extra_hosts)) - ] + extra_hosts = format_extra_hosts(extra_hosts) self['ExtraHosts'] = extra_hosts diff --git a/docker/types/healthcheck.py b/docker/types/healthcheck.py index 8ea9a35..5a6a931 100644 --- a/docker/types/healthcheck.py +++ b/docker/types/healthcheck.py @@ -4,6 +4,30 @@ import six class Healthcheck(DictType): + """ + Defines a healthcheck configuration for a container or service. + + Args: + + test (:py:class:`list` or str): Test to perform to determine + container health. Possible values: + - Empty list: Inherit healthcheck from parent image + - ``["NONE"]``: Disable healthcheck + - ``["CMD", args...]``: exec arguments directly. + - ``["CMD-SHELL", command]``: RUn command in the system's + default shell. + If a string is provided, it will be used as a ``CMD-SHELL`` + command. + interval (int): The time to wait between checks in nanoseconds. It + should be 0 or at least 1000000 (1 ms). + timeout (int): The time to wait before considering the check to + have hung. It should be 0 or at least 1000000 (1 ms). + retries (integer): The number of consecutive failures needed to + consider a container as unhealthy. + start_period (integer): Start period for the container to + initialize before starting health-retries countdown in + nanoseconds. It should be 0 or at least 1000000 (1 ms). + """ def __init__(self, **kwargs): test = kwargs.get('test', kwargs.get('Test')) if isinstance(test, six.string_types): diff --git a/docker/types/services.py b/docker/types/services.py index c276740..c77db16 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -3,7 +3,8 @@ import six from .. import errors from ..constants import IS_WINDOWS_PLATFORM from ..utils import ( - check_resource, format_environment, parse_bytes, split_command + check_resource, format_environment, format_extra_hosts, parse_bytes, + split_command, ) @@ -84,13 +85,31 @@ class ContainerSpec(dict): :py:class:`~docker.types.Mount` class for details. stop_grace_period (int): Amount of time to wait for the container to terminate before forcefully killing it. - secrets (list of py:class:`SecretReference`): List of secrets to be + secrets (:py:class:`list`): List of :py:class:`SecretReference` to be made available inside the containers. tty (boolean): Whether a pseudo-TTY should be allocated. + groups (:py:class:`list`): A list of additional groups that the + container process will run as. + open_stdin (boolean): Open ``stdin`` + read_only (boolean): Mount the container's root filesystem as read + only. + stop_signal (string): Set signal to stop the service's containers + healthcheck (Healthcheck): Healthcheck + configuration for this service. + hosts (:py:class:`dict`): A set of host to IP mappings to add to + the container's `hosts` file. + dns_config (DNSConfig): Specification for DNS + related configurations in resolver configuration file. + configs (:py:class:`list`): List of :py:class:`ConfigReference` that + will be exposed to the service. + privileges (Privileges): Security options for the service's containers. """ def __init__(self, image, command=None, args=None, hostname=None, env=None, workdir=None, user=None, labels=None, mounts=None, - stop_grace_period=None, secrets=None, tty=None): + stop_grace_period=None, secrets=None, tty=None, groups=None, + open_stdin=None, read_only=None, stop_signal=None, + healthcheck=None, hosts=None, dns_config=None, configs=None, + privileges=None): self['Image'] = image if isinstance(command, six.string_types): @@ -109,8 +128,17 @@ class ContainerSpec(dict): self['Dir'] = workdir if user is not None: self['User'] = user + if groups is not None: + self['Groups'] = groups + if stop_signal is not None: + self['StopSignal'] = stop_signal + if stop_grace_period is not None: + self['StopGracePeriod'] = stop_grace_period if labels is not None: self['Labels'] = labels + if hosts is not None: + self['Hosts'] = format_extra_hosts(hosts) + if mounts is not None: parsed_mounts = [] for mount in mounts: @@ -120,16 +148,30 @@ class ContainerSpec(dict): # If mount already parsed parsed_mounts.append(mount) self['Mounts'] = parsed_mounts - if stop_grace_period is not None: - self['StopGracePeriod'] = stop_grace_period if secrets is not None: if not isinstance(secrets, list): raise TypeError('secrets must be a list') self['Secrets'] = secrets + if configs is not None: + if not isinstance(configs, list): + raise TypeError('configs must be a list') + self['Configs'] = configs + + if dns_config is not None: + self['DNSConfig'] = dns_config + if privileges is not None: + self['Privileges'] = privileges + if healthcheck is not None: + self['Healthcheck'] = healthcheck + if tty is not None: self['TTY'] = tty + if open_stdin is not None: + self['OpenStdin'] = open_stdin + if read_only is not None: + self['ReadOnly'] = read_only class Mount(dict): @@ -487,6 +529,34 @@ class SecretReference(dict): } +class ConfigReference(dict): + """ + Config reference to be used as part of a :py:class:`ContainerSpec`. + Describes how a config is made accessible inside the service's + containers. + + Args: + config_id (string): Config's ID + config_name (string): Config's name as defined at its creation. + filename (string): Name of the file containing the config. Defaults + to the config's name if not specified. + uid (string): UID of the config file's owner. Default: 0 + gid (string): GID of the config file's group. Default: 0 + mode (int): File access mode inside the container. Default: 0o444 + """ + @check_resource('config_id') + def __init__(self, config_id, config_name, filename=None, uid=None, + gid=None, mode=0o444): + self['ConfigName'] = config_name + self['ConfigID'] = config_id + self['File'] = { + 'Name': filename or config_name, + 'UID': uid or '0', + 'GID': gid or '0', + 'Mode': mode + } + + class Placement(dict): """ Placement constraints to be used as part of a :py:class:`TaskTemplate` @@ -510,3 +580,75 @@ class Placement(dict): self['Platforms'].append({ 'Architecture': plat[0], 'OS': plat[1] }) + + +class DNSConfig(dict): + """ + Specification for DNS related configurations in resolver configuration + file (``resolv.conf``). Part of a :py:class:`ContainerSpec` definition. + + Args: + nameservers (:py:class:`list`): The IP addresses of the name + servers. + search (:py:class:`list`): A search list for host-name lookup. + options (:py:class:`list`): A list of internal resolver variables + to be modified (e.g., ``debug``, ``ndots:3``, etc.). + """ + def __init__(self, nameservers=None, search=None, options=None): + self['Nameservers'] = nameservers + self['Search'] = search + self['Options'] = options + + +class Privileges(dict): + """ + Security options for a service's containers. + Part of a :py:class:`ContainerSpec` definition. + + Args: + credentialspec_file (str): Load credential spec from this file. + The file is read by the daemon, and must be present in the + CredentialSpecs subdirectory in the docker data directory, + which defaults to ``C:\ProgramData\Docker\`` on Windows. + Can not be combined with credentialspec_registry. + + credentialspec_registry (str): Load credential spec from this value + in the Windows registry. The specified registry value must be + located in: ``HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion + \Virtualization\Containers\CredentialSpecs``. + Can not be combined with credentialspec_file. + + selinux_disable (boolean): Disable SELinux + selinux_user (string): SELinux user label + selinux_role (string): SELinux role label + selinux_type (string): SELinux type label + selinux_level (string): SELinux level label + """ + def __init__(self, credentialspec_file=None, credentialspec_registry=None, + selinux_disable=None, selinux_user=None, selinux_role=None, + selinux_type=None, selinux_level=None): + credential_spec = {} + if credentialspec_registry is not None: + credential_spec['Registry'] = credentialspec_registry + if credentialspec_file is not None: + credential_spec['File'] = credentialspec_file + + if len(credential_spec) > 1: + raise errors.InvalidArgument( + 'credentialspec_file and credentialspec_registry are mutually' + ' exclusive' + ) + + selinux_context = { + 'Disable': selinux_disable, + 'User': selinux_user, + 'Role': selinux_role, + 'Type': selinux_type, + 'Level': selinux_level, + } + + if len(credential_spec) > 0: + self['CredentialSpec'] = credential_spec + + if len(selinux_context) > 0: + self['SELinuxContext'] = selinux_context diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index b758cbd..c162e3b 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -8,6 +8,6 @@ from .utils import ( create_host_config, parse_bytes, ping_registry, parse_env_file, version_lt, version_gte, decode_json_header, split_command, create_ipam_config, create_ipam_pool, parse_devices, normalize_links, convert_service_networks, - format_environment, create_archive + format_environment, create_archive, format_extra_hosts ) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index d9a6d7c..a123fd8 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -564,6 +564,12 @@ def format_environment(environment): return [format_env(*var) for var in six.iteritems(environment)] +def format_extra_hosts(extra_hosts): + return [ + '{}:{}'.format(k, v) for k, v in sorted(six.iteritems(extra_hosts)) + ] + + def create_host_config(self, *args, **kwargs): raise errors.DeprecatedMethod( 'utils.create_host_config has been removed. Please use a ' diff --git a/docs/api.rst b/docs/api.rst index 0b10f38..2fce0a7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -122,13 +122,17 @@ Configuration types .. py:module:: docker.types -.. autoclass:: IPAMConfig -.. autoclass:: IPAMPool +.. autoclass:: ConfigReference .. autoclass:: ContainerSpec +.. autoclass:: DNSConfig .. autoclass:: DriverConfig .. autoclass:: EndpointSpec +.. autoclass:: Healthcheck +.. autoclass:: IPAMConfig +.. autoclass:: IPAMPool .. autoclass:: Mount .. autoclass:: Placement +.. autoclass:: Privileges .. autoclass:: Resources .. autoclass:: RestartPolicy .. autoclass:: SecretReference |