Skip to content

Commit abd60ae

Browse files
committed
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 <[email protected]>
1 parent 9538258 commit abd60ae

File tree

12 files changed

+129
-27
lines changed

12 files changed

+129
-27
lines changed

docker/api/container.py

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,8 @@ def kill(self, container, signal=None):
786786

787787
@utils.check_resource('container')
788788
def logs(self, container, stdout=True, stderr=True, stream=False,
789-
timestamps=False, tail='all', since=None, follow=None):
789+
timestamps=False, tail='all', since=None, follow=None,
790+
until=None):
790791
"""
791792
Get logs from a container. Similar to the ``docker logs`` command.
792793
@@ -805,6 +806,8 @@ def logs(self, container, stdout=True, stderr=True, stream=False,
805806
since (datetime or int): Show logs since a given datetime or
806807
integer epoch (in seconds)
807808
follow (bool): Follow log output
809+
until (datetime or int): Show logs that occurred before the given
810+
datetime or integer epoch (in seconds)
808811
809812
Returns:
810813
(generator or str)
@@ -827,21 +830,35 @@ def logs(self, container, stdout=True, stderr=True, stream=False,
827830
params['tail'] = tail
828831

829832
if since is not None:
830-
if utils.compare_version('1.19', self._version) < 0:
833+
if utils.version_lt(self._version, '1.19'):
831834
raise errors.InvalidVersion(
832-
'since is not supported in API < 1.19'
835+
'since is not supported for API version < 1.19'
833836
)
837+
if isinstance(since, datetime):
838+
params['since'] = utils.datetime_to_timestamp(since)
839+
elif (isinstance(since, int) and since > 0):
840+
params['since'] = since
834841
else:
835-
if isinstance(since, datetime):
836-
params['since'] = utils.datetime_to_timestamp(since)
837-
elif (isinstance(since, int) and since > 0):
838-
params['since'] = since
839-
else:
840-
raise errors.InvalidArgument(
841-
'since value should be datetime or positive int, '
842-
'not {}'.
843-
format(type(since))
844-
)
842+
raise errors.InvalidArgument(
843+
'since value should be datetime or positive int, '
844+
'not {}'.format(type(since))
845+
)
846+
847+
if until is not None:
848+
if utils.version_lt(self._version, '1.35'):
849+
raise errors.InvalidVersion(
850+
'until is not supported for API version < 1.35'
851+
)
852+
if isinstance(until, datetime):
853+
params['until'] = utils.datetime_to_timestamp(until)
854+
elif (isinstance(until, int) and until > 0):
855+
params['until'] = until
856+
else:
857+
raise errors.InvalidArgument(
858+
'until value should be datetime or positive int, '
859+
'not {}'.format(type(until))
860+
)
861+
845862
url = self._url("/containers/{0}/logs", container)
846863
res = self._get(url, params=params, stream=stream)
847864
return self._get_result(container, stream, res)
@@ -1241,7 +1258,7 @@ def update_container(
12411258
return self._result(res, True)
12421259

12431260
@utils.check_resource('container')
1244-
def wait(self, container, timeout=None):
1261+
def wait(self, container, timeout=None, condition=None):
12451262
"""
12461263
Block until a container stops, then return its exit code. Similar to
12471264
the ``docker wait`` command.
@@ -1250,10 +1267,13 @@ def wait(self, container, timeout=None):
12501267
container (str or dict): The container to wait on. If a dict, the
12511268
``Id`` key is used.
12521269
timeout (int): Request timeout
1270+
condition (str): Wait until a container state reaches the given
1271+
condition, either ``not-running`` (default), ``next-exit``,
1272+
or ``removed``
12531273
12541274
Returns:
1255-
(int): The exit code of the container. Returns ``-1`` if the API
1256-
responds without a ``StatusCode`` attribute.
1275+
(int or dict): The exit code of the container. Returns the full API
1276+
response if no ``StatusCode`` field is included.
12571277
12581278
Raises:
12591279
:py:class:`requests.exceptions.ReadTimeout`
@@ -1262,9 +1282,17 @@ def wait(self, container, timeout=None):
12621282
If the server returns an error.
12631283
"""
12641284
url = self._url("/containers/{0}/wait", container)
1265-
res = self._post(url, timeout=timeout)
1285+
params = {}
1286+
if condition is not None:
1287+
if utils.version_lt(self._version, '1.30'):
1288+
raise errors.InvalidVersion(
1289+
'wait condition is not supported for API version < 1.30'
1290+
)
1291+
params['condition'] = condition
1292+
1293+
res = self._post(url, timeout=timeout, params=params)
12661294
self._raise_for_status(res)
12671295
json_ = res.json()
12681296
if 'StatusCode' in json_:
12691297
return json_['StatusCode']
1270-
return -1
1298+
return json_

docker/api/exec_api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class ExecApiMixin(object):
99
@utils.check_resource('container')
1010
def exec_create(self, container, cmd, stdout=True, stderr=True,
1111
stdin=False, tty=False, privileged=False, user='',
12-
environment=None):
12+
environment=None, workdir=None):
1313
"""
1414
Sets up an exec instance in a running container.
1515
@@ -26,6 +26,7 @@ def exec_create(self, container, cmd, stdout=True, stderr=True,
2626
environment (dict or list): A dictionary or a list of strings in
2727
the following format ``["PASSWORD=xxx"]`` or
2828
``{"PASSWORD": "xxx"}``.
29+
workdir (str): Path to working directory for this exec session
2930
3031
Returns:
3132
(dict): A dictionary with an exec ``Id`` key.
@@ -66,6 +67,13 @@ def exec_create(self, container, cmd, stdout=True, stderr=True,
6667
'Env': environment,
6768
}
6869

70+
if workdir is not None:
71+
if utils.version_lt(self._version, '1.35'):
72+
raise errors.InvalidVersion(
73+
'workdir is not supported for API version < 1.35'
74+
)
75+
data['WorkingDir'] = workdir
76+
6977
url = self._url('/containers/{0}/exec', container)
7078
res = self._post_json(url, data=data)
7179
return self._result(res, True)

docker/api/service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ def raise_version_error(param, min_version):
6565
if container_spec.get('Privileges') is not None:
6666
raise_version_error('ContainerSpec.privileges', '1.30')
6767

68+
if utils.version_lt(version, '1.35'):
69+
if container_spec.get('Isolation') is not None:
70+
raise_version_error('ContainerSpec.isolation', '1.35')
71+
6872

6973
def _merge_task_template(current, override):
7074
merged = current.copy()

docker/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22
from .version import version
33

4-
DEFAULT_DOCKER_API_VERSION = '1.30'
4+
DEFAULT_DOCKER_API_VERSION = '1.35'
55
MINIMUM_DOCKER_API_VERSION = '1.21'
66
DEFAULT_TIMEOUT_SECONDS = 60
77
STREAM_HEADER_SIZE_BYTES = 8

docker/models/containers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def diff(self):
126126

127127
def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
128128
privileged=False, user='', detach=False, stream=False,
129-
socket=False, environment=None):
129+
socket=False, environment=None, workdir=None):
130130
"""
131131
Run a command inside this container. Similar to
132132
``docker exec``.
@@ -147,6 +147,7 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
147147
environment (dict or list): A dictionary or a list of strings in
148148
the following format ``["PASSWORD=xxx"]`` or
149149
``{"PASSWORD": "xxx"}``.
150+
workdir (str): Path to working directory for this exec session
150151
151152
Returns:
152153
(generator or str):
@@ -159,7 +160,8 @@ def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
159160
"""
160161
resp = self.client.api.exec_create(
161162
self.id, cmd, stdout=stdout, stderr=stderr, stdin=stdin, tty=tty,
162-
privileged=privileged, user=user, environment=environment
163+
privileged=privileged, user=user, environment=environment,
164+
workdir=workdir
163165
)
164166
return self.client.api.exec_start(
165167
resp['Id'], detach=detach, tty=tty, stream=stream, socket=socket
@@ -427,6 +429,9 @@ def wait(self, **kwargs):
427429
428430
Args:
429431
timeout (int): Request timeout
432+
condition (str): Wait until a container state reaches the given
433+
condition, either ``not-running`` (default), ``next-exit``,
434+
or ``removed``
430435
431436
Returns:
432437
(int): The exit code of the container. Returns ``-1`` if the API

docker/models/services.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ def create(self, image, command=None, **kwargs):
144144
env (list of str): Environment variables, in the form
145145
``KEY=val``.
146146
hostname (string): Hostname to set on the container.
147+
isolation (string): Isolation technology used by the service's
148+
containers. Only used for Windows containers.
147149
labels (dict): Labels to apply to the service.
148150
log_driver (str): Log driver to use for containers.
149151
log_driver_options (dict): Log driver options.
@@ -255,6 +257,7 @@ def list(self, **kwargs):
255257
'hostname',
256258
'hosts',
257259
'image',
260+
'isolation',
258261
'labels',
259262
'mounts',
260263
'open_stdin',

docker/types/services.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,19 +102,21 @@ class ContainerSpec(dict):
102102
healthcheck (Healthcheck): Healthcheck
103103
configuration for this service.
104104
hosts (:py:class:`dict`): A set of host to IP mappings to add to
105-
the container's `hosts` file.
105+
the container's ``hosts`` file.
106106
dns_config (DNSConfig): Specification for DNS
107107
related configurations in resolver configuration file.
108108
configs (:py:class:`list`): List of :py:class:`ConfigReference` that
109109
will be exposed to the service.
110110
privileges (Privileges): Security options for the service's containers.
111+
isolation (string): Isolation technology used by the service's
112+
containers. Only used for Windows containers.
111113
"""
112114
def __init__(self, image, command=None, args=None, hostname=None, env=None,
113115
workdir=None, user=None, labels=None, mounts=None,
114116
stop_grace_period=None, secrets=None, tty=None, groups=None,
115117
open_stdin=None, read_only=None, stop_signal=None,
116118
healthcheck=None, hosts=None, dns_config=None, configs=None,
117-
privileges=None):
119+
privileges=None, isolation=None):
118120
self['Image'] = image
119121

120122
if isinstance(command, six.string_types):
@@ -178,6 +180,9 @@ def __init__(self, image, command=None, args=None, hostname=None, env=None,
178180
if read_only is not None:
179181
self['ReadOnly'] = read_only
180182

183+
if isolation is not None:
184+
self['Isolation'] = isolation
185+
181186

182187
class Mount(dict):
183188
"""

tests/integration/api_container_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import signal
33
import tempfile
4+
from datetime import datetime
45

56
import docker
67
from docker.constants import IS_WINDOWS_PLATFORM
@@ -9,6 +10,7 @@
910

1011
import pytest
1112

13+
import requests
1214
import six
1315

1416
from .base import BUSYBOX, BaseAPIIntegrationTest
@@ -816,6 +818,21 @@ def test_wait_with_dict_instead_of_id(self):
816818
self.assertIn('ExitCode', inspect['State'])
817819
self.assertEqual(inspect['State']['ExitCode'], exitcode)
818820

821+
@requires_api_version('1.30')
822+
def test_wait_with_condition(self):
823+
ctnr = self.client.create_container(BUSYBOX, 'true')
824+
self.tmp_containers.append(ctnr)
825+
with pytest.raises(requests.exceptions.ConnectionError):
826+
self.client.wait(ctnr, condition='removed', timeout=1)
827+
828+
ctnr = self.client.create_container(
829+
BUSYBOX, ['sleep', '3'],
830+
host_config=self.client.create_host_config(auto_remove=True)
831+
)
832+
self.tmp_containers.append(ctnr)
833+
self.client.start(ctnr)
834+
assert self.client.wait(ctnr, condition='removed', timeout=5) == 0
835+
819836

820837
class LogsTest(BaseAPIIntegrationTest):
821838
def test_logs(self):
@@ -888,6 +905,22 @@ def test_logs_with_tail_0(self):
888905
logs = self.client.logs(id, tail=0)
889906
self.assertEqual(logs, ''.encode(encoding='ascii'))
890907

908+
@requires_api_version('1.35')
909+
def test_logs_with_until(self):
910+
snippet = 'Shanghai Teahouse (Hong Meiling)'
911+
container = self.client.create_container(
912+
BUSYBOX, 'echo "{0}"'.format(snippet)
913+
)
914+
915+
self.tmp_containers.append(container)
916+
self.client.start(container)
917+
exitcode = self.client.wait(container)
918+
assert exitcode == 0
919+
logs_until_1 = self.client.logs(container, until=1)
920+
assert logs_until_1 == b''
921+
logs_until_now = self.client.logs(container, datetime.now())
922+
assert logs_until_now == (snippet + '\n').encode(encoding='ascii')
923+
891924

892925
class DiffTest(BaseAPIIntegrationTest):
893926
def test_diff(self):

tests/integration/api_exec_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,15 @@ def test_exec_command_with_env(self):
136136

137137
exec_log = self.client.exec_start(res)
138138
assert b'X=Y\n' in exec_log
139+
140+
@requires_api_version('1.35')
141+
def test_exec_command_with_workdir(self):
142+
container = self.client.create_container(
143+
BUSYBOX, 'cat', detach=True, stdin_open=True
144+
)
145+
self.tmp_containers.append(container)
146+
self.client.start(container)
147+
148+
res = self.client.exec_create(container, 'pwd', workdir='/var/www')
149+
exec_log = self.client.exec_start(res)
150+
assert exec_log == b'/var/www\n'

tests/integration/models_services_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def test_update_retains_networks(self):
195195
image="alpine",
196196
command="sleep 300"
197197
)
198+
service.reload()
198199
service.update(
199200
# create argument
200201
name=service.name,

0 commit comments

Comments
 (0)