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

Merge pull request #764 from docker/744-volume-api-support

Volume API support
...@@ -4,3 +4,4 @@ from .container import ContainerApiMixin ...@@ -4,3 +4,4 @@ from .container import ContainerApiMixin
from .daemon import DaemonApiMixin from .daemon import DaemonApiMixin
from .exec_api import ExecApiMixin from .exec_api import ExecApiMixin
from .image import ImageApiMixin from .image import ImageApiMixin
from .volume import VolumeApiMixin
...@@ -4,8 +4,8 @@ import re ...@@ -4,8 +4,8 @@ import re
from .. import constants from .. import constants
from .. import errors from .. import errors
from ..auth import auth from .. import auth
from ..utils import utils from .. import utils
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -2,11 +2,11 @@ import six ...@@ -2,11 +2,11 @@ import six
import warnings import warnings
from .. import errors from .. import errors
from ..utils import utils, check_resource from .. import utils
class ContainerApiMixin(object): class ContainerApiMixin(object):
@check_resource @utils.check_resource
def attach(self, container, stdout=True, stderr=True, def attach(self, container, stdout=True, stderr=True,
stream=False, logs=False): stream=False, logs=False):
params = { params = {
...@@ -20,7 +20,7 @@ class ContainerApiMixin(object): ...@@ -20,7 +20,7 @@ class ContainerApiMixin(object):
return self._get_result(container, stream, response) return self._get_result(container, stream, response)
@check_resource @utils.check_resource
def attach_socket(self, container, params=None, ws=False): def attach_socket(self, container, params=None, ws=False):
if params is None: if params is None:
params = { params = {
...@@ -36,7 +36,7 @@ class ContainerApiMixin(object): ...@@ -36,7 +36,7 @@ class ContainerApiMixin(object):
return self._get_raw_response_socket(self.post( return self._get_raw_response_socket(self.post(
u, None, params=self._attach_params(params), stream=True)) u, None, params=self._attach_params(params), stream=True))
@check_resource @utils.check_resource
def commit(self, container, repository=None, tag=None, message=None, def commit(self, container, repository=None, tag=None, message=None,
author=None, conf=None): author=None, conf=None):
params = { params = {
...@@ -73,7 +73,7 @@ class ContainerApiMixin(object): ...@@ -73,7 +73,7 @@ class ContainerApiMixin(object):
x['Id'] = x['Id'][:12] x['Id'] = x['Id'][:12]
return res return res
@check_resource @utils.check_resource
def copy(self, container, resource): def copy(self, container, resource):
res = self._post_json( res = self._post_json(
self._url("/containers/{0}/copy".format(container)), self._url("/containers/{0}/copy".format(container)),
...@@ -131,13 +131,13 @@ class ContainerApiMixin(object): ...@@ -131,13 +131,13 @@ class ContainerApiMixin(object):
kwargs['version'] = self._version kwargs['version'] = self._version
return utils.create_host_config(*args, **kwargs) return utils.create_host_config(*args, **kwargs)
@check_resource @utils.check_resource
def diff(self, container): def diff(self, container):
return self._result( return self._result(
self._get(self._url("/containers/{0}/changes", container)), True self._get(self._url("/containers/{0}/changes", container)), True
) )
@check_resource @utils.check_resource
def export(self, container): def export(self, container):
res = self._get( res = self._get(
self._url("/containers/{0}/export", container), stream=True self._url("/containers/{0}/export", container), stream=True
...@@ -145,13 +145,13 @@ class ContainerApiMixin(object): ...@@ -145,13 +145,13 @@ class ContainerApiMixin(object):
self._raise_for_status(res) self._raise_for_status(res)
return res.raw return res.raw
@check_resource @utils.check_resource
def inspect_container(self, container): def inspect_container(self, container):
return self._result( return self._result(
self._get(self._url("/containers/{0}/json", container)), True self._get(self._url("/containers/{0}/json", container)), True
) )
@check_resource @utils.check_resource
def kill(self, container, signal=None): def kill(self, container, signal=None):
url = self._url("/containers/{0}/kill", container) url = self._url("/containers/{0}/kill", container)
params = {} params = {}
...@@ -161,7 +161,7 @@ class ContainerApiMixin(object): ...@@ -161,7 +161,7 @@ class ContainerApiMixin(object):
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def logs(self, container, stdout=True, stderr=True, stream=False, def logs(self, container, stdout=True, stderr=True, stream=False,
timestamps=False, tail='all'): timestamps=False, tail='all'):
if utils.compare_version('1.11', self._version) >= 0: if utils.compare_version('1.11', self._version) >= 0:
...@@ -185,13 +185,13 @@ class ContainerApiMixin(object): ...@@ -185,13 +185,13 @@ class ContainerApiMixin(object):
logs=True logs=True
) )
@check_resource @utils.check_resource
def pause(self, container): def pause(self, container):
url = self._url('/containers/{0}/pause', container) url = self._url('/containers/{0}/pause', container)
res = self._post(url) res = self._post(url)
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def port(self, container, private_port): def port(self, container, private_port):
res = self._get(self._url("/containers/{0}/json", container)) res = self._get(self._url("/containers/{0}/json", container))
self._raise_for_status(res) self._raise_for_status(res)
...@@ -211,7 +211,7 @@ class ContainerApiMixin(object): ...@@ -211,7 +211,7 @@ class ContainerApiMixin(object):
return h_ports return h_ports
@check_resource @utils.check_resource
def remove_container(self, container, v=False, link=False, force=False): def remove_container(self, container, v=False, link=False, force=False):
params = {'v': v, 'link': link, 'force': force} params = {'v': v, 'link': link, 'force': force}
res = self._delete( res = self._delete(
...@@ -219,32 +219,29 @@ class ContainerApiMixin(object): ...@@ -219,32 +219,29 @@ class ContainerApiMixin(object):
) )
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.minimum_version('1.17')
@utils.check_resource
def rename(self, container, name): def rename(self, container, name):
if utils.compare_version('1.17', self._version) < 0:
raise errors.InvalidVersion(
'rename was only introduced in API version 1.17'
)
url = self._url("/containers/{0}/rename", container) url = self._url("/containers/{0}/rename", container)
params = {'name': name} params = {'name': name}
res = self._post(url, params=params) res = self._post(url, params=params)
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def resize(self, container, height, width): def resize(self, container, height, width):
params = {'h': height, 'w': width} params = {'h': height, 'w': width}
url = self._url("/containers/{0}/resize", container) url = self._url("/containers/{0}/resize", container)
res = self._post(url, params=params) res = self._post(url, params=params)
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def restart(self, container, timeout=10): def restart(self, container, timeout=10):
params = {'t': timeout} params = {'t': timeout}
url = self._url("/containers/{0}/restart", container) url = self._url("/containers/{0}/restart", container)
res = self._post(url, params=params) res = self._post(url, params=params)
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def start(self, container, binds=None, port_bindings=None, lxc_conf=None, def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
publish_all_ports=None, links=None, privileged=None, publish_all_ports=None, links=None, privileged=None,
dns=None, dns_search=None, volumes_from=None, network_mode=None, dns=None, dns_search=None, volumes_from=None, network_mode=None,
...@@ -312,16 +309,13 @@ class ContainerApiMixin(object): ...@@ -312,16 +309,13 @@ class ContainerApiMixin(object):
res = self._post_json(url, data=start_config) res = self._post_json(url, data=start_config)
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.minimum_version('1.17')
@utils.check_resource
def stats(self, container, decode=None): def stats(self, container, decode=None):
if utils.compare_version('1.17', self._version) < 0:
raise errors.InvalidVersion(
'Stats retrieval is not supported in API < 1.17!')
url = self._url("/containers/{0}/stats", container) url = self._url("/containers/{0}/stats", container)
return self._stream_helper(self._get(url, stream=True), decode=decode) return self._stream_helper(self._get(url, stream=True), decode=decode)
@check_resource @utils.check_resource
def stop(self, container, timeout=10): def stop(self, container, timeout=10):
params = {'t': timeout} params = {'t': timeout}
url = self._url("/containers/{0}/stop", container) url = self._url("/containers/{0}/stop", container)
...@@ -330,18 +324,18 @@ class ContainerApiMixin(object): ...@@ -330,18 +324,18 @@ class ContainerApiMixin(object):
timeout=(timeout + (self.timeout or 0))) timeout=(timeout + (self.timeout or 0)))
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def top(self, container): def top(self, container):
u = self._url("/containers/{0}/top", container) u = self._url("/containers/{0}/top", container)
return self._result(self._get(u), True) return self._result(self._get(u), True)
@check_resource @utils.check_resource
def unpause(self, container): def unpause(self, container):
url = self._url('/containers/{0}/unpause', container) url = self._url('/containers/{0}/unpause', container)
res = self._post(url) res = self._post(url)
self._raise_for_status(res) self._raise_for_status(res)
@check_resource @utils.check_resource
def wait(self, container, timeout=None): def wait(self, container, timeout=None):
url = self._url("/containers/{0}/wait", container) url = self._url("/containers/{0}/wait", container)
res = self._post(url, timeout=timeout) res = self._post(url, timeout=timeout)
......
...@@ -3,15 +3,14 @@ import shlex ...@@ -3,15 +3,14 @@ import shlex
import six import six
from .. import errors from .. import errors
from ..utils import utils, check_resource from .. import utils
class ExecApiMixin(object): class ExecApiMixin(object):
@check_resource @utils.minimum_version('1.15')
@utils.check_resource
def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
privileged=False, user=''): privileged=False, user=''):
if utils.compare_version('1.15', self._version) < 0:
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
if privileged and utils.compare_version('1.19', self._version) < 0: if privileged and utils.compare_version('1.19', self._version) < 0:
raise errors.InvalidVersion( raise errors.InvalidVersion(
'Privileged exec is not supported in API < 1.19' 'Privileged exec is not supported in API < 1.19'
...@@ -38,19 +37,15 @@ class ExecApiMixin(object): ...@@ -38,19 +37,15 @@ class ExecApiMixin(object):
res = self._post_json(url, data=data) res = self._post_json(url, data=data)
return self._result(res, True) return self._result(res, True)
@utils.minimum_version('1.16')
def exec_inspect(self, exec_id): def exec_inspect(self, exec_id):
if utils.compare_version('1.16', self._version) < 0:
raise errors.InvalidVersion(
'exec_inspect is not supported in API < 1.16'
)
if isinstance(exec_id, dict): if isinstance(exec_id, dict):
exec_id = exec_id.get('Id') exec_id = exec_id.get('Id')
res = self._get(self._url("/exec/{0}/json", exec_id)) res = self._get(self._url("/exec/{0}/json", exec_id))
return self._result(res, True) return self._result(res, True)
@utils.minimum_version('1.15')
def exec_resize(self, exec_id, height=None, width=None): def exec_resize(self, exec_id, height=None, width=None):
if utils.compare_version('1.15', self._version) < 0:
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
if isinstance(exec_id, dict): if isinstance(exec_id, dict):
exec_id = exec_id.get('Id') exec_id = exec_id.get('Id')
...@@ -59,9 +54,8 @@ class ExecApiMixin(object): ...@@ -59,9 +54,8 @@ class ExecApiMixin(object):
res = self._post(url, params=params) res = self._post(url, params=params)
self._raise_for_status(res) self._raise_for_status(res)
@utils.minimum_version('1.15')
def exec_start(self, exec_id, detach=False, tty=False, stream=False): def exec_start(self, exec_id, detach=False, tty=False, stream=False):
if utils.compare_version('1.15', self._version) < 0:
raise errors.InvalidVersion('Exec is not supported in API < 1.15')
if isinstance(exec_id, dict): if isinstance(exec_id, dict):
exec_id = exec_id.get('Id') exec_id = exec_id.get('Id')
......
...@@ -4,7 +4,7 @@ import warnings ...@@ -4,7 +4,7 @@ import warnings
from ..auth import auth from ..auth import auth
from ..constants import INSECURE_REGISTRY_DEPRECATION_WARNING from ..constants import INSECURE_REGISTRY_DEPRECATION_WARNING
from ..utils import utils, check_resource from .. import utils
from .. import errors from .. import errors
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -12,13 +12,13 @@ log = logging.getLogger(__name__) ...@@ -12,13 +12,13 @@ log = logging.getLogger(__name__)
class ImageApiMixin(object): class ImageApiMixin(object):
@check_resource @utils.check_resource
def get_image(self, image): def get_image(self, image):
res = self._get(self._url("/images/{0}/get", image), stream=True) res = self._get(self._url("/images/{0}/get", image), stream=True)
self._raise_for_status(res) self._raise_for_status(res)
return res.raw return res.raw
@check_resource @utils.check_resource
def history(self, image): def history(self, image):
res = self._get(self._url("/images/{0}/history", image)) res = self._get(self._url("/images/{0}/history", image))
return self._result(res, True) return self._result(res, True)
...@@ -124,7 +124,7 @@ class ImageApiMixin(object): ...@@ -124,7 +124,7 @@ class ImageApiMixin(object):
return self._result( return self._result(
self._post(u, data=None, params=params)) self._post(u, data=None, params=params))
@check_resource @utils.check_resource
def insert(self, image, url, path): def insert(self, image, url, path):
if utils.compare_version('1.12', self._version) >= 0: if utils.compare_version('1.12', self._version) >= 0:
raise errors.DeprecatedMethod( raise errors.DeprecatedMethod(
...@@ -137,7 +137,7 @@ class ImageApiMixin(object): ...@@ -137,7 +137,7 @@ class ImageApiMixin(object):
} }
return self._result(self._post(api_url, params=params)) return self._result(self._post(api_url, params=params))
@check_resource @utils.check_resource
def inspect_image(self, image): def inspect_image(self, image):
return self._result( return self._result(
self._get(self._url("/images/{0}/json", image)), True self._get(self._url("/images/{0}/json", image)), True
...@@ -246,7 +246,7 @@ class ImageApiMixin(object): ...@@ -246,7 +246,7 @@ class ImageApiMixin(object):
return self._result(response) return self._result(response)
@check_resource @utils.check_resource
def remove_image(self, image, force=False, noprune=False): def remove_image(self, image, force=False, noprune=False):
params = {'force': force, 'noprune': noprune} params = {'force': force, 'noprune': noprune}
res = self._delete(self._url("/images/{0}", image), params=params) res = self._delete(self._url("/images/{0}", image), params=params)
...@@ -258,7 +258,7 @@ class ImageApiMixin(object): ...@@ -258,7 +258,7 @@ class ImageApiMixin(object):
True True
) )
@check_resource @utils.check_resource
def tag(self, image, repository, tag=None, force=False): def tag(self, image, repository, tag=None, force=False):
params = { params = {
'tag': tag, 'tag': tag,
......
from .. import utils
class VolumeApiMixin(object):
@utils.minimum_version('1.21')
def volumes(self, filters=None):
params = {
'filter': utils.convert_filters(filters) if filters else None
}
url = self._url('/volumes')
return self._result(self._get(url, params=params), True)
@utils.minimum_version('1.21')
def create_volume(self, name, driver=None, driver_opts=None):
url = self._url('/volumes')
if driver_opts is not None and not isinstance(driver_opts, dict):
raise TypeError('driver_opts must be a dictionary')
data = {
'Name': name,
'Driver': driver,
'DriverOpts': driver_opts,
}
return self._result(self._post_json(url, data=data), True)
@utils.minimum_version('1.21')
def inspect_volume(self, name):
url = self._url('/volumes/{0}', name)
return self._result(self._get(url), True)
@utils.minimum_version('1.21')
def remove_volume(self, name):
url = self._url('/volumes/{0}', name)
resp = self._delete(url)
self._raise_for_status(resp)
return True
...@@ -38,7 +38,8 @@ class Client( ...@@ -38,7 +38,8 @@ class Client(
api.ContainerApiMixin, api.ContainerApiMixin,
api.DaemonApiMixin, api.DaemonApiMixin,
api.ExecApiMixin, api.ExecApiMixin,
api.ImageApiMixin): api.ImageApiMixin,
api.VolumeApiMixin):
def __init__(self, base_url=None, version=None, def __init__(self, base_url=None, version=None,
timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False):
super(Client, self).__init__() super(Client, self).__init__()
......
...@@ -2,8 +2,9 @@ from .utils import ( ...@@ -2,8 +2,9 @@ from .utils import (
compare_version, convert_port_bindings, convert_volume_binds, compare_version, convert_port_bindings, convert_volume_binds,
mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host, mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host,
kwargs_from_env, convert_filters, create_host_config, kwargs_from_env, convert_filters, create_host_config,
create_container_config, parse_bytes, ping_registry, parse_env_file create_container_config, parse_bytes, ping_registry, parse_env_file,
version_lt, version_gte
) # flake8: noqa ) # flake8: noqa
from .types import Ulimit, LogConfig # flake8: noqa from .types import Ulimit, LogConfig # flake8: noqa
from .decorators import check_resource #flake8: noqa from .decorators import check_resource, minimum_version #flake8: noqa
import functools import functools
from .. import errors from .. import errors
from . import utils
def check_resource(f): def check_resource(f):
...@@ -19,3 +20,18 @@ def check_resource(f): ...@@ -19,3 +20,18 @@ def check_resource(f):
) )
return f(self, resource_id, *args, **kwargs) return f(self, resource_id, *args, **kwargs)
return wrapped return wrapped
def minimum_version(version):
def decorator(f):
@functools.wraps(f)
def wrapper(self, *args, **kwargs):
if utils.version_lt(self._version, version):
raise errors.InvalidVersion(
'{0} is not available for version < {1}'.format(
f.__name__, version
)
)
return f(self, *args, **kwargs)
return wrapper
return decorator
...@@ -164,6 +164,14 @@ def compare_version(v1, v2): ...@@ -164,6 +164,14 @@ def compare_version(v1, v2):
return 1 return 1
def version_lt(v1, v2):
return compare_version(v1, v2) > 0
def version_gte(v1, v2):
return not version_lt(v1, v2)
def ping_registry(url): def ping_registry(url):
warnings.warn( warnings.warn(
'The `ping_registry` method is deprecated and will be removed.', 'The `ping_registry` method is deprecated and will be removed.',
......
...@@ -255,6 +255,29 @@ The utility can be used as follows: ...@@ -255,6 +255,29 @@ The utility can be used as follows:
You can now use this with 'environment' for `create_container`. You can now use this with 'environment' for `create_container`.
## create_volume
Create and register a named volume
**Params**:
* name (str): Name of the volume
* driver (str): Name of the driver used to create the volume
* driver_opts (dict): Driver options as a key-value dictionary
**Returns** (dict): The created volume reference object
```python
>>> from docker import Client
>>> cli = Client()
>>> volume = cli.create_volume(
name='foobar', driver='local', driver_opts={'foo': 'bar', 'baz': 'false'}
)
>>> print(volume)
{u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data', u'Driver': u'local', u'Name': u'foobar'}
```
## diff ## diff
Inspect changes on a container's filesystem Inspect changes on a container's filesystem
...@@ -526,6 +549,21 @@ Identical to the `docker inspect` command, but only for images ...@@ -526,6 +549,21 @@ Identical to the `docker inspect` command, but only for images
**Returns** (dict): Nearly the same output as `docker inspect`, just as a **Returns** (dict): Nearly the same output as `docker inspect`, just as a
single dict single dict
## inspect_volume
Retrieve volume info by name.
**Params**:
* name (str): volume name
**Returns** (dict): Volume information dictionary
```python
>>> cli.inspect_volume('foobar')
{u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data', u'Driver': u'local', u'Name': u'foobar'}
```
## kill ## kill
Kill a container or send a signal to a container Kill a container or send a signal to a container
...@@ -695,6 +733,17 @@ Remove an image. Similar to the `docker rmi` command. ...@@ -695,6 +733,17 @@ Remove an image. Similar to the `docker rmi` command.
* force (bool): Force removal of the image * force (bool): Force removal of the image
* noprune (bool): Do not delete untagged parents * noprune (bool): Do not delete untagged parents
## remove_volume
Remove a volume. Similar to the `docker volume rm` command.
**Params**:
* name (str): The volume's name
**Returns** (bool): True on successful removal. Failure will raise a
`docker.errors.APIError` exception.
## rename ## rename
Rename a container. Similar to the `docker rename` command. Rename a container. Similar to the `docker rename` command.
...@@ -851,6 +900,7 @@ Unpauses all processes within a container. ...@@ -851,6 +900,7 @@ Unpauses all processes within a container.
* container (str): The container to unpause * container (str): The container to unpause
## version ## version
Nearly identical to the `docker version` command. Nearly identical to the `docker version` command.
**Returns** (dict): The server version information **Returns** (dict): The server version information
...@@ -870,6 +920,23 @@ Nearly identical to the `docker version` command. ...@@ -870,6 +920,23 @@ Nearly identical to the `docker version` command.
} }
``` ```
## volumes
List volumes currently registered by the docker daemon. Similar to the `docker volume ls` command.
**Params**
* filters (dict): Server-side list filtering options.
**Returns** (dict): Dictionary with list of volume objects as value of the `Volumes` key.
```python
>>> cli.volumes()
{u'Volumes': [
{u'Mountpoint': u'/var/lib/docker/volumes/foobar/_data', u'Driver': u'local', u'Name': u'foobar'},
{u'Mountpoint': u'/var/lib/docker/volumes/baz/_data', u'Driver': u'local', u'Name': u'baz'}
]}
```
## wait ## wait
Identical to the `docker wait` command. Block until a container stops, then Identical to the `docker wait` command. Block until a container stops, then
...@@ -893,6 +960,5 @@ exception will be raised. ...@@ -893,6 +960,5 @@ exception will be raised.
TODO: TODO:
* load_image * load_image
* resize
--> -->
import sys import sys
import unittest import unittest
import pytest
import six import six
import docker
class BaseTestCase(unittest.TestCase): class BaseTestCase(unittest.TestCase):
def assertIn(self, object, collection): def assertIn(self, object, collection):
if six.PY2 and sys.version_info[1] <= 6: if six.PY2 and sys.version_info[1] <= 6:
return self.assertTrue(object in collection) return self.assertTrue(object in collection)
return super(BaseTestCase, self).assertIn(object, collection) return super(BaseTestCase, self).assertIn(object, collection)
def requires_api_version(version):
return pytest.mark.skipif(
docker.utils.version_lt(
docker.constants.DEFAULT_DOCKER_API_VERSION, version
),
reason="API version is too low (< {0})".format(version)
)
...@@ -13,8 +13,9 @@ ...@@ -13,8 +13,9 @@
# limitations under the License. # limitations under the License.
from . import fake_stat from . import fake_stat
from docker import constants
CURRENT_VERSION = 'v1.19' CURRENT_VERSION = 'v{0}'.format(constants.DEFAULT_DOCKER_API_VERSION)
FAKE_CONTAINER_ID = '3cc2351ab11b' FAKE_CONTAINER_ID = '3cc2351ab11b'
FAKE_IMAGE_ID = 'e9aa60c60128' FAKE_IMAGE_ID = 'e9aa60c60128'
...@@ -26,6 +27,7 @@ FAKE_TAG_NAME = 'tag' ...@@ -26,6 +27,7 @@ FAKE_TAG_NAME = 'tag'
FAKE_FILE_NAME = 'file' FAKE_FILE_NAME = 'file'
FAKE_URL = 'myurl' FAKE_URL = 'myurl'
FAKE_PATH = '/path' FAKE_PATH = '/path'
FAKE_VOLUME_NAME = 'perfectcherryblossom'
# Each method is prefixed with HTTP method (get, post...) # Each method is prefixed with HTTP method (get, post...)
# for clarity and readability # for clarity and readability
...@@ -380,6 +382,38 @@ def get_fake_stats(): ...@@ -380,6 +382,38 @@ def get_fake_stats():
response = fake_stat.OBJ response = fake_stat.OBJ
return status_code, response return status_code, response
def get_fake_volume_list():
status_code = 200
response = {
'Volumes': [
{
'Name': 'perfectcherryblossom',
'Driver': 'local',
'Mountpoint': '/var/lib/docker/volumes/perfectcherryblossom'
}, {
'Name': 'subterraneananimism',
'Driver': 'local',
'Mountpoint': '/var/lib/docker/volumes/subterraneananimism'
}
]
}
return status_code, response
def get_fake_volume():
status_code = 200
response = {
'Name': 'perfectcherryblossom',
'Driver': 'local',
'Mountpoint': '/var/lib/docker/volumes/perfectcherryblossom'
}
return status_code, response
def fake_remove_volume():
return 204, None
# Maps real api url to fake response callback # Maps real api url to fake response callback
prefix = 'http+docker://localunixsocket' prefix = 'http+docker://localunixsocket'
fake_responses = { fake_responses = {
...@@ -463,5 +497,17 @@ fake_responses = { ...@@ -463,5 +497,17 @@ fake_responses = {
'{1}/{0}/build'.format(CURRENT_VERSION, prefix): '{1}/{0}/build'.format(CURRENT_VERSION, prefix):
post_fake_build_container, post_fake_build_container,
'{1}/{0}/events'.format(CURRENT_VERSION, prefix): '{1}/{0}/events'.format(CURRENT_VERSION, prefix):
get_fake_events get_fake_events,
('{1}/{0}/volumes'.format(CURRENT_VERSION, prefix), 'GET'):
get_fake_volume_list,
('{1}/{0}/volumes'.format(CURRENT_VERSION, prefix), 'POST'):
get_fake_volume,
('{1}/{0}/volumes/{2}'.format(
CURRENT_VERSION, prefix, FAKE_VOLUME_NAME
), 'GET'):
get_fake_volume,
('{1}/{0}/volumes/{2}'.format(
CURRENT_VERSION, prefix, FAKE_VOLUME_NAME
), 'DELETE'):
fake_remove_volume,
} }
...@@ -27,17 +27,18 @@ import time ...@@ -27,17 +27,18 @@ import time
import unittest import unittest
import warnings import warnings
import docker import pytest
from docker.utils import kwargs_from_env
import six import six
from six.moves import BaseHTTPServer from six.moves import BaseHTTPServer
from six.moves import socketserver from six.moves import socketserver
from .test import Cleanup import docker
from docker.errors import APIError from docker.errors import APIError
from docker.utils import kwargs_from_env
from .base import requires_api_version
from .test import Cleanup
import pytest
# FIXME: missing tests for # FIXME: missing tests for
# export; history; insert; port; push; tag; get; load; stats # export; history; insert; port; push; tag; get; load; stats
...@@ -77,6 +78,7 @@ class BaseTestCase(unittest.TestCase): ...@@ -77,6 +78,7 @@ class BaseTestCase(unittest.TestCase):
tmp_imgs = [] tmp_imgs = []
tmp_containers = [] tmp_containers = []
tmp_folders = [] tmp_folders = []
tmp_volumes = []
def setUp(self): def setUp(self):
if six.PY2: if six.PY2:
...@@ -86,6 +88,7 @@ class BaseTestCase(unittest.TestCase): ...@@ -86,6 +88,7 @@ class BaseTestCase(unittest.TestCase):
self.tmp_imgs = [] self.tmp_imgs = []
self.tmp_containers = [] self.tmp_containers = []
self.tmp_folders = [] self.tmp_folders = []
self.tmp_volumes = []
def tearDown(self): def tearDown(self):
for img in self.tmp_imgs: for img in self.tmp_imgs:
...@@ -101,6 +104,13 @@ class BaseTestCase(unittest.TestCase): ...@@ -101,6 +104,13 @@ class BaseTestCase(unittest.TestCase):
pass pass
for folder in self.tmp_folders: for folder in self.tmp_folders:
shutil.rmtree(folder) shutil.rmtree(folder)
for volume in self.tmp_volumes:
try:
self.client.remove_volume(volume)
except docker.errors.APIError:
pass
self.client.close() self.client.close()
######################### #########################
...@@ -223,6 +233,8 @@ class TestCreateContainerWithBinds(BaseTestCase): ...@@ -223,6 +233,8 @@ class TestCreateContainerWithBinds(BaseTestCase):
if six.PY3: if six.PY3:
logs = logs.decode('utf-8') logs = logs.decode('utf-8')
self.assertIn(filename, logs) self.assertIn(filename, logs)
# FIXME: format changes in API version >= 1.20
inspect_data = self.client.inspect_container(container_id) inspect_data = self.client.inspect_container(container_id)
self.assertIn('Volumes', inspect_data) self.assertIn('Volumes', inspect_data)
self.assertIn(mount_dest, inspect_data['Volumes']) self.assertIn(mount_dest, inspect_data['Volumes'])
...@@ -265,6 +277,8 @@ class TestCreateContainerWithRoBinds(BaseTestCase): ...@@ -265,6 +277,8 @@ class TestCreateContainerWithRoBinds(BaseTestCase):
if six.PY3: if six.PY3:
logs = logs.decode('utf-8') logs = logs.decode('utf-8')
self.assertIn(filename, logs) self.assertIn(filename, logs)
# FIXME: format changes in API version >= 1.20
inspect_data = self.client.inspect_container(container_id) inspect_data = self.client.inspect_container(container_id)
self.assertIn('Volumes', inspect_data) self.assertIn('Volumes', inspect_data)
self.assertIn(mount_dest, inspect_data['Volumes']) self.assertIn(mount_dest, inspect_data['Volumes'])
...@@ -273,6 +287,7 @@ class TestCreateContainerWithRoBinds(BaseTestCase): ...@@ -273,6 +287,7 @@ class TestCreateContainerWithRoBinds(BaseTestCase):
self.assertFalse(inspect_data['VolumesRW'][mount_dest]) self.assertFalse(inspect_data['VolumesRW'][mount_dest])
@requires_api_version('1.20')
class CreateContainerWithGroupAddTest(BaseTestCase): class CreateContainerWithGroupAddTest(BaseTestCase):
def test_group_id_ints(self): def test_group_id_ints(self):
container = self.client.create_container( container = self.client.create_container(
...@@ -1370,9 +1385,63 @@ class TestImportFromURL(ImportTestCase): ...@@ -1370,9 +1385,63 @@ class TestImportFromURL(ImportTestCase):
################# #################
# BUILDER TESTS # # VOLUMES TESTS #
################# #################
@requires_api_version('1.21')
class TestVolumes(BaseTestCase):
def test_create_volume(self):
name = 'perfectcherryblossom'
self.tmp_volumes.append(name)
result = self.client.create_volume(name)
self.assertIn('Name', result)
self.assertEqual(result['Name'], name)
self.assertIn('Driver', result)
self.assertEqual(result['Driver'], 'local')
def test_create_volume_invalid_driver(self):
driver_name = 'invalid.driver'
with pytest.raises(docker.errors.NotFound):
self.client.create_volume('perfectcherryblossom', driver_name)
def test_list_volumes(self):
name = 'imperishablenight'
self.tmp_volumes.append(name)
volume_info = self.client.create_volume(name)
result = self.client.volumes()
self.assertIn('Volumes', result)
volumes = result['Volumes']
self.assertIn(volume_info, volumes)
def test_inspect_volume(self):
name = 'embodimentofscarletdevil'
self.tmp_volumes.append(name)
volume_info = self.client.create_volume(name)
result = self.client.inspect_volume(name)
self.assertEqual(volume_info, result)
def test_inspect_nonexistent_volume(self):
name = 'embodimentofscarletdevil'
with pytest.raises(docker.errors.NotFound):
self.client.inspect_volume(name)
def test_remove_volume(self):
name = 'shootthebullet'
self.tmp_volumes.append(name)
self.client.create_volume(name)
result = self.client.remove_volume(name)
self.assertTrue(result)
def test_remove_nonexistent_volume(self):
name = 'shootthebullet'
with pytest.raises(docker.errors.NotFound):
self.client.remove_volume(name)
#################
# BUILDER TESTS #
#################
class TestBuild(BaseTestCase): class TestBuild(BaseTestCase):
def runTest(self): def runTest(self):
......
This diff is collapsed.
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