diff options
author | Felipe Sateler <fsateler@debian.org> | 2017-11-05 10:30:23 -0300 |
---|---|---|
committer | Felipe Sateler <fsateler@debian.org> | 2017-11-05 10:30:23 -0300 |
commit | 1242ea598cfdb70ec202032871b8dd5a52e350cd (patch) | |
tree | 2cb076ef0a03b524a241f5c76766890b4623fc8b | |
parent | eb4e47656d568a00605a36386177845d6b641603 (diff) | |
parent | 1211d81fdfa98c6fd84ca5e0fbff8bdbb333de17 (diff) |
Update upstream source from tag 'upstream/2.5.1'
Update to upstream version '2.5.1'
with Debian dir 7d08f502e66fc28447e54729530e4970c3e9a84f
36 files changed, 435 insertions, 69 deletions
@@ -1,11 +1,11 @@ Metadata-Version: 1.1 Name: docker -Version: 2.4.2 +Version: 2.5.1 Summary: A Python library for the Docker Engine API. Home-page: https://github.com/docker/docker-py Author: Joffrey F Author-email: joffrey@docker.com -License: UNKNOWN +License: Apache License 2.0 Description: Docker SDK for Python ===================== @@ -26,6 +26,13 @@ Description: Docker SDK for Python pip install docker + If you are intending to connect to a docker host via TLS, add + ``docker[tls]`` to your requirements instead, or install with pip: + + :: + + pip install docker[tls] + Usage ----- @@ -41,7 +48,7 @@ Description: Docker SDK for Python .. code:: python - >>> client.containers.run("ubuntu", "echo hello world") + >>> client.containers.run("ubuntu:latest", "echo hello world") 'hello world\n' You can run containers in the background: @@ -10,6 +10,10 @@ The latest stable version [is available on PyPI](https://pypi.python.org/pypi/do pip install docker +If you are intending to connect to a docker host via TLS, add `docker[tls]` to your requirements instead, or install with pip: + + pip install docker[tls] + ## Usage Connect to Docker using the default socket or the configuration in your environment: @@ -22,7 +26,7 @@ client = docker.from_env() You can run containers: ```python ->>> client.containers.run("ubuntu", "echo hello world") +>>> client.containers.run("ubuntu:latest", "echo hello world") 'hello world\n' ``` @@ -18,6 +18,13 @@ your ``requirements.txt`` file or install with pip: pip install docker +If you are intending to connect to a docker host via TLS, add +``docker[tls]`` to your requirements instead, or install with pip: + +:: + + pip install docker[tls] + Usage ----- @@ -33,7 +40,7 @@ You can run containers: .. code:: python - >>> client.containers.run("ubuntu", "echo hello world") + >>> client.containers.run("ubuntu:latest", "echo hello world") 'hello world\n' You can run containers in the background: diff --git a/docker.egg-info/PKG-INFO b/docker.egg-info/PKG-INFO index 8dfa851..85d5adc 100644 --- a/docker.egg-info/PKG-INFO +++ b/docker.egg-info/PKG-INFO @@ -1,11 +1,11 @@ Metadata-Version: 1.1 Name: docker -Version: 2.4.2 +Version: 2.5.1 Summary: A Python library for the Docker Engine API. Home-page: https://github.com/docker/docker-py Author: Joffrey F Author-email: joffrey@docker.com -License: UNKNOWN +License: Apache License 2.0 Description: Docker SDK for Python ===================== @@ -26,6 +26,13 @@ Description: Docker SDK for Python pip install docker + If you are intending to connect to a docker host via TLS, add + ``docker[tls]`` to your requirements instead, or install with pip: + + :: + + pip install docker[tls] + Usage ----- @@ -41,7 +48,7 @@ Description: Docker SDK for Python .. code:: python - >>> client.containers.run("ubuntu", "echo hello world") + >>> client.containers.run("ubuntu:latest", "echo hello world") 'hello world\n' You can run containers in the background: diff --git a/docker.egg-info/requires.txt b/docker.egg-info/requires.txt index 981d1a9..b79f011 100644 --- a/docker.egg-info/requires.txt +++ b/docker.egg-info/requires.txt @@ -1,10 +1,15 @@ -requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0 -six >= 1.4.0 -websocket-client >= 0.32.0 -docker-pycreds >= 0.2.1 +requests!=2.11.0,!=2.12.2,!=2.18.0,>=2.5.2 +six>=1.4.0 +websocket-client>=0.32.0 +docker-pycreds>=0.2.1 [:python_version < "3.3"] -ipaddress >= 1.0.16 +ipaddress>=1.0.16 [:python_version < "3.5"] -backports.ssl_match_hostname >= 3.5 +backports.ssl_match_hostname>=3.5 + +[tls] +pyOpenSSL>=0.14 +cryptography>=1.3.4 +idna>=2.0.0 diff --git a/docker/api/build.py b/docker/api/build.py index cbef4a8..f9678a3 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -18,7 +18,8 @@ class BuildApiMixin(object): custom_context=False, encoding=None, pull=False, forcerm=False, dockerfile=None, container_limits=None, decode=False, buildargs=None, gzip=False, shmsize=None, - labels=None, cache_from=None, target=None, network_mode=None): + labels=None, cache_from=None, target=None, network_mode=None, + squash=None): """ Similar to the ``docker build`` command. Either ``path`` or ``fileobj`` needs to be set. ``path`` can be a local path (to a directory @@ -98,6 +99,8 @@ class BuildApiMixin(object): Dockerfile network_mode (str): networking mode for the run commands during build + squash (bool): Squash the resulting images layers into a + single layer. Returns: A generator for the build output. @@ -218,6 +221,14 @@ class BuildApiMixin(object): 'network_mode was only introduced in API version 1.25' ) + if squash: + if utils.version_gte(self._version, '1.25'): + params.update({'squash': squash}) + else: + raise errors.InvalidVersion( + 'squash was only introduced in API version 1.25' + ) + if context is not None: headers = {'Content-Type': 'application/tar'} if encoding: @@ -274,7 +285,10 @@ class BuildApiMixin(object): self._auth_configs, registry ) else: - auth_data = self._auth_configs + auth_data = self._auth_configs.copy() + # See https://github.com/docker/docker-py/issues/1683 + if auth.INDEX_NAME in auth_data: + auth_data[auth.INDEX_URL] = auth_data[auth.INDEX_NAME] log.debug( 'Sending auth config ({0})'.format( diff --git a/docker/api/client.py b/docker/api/client.py index 65b5baa..1de10c7 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -32,7 +32,7 @@ from ..errors import ( from ..tls import TLSConfig from ..transport import SSLAdapter, UnixAdapter from ..utils import utils, check_resource, update_headers -from ..utils.socket import frames_iter +from ..utils.socket import frames_iter, socket_raw_iter from ..utils.json_stream import json_stream try: from ..transport import NpipeAdapter @@ -362,13 +362,19 @@ class APIClient( for out in response.iter_content(chunk_size=1, decode_unicode=True): yield out - def _read_from_socket(self, response, stream): + def _read_from_socket(self, response, stream, tty=False): socket = self._get_raw_response_socket(response) + gen = None + if tty is False: + gen = frames_iter(socket) + else: + gen = socket_raw_iter(socket) + if stream: - return frames_iter(socket) + return gen else: - return six.binary_type().join(frames_iter(socket)) + return six.binary_type().join(gen) def _disable_socket_timeout(self, socket): """ Depending on the combination of python version and whether we're @@ -398,9 +404,13 @@ class APIClient( s.settimeout(None) - def _get_result(self, container, stream, res): + @check_resource('container') + def _check_is_tty(self, container): cont = self.inspect_container(container) - return self._get_result_tty(stream, res, cont['Config']['Tty']) + return cont['Config']['Tty'] + + def _get_result(self, container, stream, res): + return self._get_result_tty(stream, res, self._check_is_tty(container)) def _get_result_tty(self, stream, res, is_tty): # Stream multi-plexing was only introduced in API v1.6. Anything diff --git a/docker/api/container.py b/docker/api/container.py index 532a9c6..918f8a3 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -50,9 +50,11 @@ class ContainerApiMixin(object): } u = self._url("/containers/{0}/attach", container) - response = self._post(u, headers=headers, params=params, stream=stream) + response = self._post(u, headers=headers, params=params, stream=True) - return self._read_from_socket(response, stream) + return self._read_from_socket( + response, stream, self._check_is_tty(container) + ) @utils.check_resource('container') def attach_socket(self, container, params=None, ws=False): @@ -399,7 +401,7 @@ class ContainerApiMixin(object): name (str): A name for the container entrypoint (str or list): An entrypoint working_dir (str): Path to the working directory - domainname (str or list): Set custom DNS search domains + domainname (str): The domain name to use for the container memswap_limit (int): host_config (dict): A dictionary created with :py:meth:`create_host_config`. diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index 2b407ce..6f42524 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -153,4 +153,4 @@ class ExecApiMixin(object): return self._result(res) if socket: return self._get_raw_response_socket(res) - return self._read_from_socket(res, stream) + return self._read_from_socket(res, stream, tty) diff --git a/docker/api/image.py b/docker/api/image.py index 181c4a1..41cc267 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -262,7 +262,7 @@ class ImageApiMixin(object): self._get(self._url("/images/{0}/json", image)), True ) - def load_image(self, data): + def load_image(self, data, quiet=None): """ Load an image that was previously saved using :py:meth:`~docker.api.image.ImageApiMixin.get_image` (or ``docker @@ -270,8 +270,31 @@ class ImageApiMixin(object): Args: data (binary): Image data to be loaded. + quiet (boolean): Suppress progress details in response. + + Returns: + (generator): Progress output as JSON objects. Only available for + API version >= 1.23 + + Raises: + :py:class:`docker.errors.APIError` + If the server returns an error. """ - res = self._post(self._url("/images/load"), data=data) + params = {} + + if quiet is not None: + if utils.version_lt(self._version, '1.23'): + raise errors.InvalidVersion( + 'quiet is not supported in API version < 1.23' + ) + params['quiet'] = quiet + + res = self._post( + self._url("/images/load"), data=data, params=params, stream=True + ) + if utils.version_gte(self._version, '1.23'): + return self._stream_helper(res, decode=True) + self._raise_for_status(res) @utils.minimum_version('1.25') @@ -455,7 +478,7 @@ class ImageApiMixin(object): """ params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/{0}", image), params=params) - self._raise_for_status(res) + return self._result(res, True) def search(self, term): """ diff --git a/docker/api/network.py b/docker/api/network.py index 5ebb41a..befbb58 100644 --- a/docker/api/network.py +++ b/docker/api/network.py @@ -52,7 +52,7 @@ class NetworkApiMixin(object): options (dict): Driver options as a key-value dictionary ipam (IPAMConfig): Optional custom IP scheme for the network. check_duplicate (bool): Request daemon to check for networks with - same name. Default: ``True``. + same name. Default: ``None``. internal (bool): Restrict external access to the network. Default ``False``. labels (dict): Map of labels to set on the network. Default @@ -200,7 +200,7 @@ class NetworkApiMixin(object): res = self._get(url, params=params) return self._result(res, json=True) - @check_resource('image') + @check_resource('container') @minimum_version('1.21') def connect_container_to_network(self, container, net_id, ipv4_address=None, ipv6_address=None, @@ -237,7 +237,7 @@ class NetworkApiMixin(object): res = self._post_json(url, data=data) self._raise_for_status(res) - @check_resource('image') + @check_resource('container') @minimum_version('1.21') def disconnect_container_from_network(self, container, net_id, force=False): diff --git a/docker/api/service.py b/docker/api/service.py index cc16cc3..4b555a5 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -38,7 +38,7 @@ def _check_api_features(version, task_template, update_config): 'Placement.preferences is not supported in' ' API version < 1.27' ) - if task_template.container_spec.get('TTY'): + if task_template.get('ContainerSpec', {}).get('TTY'): if utils.version_lt(version, '1.25'): raise errors.InvalidVersion( 'ContainerSpec.TTY is not supported in API version < 1.25' diff --git a/docker/auth.py b/docker/auth.py index ec9c45b..c3fb062 100644 --- a/docker/auth.py +++ b/docker/auth.py @@ -10,7 +10,7 @@ from . import errors from .constants import IS_WINDOWS_PLATFORM INDEX_NAME = 'docker.io' -INDEX_URL = 'https://{0}/v1/'.format(INDEX_NAME) +INDEX_URL = 'https://index.{0}/v1/'.format(INDEX_NAME) DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json') LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg' TOKEN_USERNAME = '<token>' @@ -118,7 +118,7 @@ def _resolve_authconfig_credstore(authconfig, registry, credstore_name): if not registry or registry == INDEX_NAME: # The ecosystem is a little schizophrenic with index.docker.io VS # docker.io - in that case, it seems the full URL is necessary. - registry = 'https://index.docker.io/v1/' + registry = INDEX_URL log.debug("Looking for auth entry for {0}".format(repr(registry))) store = dockerpycreds.Store(credstore_name) try: diff --git a/docker/constants.py b/docker/constants.py index 91a6528..6de8fad 100644 --- a/docker/constants.py +++ b/docker/constants.py @@ -1,7 +1,7 @@ import sys from .version import version -DEFAULT_DOCKER_API_VERSION = '1.26' +DEFAULT_DOCKER_API_VERSION = '1.30' MINIMUM_DOCKER_API_VERSION = '1.21' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 diff --git a/docker/errors.py b/docker/errors.py index 0da97f4..2a2f871 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -127,8 +127,11 @@ class ContainerError(DockerException): self.command = command self.image = image self.stderr = stderr - msg = ("Command '{}' in image '{}' returned non-zero exit status {}: " - "{}").format(command, image, exit_status, stderr) + + err = ": {}".format(stderr) if stderr is not None else "" + msg = ("Command '{}' in image '{}' returned non-zero exit " + "status {}{}").format(command, image, exit_status, err) + super(ContainerError, self).__init__(msg) diff --git a/docker/models/containers.py b/docker/models/containers.py index cf01b27..688decc 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -4,6 +4,7 @@ from ..api import APIClient from ..errors import (ContainerError, ImageNotFound, create_unexpected_kwargs_error) from ..types import HostConfig +from ..utils import version_gte from .images import Image from .resource import Collection, Model @@ -667,6 +668,13 @@ class ContainerCollection(Collection): The container logs, either ``STDOUT``, ``STDERR``, or both, depending on the value of the ``stdout`` and ``stderr`` arguments. + ``STDOUT`` and ``STDERR`` may be read only if either ``json-file`` + or ``journald`` logging driver used. Thus, if you are using none of + these drivers, a ``None`` object is returned instead. See the + `Engine API documentation + <https://docs.docker.com/engine/api/v1.30/#operation/ContainerLogs/>`_ + for full details. + If ``detach`` is ``True``, a :py:class:`Container` object is returned instead. @@ -683,8 +691,11 @@ class ContainerCollection(Collection): image = image.id detach = kwargs.pop("detach", False) if detach and remove: - raise RuntimeError("The options 'detach' and 'remove' cannot be " - "used together.") + if version_gte(self.client.api._version, '1.25'): + kwargs["auto_remove"] = True + else: + raise RuntimeError("The options 'detach' and 'remove' cannot " + "be used together in api versions < 1.25.") if kwargs.get('network') and kwargs.get('network_mode'): raise RuntimeError( @@ -709,7 +720,14 @@ class ContainerCollection(Collection): if exit_status != 0: stdout = False stderr = True - out = container.logs(stdout=stdout, stderr=stderr) + + logging_driver = container.attrs['HostConfig']['LogConfig']['Type'] + + if logging_driver == 'json-file' or logging_driver == 'journald': + out = container.logs(stdout=stdout, stderr=stderr) + else: + out = None + if remove: container.remove() if exit_status != 0: @@ -835,6 +853,7 @@ RUN_CREATE_KWARGS = [ # kwargs to copy straight from run to host_config RUN_HOST_CONFIG_KWARGS = [ + 'auto_remove', 'blkio_weight_device', 'blkio_weight', 'cap_add', diff --git a/docker/models/images.py b/docker/models/images.py index d4e24c6..d1b29ad 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -224,7 +224,7 @@ class ImageCollection(Collection): If the server returns an error. """ resp = self.client.api.images(name=name, all=all, filters=filters) - return [self.prepare_model(r) for r in resp] + return [self.get(r["Id"]) for r in resp] def load(self, data): """ @@ -235,6 +235,9 @@ class ImageCollection(Collection): Args: data (binary): Image data to be loaded. + Returns: + (generator): Progress output as JSON objects + Raises: :py:class:`docker.errors.APIError` If the server returns an error. diff --git a/docker/utils/build.py b/docker/utils/build.py index 79b7249..d4223e7 100644 --- a/docker/utils/build.py +++ b/docker/utils/build.py @@ -26,6 +26,7 @@ def exclude_paths(root, patterns, dockerfile=None): if dockerfile is None: dockerfile = 'Dockerfile' + patterns = [p.lstrip('/') for p in patterns] exceptions = [p for p in patterns if p.startswith('!')] include_patterns = [p[1:] for p in exceptions] diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py index e95b63c..42461dd 100644 --- a/docker/utils/fnmatch.py +++ b/docker/utils/fnmatch.py @@ -65,19 +65,32 @@ def translate(pat): There is no way to quote meta-characters. """ - recursive_mode = False i, n = 0, len(pat) - res = '' + res = '^' while i < n: c = pat[i] i = i + 1 if c == '*': if i < n and pat[i] == '*': - recursive_mode = True + # is some flavor of "**" i = i + 1 - res = res + '.*' + # Treat **/ as ** so eat the "/" + if i < n and pat[i] == '/': + i = i + 1 + if i >= n: + # is "**EOF" - to align with .gitignore just accept all + res = res + '.*' + else: + # is "**" + # Note that this allows for any # of /'s (even 0) because + # the .* will eat everything, even /'s + res = res + '(.*/)?' + else: + # is "*" so map it to anything but "/" + res = res + '[^/]*' elif c == '?': - res = res + '.' + # "?" is any char except "/" + res = res + '[^/]' elif c == '[': j = i if j < n and pat[j] == '!': @@ -96,8 +109,6 @@ def translate(pat): elif stuff[0] == '^': stuff = '\\' + stuff res = '%s[%s]' % (res, stuff) - elif recursive_mode and c == '/': - res = res + re.escape(c) + '?' else: res = res + re.escape(c) - return res + '\Z(?ms)' + return res + '$' diff --git a/docker/utils/socket.py b/docker/utils/socket.py index 4080f25..54392d2 100644 --- a/docker/utils/socket.py +++ b/docker/utils/socket.py @@ -75,5 +75,24 @@ def frames_iter(socket): break while n > 0: result = read(socket, n) - n -= len(result) + if result is None: + continue + data_length = len(result) + if data_length == 0: + # We have reached EOF + return + n -= data_length yield result + + +def socket_raw_iter(socket): + """ + Returns a generator of data read from the socket. + This is used for non-multiplexed streams. + """ + while True: + result = read(socket) + if len(result) == 0: + # We have reached EOF + return + yield result diff --git a/docker/version.py b/docker/version.py index af1bd5b..273270d 100644 --- a/docker/version.py +++ b/docker/version.py @@ -1,2 +1,2 @@ -version = "2.4.2" +version = "2.5.1" version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) diff --git a/requirements.txt b/requirements.txt index 3754131..f3c61e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,16 @@ -requests==2.11.1 -six>=1.4.0 -websocket-client==0.32.0 -backports.ssl_match_hostname>=3.5 ; python_version < '3.5' -ipaddress==1.0.16 ; python_version < '3.3' +appdirs==1.4.3 +asn1crypto==0.22.0 +backports.ssl-match-hostname==3.5.0.1 +cffi==1.10.0 +cryptography==1.9 docker-pycreds==0.2.1 +enum34==1.1.6 +idna==2.5 +ipaddress==1.0.18 +packaging==16.8 +pycparser==2.17 +pyOpenSSL==17.0.0 +pyparsing==2.2.0 +requests==2.14.2 +six==1.10.0 +websocket-client==0.40.0 @@ -8,5 +8,4 @@ license = Apache License 2.0 [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 @@ -35,6 +35,16 @@ extras_require = { # ssl_match_hostname to verify hosts match with certificates via # ServerAltname: https://pypi.python.org/pypi/backports.ssl_match_hostname ':python_version < "3.3"': 'ipaddress >= 1.0.16', + + # If using docker-py over TLS, highly recommend this option is + # pip-installed or pinned. + + # TODO: if pip installing both "requests" and "requests[security]", the + # extra package from the "security" option are not installed (see + # https://github.com/pypa/pip/issues/4391). Once that's fixed, instead of + # installing the extra dependencies, install the following instead: + # 'requests[security] >= 2.5.2, != 2.11.0, != 2.12.2' + 'tls': ['pyOpenSSL>=0.14', 'cryptography>=1.3.4', 'idna>=2.0.0'], } version = None diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 609964f..d0aa5c2 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -9,7 +9,7 @@ import pytest import six from .base import BaseAPIIntegrationTest -from ..helpers import requires_api_version +from ..helpers import requires_api_version, requires_experimental class BuildTest(BaseAPIIntegrationTest): @@ -244,6 +244,32 @@ class BuildTest(BaseAPIIntegrationTest): with pytest.raises(errors.NotFound): self.client.inspect_image('dockerpytest_nonebuild') + @requires_experimental(until=None) + @requires_api_version('1.25') + def test_build_squash(self): + script = io.BytesIO('\n'.join([ + 'FROM busybox', + 'RUN echo blah > /file_1', + 'RUN echo blahblah > /file_2', + 'RUN echo blahblahblah > /file_3' + ]).encode('ascii')) + + def build_squashed(squash): + tag = 'squash' if squash else 'nosquash' + stream = self.client.build( + fileobj=script, tag=tag, squash=squash + ) + self.tmp_imgs.append(tag) + for chunk in stream: + pass + + return self.client.inspect_image(tag) + + non_squashed = build_squashed(False) + squashed = build_squashed(True) + self.assertEqual(len(non_squashed['RootFS']['Layers']), 4) + self.assertEqual(len(squashed['RootFS']['Layers']), 2) + def test_build_stderr_data(self): control_chars = ['\x1b[91m', '\x1b[0m'] snippet = 'Ancient Temple (Mystic Oriental Dream ~ Ancient Temple)' diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index f8b474a..a972c1c 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -1092,20 +1092,28 @@ class AttachContainerTest(BaseAPIIntegrationTest): command = "printf '{0}'".format(line) container = self.client.create_container(BUSYBOX, command, detach=True, tty=False) - ident = container['Id'] - self.tmp_containers.append(ident) + self.tmp_containers.append(container) opts = {"stdout": 1, "stream": 1, "logs": 1} - pty_stdout = self.client.attach_socket(ident, opts) + pty_stdout = self.client.attach_socket(container, opts) self.addCleanup(pty_stdout.close) - self.client.start(ident) + self.client.start(container) next_size = next_frame_size(pty_stdout) self.assertEqual(next_size, len(line)) data = read_exactly(pty_stdout, next_size) self.assertEqual(data.decode('utf-8'), line) + def test_attach_no_stream(self): + container = self.client.create_container( + BUSYBOX, 'echo hello' + ) + self.tmp_containers.append(container) + self.client.start(container) + output = self.client.attach(container, stream=False, logs=True) + assert output == 'hello\n'.encode(encoding='ascii') + class PauseTest(BaseAPIIntegrationTest): def test_pause_unpause(self): diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 917bc50..14fb77a 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -113,7 +113,8 @@ class RemoveImageTest(BaseAPIIntegrationTest): self.assertIn('Id', res) img_id = res['Id'] self.tmp_imgs.append(img_id) - self.client.remove_image(img_id, force=True) + logs = self.client.remove_image(img_id, force=True) + self.assertIn({"Deleted": img_id}, logs) images = self.client.images(all=True) res = [x for x in images if x['Id'].startswith(img_id)] self.assertEqual(len(res), 0) @@ -248,6 +249,19 @@ class ImportImageTest(BaseAPIIntegrationTest): assert img_data['Config']['Cmd'] == ['echo'] assert img_data['Config']['User'] == 'foobar' + # Docs say output is available in 1.23, but this test fails on 1.12.0 + @requires_api_version('1.24') + def test_get_load_image(self): + test_img = 'hello-world:latest' + self.client.pull(test_img) + data = self.client.get_image(test_img) + assert data + output = self.client.load_image(data) + assert any([ + line for line in output + if 'Loaded image: {}'.format(test_img) in line.get('stream', '') + ]) + @contextlib.contextmanager def temporary_http_file_server(self, stream): '''Serve data from an IO stream over HTTP.''' diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index 54111a7..c966916 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -376,6 +376,23 @@ class ServiceTest(BaseAPIIntegrationTest): assert 'TTY' in con_spec assert con_spec['TTY'] is True + @requires_api_version('1.25') + def test_create_service_with_tty_dict(self): + container_spec = { + 'Image': BUSYBOX, + 'Command': ['true'], + 'TTY': True + } + 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 'TaskTemplate' in svc_info['Spec'] + assert 'ContainerSpec' in svc_info['Spec']['TaskTemplate'] + con_spec = svc_info['Spec']['TaskTemplate']['ContainerSpec'] + assert 'TTY' in con_spec + assert con_spec['TTY'] is True + def test_create_service_global_mode(self): container_spec = docker.types.ContainerSpec( BUSYBOX, ['echo', 'hello'] diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index b76a88f..ce3349b 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -88,6 +88,24 @@ class ContainerCollectionTest(BaseIntegrationTest): assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + def test_run_with_none_driver(self): + client = docker.from_env(version=TEST_API_VERSION) + + out = client.containers.run( + "alpine", "echo hello", + log_config=dict(type='none') + ) + self.assertEqual(out, None) + + def test_run_with_json_file_driver(self): + client = docker.from_env(version=TEST_API_VERSION) + + out = client.containers.run( + "alpine", "echo hello", + log_config=dict(type='json-file') + ) + self.assertEqual(out, b'hello\n') + def test_get(self): client = docker.from_env(version=TEST_API_VERSION) container = client.containers.run("alpine", "sleep 300", detach=True) diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index 36b2a46..f1e42cc 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -369,5 +369,19 @@ class ImageTest(BaseAPIClientTest): 'POST', url_prefix + 'images/load', data='Byte Stream....', + stream=True, + params={}, + timeout=DEFAULT_TIMEOUT_SECONDS + ) + + def test_load_image_quiet(self): + self.client.load_image('Byte Stream....', quiet=True) + + fake_request.assert_called_with( + 'POST', + url_prefix + 'images/load', + data='Byte Stream....', + stream=True, + params={'quiet': True}, timeout=DEFAULT_TIMEOUT_SECONDS ) diff --git a/tests/unit/api_network_test.py b/tests/unit/api_network_test.py index f997a1b..96cdc4b 100644 --- a/tests/unit/api_network_test.py +++ b/tests/unit/api_network_test.py @@ -147,8 +147,8 @@ class NetworkTest(BaseAPIClientTest): with mock.patch('docker.api.client.APIClient.post', post): self.client.connect_container_to_network( - {'Id': container_id}, - network_id, + container={'Id': container_id}, + net_id=network_id, aliases=['foo', 'bar'], links=[('baz', 'quux')] ) @@ -176,7 +176,7 @@ class NetworkTest(BaseAPIClientTest): with mock.patch('docker.api.client.APIClient.post', post): self.client.disconnect_container_from_network( - {'Id': container_id}, network_id) + container={'Id': container_id}, net_id=network_id) self.assertEqual( post.call_args[0][0], diff --git a/tests/unit/api_test.py b/tests/unit/api_test.py index 83848c5..6ac92c4 100644 --- a/tests/unit/api_test.py +++ b/tests/unit/api_test.py @@ -83,7 +83,7 @@ def fake_delete(self, url, *args, **kwargs): return fake_request('DELETE', url, *args, **kwargs) -def fake_read_from_socket(self, response, stream): +def fake_read_from_socket(self, response, stream, tty=False): return six.binary_type() diff --git a/tests/unit/errors_test.py b/tests/unit/errors_test.py index b78af4e..9678669 100644 --- a/tests/unit/errors_test.py +++ b/tests/unit/errors_test.py @@ -2,8 +2,10 @@ import unittest import requests -from docker.errors import (APIError, DockerException, +from docker.errors import (APIError, ContainerError, DockerException, create_unexpected_kwargs_error) +from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID +from .fake_api_client import make_fake_client class APIErrorTest(unittest.TestCase): @@ -77,6 +79,36 @@ class APIErrorTest(unittest.TestCase): assert err.is_client_error() is True +class ContainerErrorTest(unittest.TestCase): + def test_container_without_stderr(self): + """The massage does not contain stderr""" + client = make_fake_client() + container = client.containers.get(FAKE_CONTAINER_ID) + command = "echo Hello World" + exit_status = 42 + image = FAKE_IMAGE_ID + stderr = None + + err = ContainerError(container, exit_status, command, image, stderr) + msg = ("Command '{}' in image '{}' returned non-zero exit status {}" + ).format(command, image, exit_status, stderr) + assert str(err) == msg + + def test_container_with_stderr(self): + """The massage contains stderr""" + client = make_fake_client() + container = client.containers.get(FAKE_CONTAINER_ID) + command = "echo Hello World" + exit_status = 42 + image = FAKE_IMAGE_ID + stderr = "Something went wrong" + + err = ContainerError(container, exit_status, command, image, stderr) + msg = ("Command '{}' in image '{}' returned non-zero exit status {}: " + "{}").format(command, image, exit_status, stderr) + assert str(err) == msg + + class CreateUnexpectedKwargsErrorTest(unittest.TestCase): def test_create_unexpected_kwargs_error_single(self): e = create_unexpected_kwargs_error('f', {'foo': 'bar'}) diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index ff0f1b6..2ba85bb 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -146,6 +146,12 @@ def get_fake_inspect_container(tty=False): "StartedAt": "2013-09-25T14:01:18.869545111+02:00", "Ghost": False }, + "HostConfig": { + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + }, "MacAddress": "02:42:ac:11:00:0a" } return status_code, response diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 70c8648..5eaa45a 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -273,9 +273,39 @@ class ContainerCollectionTest(unittest.TestCase): client.api.remove_container.assert_called_with(FAKE_CONTAINER_ID) client = make_fake_client() + client.api._version = '1.24' with self.assertRaises(RuntimeError): client.containers.run("alpine", detach=True, remove=True) + client = make_fake_client() + client.api._version = '1.23' + with self.assertRaises(RuntimeError): + client.containers.run("alpine", detach=True, remove=True) + + client = make_fake_client() + client.api._version = '1.25' + client.containers.run("alpine", detach=True, remove=True) + client.api.remove_container.assert_not_called() + client.api.create_container.assert_called_with( + command=None, + image='alpine', + detach=True, + host_config={'AutoRemove': True, + 'NetworkMode': 'default'} + ) + + client = make_fake_client() + client.api._version = '1.26' + client.containers.run("alpine", detach=True, remove=True) + client.api.remove_container.assert_not_called() + client.api.create_container.assert_called_with( + command=None, + image='alpine', + detach=True, + host_config={'AutoRemove': True, + 'NetworkMode': 'default'} + ) + def test_create(self): client = make_fake_client() container = client.containers.create( diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index a2d463d..2fa1d05 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -639,6 +639,14 @@ class ExcludePathsTest(unittest.TestCase): 'foo', 'foo/bar', 'bar', + 'target', + 'target/subdir', + 'subdir', + 'subdir/target', + 'subdir/target/subdir', + 'subdir/subdir2', + 'subdir/subdir2/target', + 'subdir/subdir2/target/subdir' ] files = [ @@ -654,6 +662,14 @@ class ExcludePathsTest(unittest.TestCase): 'foo/bar/a.py', 'bar/a.py', 'foo/Dockerfile3', + 'target/file.txt', + 'target/subdir/file.txt', + 'subdir/file.txt', + 'subdir/target/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/file.txt', + 'subdir/subdir2/target/file.txt', + 'subdir/subdir2/target/subdir/file.txt', ] all_paths = set(dirs + files) @@ -752,6 +768,11 @@ class ExcludePathsTest(unittest.TestCase): self.all_paths - set(['foo/a.py']) ) + def test_single_subdir_single_filename_leading_slash(self): + assert self.exclude(['/foo/a.py']) == convert_paths( + self.all_paths - set(['foo/a.py']) + ) + def test_single_subdir_with_path_traversal(self): assert self.exclude(['foo/whoops/../a.py']) == convert_paths( self.all_paths - set(['foo/a.py']) @@ -844,6 +865,32 @@ class ExcludePathsTest(unittest.TestCase): self.all_paths - set(['foo/bar', 'foo/bar/a.py']) ) + def test_single_and_double_wildcard(self): + assert self.exclude(['**/target/*/*']) == convert_paths( + self.all_paths - set( + ['target/subdir/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/target/subdir/file.txt'] + ) + ) + + def test_trailing_double_wildcard(self): + assert self.exclude(['subdir/**']) == convert_paths( + self.all_paths - set( + ['subdir/file.txt', + 'subdir/target/file.txt', + 'subdir/target/subdir/file.txt', + 'subdir/subdir2/file.txt', + 'subdir/subdir2/target/file.txt', + 'subdir/subdir2/target/subdir/file.txt', + 'subdir/target', + 'subdir/target/subdir', + 'subdir/subdir2', + 'subdir/subdir2/target', + 'subdir/subdir2/target/subdir'] + ) + ) + class TarTest(unittest.TestCase): def test_tar_with_excludes(self): |