diff options
author | Joffrey F <joffrey@docker.com> | 2018-01-25 14:17:26 -0800 |
---|---|---|
committer | Joffrey F <joffrey@docker.com> | 2018-01-26 13:56:01 -0800 |
commit | abd60aedc7e3df813006919222d86717eb8c6fc2 (patch) | |
tree | 1dd70cf2e1f57efe3a983334dc70fc03f68bc9dd | |
parent | 9538258371b1573e2455db41ac0d480e4b7ade9c (diff) | |
download | docker-py-abd60aedc7e3df813006919222d86717eb8c6fc2.tar.gz |
Bump default API version to 1.35
Add ContainerSpec.isolation support
Add until support in logs
Add condition support in wait
Add workdir support in exec_create
Signed-off-by: Joffrey F <joffrey@docker.com>
-rw-r--r-- | docker/api/container.py | 64 | ||||
-rw-r--r-- | docker/api/exec_api.py | 10 | ||||
-rw-r--r-- | docker/api/service.py | 4 | ||||
-rw-r--r-- | docker/constants.py | 2 | ||||
-rw-r--r-- | docker/models/containers.py | 9 | ||||
-rw-r--r-- | docker/models/services.py | 3 | ||||
-rw-r--r-- | docker/types/services.py | 9 | ||||
-rw-r--r-- | tests/integration/api_container_test.py | 33 | ||||
-rw-r--r-- | tests/integration/api_exec_test.py | 12 | ||||
-rw-r--r-- | tests/integration/models_services_test.py | 1 | ||||
-rw-r--r-- | tests/unit/api_container_test.py | 6 | ||||
-rw-r--r-- | tests/unit/models_containers_test.py | 3 |
12 files changed, 129 insertions, 27 deletions
diff --git a/docker/api/container.py b/docker/api/container.py index 494f7b4..b08032c 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -786,7 +786,8 @@ class ContainerApiMixin(object): @utils.check_resource('container') def logs(self, container, stdout=True, stderr=True, stream=False, - timestamps=False, tail='all', since=None, follow=None): + timestamps=False, tail='all', since=None, follow=None, + until=None): """ Get logs from a container. Similar to the ``docker logs`` command. @@ -805,6 +806,8 @@ class ContainerApiMixin(object): since (datetime or int): Show logs since a given datetime or integer epoch (in seconds) follow (bool): Follow log output + until (datetime or int): Show logs that occurred before the given + datetime or integer epoch (in seconds) Returns: (generator or str) @@ -827,21 +830,35 @@ class ContainerApiMixin(object): params['tail'] = tail if since is not None: - if utils.compare_version('1.19', self._version) < 0: + if utils.version_lt(self._version, '1.19'): raise errors.InvalidVersion( - 'since is not supported in API < 1.19' + 'since is not supported for API version < 1.19' ) + if isinstance(since, datetime): + params['since'] = utils.datetime_to_timestamp(since) + elif (isinstance(since, int) and since > 0): + params['since'] = since else: - if isinstance(since, datetime): - params['since'] = utils.datetime_to_timestamp(since) - elif (isinstance(since, int) and since > 0): - params['since'] = since - else: - raise errors.InvalidArgument( - 'since value should be datetime or positive int, ' - 'not {}'. - format(type(since)) - ) + raise errors.InvalidArgument( + 'since value should be datetime or positive int, ' + 'not {}'.format(type(since)) + ) + + if until is not None: + if utils.version_lt(self._version, '1.35'): + raise errors.InvalidVersion( + 'until is not supported for API version < 1.35' + ) + if isinstance(until, datetime): + params['until'] = utils.datetime_to_timestamp(until) + elif (isinstance(until, int) and until > 0): + params['until'] = until + else: + raise errors.InvalidArgument( + 'until value should be datetime or positive int, ' + 'not {}'.format(type(until)) + ) + url = self._url("/containers/{0}/logs", container) res = self._get(url, params=params, stream=stream) return self._get_result(container, stream, res) @@ -1241,7 +1258,7 @@ class ContainerApiMixin(object): return self._result(res, True) @utils.check_resource('container') - def wait(self, container, timeout=None): + def wait(self, container, timeout=None, condition=None): """ Block until a container stops, then return its exit code. Similar to the ``docker wait`` command. @@ -1250,10 +1267,13 @@ class ContainerApiMixin(object): container (str or dict): The container to wait on. If a dict, the ``Id`` key is used. timeout (int): Request timeout + condition (str): Wait until a container state reaches the given + condition, either ``not-running`` (default), ``next-exit``, + or ``removed`` Returns: - (int): The exit code of the container. Returns ``-1`` if the API - responds without a ``StatusCode`` attribute. + (int or dict): The exit code of the container. Returns the full API + response if no ``StatusCode`` field is included. Raises: :py:class:`requests.exceptions.ReadTimeout` @@ -1262,9 +1282,17 @@ class ContainerApiMixin(object): If the server returns an error. """ url = self._url("/containers/{0}/wait", container) - res = self._post(url, timeout=timeout) + params = {} + if condition is not None: + if utils.version_lt(self._version, '1.30'): + raise errors.InvalidVersion( + 'wait condition is not supported for API version < 1.30' + ) + params['condition'] = condition + + res = self._post(url, timeout=timeout, params=params) self._raise_for_status(res) json_ = res.json() if 'StatusCode' in json_: return json_['StatusCode'] - return -1 + return json_ diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index cff5cfa..029c984 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -9,7 +9,7 @@ class ExecApiMixin(object): @utils.check_resource('container') def exec_create(self, container, cmd, stdout=True, stderr=True, stdin=False, tty=False, privileged=False, user='', - environment=None): + environment=None, workdir=None): """ Sets up an exec instance in a running container. @@ -26,6 +26,7 @@ class ExecApiMixin(object): environment (dict or list): A dictionary or a list of strings in the following format ``["PASSWORD=xxx"]`` or ``{"PASSWORD": "xxx"}``. + workdir (str): Path to working directory for this exec session Returns: (dict): A dictionary with an exec ``Id`` key. @@ -66,6 +67,13 @@ class ExecApiMixin(object): 'Env': environment, } + if workdir is not None: + if utils.version_lt(self._version, '1.35'): + raise errors.InvalidVersion( + 'workdir is not supported for API version < 1.35' + ) + data['WorkingDir'] = workdir + url = self._url('/containers/{0}/exec', container) res = self._post_json(url, data=data) return self._result(res, True) diff --git a/docker/api/service.py b/docker/api/service.py index 1a8b8b5..4f7123e 100644 --- a/docker/api/service.py +++ b/docker/api/service.py @@ -65,6 +65,10 @@ def _check_api_features(version, task_template, update_config): if container_spec.get('Privileges') is not None: raise_version_error('ContainerSpec.privileges', '1.30') + if utils.version_lt(version, '1.35'): + if container_spec.get('Isolation') is not None: + raise_version_error('ContainerSpec.isolation', '1.35') + def _merge_task_template(current, override): merged = current.copy() diff --git a/docker/constants.py b/docker/constants.py index 6de8fad..9ab3673 100644 --- a/docker/constants.py +++ b/docker/constants.py @@ -1,7 +1,7 @@ import sys from .version import version -DEFAULT_DOCKER_API_VERSION = '1.30' +DEFAULT_DOCKER_API_VERSION = '1.35' MINIMUM_DOCKER_API_VERSION = '1.21' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 diff --git a/docker/models/containers.py b/docker/models/containers.py index 08f63ed..bdc05cd 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -126,7 +126,7 @@ class Container(Model): def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False, privileged=False, user='', detach=False, stream=False, - socket=False, environment=None): + socket=False, environment=None, workdir=None): """ Run a command inside this container. Similar to ``docker exec``. @@ -147,6 +147,7 @@ class Container(Model): environment (dict or list): A dictionary or a list of strings in the following format ``["PASSWORD=xxx"]`` or ``{"PASSWORD": "xxx"}``. + workdir (str): Path to working directory for this exec session Returns: (generator or str): @@ -159,7 +160,8 @@ class Container(Model): """ resp = self.client.api.exec_create( self.id, cmd, stdout=stdout, stderr=stderr, stdin=stdin, tty=tty, - privileged=privileged, user=user, environment=environment + privileged=privileged, user=user, environment=environment, + workdir=workdir ) return self.client.api.exec_start( resp['Id'], detach=detach, tty=tty, stream=stream, socket=socket @@ -427,6 +429,9 @@ class Container(Model): Args: timeout (int): Request timeout + condition (str): Wait until a container state reaches the given + condition, either ``not-running`` (default), ``next-exit``, + or ``removed`` Returns: (int): The exit code of the container. Returns ``-1`` if the API diff --git a/docker/models/services.py b/docker/models/services.py index 337ed44..8a633df 100644 --- a/docker/models/services.py +++ b/docker/models/services.py @@ -144,6 +144,8 @@ class ServiceCollection(Collection): env (list of str): Environment variables, in the form ``KEY=val``. hostname (string): Hostname to set on the container. + isolation (string): Isolation technology used by the service's + containers. Only used for Windows containers. labels (dict): Labels to apply to the service. log_driver (str): Log driver to use for containers. log_driver_options (dict): Log driver options. @@ -255,6 +257,7 @@ CONTAINER_SPEC_KWARGS = [ 'hostname', 'hosts', 'image', + 'isolation', 'labels', 'mounts', 'open_stdin', diff --git a/docker/types/services.py b/docker/types/services.py index a1f34e0..ef1ca69 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -102,19 +102,21 @@ class ContainerSpec(dict): healthcheck (Healthcheck): Healthcheck configuration for this service. hosts (:py:class:`dict`): A set of host to IP mappings to add to - the container's `hosts` file. + the container's ``hosts`` file. dns_config (DNSConfig): Specification for DNS related configurations in resolver configuration file. configs (:py:class:`list`): List of :py:class:`ConfigReference` that will be exposed to the service. privileges (Privileges): Security options for the service's containers. + isolation (string): Isolation technology used by the service's + containers. Only used for Windows containers. """ def __init__(self, image, command=None, args=None, hostname=None, env=None, workdir=None, user=None, labels=None, mounts=None, stop_grace_period=None, secrets=None, tty=None, groups=None, open_stdin=None, read_only=None, stop_signal=None, healthcheck=None, hosts=None, dns_config=None, configs=None, - privileges=None): + privileges=None, isolation=None): self['Image'] = image if isinstance(command, six.string_types): @@ -178,6 +180,9 @@ class ContainerSpec(dict): if read_only is not None: self['ReadOnly'] = read_only + if isolation is not None: + self['Isolation'] = isolation + class Mount(dict): """ diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 5e30eee..5d06bc4 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -1,6 +1,7 @@ import os import signal import tempfile +from datetime import datetime import docker from docker.constants import IS_WINDOWS_PLATFORM @@ -9,6 +10,7 @@ from docker.utils.socket import read_exactly import pytest +import requests import six from .base import BUSYBOX, BaseAPIIntegrationTest @@ -816,6 +818,21 @@ class WaitTest(BaseAPIIntegrationTest): self.assertIn('ExitCode', inspect['State']) self.assertEqual(inspect['State']['ExitCode'], exitcode) + @requires_api_version('1.30') + def test_wait_with_condition(self): + ctnr = self.client.create_container(BUSYBOX, 'true') + self.tmp_containers.append(ctnr) + with pytest.raises(requests.exceptions.ConnectionError): + self.client.wait(ctnr, condition='removed', timeout=1) + + ctnr = self.client.create_container( + BUSYBOX, ['sleep', '3'], + host_config=self.client.create_host_config(auto_remove=True) + ) + self.tmp_containers.append(ctnr) + self.client.start(ctnr) + assert self.client.wait(ctnr, condition='removed', timeout=5) == 0 + class LogsTest(BaseAPIIntegrationTest): def test_logs(self): @@ -888,6 +905,22 @@ Line2''' logs = self.client.logs(id, tail=0) self.assertEqual(logs, ''.encode(encoding='ascii')) + @requires_api_version('1.35') + def test_logs_with_until(self): + snippet = 'Shanghai Teahouse (Hong Meiling)' + container = self.client.create_container( + BUSYBOX, 'echo "{0}"'.format(snippet) + ) + + self.tmp_containers.append(container) + self.client.start(container) + exitcode = self.client.wait(container) + assert exitcode == 0 + logs_until_1 = self.client.logs(container, until=1) + assert logs_until_1 == b'' + logs_until_now = self.client.logs(container, datetime.now()) + assert logs_until_now == (snippet + '\n').encode(encoding='ascii') + class DiffTest(BaseAPIIntegrationTest): def test_diff(self): diff --git a/tests/integration/api_exec_test.py b/tests/integration/api_exec_test.py index 7a65041..0d42e19 100644 --- a/tests/integration/api_exec_test.py +++ b/tests/integration/api_exec_test.py @@ -136,3 +136,15 @@ class ExecTest(BaseAPIIntegrationTest): exec_log = self.client.exec_start(res) assert b'X=Y\n' in exec_log + + @requires_api_version('1.35') + def test_exec_command_with_workdir(self): + container = self.client.create_container( + BUSYBOX, 'cat', detach=True, stdin_open=True + ) + self.tmp_containers.append(container) + self.client.start(container) + + res = self.client.exec_create(container, 'pwd', workdir='/var/www') + exec_log = self.client.exec_start(res) + assert exec_log == b'/var/www\n' diff --git a/tests/integration/models_services_test.py b/tests/integration/models_services_test.py index ce83428..827242a 100644 --- a/tests/integration/models_services_test.py +++ b/tests/integration/models_services_test.py @@ -195,6 +195,7 @@ class ServiceTest(unittest.TestCase): image="alpine", command="sleep 300" ) + service.reload() service.update( # create argument name=service.name, diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index 3b135a8..8a897cc 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -1263,7 +1263,8 @@ class ContainerTest(BaseAPIClientTest): fake_request.assert_called_with( 'POST', url_prefix + 'containers/3cc2351ab11b/wait', - timeout=None + timeout=None, + params={} ) def test_wait_with_dict_instead_of_id(self): @@ -1272,7 +1273,8 @@ class ContainerTest(BaseAPIClientTest): fake_request.assert_called_with( 'POST', url_prefix + 'containers/3cc2351ab11b/wait', - timeout=None + timeout=None, + params={} ) def test_logs(self): diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 95295a9..62a29b3 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -394,7 +394,8 @@ class ContainerTest(unittest.TestCase): container.exec_run("echo hello world", privileged=True, stream=True) client.api.exec_create.assert_called_with( FAKE_CONTAINER_ID, "echo hello world", stdout=True, stderr=True, - stdin=False, tty=False, privileged=True, user='', environment=None + stdin=False, tty=False, privileged=True, user='', environment=None, + workdir=None ) client.api.exec_start.assert_called_with( FAKE_EXEC_ID, detach=False, tty=False, stream=True, socket=False |