diff options
Diffstat (limited to 'docker/types')
-rw-r--r-- | docker/types/__init__.py | 9 | ||||
-rw-r--r-- | docker/types/base.py | 5 | ||||
-rw-r--r-- | docker/types/containers.py | 245 | ||||
-rw-r--r-- | docker/types/daemon.py | 18 | ||||
-rw-r--r-- | docker/types/healthcheck.py | 14 | ||||
-rw-r--r-- | docker/types/networks.py | 11 | ||||
-rw-r--r-- | docker/types/services.py | 150 |
7 files changed, 371 insertions, 81 deletions
diff --git a/docker/types/__init__.py b/docker/types/__init__.py index 0b0d847..b425746 100644 --- a/docker/types/__init__.py +++ b/docker/types/__init__.py @@ -1,11 +1,14 @@ # flake8: noqa -from .containers import ContainerConfig, HostConfig, LogConfig, Ulimit +from .containers import ( + ContainerConfig, HostConfig, LogConfig, Ulimit, DeviceRequest +) from .daemon import CancellableStream from .healthcheck import Healthcheck from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig from .services import ( ConfigReference, ContainerSpec, DNSConfig, DriverConfig, EndpointSpec, - Mount, Placement, Privileges, Resources, RestartPolicy, SecretReference, - ServiceMode, TaskTemplate, UpdateConfig + Mount, Placement, PlacementPreference, Privileges, Resources, + RestartPolicy, RollbackConfig, SecretReference, ServiceMode, TaskTemplate, + UpdateConfig, NetworkAttachmentConfig ) from .swarm import SwarmSpec, SwarmExternalCA diff --git a/docker/types/base.py b/docker/types/base.py index 6891062..8851f1e 100644 --- a/docker/types/base.py +++ b/docker/types/base.py @@ -1,7 +1,4 @@ -import six - - class DictType(dict): def __init__(self, init): - for k, v in six.iteritems(init): + for k, v in init.items(): self[k] = v diff --git a/docker/types/containers.py b/docker/types/containers.py index 2521420..f1b60b2 100644 --- a/docker/types/containers.py +++ b/docker/types/containers.py @@ -1,5 +1,3 @@ -import six - from .. import errors from ..utils.utils import ( convert_port_bindings, convert_tmpfs_mounts, convert_volume_binds, @@ -10,7 +8,7 @@ from .base import DictType from .healthcheck import Healthcheck -class LogConfigTypesEnum(object): +class LogConfigTypesEnum: _values = ( 'json-file', 'syslog', @@ -23,6 +21,35 @@ class LogConfigTypesEnum(object): class LogConfig(DictType): + """ + Configure logging for a container, when provided as an argument to + :py:meth:`~docker.api.container.ContainerApiMixin.create_host_config`. + You may refer to the + `official logging driver documentation <https://docs.docker.com/config/containers/logging/configure/>`_ + for more information. + + Args: + type (str): Indicate which log driver to use. A set of valid drivers + is provided as part of the :py:attr:`LogConfig.types` + enum. Other values may be accepted depending on the engine version + and available logging plugins. + config (dict): A driver-dependent configuration dictionary. Please + refer to the driver's documentation for a list of valid config + keys. + + Example: + + >>> from docker.types import LogConfig + >>> lc = LogConfig(type=LogConfig.types.JSON, config={ + ... 'max-size': '1g', + ... 'labels': 'production_status,geo' + ... }) + >>> hc = client.create_host_config(log_config=lc) + >>> container = client.create_container('busybox', 'true', + ... host_config=hc) + >>> client.inspect_container(container)['HostConfig']['LogConfig'] + {'Type': 'json-file', 'Config': {'labels': 'production_status,geo', 'max-size': '1g'}} + """ # noqa: E501 types = LogConfigTypesEnum def __init__(self, **kwargs): @@ -32,7 +59,7 @@ class LogConfig(DictType): if config and not isinstance(config, dict): raise ValueError("LogConfig.config must be a dictionary") - super(LogConfig, self).__init__({ + super().__init__({ 'Type': log_driver_type, 'Config': config }) @@ -50,25 +77,51 @@ class LogConfig(DictType): return self['Config'] def set_config_value(self, key, value): + """ Set a the value for ``key`` to ``value`` inside the ``config`` + dict. + """ self.config[key] = value def unset_config(self, key): + """ Remove the ``key`` property from the ``config`` dict. """ if key in self.config: del self.config[key] class Ulimit(DictType): + """ + Create a ulimit declaration to be used with + :py:meth:`~docker.api.container.ContainerApiMixin.create_host_config`. + + Args: + + name (str): Which ulimit will this apply to. The valid names can be + found in '/etc/security/limits.conf' on a gnu/linux system. + soft (int): The soft limit for this ulimit. Optional. + hard (int): The hard limit for this ulimit. Optional. + + Example: + + >>> nproc_limit = docker.types.Ulimit(name='nproc', soft=1024) + >>> hc = client.create_host_config(ulimits=[nproc_limit]) + >>> container = client.create_container( + 'busybox', 'true', host_config=hc + ) + >>> client.inspect_container(container)['HostConfig']['Ulimits'] + [{'Name': 'nproc', 'Hard': 0, 'Soft': 1024}] + + """ def __init__(self, **kwargs): name = kwargs.get('name', kwargs.get('Name')) soft = kwargs.get('soft', kwargs.get('Soft')) hard = kwargs.get('hard', kwargs.get('Hard')) - if not isinstance(name, six.string_types): + if not isinstance(name, str): raise ValueError("Ulimit.name must be a string") if soft and not isinstance(soft, int): raise ValueError("Ulimit.soft must be an integer") if hard and not isinstance(hard, int): raise ValueError("Ulimit.hard must be an integer") - super(Ulimit, self).__init__({ + super().__init__({ 'Name': name, 'Soft': soft, 'Hard': hard @@ -99,6 +152,104 @@ class Ulimit(DictType): self['Hard'] = value +class DeviceRequest(DictType): + """ + Create a device request to be used with + :py:meth:`~docker.api.container.ContainerApiMixin.create_host_config`. + + Args: + + driver (str): Which driver to use for this device. Optional. + count (int): Number or devices to request. Optional. + Set to -1 to request all available devices. + device_ids (list): List of strings for device IDs. Optional. + Set either ``count`` or ``device_ids``. + capabilities (list): List of lists of strings to request + capabilities. Optional. The global list acts like an OR, + and the sub-lists are AND. The driver will try to satisfy + one of the sub-lists. + Available capabilities for the ``nvidia`` driver can be found + `here <https://github.com/NVIDIA/nvidia-container-runtime>`_. + options (dict): Driver-specific options. Optional. + """ + + def __init__(self, **kwargs): + driver = kwargs.get('driver', kwargs.get('Driver')) + count = kwargs.get('count', kwargs.get('Count')) + device_ids = kwargs.get('device_ids', kwargs.get('DeviceIDs')) + capabilities = kwargs.get('capabilities', kwargs.get('Capabilities')) + options = kwargs.get('options', kwargs.get('Options')) + + if driver is None: + driver = '' + elif not isinstance(driver, str): + raise ValueError('DeviceRequest.driver must be a string') + if count is None: + count = 0 + elif not isinstance(count, int): + raise ValueError('DeviceRequest.count must be an integer') + if device_ids is None: + device_ids = [] + elif not isinstance(device_ids, list): + raise ValueError('DeviceRequest.device_ids must be a list') + if capabilities is None: + capabilities = [] + elif not isinstance(capabilities, list): + raise ValueError('DeviceRequest.capabilities must be a list') + if options is None: + options = {} + elif not isinstance(options, dict): + raise ValueError('DeviceRequest.options must be a dict') + + super().__init__({ + 'Driver': driver, + 'Count': count, + 'DeviceIDs': device_ids, + 'Capabilities': capabilities, + 'Options': options + }) + + @property + def driver(self): + return self['Driver'] + + @driver.setter + def driver(self, value): + self['Driver'] = value + + @property + def count(self): + return self['Count'] + + @count.setter + def count(self, value): + self['Count'] = value + + @property + def device_ids(self): + return self['DeviceIDs'] + + @device_ids.setter + def device_ids(self, value): + self['DeviceIDs'] = value + + @property + def capabilities(self): + return self['Capabilities'] + + @capabilities.setter + def capabilities(self, value): + self['Capabilities'] = value + + @property + def options(self): + return self['Options'] + + @options.setter + def options(self, value): + self['Options'] = value + + class HostConfig(dict): def __init__(self, version, binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=False, links=None, @@ -115,13 +266,13 @@ class HostConfig(dict): device_read_iops=None, device_write_iops=None, oom_kill_disable=False, shm_size=None, sysctls=None, tmpfs=None, oom_score_adj=None, dns_opt=None, cpu_shares=None, - cpuset_cpus=None, userns_mode=None, pids_limit=None, - isolation=None, auto_remove=False, storage_opt=None, - init=None, init_path=None, volume_driver=None, - cpu_count=None, cpu_percent=None, nano_cpus=None, - cpuset_mems=None, runtime=None, mounts=None, + cpuset_cpus=None, userns_mode=None, uts_mode=None, + pids_limit=None, isolation=None, auto_remove=False, + storage_opt=None, init=None, init_path=None, + volume_driver=None, cpu_count=None, cpu_percent=None, + nano_cpus=None, cpuset_mems=None, runtime=None, mounts=None, cpu_rt_period=None, cpu_rt_runtime=None, - device_cgroup_rules=None): + device_cgroup_rules=None, device_requests=None): if mem_limit is not None: self['Memory'] = parse_bytes(mem_limit) @@ -144,7 +295,7 @@ class HostConfig(dict): self['MemorySwappiness'] = mem_swappiness if shm_size is not None: - if isinstance(shm_size, six.string_types): + if isinstance(shm_size, str): shm_size = parse_bytes(shm_size) self['ShmSize'] = shm_size @@ -181,10 +332,11 @@ class HostConfig(dict): if dns_search: self['DnsSearch'] = dns_search - if network_mode: - self['NetworkMode'] = network_mode - elif network_mode is None: - self['NetworkMode'] = 'default' + if network_mode == 'host' and port_bindings: + raise host_config_incompatible_error( + 'network_mode', 'host', 'port_bindings' + ) + self['NetworkMode'] = network_mode or 'default' if restart_policy: if not isinstance(restart_policy, dict): @@ -204,7 +356,7 @@ class HostConfig(dict): self['Devices'] = parse_devices(devices) if group_add: - self['GroupAdd'] = [six.text_type(grp) for grp in group_add] + self['GroupAdd'] = [str(grp) for grp in group_add] if dns is not None: self['Dns'] = dns @@ -224,11 +376,11 @@ class HostConfig(dict): if not isinstance(sysctls, dict): raise host_config_type_error('sysctls', sysctls, 'dict') self['Sysctls'] = {} - for k, v in six.iteritems(sysctls): - self['Sysctls'][k] = six.text_type(v) + for k, v in sysctls.items(): + self['Sysctls'][k] = str(v) if volumes_from is not None: - if isinstance(volumes_from, six.string_types): + if isinstance(volumes_from, str): volumes_from = volumes_from.split(',') self['VolumesFrom'] = volumes_from @@ -250,7 +402,7 @@ class HostConfig(dict): if isinstance(lxc_conf, dict): formatted = [] - for k, v in six.iteritems(lxc_conf): + for k, v in lxc_conf.items(): formatted.append({'Key': k, 'Value': str(v)}) lxc_conf = formatted @@ -264,10 +416,10 @@ class HostConfig(dict): if not isinstance(ulimits, list): raise host_config_type_error('ulimits', ulimits, 'list') self['Ulimits'] = [] - for l in ulimits: - if not isinstance(l, Ulimit): - l = Ulimit(**l) - self['Ulimits'].append(l) + for lmt in ulimits: + if not isinstance(lmt, Ulimit): + lmt = Ulimit(**lmt) + self['Ulimits'].append(lmt) if log_config is not None: if not isinstance(log_config, LogConfig): @@ -392,6 +544,11 @@ class HostConfig(dict): raise host_config_value_error("userns_mode", userns_mode) self['UsernsMode'] = userns_mode + if uts_mode: + if uts_mode != "host": + raise host_config_value_error("uts_mode", uts_mode) + self['UTSMode'] = uts_mode + if pids_limit: if not isinstance(pids_limit, int): raise host_config_type_error('pids_limit', pids_limit, 'int') @@ -400,7 +557,7 @@ class HostConfig(dict): self["PidsLimit"] = pids_limit if isolation: - if not isinstance(isolation, six.string_types): + if not isinstance(isolation, str): raise host_config_type_error('isolation', isolation, 'string') if version_lt(version, '1.24'): raise host_config_version_error('isolation', '1.24') @@ -450,7 +607,7 @@ class HostConfig(dict): self['CpuPercent'] = cpu_percent if nano_cpus: - if not isinstance(nano_cpus, six.integer_types): + if not isinstance(nano_cpus, int): raise host_config_type_error('nano_cpus', nano_cpus, 'int') if version_lt(version, '1.25'): raise host_config_version_error('nano_cpus', '1.25') @@ -476,6 +633,19 @@ class HostConfig(dict): ) self['DeviceCgroupRules'] = device_cgroup_rules + if device_requests is not None: + if version_lt(version, '1.40'): + raise host_config_version_error('device_requests', '1.40') + if not isinstance(device_requests, list): + raise host_config_type_error( + 'device_requests', device_requests, 'list' + ) + self['DeviceRequests'] = [] + for req in device_requests: + if not isinstance(req, DeviceRequest): + req = DeviceRequest(**req) + self['DeviceRequests'].append(req) + def host_config_type_error(param, param_value, expected): error_msg = 'Invalid type for {0} param: expected {1} but found {2}' @@ -493,6 +663,13 @@ def host_config_value_error(param, param_value): return ValueError(error_msg.format(param, param_value)) +def host_config_incompatible_error(param, param_value, incompatible_param): + error_msg = '\"{1}\" {0} is incompatible with {2}' + return errors.InvalidArgument( + error_msg.format(param, param_value, incompatible_param) + ) + + class ContainerConfig(dict): def __init__( self, version, image, command, hostname=None, user=None, detach=False, @@ -520,17 +697,17 @@ class ContainerConfig(dict): 'version 1.29' ) - if isinstance(command, six.string_types): + if isinstance(command, str): command = split_command(command) - if isinstance(entrypoint, six.string_types): + if isinstance(entrypoint, str): entrypoint = split_command(entrypoint) if isinstance(environment, dict): environment = format_environment(environment) if isinstance(labels, list): - labels = dict((lbl, six.text_type('')) for lbl in labels) + labels = {lbl: '' for lbl in labels} if isinstance(ports, list): exposed_ports = {} @@ -541,10 +718,10 @@ class ContainerConfig(dict): if len(port_definition) == 2: proto = port_definition[1] port = port_definition[0] - exposed_ports['{0}/{1}'.format(port, proto)] = {} + exposed_ports[f'{port}/{proto}'] = {} ports = exposed_ports - if isinstance(volumes, six.string_types): + if isinstance(volumes, str): volumes = [volumes, ] if isinstance(volumes, list): @@ -573,7 +750,7 @@ class ContainerConfig(dict): 'Hostname': hostname, 'Domainname': domainname, 'ExposedPorts': ports, - 'User': six.text_type(user) if user else None, + 'User': str(user) if user is not None else None, 'Tty': tty, 'OpenStdin': stdin_open, 'StdinOnce': stdin_once, diff --git a/docker/types/daemon.py b/docker/types/daemon.py index ee8624e..10e8101 100644 --- a/docker/types/daemon.py +++ b/docker/types/daemon.py @@ -5,15 +5,17 @@ try: except ImportError: import urllib3 +from ..errors import DockerException -class CancellableStream(object): + +class CancellableStream: """ Stream wrapper for real-time events, logs, etc. from the server. Example: >>> events = client.events() >>> for event in events: - ... print event + ... print(event) >>> # and cancel from another thread >>> events.close() """ @@ -30,7 +32,7 @@ class CancellableStream(object): return next(self._stream) except urllib3.exceptions.ProtocolError: raise StopIteration - except socket.error: + except OSError: raise StopIteration next = __next__ @@ -55,9 +57,17 @@ class CancellableStream(object): elif hasattr(sock_raw, '_sock'): sock = sock_raw._sock + elif hasattr(sock_fp, 'channel'): + # We're working with a paramiko (SSH) channel, which doesn't + # support cancelable streams with the current implementation + raise DockerException( + 'Cancellable streams not supported for the SSH protocol' + ) else: sock = sock_fp._sock - if isinstance(sock, urllib3.contrib.pyopenssl.WrappedSocket): + + if hasattr(urllib3.contrib, 'pyopenssl') and isinstance( + sock, urllib3.contrib.pyopenssl.WrappedSocket): sock = sock.socket sock.shutdown(socket.SHUT_RDWR) diff --git a/docker/types/healthcheck.py b/docker/types/healthcheck.py index 61857c2..dfc88a9 100644 --- a/docker/types/healthcheck.py +++ b/docker/types/healthcheck.py @@ -1,7 +1,5 @@ from .base import DictType -import six - class Healthcheck(DictType): """ @@ -14,7 +12,7 @@ class Healthcheck(DictType): - Empty list: Inherit healthcheck from parent image - ``["NONE"]``: Disable healthcheck - ``["CMD", args...]``: exec arguments directly. - - ``["CMD-SHELL", command]``: RUn command in the system's + - ``["CMD-SHELL", command]``: Run command in the system's default shell. If a string is provided, it will be used as a ``CMD-SHELL`` @@ -23,15 +21,15 @@ class Healthcheck(DictType): 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 + retries (int): The number of consecutive failures needed to consider a container as unhealthy. - start_period (integer): Start period for the container to + start_period (int): 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): + if isinstance(test, str): test = ["CMD-SHELL", test] interval = kwargs.get('interval', kwargs.get('Interval')) @@ -39,7 +37,7 @@ class Healthcheck(DictType): retries = kwargs.get('retries', kwargs.get('Retries')) start_period = kwargs.get('start_period', kwargs.get('StartPeriod')) - super(Healthcheck, self).__init__({ + super().__init__({ 'Test': test, 'Interval': interval, 'Timeout': timeout, @@ -53,6 +51,8 @@ class Healthcheck(DictType): @test.setter def test(self, value): + if isinstance(value, str): + value = ["CMD-SHELL", value] self['Test'] = value @property diff --git a/docker/types/networks.py b/docker/types/networks.py index 1c7b2c9..1370dc1 100644 --- a/docker/types/networks.py +++ b/docker/types/networks.py @@ -4,7 +4,7 @@ from ..utils import normalize_links, version_lt class EndpointConfig(dict): def __init__(self, version, aliases=None, links=None, ipv4_address=None, - ipv6_address=None, link_local_ips=None): + ipv6_address=None, link_local_ips=None, driver_opt=None): if version_lt(version, '1.22'): raise errors.InvalidVersion( 'Endpoint config is not supported for API version < 1.22' @@ -33,6 +33,15 @@ class EndpointConfig(dict): if ipam_config: self['IPAMConfig'] = ipam_config + if driver_opt: + if version_lt(version, '1.32'): + raise errors.InvalidVersion( + 'DriverOpts is not supported for API version < 1.32' + ) + if not isinstance(driver_opt, dict): + raise TypeError('driver_opt must be a dictionary') + self['DriverOpts'] = driver_opt + class NetworkingConfig(dict): def __init__(self, endpoints_config=None): diff --git a/docker/types/services.py b/docker/types/services.py index 31f4750..fe7cc26 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -1,5 +1,3 @@ -import six - from .. import errors from ..constants import IS_WINDOWS_PLATFORM from ..utils import ( @@ -26,8 +24,8 @@ class TaskTemplate(dict): placement (Placement): Placement instructions for the scheduler. If a list is passed instead, it is assumed to be a list of constraints as part of a :py:class:`Placement` object. - networks (:py:class:`list`): List of network names or IDs to attach - the containers to. + networks (:py:class:`list`): List of network names or IDs or + :py:class:`NetworkAttachmentConfig` to attach the service to. force_update (int): A counter that triggers an update even if no relevant parameters have been changed. """ @@ -110,16 +108,23 @@ class ContainerSpec(dict): privileges (Privileges): Security options for the service's containers. isolation (string): Isolation technology used by the service's containers. Only used for Windows containers. + init (boolean): Run an init inside the container that forwards signals + and reaps processes. + cap_add (:py:class:`list`): A list of kernel capabilities to add to the + default set for the container. + cap_drop (:py:class:`list`): A list of kernel capabilities to drop from + the default set for the container. """ 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, groups=None, open_stdin=None, read_only=None, stop_signal=None, healthcheck=None, hosts=None, dns_config=None, configs=None, - privileges=None, isolation=None): + privileges=None, isolation=None, init=None, cap_add=None, + cap_drop=None): self['Image'] = image - if isinstance(command, six.string_types): + if isinstance(command, str): command = split_command(command) self['Command'] = command self['Args'] = args @@ -149,7 +154,7 @@ class ContainerSpec(dict): if mounts is not None: parsed_mounts = [] for mount in mounts: - if isinstance(mount, six.string_types): + if isinstance(mount, str): parsed_mounts.append(Mount.parse_mount_string(mount)) else: # If mount already parsed @@ -183,6 +188,21 @@ class ContainerSpec(dict): if isolation is not None: self['Isolation'] = isolation + if init is not None: + self['Init'] = init + + if cap_add is not None: + if not isinstance(cap_add, list): + raise TypeError('cap_add must be a list') + + self['CapabilityAdd'] = cap_add + + if cap_drop is not None: + if not isinstance(cap_drop, list): + raise TypeError('cap_drop must be a list') + + self['CapabilityDrop'] = cap_drop + class Mount(dict): """ @@ -219,7 +239,7 @@ class Mount(dict): self['Source'] = source if type not in ('bind', 'volume', 'tmpfs', 'npipe'): raise errors.InvalidArgument( - 'Unsupported mount type: "{}"'.format(type) + f'Unsupported mount type: "{type}"' ) self['Type'] = type self['ReadOnly'] = read_only @@ -255,7 +275,7 @@ class Mount(dict): elif type == 'tmpfs': tmpfs_opts = {} if tmpfs_mode: - if not isinstance(tmpfs_mode, six.integer_types): + if not isinstance(tmpfs_mode, int): raise errors.InvalidArgument( 'tmpfs_mode must be an integer' ) @@ -275,7 +295,7 @@ class Mount(dict): parts = string.split(':') if len(parts) > 3: raise errors.InvalidArgument( - 'Invalid mount format "{0}"'.format(string) + f'Invalid mount format "{string}"' ) if len(parts) == 1: return cls(target=parts[0], source=None) @@ -342,7 +362,7 @@ def _convert_generic_resources_dict(generic_resources): ' (found {})'.format(type(generic_resources)) ) resources = [] - for kind, value in six.iteritems(generic_resources): + for kind, value in generic_resources.items(): resource_type = None if isinstance(value, int): resource_type = 'DiscreteResourceSpec' @@ -368,10 +388,11 @@ class UpdateConfig(dict): parallelism (int): Maximum number of tasks to be updated in one iteration (0 means unlimited parallelism). Default: 0. - delay (int): Amount of time between updates. + delay (int): Amount of time between updates, in nanoseconds. failure_action (string): Action to take if an updated task fails to run, or stops running during the update. Acceptable values are - ``continue`` and ``pause``. Default: ``continue`` + ``continue``, ``pause``, as well as ``rollback`` since API v1.28. + Default: ``continue`` monitor (int): Amount of time to monitor each updated task for failures, in nanoseconds. max_failure_ratio (float): The fraction of tasks that may fail during @@ -385,9 +406,9 @@ class UpdateConfig(dict): self['Parallelism'] = parallelism if delay is not None: self['Delay'] = delay - if failure_action not in ('pause', 'continue'): + if failure_action not in ('pause', 'continue', 'rollback'): raise errors.InvalidArgument( - 'failure_action must be either `pause` or `continue`.' + 'failure_action must be one of `pause`, `continue`, `rollback`' ) self['FailureAction'] = failure_action @@ -413,7 +434,31 @@ class UpdateConfig(dict): self['Order'] = order -class RestartConditionTypesEnum(object): +class RollbackConfig(UpdateConfig): + """ + Used to specify the way containe rollbacks should be performed by a service + + Args: + parallelism (int): Maximum number of tasks to be rolled back in one + iteration (0 means unlimited parallelism). Default: 0 + delay (int): Amount of time between rollbacks, in nanoseconds. + failure_action (string): Action to take if a rolled back task fails to + run, or stops running during the rollback. Acceptable values are + ``continue``, ``pause`` or ``rollback``. + Default: ``continue`` + monitor (int): Amount of time to monitor each rolled back task for + failures, in nanoseconds. + max_failure_ratio (float): The fraction of tasks that may fail during + a rollback before the failure action is invoked, specified as a + floating point number between 0 and 1. Default: 0 + order (string): Specifies the order of operations when rolling out a + rolled back task. Either ``start_first`` or ``stop_first`` are + accepted. + """ + pass + + +class RestartConditionTypesEnum: _values = ( 'none', 'on-failure', @@ -444,7 +489,7 @@ class RestartPolicy(dict): max_attempts=0, window=0): if condition not in self.condition_types._values: raise TypeError( - 'Invalid RestartPolicy condition {0}'.format(condition) + f'Invalid RestartPolicy condition {condition}' ) self['Condition'] = condition @@ -503,7 +548,7 @@ def convert_service_ports(ports): ) result = [] - for k, v in six.iteritems(ports): + for k, v in ports.items(): port_spec = { 'Protocol': 'tcp', 'PublishedPort': k @@ -623,18 +668,28 @@ class Placement(dict): Placement constraints to be used as part of a :py:class:`TaskTemplate` Args: - constraints (:py:class:`list`): A list of constraints - preferences (:py:class:`list`): Preferences provide a way to make - the scheduler aware of factors such as topology. They are - provided in order from highest to lowest precedence. - platforms (:py:class:`list`): A list of platforms expressed as - ``(arch, os)`` tuples - """ - def __init__(self, constraints=None, preferences=None, platforms=None): + constraints (:py:class:`list` of str): A list of constraints + preferences (:py:class:`list` of tuple): Preferences provide a way + to make the scheduler aware of factors such as topology. They + are provided in order from highest to lowest precedence and + are expressed as ``(strategy, descriptor)`` tuples. See + :py:class:`PlacementPreference` for details. + maxreplicas (int): Maximum number of replicas per node + platforms (:py:class:`list` of tuple): A list of platforms + expressed as ``(arch, os)`` tuples + """ + def __init__(self, constraints=None, preferences=None, platforms=None, + maxreplicas=None): if constraints is not None: self['Constraints'] = constraints if preferences is not None: - self['Preferences'] = preferences + self['Preferences'] = [] + for pref in preferences: + if isinstance(pref, tuple): + pref = PlacementPreference(*pref) + self['Preferences'].append(pref) + if maxreplicas is not None: + self['MaxReplicas'] = maxreplicas if platforms: self['Platforms'] = [] for plat in platforms: @@ -643,6 +698,27 @@ class Placement(dict): }) +class PlacementPreference(dict): + """ + Placement preference to be used as an element in the list of + preferences for :py:class:`Placement` objects. + + Args: + strategy (string): The placement strategy to implement. Currently, + the only supported strategy is ``spread``. + descriptor (string): A label descriptor. For the spread strategy, + the scheduler will try to spread tasks evenly over groups of + nodes identified by this label. + """ + def __init__(self, strategy, descriptor): + if strategy != 'spread': + raise errors.InvalidArgument( + 'PlacementPreference strategy value is invalid ({}):' + ' must be "spread".'.format(strategy) + ) + self['Spread'] = {'SpreadDescriptor': descriptor} + + class DNSConfig(dict): """ Specification for DNS related configurations in resolver configuration @@ -662,7 +738,7 @@ class DNSConfig(dict): class Privileges(dict): - """ + r""" Security options for a service's containers. Part of a :py:class:`ContainerSpec` definition. @@ -713,3 +789,21 @@ class Privileges(dict): if len(selinux_context) > 0: self['SELinuxContext'] = selinux_context + + +class NetworkAttachmentConfig(dict): + """ + Network attachment options for a service. + + Args: + target (str): The target network for attachment. + Can be a network name or ID. + aliases (:py:class:`list`): A list of discoverable alternate names + for the service. + options (:py:class:`dict`): Driver attachment options for the + network target. + """ + def __init__(self, target, aliases=None, options=None): + self['Target'] = target + self['Aliases'] = aliases + self['DriverOpts'] = options |