Skip to content

Commit 04eb7a2

Browse files
authored
Merge pull request #1867 from docker/bump_api_version_1.35
Bump default API version to 1.35
2 parents 9538258 + abd60ae commit 04eb7a2

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)