mirror of https://github.com/docker/docker-py.git
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>
This commit is contained in:
parent
9538258371
commit
abd60aedc7
|
|
@ -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_
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ class ServiceTest(unittest.TestCase):
|
|||
image="alpine",
|
||||
command="sleep 300"
|
||||
)
|
||||
service.reload()
|
||||
service.update(
|
||||
# create argument
|
||||
name=service.name,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue