Skip to content

Commit 5bed7b8

Browse files
authored
Merge pull request #1838 from docker/2.7.0-release
2.7.0 release
2 parents d400795 + 598f167 commit 5bed7b8

33 files changed

+1063
-99
lines changed

docker/api/client.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,21 @@ class APIClient(
6363
>>> import docker
6464
>>> client = docker.APIClient(base_url='unix://var/run/docker.sock')
6565
>>> client.version()
66-
{u'ApiVersion': u'1.24',
66+
{u'ApiVersion': u'1.33',
6767
u'Arch': u'amd64',
68-
u'BuildTime': u'2016-09-27T23:38:15.810178467+00:00',
69-
u'Experimental': True,
70-
u'GitCommit': u'45bed2c',
71-
u'GoVersion': u'go1.6.3',
72-
u'KernelVersion': u'4.4.22-moby',
68+
u'BuildTime': u'2017-11-19T18:46:37.000000000+00:00',
69+
u'GitCommit': u'f4ffd2511c',
70+
u'GoVersion': u'go1.9.2',
71+
u'KernelVersion': u'4.14.3-1-ARCH',
72+
u'MinAPIVersion': u'1.12',
7373
u'Os': u'linux',
74-
u'Version': u'1.12.2-rc1'}
74+
u'Version': u'17.10.0-ce'}
7575
7676
Args:
7777
base_url (str): URL to the Docker server. For example,
7878
``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``.
7979
version (str): The version of the API to use. Set to ``auto`` to
80-
automatically detect the server's version. Default: ``1.26``
80+
automatically detect the server's version. Default: ``1.30``
8181
timeout (int): Default timeout for API calls, in seconds.
8282
tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass
8383
``True`` to enable it with default options, or pass a
@@ -206,7 +206,7 @@ def _url(self, pathfmt, *args, **kwargs):
206206
'instead'.format(arg, type(arg))
207207
)
208208

209-
quote_f = partial(six.moves.urllib.parse.quote_plus, safe="/:")
209+
quote_f = partial(six.moves.urllib.parse.quote, safe="/:")
210210
args = map(quote_f, args)
211211

212212
if kwargs.get('versioned_api', True):

docker/api/container.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False,
139139
Args:
140140
quiet (bool): Only display numeric Ids
141141
all (bool): Show all containers. Only running containers are shown
142-
by default trunc (bool): Truncate output
142+
by default
143+
trunc (bool): Truncate output
143144
latest (bool): Show only the latest created container, include
144145
non-running ones.
145146
since (str): Show only containers created since Id or Name, include
@@ -1112,20 +1113,26 @@ def stats(self, container, decode=None, stream=True):
11121113
json=True)
11131114

11141115
@utils.check_resource('container')
1115-
def stop(self, container, timeout=10):
1116+
def stop(self, container, timeout=None):
11161117
"""
11171118
Stops a container. Similar to the ``docker stop`` command.
11181119
11191120
Args:
11201121
container (str): The container to stop
11211122
timeout (int): Timeout in seconds to wait for the container to
1122-
stop before sending a ``SIGKILL``. Default: 10
1123+
stop before sending a ``SIGKILL``. If None, then the
1124+
StopTimeout value of the container will be used.
1125+
Default: None
11231126
11241127
Raises:
11251128
:py:class:`docker.errors.APIError`
11261129
If the server returns an error.
11271130
"""
1128-
params = {'t': timeout}
1131+
if timeout is None:
1132+
params = {}
1133+
timeout = 10
1134+
else:
1135+
params = {'t': timeout}
11291136
url = self._url("/containers/{0}/stop", container)
11301137

11311138
res = self._post(url, params=params,

docker/api/service.py

+67-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def raise_version_error(param, min_version):
1919
if 'Monitor' in update_config:
2020
raise_version_error('UpdateConfig.monitor', '1.25')
2121

22+
if utils.version_lt(version, '1.29'):
23+
if 'Order' in update_config:
24+
raise_version_error('UpdateConfig.order', '1.29')
25+
2226
if task_template is not None:
2327
if 'ForceUpdate' in task_template and utils.version_lt(
2428
version, '1.25'):
@@ -62,6 +66,21 @@ def raise_version_error(param, min_version):
6266
raise_version_error('ContainerSpec.privileges', '1.30')
6367

6468

69+
def _merge_task_template(current, override):
70+
merged = current.copy()
71+
if override is not None:
72+
for ts_key, ts_value in override.items():
73+
if ts_key == 'ContainerSpec':
74+
if 'ContainerSpec' not in merged:
75+
merged['ContainerSpec'] = {}
76+
for cs_key, cs_value in override['ContainerSpec'].items():
77+
if cs_value is not None:
78+
merged['ContainerSpec'][cs_key] = cs_value
79+
elif ts_value is not None:
80+
merged[ts_key] = ts_value
81+
return merged
82+
83+
6584
class ServiceApiMixin(object):
6685
@utils.minimum_version('1.24')
6786
def create_service(
@@ -306,7 +325,7 @@ def tasks(self, filters=None):
306325
def update_service(self, service, version, task_template=None, name=None,
307326
labels=None, mode=None, update_config=None,
308327
networks=None, endpoint_config=None,
309-
endpoint_spec=None):
328+
endpoint_spec=None, fetch_current_spec=False):
310329
"""
311330
Update a service.
312331
@@ -328,6 +347,8 @@ def update_service(self, service, version, task_template=None, name=None,
328347
the service to. Default: ``None``.
329348
endpoint_spec (EndpointSpec): Properties that can be configured to
330349
access and load balance a service. Default: ``None``.
350+
fetch_current_spec (boolean): Use the undefined settings from the
351+
current specification of the service. Default: ``False``
331352
332353
Returns:
333354
``True`` if successful.
@@ -345,32 +366,64 @@ def update_service(self, service, version, task_template=None, name=None,
345366

346367
_check_api_features(self._version, task_template, update_config)
347368

369+
if fetch_current_spec:
370+
inspect_defaults = True
371+
if utils.version_lt(self._version, '1.29'):
372+
inspect_defaults = None
373+
current = self.inspect_service(
374+
service, insert_defaults=inspect_defaults
375+
)['Spec']
376+
377+
else:
378+
current = {}
379+
348380
url = self._url('/services/{0}/update', service)
349381
data = {}
350382
headers = {}
351-
if name is not None:
352-
data['Name'] = name
353-
if labels is not None:
354-
data['Labels'] = labels
383+
384+
data['Name'] = current.get('Name') if name is None else name
385+
386+
data['Labels'] = current.get('Labels') if labels is None else labels
387+
355388
if mode is not None:
356389
if not isinstance(mode, dict):
357390
mode = ServiceMode(mode)
358391
data['Mode'] = mode
359-
if task_template is not None:
360-
image = task_template.get('ContainerSpec', {}).get('Image', None)
361-
if image is not None:
362-
registry, repo_name = auth.resolve_repository_name(image)
363-
auth_header = auth.get_config_header(self, registry)
364-
if auth_header:
365-
headers['X-Registry-Auth'] = auth_header
366-
data['TaskTemplate'] = task_template
392+
else:
393+
data['Mode'] = current.get('Mode')
394+
395+
data['TaskTemplate'] = _merge_task_template(
396+
current.get('TaskTemplate', {}), task_template
397+
)
398+
399+
container_spec = data['TaskTemplate'].get('ContainerSpec', {})
400+
image = container_spec.get('Image', None)
401+
if image is not None:
402+
registry, repo_name = auth.resolve_repository_name(image)
403+
auth_header = auth.get_config_header(self, registry)
404+
if auth_header:
405+
headers['X-Registry-Auth'] = auth_header
406+
367407
if update_config is not None:
368408
data['UpdateConfig'] = update_config
409+
else:
410+
data['UpdateConfig'] = current.get('UpdateConfig')
369411

370412
if networks is not None:
371-
data['Networks'] = utils.convert_service_networks(networks)
413+
converted_networks = utils.convert_service_networks(networks)
414+
data['TaskTemplate']['Networks'] = converted_networks
415+
elif data['TaskTemplate'].get('Networks') is None:
416+
current_task_template = current.get('TaskTemplate', {})
417+
current_networks = current_task_template.get('Networks')
418+
if current_networks is None:
419+
current_networks = current.get('Networks')
420+
if current_networks is not None:
421+
data['TaskTemplate']['Networks'] = current_networks
422+
372423
if endpoint_spec is not None:
373424
data['EndpointSpec'] = endpoint_spec
425+
else:
426+
data['EndpointSpec'] = current.get('EndpointSpec')
374427

375428
resp = self._post_json(
376429
url, data=data, params={'version': version}, headers=headers

docker/api/swarm.py

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22
from six.moves import http_client
3+
from .. import errors
34
from .. import types
45
from .. import utils
6+
57
log = logging.getLogger(__name__)
68

79

@@ -68,6 +70,16 @@ def create_swarm_spec(self, *args, **kwargs):
6870
kwargs['external_cas'] = [ext_ca]
6971
return types.SwarmSpec(self._version, *args, **kwargs)
7072

73+
@utils.minimum_version('1.24')
74+
def get_unlock_key(self):
75+
"""
76+
Get the unlock key for this Swarm manager.
77+
78+
Returns:
79+
A ``dict`` containing an ``UnlockKey`` member
80+
"""
81+
return self._result(self._get(self._url('/swarm/unlockkey')), True)
82+
7183
@utils.minimum_version('1.24')
7284
def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
7385
force_new_cluster=False, swarm_spec=None):
@@ -152,7 +164,7 @@ def inspect_node(self, node_id):
152164
return self._result(self._get(url), True)
153165

154166
@utils.minimum_version('1.24')
155-
def join_swarm(self, remote_addrs, join_token, listen_addr=None,
167+
def join_swarm(self, remote_addrs, join_token, listen_addr='0.0.0.0:2377',
156168
advertise_addr=None):
157169
"""
158170
Make this Engine join a swarm that has already been created.
@@ -270,10 +282,46 @@ def remove_node(self, node_id, force=False):
270282
self._raise_for_status(res)
271283
return True
272284

285+
@utils.minimum_version('1.24')
286+
def unlock_swarm(self, key):
287+
"""
288+
Unlock a locked swarm.
289+
290+
Args:
291+
key (string): The unlock key as provided by
292+
:py:meth:`get_unlock_key`
293+
294+
Raises:
295+
:py:class:`docker.errors.InvalidArgument`
296+
If the key argument is in an incompatible format
297+
298+
:py:class:`docker.errors.APIError`
299+
If the server returns an error.
300+
301+
Returns:
302+
`True` if the request was successful.
303+
304+
Example:
305+
306+
>>> key = client.get_unlock_key()
307+
>>> client.unlock_node(key)
308+
309+
"""
310+
if isinstance(key, dict):
311+
if 'UnlockKey' not in key:
312+
raise errors.InvalidArgument('Invalid unlock key format')
313+
else:
314+
key = {'UnlockKey': key}
315+
316+
url = self._url('/swarm/unlock')
317+
res = self._post_json(url, data=key)
318+
self._raise_for_status(res)
319+
return True
320+
273321
@utils.minimum_version('1.24')
274322
def update_node(self, node_id, version, node_spec=None):
275323
"""
276-
Update the Node's configuration
324+
Update the node's configuration
277325
278326
Args:
279327

docker/auth.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def parse_auth(entries, raise_on_error=False):
203203
# https://github.com/docker/compose/issues/3265
204204
log.debug(
205205
'Auth data for {0} is absent. Client might be using a '
206-
'credentials store instead.'
206+
'credentials store instead.'.format(registry)
207207
)
208208
conf[registry] = {}
209209
continue

docker/client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class DockerClient(object):
2626
base_url (str): URL to the Docker server. For example,
2727
``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``.
2828
version (str): The version of the API to use. Set to ``auto`` to
29-
automatically detect the server's version. Default: ``1.26``
29+
automatically detect the server's version. Default: ``1.30``
3030
timeout (int): Default timeout for API calls, in seconds.
3131
tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass
3232
``True`` to enable it with default options, or pass a
@@ -60,7 +60,7 @@ def from_env(cls, **kwargs):
6060
6161
Args:
6262
version (str): The version of the API to use. Set to ``auto`` to
63-
automatically detect the server's version. Default: ``1.26``
63+
automatically detect the server's version. Default: ``1.30``
6464
timeout (int): Default timeout for API calls, in seconds.
6565
ssl_version (int): A valid `SSL version`_.
6666
assert_hostname (bool): Verify the hostname of the server.

docker/errors.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def create_api_error_from_http_exception(e):
1818
try:
1919
explanation = response.json()['message']
2020
except ValueError:
21-
explanation = response.content.strip()
21+
explanation = (response.content or '').strip()
2222
cls = APIError
2323
if response.status_code == 404:
2424
if explanation and ('No such image' in str(explanation) or

docker/models/containers.py

+22-9
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,9 @@ def run(self, image, command=None, stdout=True, stderr=False,
629629
(e.g. ``SIGINT``).
630630
storage_opt (dict): Storage driver options per container as a
631631
key-value mapping.
632+
stream (bool): If true and ``detach`` is false, return a log
633+
generator instead of a string. Ignored if ``detach`` is true.
634+
Default: ``False``.
632635
sysctls (dict): Kernel parameters to set in the container.
633636
tmpfs (dict): Temporary filesystems to mount, as a dictionary
634637
mapping a path inside the container to options for that path.
@@ -696,6 +699,7 @@ def run(self, image, command=None, stdout=True, stderr=False,
696699
"""
697700
if isinstance(image, Image):
698701
image = image.id
702+
stream = kwargs.pop('stream', False)
699703
detach = kwargs.pop("detach", False)
700704
if detach and remove:
701705
if version_gte(self.client.api._version, '1.25'):
@@ -723,23 +727,30 @@ def run(self, image, command=None, stdout=True, stderr=False,
723727
if detach:
724728
return container
725729

726-
exit_status = container.wait()
727-
if exit_status != 0:
728-
stdout = False
729-
stderr = True
730-
731730
logging_driver = container.attrs['HostConfig']['LogConfig']['Type']
732731

732+
out = None
733733
if logging_driver == 'json-file' or logging_driver == 'journald':
734-
out = container.logs(stdout=stdout, stderr=stderr)
735-
else:
734+
out = container.logs(
735+
stdout=stdout, stderr=stderr, stream=True, follow=True
736+
)
737+
738+
exit_status = container.wait()
739+
if exit_status != 0:
736740
out = None
741+
if not kwargs.get('auto_remove'):
742+
out = container.logs(stdout=False, stderr=True)
737743

738744
if remove:
739745
container.remove()
740746
if exit_status != 0:
741-
raise ContainerError(container, exit_status, command, image, out)
742-
return out
747+
raise ContainerError(
748+
container, exit_status, command, image, out
749+
)
750+
751+
return out if stream or out is None else b''.join(
752+
[line for line in out]
753+
)
743754

744755
def create(self, image, command=None, **kwargs):
745756
"""
@@ -873,6 +884,8 @@ def prune(self, filters=None):
873884
'cpu_shares',
874885
'cpuset_cpus',
875886
'cpuset_mems',
887+
'cpu_rt_period',
888+
'cpu_rt_runtime',
876889
'device_read_bps',
877890
'device_read_iops',
878891
'device_write_bps',

0 commit comments

Comments
 (0)