summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoffrey F <f.joffrey@gmail.com>2017-12-19 14:25:53 -0800
committerGitHub <noreply@github.com>2017-12-19 14:25:53 -0800
commit5bed7b8f0dde3949779b60ec0f6305a02dd35a87 (patch)
tree4e986b245772c35f1d00f853a778ddf5c549260e
parentd4007956349f67849d7694174421e744605faa22 (diff)
parent598f16771ca4673886c3ea9b86fa280d77829beb (diff)
downloaddocker-py-5bed7b8f0dde3949779b60ec0f6305a02dd35a87.tar.gz
Merge pull request #1838 from docker/2.7.0-release2.7.0
2.7.0 release
-rw-r--r--docker/api/client.py18
-rw-r--r--docker/api/container.py15
-rw-r--r--docker/api/service.py81
-rw-r--r--docker/api/swarm.py52
-rw-r--r--docker/auth.py2
-rw-r--r--docker/client.py4
-rw-r--r--docker/errors.py2
-rw-r--r--docker/models/containers.py31
-rw-r--r--docker/models/networks.py21
-rw-r--r--docker/models/services.py11
-rw-r--r--docker/models/swarm.py8
-rw-r--r--docker/types/containers.py23
-rw-r--r--docker/types/services.py22
-rw-r--r--docker/utils/utils.py29
-rw-r--r--docker/version.py2
-rw-r--r--docs/change-log.md35
-rw-r--r--docs/swarm.rst2
-rw-r--r--setup.py2
-rw-r--r--tests/integration/api_container_test.py14
-rw-r--r--tests/integration/api_network_test.py7
-rw-r--r--tests/integration/api_service_test.py474
-rw-r--r--tests/integration/api_swarm_test.py29
-rw-r--r--tests/integration/models_containers_test.py37
-rw-r--r--tests/integration/models_networks_test.py10
-rw-r--r--tests/integration/models_services_test.py127
-rw-r--r--tests/unit/dockertypes_test.py22
-rw-r--r--tests/unit/errors_test.py16
-rw-r--r--tests/unit/fake_api.py6
-rw-r--r--tests/unit/fake_api_client.py2
-rw-r--r--tests/unit/models_containers_test.py7
-rw-r--r--tests/unit/models_networks_test.py4
-rw-r--r--tests/unit/models_services_test.py4
-rw-r--r--tests/unit/swarm_test.py43
33 files changed, 1063 insertions, 99 deletions
diff --git a/docker/api/client.py b/docker/api/client.py
index cbe74b9..f0a86d4 100644
--- a/docker/api/client.py
+++ b/docker/api/client.py
@@ -63,21 +63,21 @@ class APIClient(
>>> import docker
>>> client = docker.APIClient(base_url='unix://var/run/docker.sock')
>>> client.version()
- {u'ApiVersion': u'1.24',
+ {u'ApiVersion': u'1.33',
u'Arch': u'amd64',
- u'BuildTime': u'2016-09-27T23:38:15.810178467+00:00',
- u'Experimental': True,
- u'GitCommit': u'45bed2c',
- u'GoVersion': u'go1.6.3',
- u'KernelVersion': u'4.4.22-moby',
+ u'BuildTime': u'2017-11-19T18:46:37.000000000+00:00',
+ u'GitCommit': u'f4ffd2511c',
+ u'GoVersion': u'go1.9.2',
+ u'KernelVersion': u'4.14.3-1-ARCH',
+ u'MinAPIVersion': u'1.12',
u'Os': u'linux',
- u'Version': u'1.12.2-rc1'}
+ u'Version': u'17.10.0-ce'}
Args:
base_url (str): URL to the Docker server. For example,
``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``.
version (str): The version of the API to use. Set to ``auto`` to
- automatically detect the server's version. Default: ``1.26``
+ automatically detect the server's version. Default: ``1.30``
timeout (int): Default timeout for API calls, in seconds.
tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass
``True`` to enable it with default options, or pass a
@@ -206,7 +206,7 @@ class APIClient(
'instead'.format(arg, type(arg))
)
- quote_f = partial(six.moves.urllib.parse.quote_plus, safe="/:")
+ quote_f = partial(six.moves.urllib.parse.quote, safe="/:")
args = map(quote_f, args)
if kwargs.get('versioned_api', True):
diff --git a/docker/api/container.py b/docker/api/container.py
index f3c33c9..5d58851 100644
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -139,7 +139,8 @@ class ContainerApiMixin(object):
Args:
quiet (bool): Only display numeric Ids
all (bool): Show all containers. Only running containers are shown
- by default trunc (bool): Truncate output
+ by default
+ trunc (bool): Truncate output
latest (bool): Show only the latest created container, include
non-running ones.
since (str): Show only containers created since Id or Name, include
@@ -1112,20 +1113,26 @@ class ContainerApiMixin(object):
json=True)
@utils.check_resource('container')
- def stop(self, container, timeout=10):
+ def stop(self, container, timeout=None):
"""
Stops a container. Similar to the ``docker stop`` command.
Args:
container (str): The container to stop
timeout (int): Timeout in seconds to wait for the container to
- stop before sending a ``SIGKILL``. Default: 10
+ stop before sending a ``SIGKILL``. If None, then the
+ StopTimeout value of the container will be used.
+ Default: None
Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
- params = {'t': timeout}
+ if timeout is None:
+ params = {}
+ timeout = 10
+ else:
+ params = {'t': timeout}
url = self._url("/containers/{0}/stop", container)
res = self._post(url, params=params,
diff --git a/docker/api/service.py b/docker/api/service.py
index 4c10ef8..86f4b07 100644
--- a/docker/api/service.py
+++ b/docker/api/service.py
@@ -19,6 +19,10 @@ def _check_api_features(version, task_template, update_config):
if 'Monitor' in update_config:
raise_version_error('UpdateConfig.monitor', '1.25')
+ if utils.version_lt(version, '1.29'):
+ if 'Order' in update_config:
+ raise_version_error('UpdateConfig.order', '1.29')
+
if task_template is not None:
if 'ForceUpdate' in task_template and utils.version_lt(
version, '1.25'):
@@ -62,6 +66,21 @@ def _check_api_features(version, task_template, update_config):
raise_version_error('ContainerSpec.privileges', '1.30')
+def _merge_task_template(current, override):
+ merged = current.copy()
+ if override is not None:
+ for ts_key, ts_value in override.items():
+ if ts_key == 'ContainerSpec':
+ if 'ContainerSpec' not in merged:
+ merged['ContainerSpec'] = {}
+ for cs_key, cs_value in override['ContainerSpec'].items():
+ if cs_value is not None:
+ merged['ContainerSpec'][cs_key] = cs_value
+ elif ts_value is not None:
+ merged[ts_key] = ts_value
+ return merged
+
+
class ServiceApiMixin(object):
@utils.minimum_version('1.24')
def create_service(
@@ -306,7 +325,7 @@ class ServiceApiMixin(object):
def update_service(self, service, version, task_template=None, name=None,
labels=None, mode=None, update_config=None,
networks=None, endpoint_config=None,
- endpoint_spec=None):
+ endpoint_spec=None, fetch_current_spec=False):
"""
Update a service.
@@ -328,6 +347,8 @@ class ServiceApiMixin(object):
the service to. Default: ``None``.
endpoint_spec (EndpointSpec): Properties that can be configured to
access and load balance a service. Default: ``None``.
+ fetch_current_spec (boolean): Use the undefined settings from the
+ current specification of the service. Default: ``False``
Returns:
``True`` if successful.
@@ -345,32 +366,64 @@ class ServiceApiMixin(object):
_check_api_features(self._version, task_template, update_config)
+ if fetch_current_spec:
+ inspect_defaults = True
+ if utils.version_lt(self._version, '1.29'):
+ inspect_defaults = None
+ current = self.inspect_service(
+ service, insert_defaults=inspect_defaults
+ )['Spec']
+
+ else:
+ current = {}
+
url = self._url('/services/{0}/update', service)
data = {}
headers = {}
- if name is not None:
- data['Name'] = name
- if labels is not None:
- data['Labels'] = labels
+
+ data['Name'] = current.get('Name') if name is None else name
+
+ data['Labels'] = current.get('Labels') if labels is None else labels
+
if mode is not None:
if not isinstance(mode, dict):
mode = ServiceMode(mode)
data['Mode'] = mode
- if task_template is not None:
- image = task_template.get('ContainerSpec', {}).get('Image', None)
- if image is not None:
- registry, repo_name = auth.resolve_repository_name(image)
- auth_header = auth.get_config_header(self, registry)
- if auth_header:
- headers['X-Registry-Auth'] = auth_header
- data['TaskTemplate'] = task_template
+ else:
+ data['Mode'] = current.get('Mode')
+
+ data['TaskTemplate'] = _merge_task_template(
+ current.get('TaskTemplate', {}), task_template
+ )
+
+ container_spec = data['TaskTemplate'].get('ContainerSpec', {})
+ image = container_spec.get('Image', None)
+ if image is not None:
+ registry, repo_name = auth.resolve_repository_name(image)
+ auth_header = auth.get_config_header(self, registry)
+ if auth_header:
+ headers['X-Registry-Auth'] = auth_header
+
if update_config is not None:
data['UpdateConfig'] = update_config
+ else:
+ data['UpdateConfig'] = current.get('UpdateConfig')
if networks is not None:
- data['Networks'] = utils.convert_service_networks(networks)
+ converted_networks = utils.convert_service_networks(networks)
+ data['TaskTemplate']['Networks'] = converted_networks
+ elif data['TaskTemplate'].get('Networks') is None:
+ current_task_template = current.get('TaskTemplate', {})
+ current_networks = current_task_template.get('Networks')
+ if current_networks is None:
+ current_networks = current.get('Networks')
+ if current_networks is not None:
+ data['TaskTemplate']['Networks'] = current_networks
+
if endpoint_spec is not None:
data['EndpointSpec'] = endpoint_spec
+ else:
+ data['EndpointSpec'] = current.get('EndpointSpec')
resp = self._post_json(
url, data=data, params={'version': version}, headers=headers
diff --git a/docker/api/swarm.py b/docker/api/swarm.py
index 576fd79..04595da 100644
--- a/docker/api/swarm.py
+++ b/docker/api/swarm.py
@@ -1,7 +1,9 @@
import logging
from six.moves import http_client
+from .. import errors
from .. import types
from .. import utils
+
log = logging.getLogger(__name__)
@@ -69,6 +71,16 @@ class SwarmApiMixin(object):
return types.SwarmSpec(self._version, *args, **kwargs)
@utils.minimum_version('1.24')
+ def get_unlock_key(self):
+ """
+ Get the unlock key for this Swarm manager.
+
+ Returns:
+ A ``dict`` containing an ``UnlockKey`` member
+ """
+ return self._result(self._get(self._url('/swarm/unlockkey')), True)
+
+ @utils.minimum_version('1.24')
def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
force_new_cluster=False, swarm_spec=None):
"""
@@ -152,7 +164,7 @@ class SwarmApiMixin(object):
return self._result(self._get(url), True)
@utils.minimum_version('1.24')
- def join_swarm(self, remote_addrs, join_token, listen_addr=None,
+ def join_swarm(self, remote_addrs, join_token, listen_addr='0.0.0.0:2377',
advertise_addr=None):
"""
Make this Engine join a swarm that has already been created.
@@ -271,9 +283,45 @@ class SwarmApiMixin(object):
return True
@utils.minimum_version('1.24')
+ def unlock_swarm(self, key):
+ """
+ Unlock a locked swarm.
+
+ Args:
+ key (string): The unlock key as provided by
+ :py:meth:`get_unlock_key`
+
+ Raises:
+ :py:class:`docker.errors.InvalidArgument`
+ If the key argument is in an incompatible format
+
+ :py:class:`docker.errors.APIError`
+ If the server returns an error.
+
+ Returns:
+ `True` if the request was successful.
+
+ Example:
+
+ >>> key = client.get_unlock_key()
+ >>> client.unlock_node(key)
+
+ """
+ if isinstance(key, dict):
+ if 'UnlockKey' not in key:
+ raise errors.InvalidArgument('Invalid unlock key format')
+ else:
+ key = {'UnlockKey': key}
+
+ url = self._url('/swarm/unlock')
+ res = self._post_json(url, data=key)
+ self._raise_for_status(res)
+ return True
+
+ @utils.minimum_version('1.24')
def update_node(self, node_id, version, node_spec=None):
"""
- Update the Node's configuration
+ Update the node's configuration
Args:
diff --git a/docker/auth.py b/docker/auth.py
index c3fb062..c0cae5d 100644
--- a/docker/auth.py
+++ b/docker/auth.py
@@ -203,7 +203,7 @@ def parse_auth(entries, raise_on_error=False):
# https://github.com/docker/compose/issues/3265
log.debug(
'Auth data for {0} is absent. Client might be using a '
- 'credentials store instead.'
+ 'credentials store instead.'.format(registry)
)
conf[registry] = {}
continue
diff --git a/docker/client.py b/docker/client.py
index 29968c1..467583e 100644
--- a/docker/client.py
+++ b/docker/client.py
@@ -26,7 +26,7 @@ class DockerClient(object):
base_url (str): URL to the Docker server. For example,
``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``.
version (str): The version of the API to use. Set to ``auto`` to
- automatically detect the server's version. Default: ``1.26``
+ automatically detect the server's version. Default: ``1.30``
timeout (int): Default timeout for API calls, in seconds.
tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass
``True`` to enable it with default options, or pass a
@@ -60,7 +60,7 @@ class DockerClient(object):
Args:
version (str): The version of the API to use. Set to ``auto`` to
- automatically detect the server's version. Default: ``1.26``
+ automatically detect the server's version. Default: ``1.30``
timeout (int): Default timeout for API calls, in seconds.
ssl_version (int): A valid `SSL version`_.
assert_hostname (bool): Verify the hostname of the server.
diff --git a/docker/errors.py b/docker/errors.py
index 2a2f871..50423a2 100644
--- a/docker/errors.py
+++ b/docker/errors.py
@@ -18,7 +18,7 @@ def create_api_error_from_http_exception(e):
try:
explanation = response.json()['message']
except ValueError:
- explanation = response.content.strip()
+ explanation = (response.content or '').strip()
cls = APIError
if response.status_code == 404:
if explanation and ('No such image' in str(explanation) or
diff --git a/docker/models/containers.py b/docker/models/containers.py
index 97a08b9..6ba308e 100644
--- a/docker/models/containers.py
+++ b/docker/models/containers.py
@@ -629,6 +629,9 @@ class ContainerCollection(Collection):
(e.g. ``SIGINT``).
storage_opt (dict): Storage driver options per container as a
key-value mapping.
+ stream (bool): If true and ``detach`` is false, return a log
+ generator instead of a string. Ignored if ``detach`` is true.
+ Default: ``False``.
sysctls (dict): Kernel parameters to set in the container.
tmpfs (dict): Temporary filesystems to mount, as a dictionary
mapping a path inside the container to options for that path.
@@ -696,6 +699,7 @@ class ContainerCollection(Collection):
"""
if isinstance(image, Image):
image = image.id
+ stream = kwargs.pop('stream', False)
detach = kwargs.pop("detach", False)
if detach and remove:
if version_gte(self.client.api._version, '1.25'):
@@ -723,23 +727,30 @@ class ContainerCollection(Collection):
if detach:
return container
- exit_status = container.wait()
- if exit_status != 0:
- stdout = False
- stderr = True
-
logging_driver = container.attrs['HostConfig']['LogConfig']['Type']
+ out = None
if logging_driver == 'json-file' or logging_driver == 'journald':
- out = container.logs(stdout=stdout, stderr=stderr)
- else:
+ out = container.logs(
+ stdout=stdout, stderr=stderr, stream=True, follow=True
+ )
+
+ exit_status = container.wait()
+ if exit_status != 0:
out = None
+ if not kwargs.get('auto_remove'):
+ out = container.logs(stdout=False, stderr=True)
if remove:
container.remove()
if exit_status != 0:
- raise ContainerError(container, exit_status, command, image, out)
- return out
+ raise ContainerError(
+ container, exit_status, command, image, out
+ )
+
+ return out if stream or out is None else b''.join(
+ [line for line in out]
+ )
def create(self, image, command=None, **kwargs):
"""
@@ -873,6 +884,8 @@ RUN_HOST_CONFIG_KWARGS = [
'cpu_shares',
'cpuset_cpus',
'cpuset_mems',
+ 'cpu_rt_period',
+ 'cpu_rt_runtime',
'device_read_bps',
'device_read_iops',
'device_write_bps',
diff --git a/docker/models/networks.py b/docker/models/networks.py
index 158af99..1c2fbf2 100644
--- a/docker/models/networks.py
+++ b/docker/models/networks.py
@@ -1,4 +1,5 @@
from ..api import APIClient
+from ..utils import version_gte
from .containers import Container
from .resource import Model, Collection
@@ -153,7 +154,7 @@ class NetworkCollection(Collection):
resp = self.client.api.create_network(name, *args, **kwargs)
return self.get(resp['Id'])
- def get(self, network_id):
+ def get(self, network_id, *args, **kwargs):
"""
Get a network by its ID.
@@ -175,7 +176,9 @@ class NetworkCollection(Collection):
If the server returns an error.
"""
- return self.prepare_model(self.client.api.inspect_network(network_id))
+ return self.prepare_model(
+ self.client.api.inspect_network(network_id, *args, **kwargs)
+ )
def list(self, *args, **kwargs):
"""
@@ -184,6 +187,13 @@ class NetworkCollection(Collection):
Args:
names (:py:class:`list`): List of names to filter by.
ids (:py:class:`list`): List of ids to filter by.
+ filters (dict): Filters to be processed on the network list.
+ Available filters:
+ - ``driver=[<driver-name>]`` Matches a network's driver.
+ - ``label=[<key>]`` or ``label=[<key>=<value>]``.
+ - ``type=["custom"|"builtin"]`` Filters networks by type.
+ greedy (bool): Fetch more details for each network individually.
+ You might want this to get the containers attached to them.
Returns:
(list of :py:class:`Network`) The networks on the server.
@@ -192,8 +202,13 @@ class NetworkCollection(Collection):
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
+ greedy = kwargs.pop('greedy', False)
resp = self.client.api.networks(*args, **kwargs)
- return [self.prepare_model(item) for item in resp]
+ networks = [self.prepare_model(item) for item in resp]
+ if greedy and version_gte(self.client.api._version, '1.28'):
+ for net in networks:
+ net.reload()
+ return networks
def prune(self, filters=None):
self.client.api.prune_networks(filters=filters)
diff --git a/docker/models/services.py b/docker/models/services.py
index 6fc5c2a..009e455 100644
--- a/docker/models/services.py
+++ b/docker/models/services.py
@@ -251,6 +251,7 @@ CONTAINER_SPEC_KWARGS = [
# kwargs to copy straight over to TaskTemplate
TASK_TEMPLATE_KWARGS = [
+ 'networks',
'resources',
'restart_policy',
]
@@ -261,7 +262,6 @@ CREATE_SERVICE_KWARGS = [
'labels',
'mode',
'update_config',
- 'networks',
'endpoint_spec',
]
@@ -295,6 +295,15 @@ def _get_create_service_kwargs(func_name, kwargs):
'Options': kwargs.pop('log_driver_options', {})
}
+ if func_name == 'update':
+ if 'force_update' in kwargs:
+ task_template_kwargs['force_update'] = kwargs.pop('force_update')
+
+ # fetch the current spec by default if updating the service
+ # through the model
+ fetch_current_spec = kwargs.pop('fetch_current_spec', True)
+ create_kwargs['fetch_current_spec'] = fetch_current_spec
+
# All kwargs should have been consumed by this point, so raise
# error if any are left
if kwargs:
diff --git a/docker/models/swarm.py b/docker/models/swarm.py
index 5a253c5..7396e73 100644
--- a/docker/models/swarm.py
+++ b/docker/models/swarm.py
@@ -29,6 +29,10 @@ class Swarm(Model):
"""
return self.attrs.get('Version').get('Index')
+ def get_unlock_key(self):
+ return self.client.api.get_unlock_key()
+ get_unlock_key.__doc__ = APIClient.get_unlock_key.__doc__
+
def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
force_new_cluster=False, **kwargs):
"""
@@ -128,6 +132,10 @@ class Swarm(Model):
"""
self.attrs = self.client.api.inspect_swarm()
+ def unlock(self, key):
+ return self.client.api.unlock_swarm(key)
+ unlock.__doc__ = APIClient.unlock_swarm.__doc__
+
def update(self, rotate_worker_token=False, rotate_manager_token=False,
**kwargs):
"""
diff --git a/docker/types/containers.py b/docker/types/containers.py
index 13bea71..15dd86c 100644
--- a/docker/types/containers.py
+++ b/docker/types/containers.py
@@ -120,7 +120,8 @@ class HostConfig(dict):
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_mems=None, runtime=None, mounts=None,
+ cpu_rt_period=None, cpu_rt_runtime=None):
if mem_limit is not None:
self['Memory'] = parse_bytes(mem_limit)
@@ -336,6 +337,26 @@ class HostConfig(dict):
)
self['CpusetMems'] = cpuset_mems
+ if cpu_rt_period:
+ if version_lt(version, '1.25'):
+ raise host_config_version_error('cpu_rt_period', '1.25')
+
+ if not isinstance(cpu_rt_period, int):
+ raise host_config_type_error(
+ 'cpu_rt_period', cpu_rt_period, 'int'
+ )
+ self['CPURealtimePeriod'] = cpu_rt_period
+
+ if cpu_rt_runtime:
+ if version_lt(version, '1.25'):
+ raise host_config_version_error('cpu_rt_runtime', '1.25')
+
+ if not isinstance(cpu_rt_runtime, int):
+ raise host_config_type_error(
+ 'cpu_rt_runtime', cpu_rt_runtime, 'int'
+ )
+ self['CPURealtimeRuntime'] = cpu_rt_runtime
+
if blkio_weight:
if not isinstance(blkio_weight, int):
raise host_config_type_error(
diff --git a/docker/types/services.py b/docker/types/services.py
index 9031e60..18d4d2a 100644
--- a/docker/types/services.py
+++ b/docker/types/services.py
@@ -4,7 +4,7 @@ from .. import errors
from ..constants import IS_WINDOWS_PLATFORM
from ..utils import (
check_resource, format_environment, format_extra_hosts, parse_bytes,
- split_command,
+ split_command, convert_service_networks,
)
@@ -26,11 +26,14 @@ 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.
force_update (int): A counter that triggers an update even if no
relevant parameters have been changed.
"""
def __init__(self, container_spec, resources=None, restart_policy=None,
- placement=None, log_driver=None, force_update=None):
+ placement=None, log_driver=None, networks=None,
+ force_update=None):
self['ContainerSpec'] = container_spec
if resources:
self['Resources'] = resources
@@ -42,6 +45,8 @@ class TaskTemplate(dict):
self['Placement'] = placement
if log_driver:
self['LogDriver'] = log_driver
+ if networks:
+ self['Networks'] = convert_service_networks(networks)
if force_update is not None:
if not isinstance(force_update, int):
@@ -137,7 +142,7 @@ class ContainerSpec(dict):
if labels is not None:
self['Labels'] = labels
if hosts is not None:
- self['Hosts'] = format_extra_hosts(hosts)
+ self['Hosts'] = format_extra_hosts(hosts, task=True)
if mounts is not None:
parsed_mounts = []
@@ -334,9 +339,11 @@ class UpdateConfig(dict):
max_failure_ratio (float): The fraction of tasks that may fail during
an update 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 an
+ updated task. Either ``start_first`` or ``stop_first`` are accepted.
"""
def __init__(self, parallelism=0, delay=None, failure_action='continue',
- monitor=None, max_failure_ratio=None):
+ monitor=None, max_failure_ratio=None, order=None):
self['Parallelism'] = parallelism
if delay is not None:
self['Delay'] = delay
@@ -360,6 +367,13 @@ class UpdateConfig(dict):
)
self['MaxFailureRatio'] = max_failure_ratio
+ if order is not None:
+ if order not in ('start-first', 'stop-first'):
+ raise errors.InvalidArgument(
+ 'order must be either `start-first` or `stop-first`'
+ )
+ self['Order'] = order
+
class RestartConditionTypesEnum(object):
_values = (
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
index a123fd8..2de995c 100644
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -97,7 +97,12 @@ def create_archive(root, files=None, fileobj=None, gzip=False):
if files is None:
files = build_file_list(root)
for path in files:
- i = t.gettarinfo(os.path.join(root, path), arcname=path)
+ full_path = os.path.join(root, path)
+ if not os.access(full_path, os.R_OK):
+ raise IOError(
+ 'Can not access file in context: {}'.format(full_path)
+ )
+ i = t.gettarinfo(full_path, arcname=path)
if i is None:
# This happens when we encounter a socket file. We can safely
# ignore it and proceed.
@@ -108,12 +113,14 @@ def create_archive(root, files=None, fileobj=None, gzip=False):
# and directories executable by default.
i.mode = i.mode & 0o755 | 0o111
- try:
- # We open the file object in binary mode for Windows support.
- with open(os.path.join(root, path), 'rb') as f:
- t.addfile(i, f)
- except IOError:
- # When we encounter a directory the file object is set to None.
+ if i.isfile():
+ try:
+ with open(full_path, 'rb') as f:
+ t.addfile(i, f)
+ except IOError:
+ t.addfile(i, None)
+ else:
+ # Directories, FIFOs, symlinks... don't need to be read.
t.addfile(i, None)
t.close()
fileobj.seek(0)
@@ -564,7 +571,13 @@ def format_environment(environment):
return [format_env(*var) for var in six.iteritems(environment)]
-def format_extra_hosts(extra_hosts):
+def format_extra_hosts(extra_hosts, task=False):
+ # Use format dictated by Swarm API if container is part of a task
+ if task:
+ return [
+ '{} {}'.format(v, k) for k, v in sorted(six.iteritems(extra_hosts))
+ ]
+
return [
'{}:{}'.format(k, v) for k, v in sorted(six.iteritems(extra_hosts))
]
diff --git a/docker/version.py b/docker/version.py
index 87c8643..2502183 100644
--- a/docker/version.py
+++ b/docker/version.py
@@ -1,2 +1,2 @@
-version = "2.6.1"
+version = "2.7.0"
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])
diff --git a/docs/change-log.md b/docs/change-log.md
index 57293f3..b8298a7 100644
--- a/docs/change-log.md
+++ b/docs/change-log.md
@@ -1,6 +1,41 @@
Change log
==========
+2.7.0
+-----
+
+[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/41?closed=1)
+
+### Features
+
+* Added `unlock_swarm` and `get_unlock_key` methods to the `APIClient`.
+ * Added `unlock` and `get_unlock_key` to `DockerClient.swarm`.
+* Added a `greedy` parameter to `DockerClient.networks.list`, yielding
+ additional details about the listed networks.
+* Added `cpu_rt_runtime` and `cpu_rt_period` as parameters to
+ `APIClient.create_host_config` and `DockerClient.containers.run`.
+* Added the `order` argument to `UpdateConfig`.
+* Added `fetch_current_spec` to `APIClient.update_service` and `Service.update`
+ that will retrieve the current configuration of the service and merge it with
+ the provided parameters to determine the new configuration.
+
+### Bugfixes
+
+* Fixed a bug where the `build` method tried to include inaccessible files
+ in the context, leading to obscure errors during the build phase
+ (inaccessible files inside the context now raise an `IOError` instead).
+* Fixed a bug where the `build` method would try to read from FIFOs present
+ inside the build context, causing it to hang.
+* `APIClient.stop` will no longer override the `stop_timeout` value present
+ in the container's configuration.
+* Fixed a bug preventing removal of networks with names containing a space.
+* Fixed a bug where `DockerClient.containers.run` would crash if the
+ `auto_remove` parameter was set to `True`.
+* Changed the default value of `listen_addr` in `join_swarm` to match the
+ one in `init_swarm`.
+* Fixed a bug where handling HTTP errors with no body would cause an unexpected
+ exception to be thrown while generating an `APIError` object.
+
2.6.1
-----
diff --git a/docs/swarm.rst b/docs/swarm.rst
index 0c21bae..cab9def 100644
--- a/docs/swarm.rst
+++ b/docs/swarm.rst
@@ -12,9 +12,11 @@ These methods are available on ``client.swarm``:
.. rst-class:: hide-signature
.. py:class:: Swarm
+ .. automethod:: get_unlock_key()
.. automethod:: init()
.. automethod:: join()
.. automethod:: leave()
+ .. automethod:: unlock()
.. automethod:: update()
.. automethod:: reload()
diff --git a/setup.py b/setup.py
index 4a33c8d..d59d812 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ ROOT_DIR = os.path.dirname(__file__)
SOURCE_DIR = os.path.join(ROOT_DIR)
requirements = [
- 'requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0',
+ 'requests >= 2.14.2, != 2.18.0',
'six >= 1.4.0',
'websocket-client >= 0.32.0',
'docker-pycreds >= 0.2.1'
diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py
index f03ccdb..5e30eee 100644
--- a/tests/integration/api_container_test.py
+++ b/tests/integration/api_container_test.py
@@ -464,6 +464,20 @@ class CreateContainerTest(BaseAPIIntegrationTest):
config = self.client.inspect_container(ctnr)
assert config['HostConfig']['InitPath'] == "/usr/libexec/docker-init"
+ @requires_api_version('1.24')
+ @pytest.mark.xfail(not os.path.exists('/sys/fs/cgroup/cpu.rt_runtime_us'),
+ reason='CONFIG_RT_GROUP_SCHED isn\'t enabled')
+ def test_create_with_cpu_rt_options(self):
+ ctnr = self.client.create_container(
+ BUSYBOX, 'true', host_config=self.client.create_host_config(
+ cpu_rt_period=1000, cpu_rt_runtime=500
+ )
+ )
+ self.tmp_containers.append(ctnr)
+ config = self.client.inspect_container(ctnr)
+ assert config['HostConfig']['CpuRealtimeRuntime'] == 500
+ assert config['HostConfig']['CpuRealtimePeriod'] == 1000
+
class VolumeBindTest(BaseAPIIntegrationTest):
def setUp(self):
diff --git a/tests/integration/api_network_test.py b/tests/integration/api_network_test.py
index f4fefde..10e09dd 100644
--- a/tests/integration/api_network_test.py
+++ b/tests/integration/api_network_test.py
@@ -484,3 +484,10 @@ class TestNetworks(BaseAPIIntegrationTest):
assert self.client.inspect_network(net_name_swarm, scope='swarm')
with pytest.raises(docker.errors.NotFound):
self.client.inspect_network(net_name_swarm, scope='local')
+
+ @requires_api_version('1.21')
+ def test_create_remove_network_with_space_in_name(self):
+ net_id = self.client.create_network('test 01')
+ self.tmp_networks.append(net_id)
+ assert self.client.inspect_network('test 01')
+ assert self.client.remove_network('test 01') is None # does not raise
diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py
index b931154..4a2093d 100644
--- a/tests/integration/api_service_test.py
+++ b/tests/integration/api_service_test.py
@@ -386,6 +386,24 @@ class ServiceTest(BaseAPIIntegrationTest):
assert 'Env' in con_spec
assert con_spec['Env'] == ['DOCKER_PY_TEST=1']
+ @requires_api_version('1.29')
+ def test_create_service_with_update_order(self):
+ container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ update_config = docker.types.UpdateConfig(
+ parallelism=10, delay=5, order='start-first'
+ )
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, update_config=update_config, name=name
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'UpdateConfig' in svc_info['Spec']
+ uc = svc_info['Spec']['UpdateConfig']
+ assert update_config['Parallelism'] == uc['Parallelism']
+ assert update_config['Delay'] == uc['Delay']
+ assert update_config['Order'] == uc['Order']
+
@requires_api_version('1.25')
def test_create_service_with_tty(self):
container_spec = docker.types.ContainerSpec(
@@ -588,8 +606,8 @@ class ServiceTest(BaseAPIIntegrationTest):
assert 'Hosts' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
hosts = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Hosts']
assert len(hosts) == 2
- assert 'foobar:127.0.0.1' in hosts
- assert 'baz:8.8.8.8' in hosts
+ assert '127.0.0.1 foobar' in hosts
+ assert '8.8.8.8 baz' in hosts
@requires_api_version('1.25')
def test_create_service_with_hostname(self):
@@ -710,3 +728,455 @@ class ServiceTest(BaseAPIIntegrationTest):
svc_info['Spec']['TaskTemplate']['ContainerSpec']['Privileges']
)
assert privileges['SELinuxContext']['Disable'] is True
+
+ @requires_api_version('1.25')
+ def test_update_service_with_defaults_name(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello']
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(task_tmpl, name=name)
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Name' in svc_info['Spec']
+ assert svc_info['Spec']['Name'] == name
+ version_index = svc_info['Version']['Index']
+
+ task_tmpl = docker.types.TaskTemplate(container_spec, force_update=10)
+ self._update_service(
+ svc_id, name, version_index, task_tmpl, fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'Name' in svc_info['Spec']
+ assert svc_info['Spec']['Name'] == name
+
+ @requires_api_version('1.25')
+ def test_update_service_with_defaults_labels(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello']
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, labels={'service.label': 'SampleLabel'}
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Labels' in svc_info['Spec']
+ assert 'service.label' in svc_info['Spec']['Labels']
+ assert svc_info['Spec']['Labels']['service.label'] == 'SampleLabel'
+ version_index = svc_info['Version']['Index']
+
+ task_tmpl = docker.types.TaskTemplate(container_spec, force_update=10)
+ self._update_service(
+ svc_id, name, version_index, task_tmpl, name=name,
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'Labels' in svc_info['Spec']
+ assert 'service.label' in svc_info['Spec']['Labels']
+ assert svc_info['Spec']['Labels']['service.label'] == 'SampleLabel'
+
+ def test_update_service_with_defaults_mode(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello']
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name,
+ mode=docker.types.ServiceMode(mode='replicated', replicas=2)
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Mode' in svc_info['Spec']
+ assert 'Replicated' in svc_info['Spec']['Mode']
+ assert 'Replicas' in svc_info['Spec']['Mode']['Replicated']
+ assert svc_info['Spec']['Mode']['Replicated']['Replicas'] == 2
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, labels={'force': 'update'},
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'Mode' in svc_info['Spec']
+ assert 'Replicated' in svc_info['Spec']['Mode']
+ assert 'Replicas' in svc_info['Spec']['Mode']['Replicated']
+ assert svc_info['Spec']['Mode']['Replicated']['Replicas'] == 2
+
+ def test_update_service_with_defaults_container_labels(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello'],
+ labels={'container.label': 'SampleLabel'}
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, labels={'service.label': 'SampleLabel'}
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'TaskTemplate' in svc_info['Spec']
+ assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+ assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+ assert labels['container.label'] == 'SampleLabel'
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, labels={'force': 'update'},
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'TaskTemplate' in svc_info['Spec']
+ assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+ assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+ assert labels['container.label'] == 'SampleLabel'
+
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello']
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ self._update_service(
+ svc_id, name, new_index, task_tmpl, fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ newer_index = svc_info['Version']['Index']
+ assert newer_index > new_index
+ assert 'TaskTemplate' in svc_info['Spec']
+ assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+ assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+ assert labels['container.label'] == 'SampleLabel'
+
+ def test_update_service_with_defaults_update_config(self):
+ container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ update_config = docker.types.UpdateConfig(
+ parallelism=10, delay=5, failure_action='pause'
+ )
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, update_config=update_config, name=name
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'UpdateConfig' in svc_info['Spec']
+ uc = svc_info['Spec']['UpdateConfig']
+ assert update_config['Parallelism'] == uc['Parallelism']
+ assert update_config['Delay'] == uc['Delay']
+ assert update_config['FailureAction'] == uc['FailureAction']
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, labels={'force': 'update'},
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'UpdateConfig' in svc_info['Spec']
+ uc = svc_info['Spec']['UpdateConfig']
+ assert update_config['Parallelism'] == uc['Parallelism']
+ assert update_config['Delay'] == uc['Delay']
+ assert update_config['FailureAction'] == uc['FailureAction']
+
+ def test_update_service_with_defaults_networks(self):
+ net1 = self.client.create_network(
+ 'dockerpytest_1', driver='overlay', ipam={'Driver': 'default'}
+ )
+ self.tmp_networks.append(net1['Id'])
+ net2 = self.client.create_network(
+ 'dockerpytest_2', driver='overlay', ipam={'Driver': 'default'}
+ )
+ self.tmp_networks.append(net2['Id'])
+ container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, networks=[
+ 'dockerpytest_1', {'Target': 'dockerpytest_2'}
+ ]
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Networks' in svc_info['Spec']
+ assert svc_info['Spec']['Networks'] == [
+ {'Target': net1['Id']}, {'Target': net2['Id']}
+ ]
+
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, labels={'force': 'update'},
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'Networks' in svc_info['Spec']['TaskTemplate']
+ assert svc_info['Spec']['TaskTemplate']['Networks'] == [
+ {'Target': net1['Id']}, {'Target': net2['Id']}
+ ]
+
+ self._update_service(
+ svc_id, name, new_index, networks=[net1['Id']],
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Networks' in svc_info['Spec']['TaskTemplate']
+ assert svc_info['Spec']['TaskTemplate']['Networks'] == [
+ {'Target': net1['Id']}
+ ]
+
+ def test_update_service_with_defaults_endpoint_spec(self):
+ container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ endpoint_spec = docker.types.EndpointSpec(ports={
+ 12357: (1990, 'udp'),
+ 12562: (678,),
+ 53243: 8080,
+ })
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, endpoint_spec=endpoint_spec
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ print(svc_info)
+ ports = svc_info['Spec']['EndpointSpec']['Ports']
+ for port in ports:
+ if port['PublishedPort'] == 12562:
+ assert port['TargetPort'] == 678
+ assert port['Protocol'] == 'tcp'
+ elif port['PublishedPort'] == 53243:
+ assert port['TargetPort'] == 8080
+ assert port['Protocol'] == 'tcp'
+ elif port['PublishedPort'] == 12357:
+ assert port['TargetPort'] == 1990
+ assert port['Protocol'] == 'udp'
+ else:
+ self.fail('Invalid port specification: {0}'.format(port))
+
+ assert len(ports) == 3
+
+ svc_info = self.client.inspect_service(svc_id)
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, labels={'force': 'update'},
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+
+ ports = svc_info['Spec']['EndpointSpec']['Ports']
+ for port in ports:
+ if port['PublishedPort'] == 12562:
+ assert port['TargetPort'] == 678
+ assert port['Protocol'] == 'tcp'
+ elif port['PublishedPort'] == 53243:
+ assert port['TargetPort'] == 8080
+ assert port['Protocol'] == 'tcp'
+ elif port['PublishedPort'] == 12357:
+ assert port['TargetPort'] == 1990
+ assert port['Protocol'] == 'udp'
+ else:
+ self.fail('Invalid port specification: {0}'.format(port))
+
+ assert len(ports) == 3
+
+ @requires_api_version('1.25')
+ def test_update_service_remove_healthcheck(self):
+ second = 1000000000
+ hc = docker.types.Healthcheck(
+ test='true', retries=3, timeout=1 * second,
+ start_period=3 * second, interval=int(second / 2),
+ )
+ container_spec = docker.types.ContainerSpec(
+ BUSYBOX, ['sleep', '999'], healthcheck=hc
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(task_tmpl, name=name)
+ svc_info = self.client.inspect_service(svc_id)
+ assert (
+ 'Healthcheck' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ )
+ assert (
+ hc ==
+ svc_info['Spec']['TaskTemplate']['ContainerSpec']['Healthcheck']
+ )
+
+ container_spec = docker.types.ContainerSpec(
+ BUSYBOX, ['sleep', '999'], healthcheck={}
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, task_tmpl, fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ container_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ assert (
+ 'Healthcheck' not in container_spec or
+ not container_spec['Healthcheck']
+ )
+
+ def test_update_service_remove_labels(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello']
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, labels={'service.label': 'SampleLabel'}
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Labels' in svc_info['Spec']
+ assert 'service.label' in svc_info['Spec']['Labels']
+ assert svc_info['Spec']['Labels']['service.label'] == 'SampleLabel'
+ version_index = svc_info['Version']['Index']
+
+ self._update_service(
+ svc_id, name, version_index, labels={}, fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert not svc_info['Spec'].get('Labels')
+
+ def test_update_service_remove_container_labels(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello'],
+ labels={'container.label': 'SampleLabel'}
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, labels={'service.label': 'SampleLabel'}
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'TaskTemplate' in svc_info['Spec']
+ assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+ assert 'Labels' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ labels = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Labels']
+ assert labels['container.label'] == 'SampleLabel'
+ version_index = svc_info['Version']['Index']
+
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello'],
+ labels={}
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ self._update_service(
+ svc_id, name, version_index, task_tmpl, fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+ assert 'TaskTemplate' in svc_info['Spec']
+ assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate']
+ container_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec']
+ assert not container_spec.get('Labels')
+
+ @requires_api_version('1.29')
+ def test_update_service_with_network_change(self):
+ container_spec = docker.types.ContainerSpec(
+ 'busybox', ['echo', 'hello']
+ )
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ net1 = self.client.create_network(
+ 'dockerpytest_1', driver='overlay', ipam={'Driver': 'default'}
+ )
+ self.tmp_networks.append(net1['Id'])
+ net2 = self.client.create_network(
+ 'dockerpytest_2', driver='overlay', ipam={'Driver': 'default'}
+ )
+ self.tmp_networks.append(net2['Id'])
+ name = self.get_service_name()
+ svc_id = self.client.create_service(
+ task_tmpl, name=name, networks=[net1['Id']]
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ assert 'Networks' in svc_info['Spec']
+ assert len(svc_info['Spec']['Networks']) > 0
+ assert svc_info['Spec']['Networks'][0]['Target'] == net1['Id']
+
+ svc_info = self.client.inspect_service(svc_id)
+ version_index = svc_info['Version']['Index']
+
+ task_tmpl = docker.types.TaskTemplate(container_spec)
+ self._update_service(
+ svc_id, name, version_index, task_tmpl, name=name,
+ networks=[net2['Id']], fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ task_template = svc_info['Spec']['TaskTemplate']
+ assert 'Networks' in task_template
+ assert len(task_template['Networks']) > 0
+ assert task_template['Networks'][0]['Target'] == net2['Id']
+
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+ assert new_index > version_index
+
+ self._update_service(
+ svc_id, name, new_index, name=name, networks=[net1['Id']],
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ task_template = svc_info['Spec']['TaskTemplate']
+ assert 'ContainerSpec' in task_template
+ new_spec = task_template['ContainerSpec']
+ assert 'Image' in new_spec
+ assert new_spec['Image'].split(':')[0] == 'busybox'
+ assert 'Command' in new_spec
+ assert new_spec['Command'] == ['echo', 'hello']
+ assert 'Networks' in task_template
+ assert len(task_template['Networks']) > 0
+ assert task_template['Networks'][0]['Target'] == net1['Id']
+
+ svc_info = self.client.inspect_service(svc_id)
+ new_index = svc_info['Version']['Index']
+
+ task_tmpl = docker.types.TaskTemplate(
+ container_spec, networks=[net2['Id']]
+ )
+ self._update_service(
+ svc_id, name, new_index, task_tmpl, name=name,
+ fetch_current_spec=True
+ )
+ svc_info = self.client.inspect_service(svc_id)
+ task_template = svc_info['Spec']['TaskTemplate']
+ assert 'Networks' in task_template
+ assert len(task_template['Networks']) > 0
+ assert task_template['Networks'][0]['Target'] == net2['Id']
+
+ def _update_service(self, svc_id, *args, **kwargs):
+ # service update tests seem to be a bit flaky
+ # give them a chance to retry the update with a new version index
+ try:
+ self.client.update_service(*args, **kwargs)
+ except docker.errors.APIError as e:
+ if e.explanation.endswith("update out of sequence"):
+ svc_info = self.client.inspect_service(svc_id)
+ version_index = svc_info['Version']['Index']
+
+ if len(args) > 1:
+ args = (args[0], version_index) + args[2:]
+ else:
+ kwargs['version'] = version_index
+
+ self.client.update_service(*args, **kwargs)
+ else:
+ raise
diff --git a/tests/integration/api_swarm_test.py b/tests/integration/api_swarm_test.py
index 34b0879..dbf3786 100644
--- a/tests/integration/api_swarm_test.py
+++ b/tests/integration/api_swarm_test.py
@@ -10,9 +10,16 @@ class SwarmTest(BaseAPIIntegrationTest):
def setUp(self):
super(SwarmTest, self).setUp()
force_leave_swarm(self.client)
+ self._unlock_key = None
def tearDown(self):
super(SwarmTest, self).tearDown()
+ try:
+ if self._unlock_key:
+ self.client.unlock_swarm(self._unlock_key)
+ except docker.errors.APIError:
+ pass
+
force_leave_swarm(self.client)
@requires_api_version('1.24')
@@ -64,12 +71,16 @@ class SwarmTest(BaseAPIIntegrationTest):
def test_init_swarm_with_autolock_managers(self):
spec = self.client.create_swarm_spec(autolock_managers=True)
assert self.init_swarm(swarm_spec=spec)
+ # save unlock key for tearDown
+ self._unlock_key = self.client.get_unlock_key()
swarm_info = self.client.inspect_swarm()
assert (
swarm_info['Spec']['EncryptionConfig']['AutoLockManagers'] is True
)
+ assert self._unlock_key.get('UnlockKey')
+
@requires_api_version('1.25')
@pytest.mark.xfail(
reason="This doesn't seem to be taken into account by the engine"
@@ -127,24 +138,6 @@ class SwarmTest(BaseAPIIntegrationTest):
)
@requires_api_version('1.24')
- def test_update_swarm_name(self):
- assert self.init_swarm()
- swarm_info_1 = self.client.inspect_swarm()
- spec = self.client.create_swarm_spec(
- node_cert_expiry=7776000000000000, name='reimuhakurei'
- )
- assert self.client.update_swarm(
- version=swarm_info_1['Version']['Index'], swarm_spec=spec
- )
- swarm_info_2 = self.client.inspect_swarm()
-
- assert (
- swarm_info_1['Version']['Index'] !=
- swarm_info_2['Version']['Index']
- )
- assert swarm_info_2['Spec']['Name'] == 'reimuhakurei'
-
- @requires_api_version('1.24')
def test_list_nodes(self):
assert self.init_swarm()
nodes_list = self.client.nodes()
diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py
index ce3349b..d246189 100644
--- a/tests/integration/models_containers_test.py
+++ b/tests/integration/models_containers_test.py
@@ -1,7 +1,9 @@
-import docker
import tempfile
+
+import docker
+import pytest
from .base import BaseIntegrationTest, TEST_API_VERSION
-from ..helpers import random_name
+from ..helpers import random_name, requires_api_version
class ContainerCollectionTest(BaseIntegrationTest):
@@ -95,7 +97,7 @@ class ContainerCollectionTest(BaseIntegrationTest):
"alpine", "echo hello",
log_config=dict(type='none')
)
- self.assertEqual(out, None)
+ assert out is None
def test_run_with_json_file_driver(self):
client = docker.from_env(version=TEST_API_VERSION)
@@ -104,7 +106,34 @@ class ContainerCollectionTest(BaseIntegrationTest):
"alpine", "echo hello",
log_config=dict(type='json-file')
)
- self.assertEqual(out, b'hello\n')
+ assert out == b'hello\n'
+
+ @requires_api_version('1.25')
+ def test_run_with_auto_remove(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ out = client.containers.run(
+ 'alpine', 'echo hello', auto_remove=True
+ )
+ assert out == b'hello\n'
+
+ @requires_api_version('1.25')
+ def test_run_with_auto_remove_error(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ with pytest.raises(docker.errors.ContainerError) as e:
+ client.containers.run(
+ 'alpine', 'sh -c ">&2 echo error && exit 1"', auto_remove=True
+ )
+ assert e.value.exit_status == 1
+ assert e.value.stderr is None
+
+ def test_run_with_streamed_logs(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ out = client.containers.run(
+ 'alpine', 'sh -c "echo hello && echo world"', stream=True
+ )
+ logs = [line for line in out]
+ assert logs[0] == b'hello\n'
+ assert logs[1] == b'world\n'
def test_get(self):
client = docker.from_env(version=TEST_API_VERSION)
diff --git a/tests/integration/models_networks_test.py b/tests/integration/models_networks_test.py
index 105dcc5..08d7ad2 100644
--- a/tests/integration/models_networks_test.py
+++ b/tests/integration/models_networks_test.py
@@ -3,7 +3,7 @@ from .. import helpers
from .base import BaseIntegrationTest, TEST_API_VERSION
-class ImageCollectionTest(BaseIntegrationTest):
+class NetworkCollectionTest(BaseIntegrationTest):
def test_create(self):
client = docker.from_env(version=TEST_API_VERSION)
@@ -47,7 +47,7 @@ class ImageCollectionTest(BaseIntegrationTest):
assert network.id not in [n.id for n in client.networks.list()]
-class ImageTest(BaseIntegrationTest):
+class NetworkTest(BaseIntegrationTest):
def test_connect_disconnect(self):
client = docker.from_env(version=TEST_API_VERSION)
@@ -59,6 +59,12 @@ class ImageTest(BaseIntegrationTest):
network.connect(container)
container.start()
assert client.networks.get(network.id).containers == [container]
+ network_containers = list(
+ c
+ for net in client.networks.list(ids=[network.id], greedy=True)
+ for c in net.containers
+ )
+ assert network_containers == [container]
network.disconnect(container)
assert network.containers == []
assert client.networks.get(network.id).containers == []
diff --git a/tests/integration/models_services_test.py b/tests/integration/models_services_test.py
index 6b5dab5..ca8be48 100644
--- a/tests/integration/models_services_test.py
+++ b/tests/integration/models_services_test.py
@@ -1,7 +1,6 @@
import unittest
import docker
-import pytest
from .. import helpers
from .base import TEST_API_VERSION
@@ -36,6 +35,25 @@ class ServiceTest(unittest.TestCase):
assert "alpine" in container_spec['Image']
assert container_spec['Labels'] == {'container': 'label'}
+ def test_create_with_network(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ name = helpers.random_name()
+ network = client.networks.create(
+ helpers.random_name(), driver='overlay'
+ )
+ service = client.services.create(
+ # create arguments
+ name=name,
+ # ContainerSpec arguments
+ image="alpine",
+ command="sleep 300",
+ networks=[network.id]
+ )
+ assert 'Networks' in service.attrs['Spec']['TaskTemplate']
+ networks = service.attrs['Spec']['TaskTemplate']['Networks']
+ assert len(networks) == 1
+ assert networks[0]['Target'] == network.id
+
def test_get(self):
client = docker.from_env(version=TEST_API_VERSION)
name = helpers.random_name()
@@ -82,7 +100,6 @@ class ServiceTest(unittest.TestCase):
assert len(tasks) == 1
assert tasks[0]['ServiceID'] == service2.id
- @pytest.mark.skip(reason="Makes Swarm unstable?")
def test_update(self):
client = docker.from_env(version=TEST_API_VERSION)
service = client.services.create(
@@ -101,3 +118,109 @@ class ServiceTest(unittest.TestCase):
service.reload()
container_spec = service.attrs['Spec']['TaskTemplate']['ContainerSpec']
assert container_spec['Command'] == ["sleep", "600"]
+
+ def test_update_retains_service_labels(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ service = client.services.create(
+ # create arguments
+ name=helpers.random_name(),
+ labels={'service.label': 'SampleLabel'},
+ # ContainerSpec arguments
+ image="alpine",
+ command="sleep 300"
+ )
+ service.update(
+ # create argument
+ name=service.name,
+ # ContainerSpec argument
+ command="sleep 600"
+ )
+ service.reload()
+ labels = service.attrs['Spec']['Labels']
+ assert labels == {'service.label': 'SampleLabel'}
+
+ def test_update_retains_container_labels(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ service = client.services.create(
+ # create arguments
+ name=helpers.random_name(),
+ # ContainerSpec arguments
+ image="alpine",
+ command="sleep 300",
+ container_labels={'container.label': 'SampleLabel'}
+ )
+ service.update(
+ # create argument
+ name=service.name,
+ # ContainerSpec argument
+ command="sleep 600"
+ )
+ service.reload()
+ container_spec = service.attrs['Spec']['TaskTemplate']['ContainerSpec']
+ assert container_spec['Labels'] == {'container.label': 'SampleLabel'}
+
+ def test_update_remove_service_labels(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ service = client.services.create(
+ # create arguments
+ name=helpers.random_name(),
+ labels={'service.label': 'SampleLabel'},
+ # ContainerSpec arguments
+ image="alpine",
+ command="sleep 300"
+ )
+ service.update(
+ # create argument
+ name=service.name,
+ labels={},
+ # ContainerSpec argument
+ command="sleep 600"
+ )
+ service.reload()
+ assert not service.attrs['Spec'].get('Labels')
+
+ def test_scale_service(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ service = client.services.create(
+ # create arguments
+ name=helpers.random_name(),
+ # ContainerSpec arguments
+ image="alpine",
+ command="sleep 300"
+ )
+ tasks = []
+ while len(tasks) == 0:
+ tasks = service.tasks()
+ assert len(tasks) == 1
+ service.update(
+ mode=docker.types.ServiceMode('replicated', replicas=2),
+ )
+ while len(tasks) == 1:
+ tasks = service.tasks()
+ assert len(tasks) >= 2
+ # check that the container spec is not overridden with None
+ service.reload()
+ spec = service.attrs['Spec']['TaskTemplate']['ContainerSpec']
+ assert spec.get('Command') == ['sleep', '300']
+
+ @helpers.requires_api_version('1.25')
+ def test_restart_service(self):
+ client = docker.from_env(version=TEST_API_VERSION)
+ service = client.services.create(
+ # create arguments
+ name=helpers.random_name(),
+ # ContainerSpec arguments
+ image="alpine",
+ command="sleep 300"
+ )
+ initial_version = service.version
+ service.update(
+ # create argument
+ name=service.name,
+ # task template argument
+ force_update=10,
+ # ContainerSpec argument
+ command="sleep 600"
+ )
+ service.reload()
+ assert service.version > initial_version
diff --git a/tests/unit/dockertypes_test.py b/tests/unit/dockertypes_test.py
index 8dbb35e..40adbb7 100644
--- a/tests/unit/dockertypes_test.py
+++ b/tests/unit/dockertypes_test.py
@@ -206,6 +206,28 @@ class HostConfigTest(unittest.TestCase):
InvalidVersion, lambda: create_host_config(
version='1.24', nano_cpus=1))
+ def test_create_host_config_with_cpu_rt_period_types(self):
+ with pytest.raises(TypeError):
+ create_host_config(version='1.25', cpu_rt_period='1000')
+
+ def test_create_host_config_with_cpu_rt_period(self):
+ config = create_host_config(version='1.25', cpu_rt_period=1000)
+ self.assertEqual(config.get('CPURealtimePeriod'), 1000)
+ self.assertRaises(
+ InvalidVersion, lambda: create_host_config(
+ version='1.24', cpu_rt_period=1000))
+
+ def test_ctrate_host_config_with_cpu_rt_runtime_types(self):
+ with pytest.raises(TypeError):
+ create_host_config(version='1.25', cpu_rt_runtime='1000')
+
+ def test_create_host_config_with_cpu_rt_runtime(self):
+ config = create_host_config(version='1.25', cpu_rt_runtime=1000)
+ self.assertEqual(config.get('CPURealtimeRuntime'), 1000)
+ self.assertRaises(
+ InvalidVersion, lambda: create_host_config(
+ version='1.24', cpu_rt_runtime=1000))
+
class ContainerConfigTest(unittest.TestCase):
def test_create_container_config_volume_driver_warning(self):
diff --git a/tests/unit/errors_test.py b/tests/unit/errors_test.py
index 9678669..e27a9b1 100644
--- a/tests/unit/errors_test.py
+++ b/tests/unit/errors_test.py
@@ -3,7 +3,8 @@ import unittest
import requests
from docker.errors import (APIError, ContainerError, DockerException,
- create_unexpected_kwargs_error)
+ create_unexpected_kwargs_error,
+ create_api_error_from_http_exception)
from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID
from .fake_api_client import make_fake_client
@@ -78,6 +79,19 @@ class APIErrorTest(unittest.TestCase):
err = APIError('', response=resp)
assert err.is_client_error() is True
+ def test_create_error_from_exception(self):
+ resp = requests.Response()
+ resp.status_code = 500
+ err = APIError('')
+ try:
+ resp.raise_for_status()
+ except requests.exceptions.HTTPError as e:
+ try:
+ create_api_error_from_http_exception(e)
+ except APIError as e:
+ err = e
+ assert err.is_server_error() is True
+
class ContainerErrorTest(unittest.TestCase):
def test_container_without_stderr(self):
diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py
index 045c342..37154a3 100644
--- a/tests/unit/fake_api.py
+++ b/tests/unit/fake_api.py
@@ -437,6 +437,10 @@ def post_fake_update_node():
return 200, None
+def post_fake_join_swarm():
+ return 200, None
+
+
def get_fake_network_list():
return 200, [{
"Name": "bridge",
@@ -601,6 +605,8 @@ fake_responses = {
CURRENT_VERSION, prefix, FAKE_NODE_ID
), 'POST'):
post_fake_update_node,
+ ('{1}/{0}/swarm/join'.format(CURRENT_VERSION, prefix), 'POST'):
+ post_fake_join_swarm,
('{1}/{0}/networks'.format(CURRENT_VERSION, prefix), 'GET'):
get_fake_network_list,
('{1}/{0}/networks/create'.format(CURRENT_VERSION, prefix), 'POST'):
diff --git a/tests/unit/fake_api_client.py b/tests/unit/fake_api_client.py
index 47890ac..f908355 100644
--- a/tests/unit/fake_api_client.py
+++ b/tests/unit/fake_api_client.py
@@ -43,7 +43,7 @@ def make_fake_api_client():
fake_api.get_fake_inspect_container()[1],
'inspect_image.return_value': fake_api.get_fake_inspect_image()[1],
'inspect_network.return_value': fake_api.get_fake_network()[1],
- 'logs.return_value': 'hello world\n',
+ 'logs.return_value': [b'hello world\n'],
'networks.return_value': fake_api.get_fake_network_list()[1],
'start.return_value': None,
'wait.return_value': 0,
diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py
index 5eaa45a..a479e83 100644
--- a/tests/unit/models_containers_test.py
+++ b/tests/unit/models_containers_test.py
@@ -12,7 +12,7 @@ class ContainerCollectionTest(unittest.TestCase):
client = make_fake_client()
out = client.containers.run("alpine", "echo hello world")
- assert out == 'hello world\n'
+ assert out == b'hello world\n'
client.api.create_container.assert_called_with(
image="alpine",
@@ -24,9 +24,8 @@ class ContainerCollectionTest(unittest.TestCase):
client.api.start.assert_called_with(FAKE_CONTAINER_ID)
client.api.wait.assert_called_with(FAKE_CONTAINER_ID)
client.api.logs.assert_called_with(
- FAKE_CONTAINER_ID,
- stderr=False,
- stdout=True
+ FAKE_CONTAINER_ID, stderr=False, stdout=True, stream=True,
+ follow=True
)
def test_create_container_args(self):
diff --git a/tests/unit/models_networks_test.py b/tests/unit/models_networks_test.py
index 943b904..58c9fce 100644
--- a/tests/unit/models_networks_test.py
+++ b/tests/unit/models_networks_test.py
@@ -4,7 +4,7 @@ from .fake_api import FAKE_NETWORK_ID, FAKE_CONTAINER_ID
from .fake_api_client import make_fake_client
-class ImageCollectionTest(unittest.TestCase):
+class NetworkCollectionTest(unittest.TestCase):
def test_create(self):
client = make_fake_client()
@@ -37,7 +37,7 @@ class ImageCollectionTest(unittest.TestCase):
assert client.api.networks.called_once_with(names=["foobar"])
-class ImageTest(unittest.TestCase):
+class NetworkTest(unittest.TestCase):
def test_connect(self):
client = make_fake_client()
diff --git a/tests/unit/models_services_test.py b/tests/unit/models_services_test.py
index e7e317d..247bb4a 100644
--- a/tests/unit/models_services_test.py
+++ b/tests/unit/models_services_test.py
@@ -35,18 +35,18 @@ class CreateServiceKwargsTest(unittest.TestCase):
'labels': {'key': 'value'},
'mode': 'global',
'update_config': {'update': 'config'},
- 'networks': ['somenet'],
'endpoint_spec': {'blah': 'blah'},
}
assert set(task_template.keys()) == set([
'ContainerSpec', 'Resources', 'RestartPolicy', 'Placement',
- 'LogDriver'
+ 'LogDriver', 'Networks'
])
assert task_template['Placement'] == {'Constraints': ['foo=bar']}
assert task_template['LogDriver'] == {
'Name': 'logdriver',
'Options': {'foo': 'bar'}
}
+ assert task_template['Networks'] == [{'Target': 'somenet'}]
assert set(task_template['ContainerSpec'].keys()) == set([
'Image', 'Command', 'Args', 'Hostname', 'Env', 'Dir', 'User',
'Labels', 'Mounts', 'StopGracePeriod'
diff --git a/tests/unit/swarm_test.py b/tests/unit/swarm_test.py
index 374f8b2..9a66c0c 100644
--- a/tests/unit/swarm_test.py
+++ b/tests/unit/swarm_test.py
@@ -30,3 +30,46 @@ class SwarmTest(BaseAPIClientTest):
self.assertEqual(
args[1]['headers']['Content-Type'], 'application/json'
)
+
+ @requires_api_version('1.24')
+ def test_join_swarm(self):
+ remote_addr = ['1.2.3.4:2377']
+ listen_addr = '2.3.4.5:2377'
+ join_token = 'A_BEAUTIFUL_JOIN_TOKEN'
+
+ data = {
+ 'RemoteAddrs': remote_addr,
+ 'ListenAddr': listen_addr,
+ 'JoinToken': join_token
+ }
+
+ self.client.join_swarm(
+ remote_addrs=remote_addr,
+ listen_addr=listen_addr,
+ join_token=join_token
+ )
+
+ args = fake_request.call_args
+
+ assert (args[0][1] == url_prefix + 'swarm/join')
+ assert (json.loads(args[1]['data']) == data)
+ assert (args[1]['headers']['Content-Type'] == 'application/json')
+
+ @requires_api_version('1.24')
+ def test_join_swarm_no_listen_address_takes_default(self):
+ remote_addr = ['1.2.3.4:2377']
+ join_token = 'A_BEAUTIFUL_JOIN_TOKEN'
+
+ data = {
+ 'RemoteAddrs': remote_addr,
+ 'ListenAddr': '0.0.0.0:2377',
+ 'JoinToken': join_token
+ }
+
+ self.client.join_swarm(remote_addrs=remote_addr, join_token=join_token)
+
+ args = fake_request.call_args
+
+ assert (args[0][1] == url_prefix + 'swarm/join')
+ assert (json.loads(args[1]['data']) == data)
+ assert (args[1]['headers']['Content-Type'] == 'application/json')