Skip to content

Commit

Permalink
Merge pull request #1838 from docker/2.7.0-release
Browse files Browse the repository at this point in the history
2.7.0 release
  • Loading branch information
shin- authored Dec 19, 2017
2 parents d400795 + 598f167 commit 5bed7b8
Show file tree
Hide file tree
Showing 33 changed files with 1,063 additions and 99 deletions.
18 changes: 9 additions & 9 deletions docker/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -206,7 +206,7 @@ def _url(self, pathfmt, *args, **kwargs):
'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):
Expand Down
15 changes: 11 additions & 4 deletions docker/api/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ def containers(self, quiet=False, all=False, trunc=False, latest=False,
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
Expand Down Expand Up @@ -1112,20 +1113,26 @@ def stats(self, container, decode=None, stream=True):
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,
Expand Down
81 changes: 67 additions & 14 deletions docker/api/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def raise_version_error(param, min_version):
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'):
Expand Down Expand Up @@ -62,6 +66,21 @@ def raise_version_error(param, min_version):
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(
Expand Down Expand Up @@ -306,7 +325,7 @@ def tasks(self, filters=None):
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.
Expand All @@ -328,6 +347,8 @@ def update_service(self, service, version, task_template=None, name=None,
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.
Expand All @@ -345,32 +366,64 @@ def update_service(self, service, version, task_template=None, name=None,

_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
Expand Down
52 changes: 50 additions & 2 deletions docker/api/swarm.py
Original file line number Diff line number Diff line change
@@ -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__)


Expand Down Expand Up @@ -68,6 +70,16 @@ def create_swarm_spec(self, *args, **kwargs):
kwargs['external_cas'] = [ext_ca]
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):
Expand Down Expand Up @@ -152,7 +164,7 @@ def inspect_node(self, node_id):
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.
Expand Down Expand Up @@ -270,10 +282,46 @@ def remove_node(self, node_id, force=False):
self._raise_for_status(res)
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:
Expand Down
2 changes: 1 addition & 1 deletion docker/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docker/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,7 +60,7 @@ def from_env(cls, **kwargs):
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.
Expand Down
2 changes: 1 addition & 1 deletion docker/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 22 additions & 9 deletions docker/models/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,9 @@ def run(self, image, command=None, stdout=True, stderr=False,
(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.
Expand Down Expand Up @@ -696,6 +699,7 @@ def run(self, image, command=None, stdout=True, stderr=False,
"""
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'):
Expand Down Expand Up @@ -723,23 +727,30 @@ def run(self, image, command=None, stdout=True, stderr=False,
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):
"""
Expand Down Expand Up @@ -873,6 +884,8 @@ def prune(self, filters=None):
'cpu_shares',
'cpuset_cpus',
'cpuset_mems',
'cpu_rt_period',
'cpu_rt_runtime',
'device_read_bps',
'device_read_iops',
'device_write_bps',
Expand Down
Loading

0 comments on commit 5bed7b8

Please sign in to comment.