Kaydet (Commit) abd60aed authored tarafından Joffrey F's avatar Joffrey F

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: 's avatarJoffrey F <joffrey@docker.com>
üst 95382583
...@@ -786,7 +786,8 @@ class ContainerApiMixin(object): ...@@ -786,7 +786,8 @@ class ContainerApiMixin(object):
@utils.check_resource('container') @utils.check_resource('container')
def logs(self, container, stdout=True, stderr=True, stream=False, 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. Get logs from a container. Similar to the ``docker logs`` command.
...@@ -805,6 +806,8 @@ class ContainerApiMixin(object): ...@@ -805,6 +806,8 @@ class ContainerApiMixin(object):
since (datetime or int): Show logs since a given datetime or since (datetime or int): Show logs since a given datetime or
integer epoch (in seconds) integer epoch (in seconds)
follow (bool): Follow log output follow (bool): Follow log output
until (datetime or int): Show logs that occurred before the given
datetime or integer epoch (in seconds)
Returns: Returns:
(generator or str) (generator or str)
...@@ -827,21 +830,35 @@ class ContainerApiMixin(object): ...@@ -827,21 +830,35 @@ class ContainerApiMixin(object):
params['tail'] = tail params['tail'] = tail
if since is not None: 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( 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: else:
if isinstance(since, datetime): raise errors.InvalidArgument(
params['since'] = utils.datetime_to_timestamp(since) 'since value should be datetime or positive int, '
elif (isinstance(since, int) and since > 0): 'not {}'.format(type(since))
params['since'] = since )
else:
raise errors.InvalidArgument( if until is not None:
'since value should be datetime or positive int, ' if utils.version_lt(self._version, '1.35'):
'not {}'. raise errors.InvalidVersion(
format(type(since)) '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) url = self._url("/containers/{0}/logs", container)
res = self._get(url, params=params, stream=stream) res = self._get(url, params=params, stream=stream)
return self._get_result(container, stream, res) return self._get_result(container, stream, res)
...@@ -1241,7 +1258,7 @@ class ContainerApiMixin(object): ...@@ -1241,7 +1258,7 @@ class ContainerApiMixin(object):
return self._result(res, True) return self._result(res, True)
@utils.check_resource('container') @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 Block until a container stops, then return its exit code. Similar to
the ``docker wait`` command. the ``docker wait`` command.
...@@ -1250,10 +1267,13 @@ class ContainerApiMixin(object): ...@@ -1250,10 +1267,13 @@ class ContainerApiMixin(object):
container (str or dict): The container to wait on. If a dict, the container (str or dict): The container to wait on. If a dict, the
``Id`` key is used. ``Id`` key is used.
timeout (int): Request timeout timeout (int): Request timeout
condition (str): Wait until a container state reaches the given
condition, either ``not-running`` (default), ``next-exit``,
or ``removed``
Returns: Returns:
(int): The exit code of the container. Returns ``-1`` if the API (int or dict): The exit code of the container. Returns the full API
responds without a ``StatusCode`` attribute. response if no ``StatusCode`` field is included.
Raises: Raises:
:py:class:`requests.exceptions.ReadTimeout` :py:class:`requests.exceptions.ReadTimeout`
...@@ -1262,9 +1282,17 @@ class ContainerApiMixin(object): ...@@ -1262,9 +1282,17 @@ class ContainerApiMixin(object):
If the server returns an error. If the server returns an error.
""" """
url = self._url("/containers/{0}/wait", container) 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) self._raise_for_status(res)
json_ = res.json() json_ = res.json()
if 'StatusCode' in json_: if 'StatusCode' in json_:
return json_['StatusCode'] return json_['StatusCode']
return -1 return json_
...@@ -9,7 +9,7 @@ class ExecApiMixin(object): ...@@ -9,7 +9,7 @@ class ExecApiMixin(object):
@utils.check_resource('container') @utils.check_resource('container')
def exec_create(self, container, cmd, stdout=True, stderr=True, def exec_create(self, container, cmd, stdout=True, stderr=True,
stdin=False, tty=False, privileged=False, user='', stdin=False, tty=False, privileged=False, user='',
environment=None): environment=None, workdir=None):
""" """
Sets up an exec instance in a running container. Sets up an exec instance in a running container.
...@@ -26,6 +26,7 @@ class ExecApiMixin(object): ...@@ -26,6 +26,7 @@ class ExecApiMixin(object):
environment (dict or list): A dictionary or a list of strings in environment (dict or list): A dictionary or a list of strings in
the following format ``["PASSWORD=xxx"]`` or the following format ``["PASSWORD=xxx"]`` or
``{"PASSWORD": "xxx"}``. ``{"PASSWORD": "xxx"}``.
workdir (str): Path to working directory for this exec session
Returns: Returns:
(dict): A dictionary with an exec ``Id`` key. (dict): A dictionary with an exec ``Id`` key.
...@@ -66,6 +67,13 @@ class ExecApiMixin(object): ...@@ -66,6 +67,13 @@ class ExecApiMixin(object):
'Env': environment, '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) url = self._url('/containers/{0}/exec', container)
res = self._post_json(url, data=data) res = self._post_json(url, data=data)
return self._result(res, True) return self._result(res, True)
......
...@@ -65,6 +65,10 @@ def _check_api_features(version, task_template, update_config): ...@@ -65,6 +65,10 @@ def _check_api_features(version, task_template, update_config):
if container_spec.get('Privileges') is not None: if container_spec.get('Privileges') is not None:
raise_version_error('ContainerSpec.privileges', '1.30') 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): def _merge_task_template(current, override):
merged = current.copy() merged = current.copy()
......
import sys import sys
from .version import version from .version import version
DEFAULT_DOCKER_API_VERSION = '1.30' DEFAULT_DOCKER_API_VERSION = '1.35'
MINIMUM_DOCKER_API_VERSION = '1.21' MINIMUM_DOCKER_API_VERSION = '1.21'
DEFAULT_TIMEOUT_SECONDS = 60 DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8 STREAM_HEADER_SIZE_BYTES = 8
......
...@@ -126,7 +126,7 @@ class Container(Model): ...@@ -126,7 +126,7 @@ class Container(Model):
def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False, def exec_run(self, cmd, stdout=True, stderr=True, stdin=False, tty=False,
privileged=False, user='', detach=False, stream=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 Run a command inside this container. Similar to
``docker exec``. ``docker exec``.
...@@ -147,6 +147,7 @@ class Container(Model): ...@@ -147,6 +147,7 @@ class Container(Model):
environment (dict or list): A dictionary or a list of strings in environment (dict or list): A dictionary or a list of strings in
the following format ``["PASSWORD=xxx"]`` or the following format ``["PASSWORD=xxx"]`` or
``{"PASSWORD": "xxx"}``. ``{"PASSWORD": "xxx"}``.
workdir (str): Path to working directory for this exec session
Returns: Returns:
(generator or str): (generator or str):
...@@ -159,7 +160,8 @@ class Container(Model): ...@@ -159,7 +160,8 @@ class Container(Model):
""" """
resp = self.client.api.exec_create( resp = self.client.api.exec_create(
self.id, cmd, stdout=stdout, stderr=stderr, stdin=stdin, tty=tty, 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( return self.client.api.exec_start(
resp['Id'], detach=detach, tty=tty, stream=stream, socket=socket resp['Id'], detach=detach, tty=tty, stream=stream, socket=socket
...@@ -427,6 +429,9 @@ class Container(Model): ...@@ -427,6 +429,9 @@ class Container(Model):
Args: Args:
timeout (int): Request timeout timeout (int): Request timeout
condition (str): Wait until a container state reaches the given
condition, either ``not-running`` (default), ``next-exit``,
or ``removed``
Returns: Returns:
(int): The exit code of the container. Returns ``-1`` if the API (int): The exit code of the container. Returns ``-1`` if the API
......
...@@ -144,6 +144,8 @@ class ServiceCollection(Collection): ...@@ -144,6 +144,8 @@ class ServiceCollection(Collection):
env (list of str): Environment variables, in the form env (list of str): Environment variables, in the form
``KEY=val``. ``KEY=val``.
hostname (string): Hostname to set on the container. 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. labels (dict): Labels to apply to the service.
log_driver (str): Log driver to use for containers. log_driver (str): Log driver to use for containers.
log_driver_options (dict): Log driver options. log_driver_options (dict): Log driver options.
...@@ -255,6 +257,7 @@ CONTAINER_SPEC_KWARGS = [ ...@@ -255,6 +257,7 @@ CONTAINER_SPEC_KWARGS = [
'hostname', 'hostname',
'hosts', 'hosts',
'image', 'image',
'isolation',
'labels', 'labels',
'mounts', 'mounts',
'open_stdin', 'open_stdin',
......
...@@ -102,19 +102,21 @@ class ContainerSpec(dict): ...@@ -102,19 +102,21 @@ class ContainerSpec(dict):
healthcheck (Healthcheck): Healthcheck healthcheck (Healthcheck): Healthcheck
configuration for this service. configuration for this service.
hosts (:py:class:`dict`): A set of host to IP mappings to add to 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 dns_config (DNSConfig): Specification for DNS
related configurations in resolver configuration file. related configurations in resolver configuration file.
configs (:py:class:`list`): List of :py:class:`ConfigReference` that configs (:py:class:`list`): List of :py:class:`ConfigReference` that
will be exposed to the service. will be exposed to the service.
privileges (Privileges): Security options for the service's containers. 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, def __init__(self, image, command=None, args=None, hostname=None, env=None,
workdir=None, user=None, labels=None, mounts=None, workdir=None, user=None, labels=None, mounts=None,
stop_grace_period=None, secrets=None, tty=None, groups=None, stop_grace_period=None, secrets=None, tty=None, groups=None,
open_stdin=None, read_only=None, stop_signal=None, open_stdin=None, read_only=None, stop_signal=None,
healthcheck=None, hosts=None, dns_config=None, configs=None, healthcheck=None, hosts=None, dns_config=None, configs=None,
privileges=None): privileges=None, isolation=None):
self['Image'] = image self['Image'] = image
if isinstance(command, six.string_types): if isinstance(command, six.string_types):
...@@ -178,6 +180,9 @@ class ContainerSpec(dict): ...@@ -178,6 +180,9 @@ class ContainerSpec(dict):
if read_only is not None: if read_only is not None:
self['ReadOnly'] = read_only self['ReadOnly'] = read_only
if isolation is not None:
self['Isolation'] = isolation
class Mount(dict): class Mount(dict):
""" """
......
import os import os
import signal import signal
import tempfile import tempfile
from datetime import datetime
import docker import docker
from docker.constants import IS_WINDOWS_PLATFORM from docker.constants import IS_WINDOWS_PLATFORM
...@@ -9,6 +10,7 @@ from docker.utils.socket import read_exactly ...@@ -9,6 +10,7 @@ from docker.utils.socket import read_exactly
import pytest import pytest
import requests
import six import six
from .base import BUSYBOX, BaseAPIIntegrationTest from .base import BUSYBOX, BaseAPIIntegrationTest
...@@ -816,6 +818,21 @@ class WaitTest(BaseAPIIntegrationTest): ...@@ -816,6 +818,21 @@ class WaitTest(BaseAPIIntegrationTest):
self.assertIn('ExitCode', inspect['State']) self.assertIn('ExitCode', inspect['State'])
self.assertEqual(inspect['State']['ExitCode'], exitcode) 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): class LogsTest(BaseAPIIntegrationTest):
def test_logs(self): def test_logs(self):
...@@ -888,6 +905,22 @@ Line2''' ...@@ -888,6 +905,22 @@ Line2'''
logs = self.client.logs(id, tail=0) logs = self.client.logs(id, tail=0)
self.assertEqual(logs, ''.encode(encoding='ascii')) 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): class DiffTest(BaseAPIIntegrationTest):
def test_diff(self): def test_diff(self):
......
...@@ -136,3 +136,15 @@ class ExecTest(BaseAPIIntegrationTest): ...@@ -136,3 +136,15 @@ class ExecTest(BaseAPIIntegrationTest):
exec_log = self.client.exec_start(res) exec_log = self.client.exec_start(res)
assert b'X=Y\n' in exec_log 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): ...@@ -195,6 +195,7 @@ class ServiceTest(unittest.TestCase):
image="alpine", image="alpine",
command="sleep 300" command="sleep 300"
) )
service.reload()
service.update( service.update(
# create argument # create argument
name=service.name, name=service.name,
......
...@@ -1263,7 +1263,8 @@ class ContainerTest(BaseAPIClientTest): ...@@ -1263,7 +1263,8 @@ class ContainerTest(BaseAPIClientTest):
fake_request.assert_called_with( fake_request.assert_called_with(
'POST', 'POST',
url_prefix + 'containers/3cc2351ab11b/wait', url_prefix + 'containers/3cc2351ab11b/wait',
timeout=None timeout=None,
params={}
) )
def test_wait_with_dict_instead_of_id(self): def test_wait_with_dict_instead_of_id(self):
...@@ -1272,7 +1273,8 @@ class ContainerTest(BaseAPIClientTest): ...@@ -1272,7 +1273,8 @@ class ContainerTest(BaseAPIClientTest):
fake_request.assert_called_with( fake_request.assert_called_with(
'POST', 'POST',
url_prefix + 'containers/3cc2351ab11b/wait', url_prefix + 'containers/3cc2351ab11b/wait',
timeout=None timeout=None,
params={}
) )
def test_logs(self): def test_logs(self):
......
...@@ -394,7 +394,8 @@ class ContainerTest(unittest.TestCase): ...@@ -394,7 +394,8 @@ class ContainerTest(unittest.TestCase):
container.exec_run("echo hello world", privileged=True, stream=True) container.exec_run("echo hello world", privileged=True, stream=True)
client.api.exec_create.assert_called_with( client.api.exec_create.assert_called_with(
FAKE_CONTAINER_ID, "echo hello world", stdout=True, stderr=True, 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( client.api.exec_start.assert_called_with(
FAKE_EXEC_ID, detach=False, tty=False, stream=True, socket=False FAKE_EXEC_ID, detach=False, tty=False, stream=True, socket=False
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment