summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelipe Sateler <fsateler@debian.org>2017-11-05 10:30:23 -0300
committerFelipe Sateler <fsateler@debian.org>2017-11-05 10:30:23 -0300
commit1242ea598cfdb70ec202032871b8dd5a52e350cd (patch)
tree2cb076ef0a03b524a241f5c76766890b4623fc8b
parenteb4e47656d568a00605a36386177845d6b641603 (diff)
parent1211d81fdfa98c6fd84ca5e0fbff8bdbb333de17 (diff)
Update upstream source from tag 'upstream/2.5.1'
Update to upstream version '2.5.1' with Debian dir 7d08f502e66fc28447e54729530e4970c3e9a84f
-rw-r--r--PKG-INFO13
-rw-r--r--README.md6
-rw-r--r--README.rst9
-rw-r--r--docker.egg-info/PKG-INFO13
-rw-r--r--docker.egg-info/requires.txt17
-rw-r--r--docker/api/build.py18
-rw-r--r--docker/api/client.py22
-rw-r--r--docker/api/container.py8
-rw-r--r--docker/api/exec_api.py2
-rw-r--r--docker/api/image.py29
-rw-r--r--docker/api/network.py6
-rw-r--r--docker/api/service.py2
-rw-r--r--docker/auth.py4
-rw-r--r--docker/constants.py2
-rw-r--r--docker/errors.py7
-rw-r--r--docker/models/containers.py25
-rw-r--r--docker/models/images.py5
-rw-r--r--docker/utils/build.py1
-rw-r--r--docker/utils/fnmatch.py27
-rw-r--r--docker/utils/socket.py21
-rw-r--r--docker/version.py2
-rw-r--r--requirements.txt20
-rw-r--r--setup.cfg1
-rw-r--r--setup.py10
-rw-r--r--tests/integration/api_build_test.py28
-rw-r--r--tests/integration/api_container_test.py16
-rw-r--r--tests/integration/api_image_test.py16
-rw-r--r--tests/integration/api_service_test.py17
-rw-r--r--tests/integration/models_containers_test.py18
-rw-r--r--tests/unit/api_image_test.py14
-rw-r--r--tests/unit/api_network_test.py6
-rw-r--r--tests/unit/api_test.py2
-rw-r--r--tests/unit/errors_test.py34
-rw-r--r--tests/unit/fake_api.py6
-rw-r--r--tests/unit/models_containers_test.py30
-rw-r--r--tests/unit/utils_test.py47
36 files changed, 435 insertions, 69 deletions
diff --git a/PKG-INFO b/PKG-INFO
index 8dfa851..85d5adc 100644
--- a/PKG-INFO
+++ b/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/README.md b/README.md
index 38963b3..3ff124d 100644
--- a/README.md
+++ b/README.md
@@ -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'
```
diff --git a/README.rst b/README.rst
index 78fc607..d0117e6 100644
--- a/README.rst
+++ b/README.rst
@@ -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
diff --git a/setup.cfg b/setup.cfg
index 81bd901..95b126b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,5 +8,4 @@ license = Apache License 2.0
[egg_info]
tag_build =
tag_date = 0
-tag_svn_revision = 0
diff --git a/setup.py b/setup.py
index 31180d2..4a33c8d 100644
--- a/setup.py
+++ b/setup.py
@@ -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):