From 46eb23b4f3fb0bf03a06b64e2872703659b0c006 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 10 Sep 2015 17:22:57 -0700 Subject: [PATCH 1/6] Basic volume API implementation. Signed-off-by: Joffrey F --- docker/api/__init__.py | 1 + docker/api/volume.py | 48 ++++++++++++++++++++++++++++++++++++++++++ docker/client.py | 3 ++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 docker/api/volume.py diff --git a/docker/api/__init__.py b/docker/api/__init__.py index 836f07e3..3fb81203 100644 --- a/docker/api/__init__.py +++ b/docker/api/__init__.py @@ -4,3 +4,4 @@ from .container import ContainerApiMixin from .daemon import DaemonApiMixin from .exec_api import ExecApiMixin from .image import ImageApiMixin +from .volume import VolumeApiMixin \ No newline at end of file diff --git a/docker/api/volume.py b/docker/api/volume.py new file mode 100644 index 00000000..4e005b82 --- /dev/null +++ b/docker/api/volume.py @@ -0,0 +1,48 @@ +import functools + +from .. import errors +from ..utils import utils + + +def check_api_version(f): + @functools.wraps(f) + def wrapped(self, *args, **kwargs): + if utils.compare_version('1.21', self._version) < 0: + raise errors.InvalidVersion( + 'The volume API is not available for API version < 1.21' + ) + return f(self, *args, **kwargs) + return wrapped + + +class VolumeApiMixin(object): + @check_api_version + 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) + + @check_api_version + def create_volume(self, name, driver=None, driver_opts=None): + url = self._url('/volumes') + if 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(url, data=data), True) + + @check_api_version + def inspect_volume(self, name): + url = self._url('/volumes/{0}', name) + return self._result(self._get(url), True) + + @check_api_version + def remove_volume(self, name): + url = self._url('/volumes/{0}', name) + return self._result(self._delete(url), True) diff --git a/docker/client.py b/docker/client.py index 2eb859cf..58d0496f 100644 --- a/docker/client.py +++ b/docker/client.py @@ -38,7 +38,8 @@ class Client( api.ContainerApiMixin, api.DaemonApiMixin, api.ExecApiMixin, - api.ImageApiMixin): + api.ImageApiMixin, + api.VolumeApiMixin): def __init__(self, base_url=None, version=None, timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): super(Client, self).__init__() From 05267f63d355ae04ea7bd4797847dead8c2f71c3 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 10 Sep 2015 18:37:05 -0700 Subject: [PATCH 2/6] Modify unit tests suite to allow mock routes to optionally be method-aware Added mock API routes for volumes API Add unit tests and integration tests for volume API Signed-off-by: Joffrey F --- docker/api/volume.py | 8 +- tests/fake_api.py | 50 +++++++- tests/integration_test.py | 70 ++++++++++- tests/test.py | 258 ++++++++++++++++++++++++++++++++------ 4 files changed, 339 insertions(+), 47 deletions(-) diff --git a/docker/api/volume.py b/docker/api/volume.py index 4e005b82..708cbe52 100644 --- a/docker/api/volume.py +++ b/docker/api/volume.py @@ -27,7 +27,7 @@ class VolumeApiMixin(object): @check_api_version def create_volume(self, name, driver=None, driver_opts=None): url = self._url('/volumes') - if not isinstance(driver_opts, dict): + if driver_opts is not None and not isinstance(driver_opts, dict): raise TypeError('driver_opts must be a dictionary') data = { @@ -35,7 +35,7 @@ class VolumeApiMixin(object): 'Driver': driver, 'DriverOpts': driver_opts, } - return self._result(self._post(url, data=data), True) + return self._result(self._post_json(url, data=data), True) @check_api_version def inspect_volume(self, name): @@ -45,4 +45,6 @@ class VolumeApiMixin(object): @check_api_version def remove_volume(self, name): url = self._url('/volumes/{0}', name) - return self._result(self._delete(url), True) + resp = self._delete(url) + self._raise_for_status(resp) + return True diff --git a/tests/fake_api.py b/tests/fake_api.py index d6639885..5a89deea 100644 --- a/tests/fake_api.py +++ b/tests/fake_api.py @@ -13,8 +13,9 @@ # limitations under the License. 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_IMAGE_ID = 'e9aa60c60128' @@ -26,6 +27,7 @@ FAKE_TAG_NAME = 'tag' FAKE_FILE_NAME = 'file' FAKE_URL = 'myurl' FAKE_PATH = '/path' +FAKE_VOLUME_NAME = 'perfectcherryblossom' # Each method is prefixed with HTTP method (get, post...) # for clarity and readability @@ -380,6 +382,38 @@ def get_fake_stats(): response = fake_stat.OBJ 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 prefix = 'http+docker://localunixsocket' fake_responses = { @@ -463,5 +497,17 @@ fake_responses = { '{1}/{0}/build'.format(CURRENT_VERSION, prefix): post_fake_build_container, '{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, } diff --git a/tests/integration_test.py b/tests/integration_test.py index a9ace223..8b15b141 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -77,6 +77,7 @@ class BaseTestCase(unittest.TestCase): tmp_imgs = [] tmp_containers = [] tmp_folders = [] + tmp_volumes = [] def setUp(self): if six.PY2: @@ -101,6 +102,13 @@ class BaseTestCase(unittest.TestCase): pass for folder in self.tmp_folders: shutil.rmtree(folder) + + for volume in self.tmp_volumes: + try: + self.client.remove_volume(volume) + except docker.errors.APIError: + pass + self.client.close() ######################### @@ -223,6 +231,8 @@ class TestCreateContainerWithBinds(BaseTestCase): if six.PY3: logs = logs.decode('utf-8') self.assertIn(filename, logs) + + # FIXME: format changes in API version >= 1.20 inspect_data = self.client.inspect_container(container_id) self.assertIn('Volumes', inspect_data) self.assertIn(mount_dest, inspect_data['Volumes']) @@ -265,6 +275,8 @@ class TestCreateContainerWithRoBinds(BaseTestCase): if six.PY3: logs = logs.decode('utf-8') self.assertIn(filename, logs) + + # FIXME: format changes in API version >= 1.20 inspect_data = self.client.inspect_container(container_id) self.assertIn('Volumes', inspect_data) self.assertIn(mount_dest, inspect_data['Volumes']) @@ -1370,9 +1382,65 @@ class TestImportFromURL(ImportTestCase): ################# -# BUILDER TESTS # +# VOLUMES TESTS # ################# +@pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION +) < 0, reason="Volume API available for version >=1.21") +class TestVolumes(BaseTestCase): + def test_create_volume(self): + name = 'perfectcherryblossom' + result = self.client.create_volume(name) + self.tmp_volumes.append(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' + volume_info = self.client.create_volume(name) + self.tmp_volumes.append(name) + result = self.client.volumes() + self.assertIn('Volumes', result) + volumes = result['Volumes'] + self.assertIn(volume_info, volumes) + + def test_inspect_volume(self): + name = 'embodimentofscarletdevil' + volume_info = self.client.create_volume(name) + self.tmp_volumes.append(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.client.create_volume(name) + self.tmp_volumes.append(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): def runTest(self): diff --git a/tests/test.py b/tests/test.py index 52a35038..16ade241 100644 --- a/tests/test.py +++ b/tests/test.py @@ -74,12 +74,36 @@ def fake_inspect_container_tty(self, container): return fake_inspect_container(self, container, tty=True) -def fake_resp(url, data=None, **kwargs): - status_code, content = fake_api.fake_responses[url]() +def fake_resp(method, url, *args, **kwargs): + key = None + if url in fake_api.fake_responses: + key = url + elif (url, method) in fake_api.fake_responses: + key = (url, method) + if not key: + raise Exception('{0} {1}'.format(method, url)) + status_code, content = fake_api.fake_responses[key]() return response(status_code=status_code, content=content) fake_request = mock.Mock(side_effect=fake_resp) + + +def fake_get(self, url, *args, **kwargs): + return fake_request('GET', url, *args, **kwargs) + + +def fake_post(self, url, *args, **kwargs): + return fake_request('POST', url, *args, **kwargs) + + +def fake_put(self, url, *args, **kwargs): + return fake_request('PUT', url, *args, **kwargs) + + +def fake_delete(self, url, *args, **kwargs): + return fake_request('DELETE', url, *args, **kwargs) + url_prefix = 'http+docker://localunixsocket/v{0}/'.format( docker.constants.DEFAULT_DOCKER_API_VERSION) @@ -109,8 +133,8 @@ class Cleanup(object): self._cleanups.append((function, args, kwargs)) -@mock.patch.multiple('docker.Client', get=fake_request, post=fake_request, - put=fake_request, delete=fake_request) +@mock.patch.multiple('docker.Client', get=fake_get, post=fake_post, + put=fake_put, delete=fake_delete) class DockerClientTest(Cleanup, base.BaseTestCase): def setUp(self): self.client = docker.Client() @@ -173,6 +197,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.version() fake_request.assert_called_with( + 'GET', url_prefix + 'version', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -191,6 +216,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.info() fake_request.assert_called_with( + 'GET', url_prefix + 'info', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -199,6 +225,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.search('busybox') fake_request.assert_called_with( + 'GET', url_prefix + 'images/search', params={'term': 'busybox'}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -213,6 +240,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.events() fake_request.assert_called_with( + 'GET', url_prefix + 'events', params={'since': None, 'until': None, 'filters': None}, stream=True @@ -227,6 +255,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.events(since=since, until=until) fake_request.assert_called_with( + 'GET', url_prefix + 'events', params={ 'since': ts - 10, @@ -244,6 +273,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): expected_filters = docker.utils.convert_filters(filters) fake_request.assert_called_with( + 'GET', url_prefix + 'events', params={ 'since': None, @@ -261,6 +291,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.images(all=True) fake_request.assert_called_with( + 'GET', url_prefix + 'images/json', params={'filter': None, 'only_ids': 0, 'all': 1}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -270,6 +301,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.images(all=True, quiet=True) fake_request.assert_called_with( + 'GET', url_prefix + 'images/json', params={'filter': None, 'only_ids': 1, 'all': 1}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -279,6 +311,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.images(quiet=True) fake_request.assert_called_with( + 'GET', url_prefix + 'images/json', params={'filter': None, 'only_ids': 1, 'all': 0}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -288,6 +321,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.images(filters={'dangling': True}) fake_request.assert_called_with( + 'GET', url_prefix + 'images/json', params={'filter': None, 'only_ids': 0, 'all': 0, 'filters': '{"dangling": ["true"]}'}, @@ -298,6 +332,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.containers(all=True) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/json', params={ 'all': 1, @@ -318,7 +353,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.create_container('busybox', 'true') args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -337,7 +372,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): volumes=[mount_dest]) args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -358,7 +393,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): volumes=mount_dest) args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -377,7 +412,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ports=[1111, (2222, 'udp'), (3333,)]) args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -400,7 +435,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): entrypoint='cowsay entry') args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -419,7 +454,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): cpu_shares=5) args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -438,7 +473,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): cpuset='0,1') args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -461,7 +496,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') data = json.loads(args[1]['data']) self.assertIn('HostConfig', data) @@ -473,7 +508,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): working_dir='/root') args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -491,7 +526,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.create_container('busybox', 'true', stdin_open=True) args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -515,7 +550,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): return args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data'])['VolumesFrom'], ','.join(vol_names)) self.assertEqual(args[1]['headers'], @@ -533,7 +568,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): name='marisa-kirisame') args = fake_request.call_args - self.assertEqual(args[0][0], + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), json.loads(''' @@ -619,7 +654,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], + args[0][1], url_prefix + 'containers/3cc2351ab11b/start' ) self.assertEqual(json.loads(args[1]['data']), {}) @@ -659,7 +694,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], + args[0][1], url_prefix + 'containers/create' ) expected_payload = self.base_create_payload() @@ -686,7 +721,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['LxcConf'] = [ @@ -715,7 +750,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() @@ -742,7 +777,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() @@ -769,7 +804,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() @@ -807,7 +842,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() @@ -840,7 +875,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') data = json.loads(args[1]['data']) port_bindings = data['HostConfig']['PortBindings'] self.assertTrue('1111/tcp' in port_bindings) @@ -899,7 +934,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], url_prefix + 'containers/create' + args[0][1], url_prefix + 'containers/create' ) expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() @@ -924,7 +959,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['Links'] = [ @@ -946,7 +981,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['Links'] = ['path:alias'] @@ -966,7 +1001,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['Privileged'] = True args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -1069,7 +1104,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], + args[0][1], url_prefix + 'containers/3cc2351ab11b/start' ) self.assertEqual(json.loads(args[1]['data']), {}) @@ -1091,7 +1126,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() @@ -1114,7 +1149,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['CapAdd'] = ['MKNOD'] @@ -1133,7 +1168,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['CapDrop'] = ['MKNOD'] @@ -1155,7 +1190,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') expected_payload = self.base_create_payload() expected_payload['HostConfig'] = self.client.create_host_config() expected_payload['HostConfig']['Devices'] = [ @@ -1189,7 +1224,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data'])['Labels'], labels_dict) self.assertEqual( args[1]['headers'], {'Content-Type': 'application/json'} @@ -1214,7 +1249,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(args[0][1], url_prefix + 'containers/create') self.assertEqual(json.loads(args[1]['data'])['Labels'], labels_dict) self.assertEqual( args[1]['headers'], {'Content-Type': 'application/json'} @@ -1238,8 +1273,9 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + - 'containers/create') + self.assertEqual( + args[0][1], url_prefix + 'containers/create' + ) expected_payload = self.base_create_payload() expected_payload['VolumeDriver'] = 'foodriver' expected_payload['HostConfig'] = self.client.create_host_config() @@ -1260,6 +1296,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/resize', params={'h': 15, 'w': 120}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1272,6 +1309,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/rename', params={'name': 'foobar'}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1281,6 +1319,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.wait(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/wait', timeout=None ) @@ -1289,6 +1328,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.wait({'Id': fake_api.FAKE_CONTAINER_ID}) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/wait', timeout=None ) @@ -1328,6 +1368,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): logs = self.client.logs(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, @@ -1346,6 +1387,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID}) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, @@ -1364,6 +1406,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, @@ -1378,6 +1421,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): tail=10) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1, 'tail': 10}, @@ -1396,6 +1440,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertTrue(m.called) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, @@ -1407,6 +1452,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.diff(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/changes', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -1415,6 +1461,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.diff({'Id': fake_api.FAKE_CONTAINER_ID}) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/changes', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -1423,6 +1470,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.port({'Id': fake_api.FAKE_CONTAINER_ID}, 1111) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/json', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -1433,6 +1481,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.stop(fake_api.FAKE_CONTAINER_ID, timeout=timeout) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/stop', params={'t': timeout}, timeout=(DEFAULT_TIMEOUT_SECONDS + timeout) @@ -1445,6 +1494,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): timeout=timeout) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/stop', params={'t': timeout}, timeout=(DEFAULT_TIMEOUT_SECONDS + timeout) @@ -1455,6 +1505,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( + 'POST', args[0][0], url_prefix + 'containers/{0}/exec'.format( fake_api.FAKE_CONTAINER_ID ) @@ -1481,7 +1532,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], url_prefix + 'exec/{0}/start'.format( + args[0][1], url_prefix + 'exec/{0}/start'.format( fake_api.FAKE_EXEC_ID ) ) @@ -1501,7 +1552,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], url_prefix + 'exec/{0}/json'.format( + args[0][1], url_prefix + 'exec/{0}/json'.format( fake_api.FAKE_EXEC_ID ) ) @@ -1510,6 +1561,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.exec_resize(fake_api.FAKE_EXEC_ID, height=20, width=60) fake_request.assert_called_with( + 'POST', url_prefix + 'exec/{0}/resize'.format(fake_api.FAKE_EXEC_ID), params={'h': 20, 'w': 60}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1519,6 +1571,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.pause(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/pause', timeout=(DEFAULT_TIMEOUT_SECONDS) ) @@ -1527,6 +1580,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.unpause(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/unpause', timeout=(DEFAULT_TIMEOUT_SECONDS) ) @@ -1535,6 +1589,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.kill(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/kill', params={}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1544,6 +1599,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.kill({'Id': fake_api.FAKE_CONTAINER_ID}) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/kill', params={}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1553,6 +1609,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.kill(fake_api.FAKE_CONTAINER_ID, signal=signal.SIGTERM) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/kill', params={'signal': signal.SIGTERM}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1562,6 +1619,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.restart(fake_api.FAKE_CONTAINER_ID, timeout=2) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/restart', params={'t': 2}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1571,6 +1629,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.restart({'Id': fake_api.FAKE_CONTAINER_ID}, timeout=2) fake_request.assert_called_with( + 'POST', url_prefix + 'containers/3cc2351ab11b/restart', params={'t': 2}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1580,6 +1639,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.remove_container(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'DELETE', url_prefix + 'containers/3cc2351ab11b', params={'v': False, 'link': False, 'force': False}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1589,6 +1649,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.remove_container({'Id': fake_api.FAKE_CONTAINER_ID}) fake_request.assert_called_with( + 'DELETE', url_prefix + 'containers/3cc2351ab11b', params={'v': False, 'link': False, 'force': False}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1598,6 +1659,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.remove_container(fake_api.FAKE_CONTAINER_ID, link=True) fake_request.assert_called_with( + 'DELETE', url_prefix + 'containers/3cc2351ab11b', params={'v': False, 'link': True, 'force': False}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1607,6 +1669,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.export(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/export', stream=True, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1616,6 +1679,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.export({'Id': fake_api.FAKE_CONTAINER_ID}) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/export', stream=True, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1625,6 +1689,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.inspect_container(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/json', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -1642,6 +1707,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.stats(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'GET', url_prefix + 'containers/3cc2351ab11b/stats', timeout=60, stream=True @@ -1656,7 +1722,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], + args[0][1], url_prefix + 'images/create' ) self.assertEqual( @@ -1670,7 +1736,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): args = fake_request.call_args self.assertEqual( - args[0][0], + args[0][1], url_prefix + 'images/create' ) self.assertEqual( @@ -1683,6 +1749,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.commit(fake_api.FAKE_CONTAINER_ID) fake_request.assert_called_with( + 'POST', url_prefix + 'commit', data='{}', headers={'Content-Type': 'application/json'}, @@ -1700,6 +1767,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.remove_image(fake_api.FAKE_IMAGE_ID) fake_request.assert_called_with( + 'DELETE', url_prefix + 'images/e9aa60c60128', params={'force': False, 'noprune': False}, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1709,6 +1777,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.history(fake_api.FAKE_IMAGE_NAME) fake_request.assert_called_with( + 'GET', url_prefix + 'images/test_image/history', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -1721,6 +1790,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'images/create', params={ 'repo': fake_api.FAKE_REPO_NAME, @@ -1741,6 +1811,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'images/create', params={ 'repo': fake_api.FAKE_REPO_NAME, @@ -1762,6 +1833,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'images/create', params={ 'repo': fake_api.FAKE_REPO_NAME, @@ -1776,6 +1848,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.inspect_image(fake_api.FAKE_IMAGE_NAME) fake_request.assert_called_with( + 'GET', url_prefix + 'images/test_image/json', timeout=DEFAULT_TIMEOUT_SECONDS ) @@ -1800,6 +1873,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): return fake_request.assert_called_with( + 'POST', url_prefix + 'images/test_image/insert', params={ 'url': fake_api.FAKE_URL, @@ -1814,6 +1888,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.push(fake_api.FAKE_IMAGE_NAME) fake_request.assert_called_with( + 'POST', url_prefix + 'images/test_image/push', params={ 'tag': None @@ -1832,6 +1907,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'images/test_image/push', params={ 'tag': fake_api.FAKE_TAG_NAME, @@ -1848,6 +1924,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.push(fake_api.FAKE_IMAGE_NAME, stream=True) fake_request.assert_called_with( + 'POST', url_prefix + 'images/test_image/push', params={ 'tag': None @@ -1862,6 +1939,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.tag(fake_api.FAKE_IMAGE_ID, fake_api.FAKE_REPO_NAME) fake_request.assert_called_with( + 'POST', url_prefix + 'images/e9aa60c60128/tag', params={ 'tag': None, @@ -1879,6 +1957,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): ) fake_request.assert_called_with( + 'POST', url_prefix + 'images/e9aa60c60128/tag', params={ 'tag': 'tag', @@ -1893,6 +1972,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): fake_api.FAKE_IMAGE_ID, fake_api.FAKE_REPO_NAME, force=True) fake_request.assert_called_with( + 'POST', url_prefix + 'images/e9aa60c60128/tag', params={ 'tag': None, @@ -1906,6 +1986,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.get_image(fake_api.FAKE_IMAGE_ID) fake_request.assert_called_with( + 'GET', url_prefix + 'images/e9aa60c60128/get', stream=True, timeout=DEFAULT_TIMEOUT_SECONDS @@ -1915,6 +1996,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.client.load_image('Byte Stream....') fake_request.assert_called_with( + 'POST', url_prefix + 'images/load', data='Byte Stream....', timeout=DEFAULT_TIMEOUT_SECONDS @@ -2021,6 +2103,100 @@ class DockerClientTest(Cleanup, base.BaseTestCase): }) ) + ################### + # VOLUMES TESTS # + ################### + + @pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION + ) < 0, reason="API version too low") + def test_list_volumes(self): + volumes = self.client.volumes() + self.assertIn('Volumes', volumes) + self.assertEqual(len(volumes['Volumes']), 2) + args = fake_request.call_args + + self.assertEqual(args[0][0], 'GET') + self.assertEqual(args[0][1], url_prefix + 'volumes') + + @pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION + ) < 0, reason="API version too low") + def test_create_volume(self): + name = 'perfectcherryblossom' + result = self.client.create_volume(name) + self.assertIn('Name', result) + self.assertEqual(result['Name'], name) + self.assertIn('Driver', result) + self.assertEqual(result['Driver'], 'local') + args = fake_request.call_args + + self.assertEqual(args[0][0], 'POST') + self.assertEqual(args[0][1], url_prefix + 'volumes') + self.assertEqual(args[1]['data'], { + 'Name': name, 'Driver': None, 'DriverOpts': None + }) + + @pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION + ) < 0, reason="API version too low") + def test_create_volume_with_driver(self): + name = 'perfectcherryblossom' + driver_name = 'sshfs' + self.client.create_volume(name, driver=driver_name) + args = fake_request.call_args + + self.assertEqual(args[0][0], 'POST') + self.assertEqual(args[0][1], url_prefix + 'volumes') + self.assertIn('Driver', args[1]['data']) + self.assertEqual(args[1]['data']['Driver'], driver_name) + + @pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION + ) < 0, reason="API version too low") + def test_create_volume_invalid_opts_type(self): + with pytest.raises(TypeError): + self.client.create_volume( + 'perfectcherryblossom', driver_opts='hello=world' + ) + + with pytest.raises(TypeError): + self.client.create_volume( + 'perfectcherryblossom', driver_opts=['hello=world'] + ) + + with pytest.raises(TypeError): + self.client.create_volume( + 'perfectcherryblossom', driver_opts='' + ) + + @pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION + ) < 0, reason="API version too low") + def test_inspect_volume(self): + name = 'perfectcherryblossom' + result = self.client.inspect_volume(name) + self.assertIn('Name', result) + self.assertEqual(result['Name'], name) + self.assertIn('Driver', result) + self.assertEqual(result['Driver'], 'local') + args = fake_request.call_args + + self.assertEqual(args[0][0], 'GET') + self.assertEqual(args[0][1], '{0}volumes/{1}'.format(url_prefix, name)) + + @pytest.mark.skipif(docker.utils.compare_version( + '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION + ) < 0, reason="API version too low") + def test_remove_volume(self): + name = 'perfectcherryblossom' + result = self.client.remove_volume(name) + self.assertIsNone(result) + args = fake_request.call_args + + self.assertEqual(args[0][0], 'DELETE') + self.assertEqual(args[0][1], '{0}volumes/{1}'.format(url_prefix, name)) + ####################### # PY SPECIFIC TESTS # ####################### From ba6df5a2c09da68fe594e07dc2a62cabbe6667d7 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 14 Sep 2015 20:31:24 +0200 Subject: [PATCH 3/6] Add documentation for volume API methods Signed-off-by: Joffrey F --- docs/api.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 607cd472..4add429b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -255,6 +255,29 @@ The utility can be used as follows: 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 Inspect changes on a container's filesystem @@ -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 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 a container or send a signal to a container @@ -695,6 +733,16 @@ Remove an image. Similar to the `docker rmi` command. * force (bool): Force removal of the image * 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 an exception. + ## rename Rename a container. Similar to the `docker rename` command. @@ -851,6 +899,7 @@ Unpauses all processes within a container. * container (str): The container to unpause ## version + Nearly identical to the `docker version` command. **Returns** (dict): The server version information @@ -870,6 +919,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 Identical to the `docker wait` command. Block until a container stops, then @@ -893,6 +959,5 @@ exception will be raised. TODO: * load_image -* resize --> From acd5e634ffa62428562c945878dcb67edd6ecf61 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 15 Sep 2015 00:00:06 +0200 Subject: [PATCH 4/6] Generic skip decorator for low API version accessible to all tests Add simpler version comparison functions Add decorator to enforce minimum version in API methods Fix utils imports Add minimum_version decorators on API methods that needed it GroupAdd test requires API version >= 1.20 Signed-off-by: Joffrey F --- docker/api/__init__.py | 2 +- docker/api/build.py | 4 +-- docker/api/container.py | 54 +++++++++++++++++--------------------- docker/api/exec_api.py | 18 +++++-------- docker/api/image.py | 14 +++++----- docker/api/volume.py | 24 ++++------------- docker/utils/__init__.py | 5 ++-- docker/utils/decorators.py | 16 +++++++++++ docker/utils/utils.py | 8 ++++++ tests/base.py | 12 +++++++++ tests/integration_test.py | 16 +++++------ tests/test.py | 24 +++++------------ 12 files changed, 98 insertions(+), 99 deletions(-) diff --git a/docker/api/__init__.py b/docker/api/__init__.py index 3fb81203..79796349 100644 --- a/docker/api/__init__.py +++ b/docker/api/__init__.py @@ -4,4 +4,4 @@ from .container import ContainerApiMixin from .daemon import DaemonApiMixin from .exec_api import ExecApiMixin from .image import ImageApiMixin -from .volume import VolumeApiMixin \ No newline at end of file +from .volume import VolumeApiMixin diff --git a/docker/api/build.py b/docker/api/build.py index ce6fd465..b303ba64 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -4,8 +4,8 @@ import re from .. import constants from .. import errors -from ..auth import auth -from ..utils import utils +from .. import auth +from .. import utils log = logging.getLogger(__name__) diff --git a/docker/api/container.py b/docker/api/container.py index f90e8a18..e7ddd733 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -2,11 +2,11 @@ import six import warnings from .. import errors -from ..utils import utils, check_resource +from .. import utils class ContainerApiMixin(object): - @check_resource + @utils.check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): params = { @@ -20,7 +20,7 @@ class ContainerApiMixin(object): return self._get_result(container, stream, response) - @check_resource + @utils.check_resource def attach_socket(self, container, params=None, ws=False): if params is None: params = { @@ -36,7 +36,7 @@ class ContainerApiMixin(object): return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) - @check_resource + @utils.check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): params = { @@ -73,7 +73,7 @@ class ContainerApiMixin(object): x['Id'] = x['Id'][:12] return res - @check_resource + @utils.check_resource def copy(self, container, resource): res = self._post_json( self._url("/containers/{0}/copy".format(container)), @@ -131,13 +131,13 @@ class ContainerApiMixin(object): kwargs['version'] = self._version return utils.create_host_config(*args, **kwargs) - @check_resource + @utils.check_resource def diff(self, container): return self._result( self._get(self._url("/containers/{0}/changes", container)), True ) - @check_resource + @utils.check_resource def export(self, container): res = self._get( self._url("/containers/{0}/export", container), stream=True @@ -145,13 +145,13 @@ class ContainerApiMixin(object): self._raise_for_status(res) return res.raw - @check_resource + @utils.check_resource def inspect_container(self, container): return self._result( self._get(self._url("/containers/{0}/json", container)), True ) - @check_resource + @utils.check_resource def kill(self, container, signal=None): url = self._url("/containers/{0}/kill", container) params = {} @@ -161,7 +161,7 @@ class ContainerApiMixin(object): self._raise_for_status(res) - @check_resource + @utils.check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): if utils.compare_version('1.11', self._version) >= 0: @@ -185,13 +185,13 @@ class ContainerApiMixin(object): logs=True ) - @check_resource + @utils.check_resource def pause(self, container): url = self._url('/containers/{0}/pause', container) res = self._post(url) self._raise_for_status(res) - @check_resource + @utils.check_resource def port(self, container, private_port): res = self._get(self._url("/containers/{0}/json", container)) self._raise_for_status(res) @@ -211,7 +211,7 @@ class ContainerApiMixin(object): return h_ports - @check_resource + @utils.check_resource def remove_container(self, container, v=False, link=False, force=False): params = {'v': v, 'link': link, 'force': force} res = self._delete( @@ -219,32 +219,29 @@ class ContainerApiMixin(object): ) self._raise_for_status(res) - @check_resource + @utils.minimum_version('1.17') + @utils.check_resource 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) params = {'name': name} res = self._post(url, params=params) self._raise_for_status(res) - @check_resource + @utils.check_resource def resize(self, container, height, width): params = {'h': height, 'w': width} url = self._url("/containers/{0}/resize", container) res = self._post(url, params=params) self._raise_for_status(res) - @check_resource + @utils.check_resource def restart(self, container, timeout=10): params = {'t': timeout} url = self._url("/containers/{0}/restart", container) res = self._post(url, params=params) self._raise_for_status(res) - @check_resource + @utils.check_resource def start(self, container, binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=None, links=None, privileged=None, dns=None, dns_search=None, volumes_from=None, network_mode=None, @@ -312,16 +309,13 @@ class ContainerApiMixin(object): res = self._post_json(url, data=start_config) self._raise_for_status(res) - @check_resource + @utils.minimum_version('1.17') + @utils.check_resource 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) return self._stream_helper(self._get(url, stream=True), decode=decode) - @check_resource + @utils.check_resource def stop(self, container, timeout=10): params = {'t': timeout} url = self._url("/containers/{0}/stop", container) @@ -330,18 +324,18 @@ class ContainerApiMixin(object): timeout=(timeout + (self.timeout or 0))) self._raise_for_status(res) - @check_resource + @utils.check_resource def top(self, container): u = self._url("/containers/{0}/top", container) return self._result(self._get(u), True) - @check_resource + @utils.check_resource def unpause(self, container): url = self._url('/containers/{0}/unpause', container) res = self._post(url) self._raise_for_status(res) - @check_resource + @utils.check_resource def wait(self, container, timeout=None): url = self._url("/containers/{0}/wait", container) res = self._post(url, timeout=timeout) diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py index 87eb143d..c66b9dd0 100644 --- a/docker/api/exec_api.py +++ b/docker/api/exec_api.py @@ -3,15 +3,14 @@ import shlex import six from .. import errors -from ..utils import utils, check_resource +from .. import utils 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, 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: raise errors.InvalidVersion( 'Privileged exec is not supported in API < 1.19' @@ -38,19 +37,15 @@ class ExecApiMixin(object): res = self._post_json(url, data=data) return self._result(res, True) + @utils.minimum_version('1.16') 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): exec_id = exec_id.get('Id') res = self._get(self._url("/exec/{0}/json", exec_id)) return self._result(res, True) + @utils.minimum_version('1.15') 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): exec_id = exec_id.get('Id') @@ -59,9 +54,8 @@ class ExecApiMixin(object): res = self._post(url, params=params) self._raise_for_status(res) + @utils.minimum_version('1.15') 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): exec_id = exec_id.get('Id') diff --git a/docker/api/image.py b/docker/api/image.py index c6939ef4..f891e210 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -4,7 +4,7 @@ import warnings from ..auth import auth from ..constants import INSECURE_REGISTRY_DEPRECATION_WARNING -from ..utils import utils, check_resource +from .. import utils from .. import errors log = logging.getLogger(__name__) @@ -12,13 +12,13 @@ log = logging.getLogger(__name__) class ImageApiMixin(object): - @check_resource + @utils.check_resource def get_image(self, image): res = self._get(self._url("/images/{0}/get", image), stream=True) self._raise_for_status(res) return res.raw - @check_resource + @utils.check_resource def history(self, image): res = self._get(self._url("/images/{0}/history", image)) return self._result(res, True) @@ -124,7 +124,7 @@ class ImageApiMixin(object): return self._result( self._post(u, data=None, params=params)) - @check_resource + @utils.check_resource def insert(self, image, url, path): if utils.compare_version('1.12', self._version) >= 0: raise errors.DeprecatedMethod( @@ -137,7 +137,7 @@ class ImageApiMixin(object): } return self._result(self._post(api_url, params=params)) - @check_resource + @utils.check_resource def inspect_image(self, image): return self._result( self._get(self._url("/images/{0}/json", image)), True @@ -246,7 +246,7 @@ class ImageApiMixin(object): return self._result(response) - @check_resource + @utils.check_resource def remove_image(self, image, force=False, noprune=False): params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/{0}", image), params=params) @@ -258,7 +258,7 @@ class ImageApiMixin(object): True ) - @check_resource + @utils.check_resource def tag(self, image, repository, tag=None, force=False): params = { 'tag': tag, diff --git a/docker/api/volume.py b/docker/api/volume.py index 708cbe52..e9e71273 100644 --- a/docker/api/volume.py +++ b/docker/api/volume.py @@ -1,22 +1,8 @@ -import functools - -from .. import errors -from ..utils import utils - - -def check_api_version(f): - @functools.wraps(f) - def wrapped(self, *args, **kwargs): - if utils.compare_version('1.21', self._version) < 0: - raise errors.InvalidVersion( - 'The volume API is not available for API version < 1.21' - ) - return f(self, *args, **kwargs) - return wrapped +from .. import utils class VolumeApiMixin(object): - @check_api_version + @utils.minimum_version('1.21') def volumes(self, filters=None): params = { 'filter': utils.convert_filters(filters) if filters else None @@ -24,7 +10,7 @@ class VolumeApiMixin(object): url = self._url('/volumes') return self._result(self._get(url, params=params), True) - @check_api_version + @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): @@ -37,12 +23,12 @@ class VolumeApiMixin(object): } return self._result(self._post_json(url, data=data), True) - @check_api_version + @utils.minimum_version('1.21') def inspect_volume(self, name): url = self._url('/volumes/{0}', name) return self._result(self._get(url), True) - @check_api_version + @utils.minimum_version('1.21') def remove_volume(self, name): url = self._url('/volumes/{0}', name) resp = self._delete(url) diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index deab01df..fd0ef5c0 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -2,8 +2,9 @@ from .utils import ( compare_version, convert_port_bindings, convert_volume_binds, mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host, 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 from .types import Ulimit, LogConfig # flake8: noqa -from .decorators import check_resource #flake8: noqa +from .decorators import check_resource, minimum_version #flake8: noqa diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py index 3c42fe4b..7d3b01a7 100644 --- a/docker/utils/decorators.py +++ b/docker/utils/decorators.py @@ -1,6 +1,7 @@ import functools from .. import errors +from . import utils def check_resource(f): @@ -19,3 +20,18 @@ def check_resource(f): ) return f(self, resource_id, *args, **kwargs) 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 diff --git a/docker/utils/utils.py b/docker/utils/utils.py index a2513419..46b35160 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -164,6 +164,14 @@ def compare_version(v1, v2): 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): warnings.warn( 'The `ping_registry` method is deprecated and will be removed.', diff --git a/tests/base.py b/tests/base.py index 1d5a370d..51b23003 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,11 +1,23 @@ import sys import unittest +import pytest import six +import docker + class BaseTestCase(unittest.TestCase): def assertIn(self, object, collection): if six.PY2 and sys.version_info[1] <= 6: return self.assertTrue(object in 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) + ) diff --git a/tests/integration_test.py b/tests/integration_test.py index 8b15b141..fc497975 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -27,17 +27,18 @@ import time import unittest import warnings -import docker -from docker.utils import kwargs_from_env +import pytest import six - from six.moves import BaseHTTPServer from six.moves import socketserver -from .test import Cleanup +import docker 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 # export; history; insert; port; push; tag; get; load; stats @@ -285,6 +286,7 @@ class TestCreateContainerWithRoBinds(BaseTestCase): self.assertFalse(inspect_data['VolumesRW'][mount_dest]) +@requires_api_version('1.20') class CreateContainerWithGroupAddTest(BaseTestCase): def test_group_id_ints(self): container = self.client.create_container( @@ -1385,9 +1387,7 @@ class TestImportFromURL(ImportTestCase): # VOLUMES TESTS # ################# -@pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION -) < 0, reason="Volume API available for version >=1.21") +@requires_api_version('1.21') class TestVolumes(BaseTestCase): def test_create_volume(self): name = 'perfectcherryblossom' diff --git a/tests/test.py b/tests/test.py index 16ade241..8ed5c14d 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2107,9 +2107,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): # VOLUMES TESTS # ################### - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_list_volumes(self): volumes = self.client.volumes() self.assertIn('Volumes', volumes) @@ -2119,9 +2117,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertEqual(args[0][0], 'GET') self.assertEqual(args[0][1], url_prefix + 'volumes') - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_create_volume(self): name = 'perfectcherryblossom' result = self.client.create_volume(name) @@ -2137,9 +2133,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): 'Name': name, 'Driver': None, 'DriverOpts': None }) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_create_volume_with_driver(self): name = 'perfectcherryblossom' driver_name = 'sshfs' @@ -2151,9 +2145,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertIn('Driver', args[1]['data']) self.assertEqual(args[1]['data']['Driver'], driver_name) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_create_volume_invalid_opts_type(self): with pytest.raises(TypeError): self.client.create_volume( @@ -2170,9 +2162,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): 'perfectcherryblossom', driver_opts='' ) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_inspect_volume(self): name = 'perfectcherryblossom' result = self.client.inspect_volume(name) @@ -2185,9 +2175,7 @@ class DockerClientTest(Cleanup, base.BaseTestCase): self.assertEqual(args[0][0], 'GET') self.assertEqual(args[0][1], '{0}volumes/{1}'.format(url_prefix, name)) - @pytest.mark.skipif(docker.utils.compare_version( - '1.21', docker.constants.DEFAULT_DOCKER_API_VERSION - ) < 0, reason="API version too low") + @base.requires_api_version('1.21') def test_remove_volume(self): name = 'perfectcherryblossom' result = self.client.remove_volume(name) From ea9e13ff2b08212e3a7e7ab501698d0f2789fe74 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 16 Sep 2015 12:25:34 -0700 Subject: [PATCH 5/6] Detail which exception is thrown by remove_volume Signed-off-by: Joffrey F --- docs/api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 4add429b..4e5d54cb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -741,7 +741,8 @@ Remove a volume. Similar to the `docker volume rm` command. * name (str): The volume's name -**Returns** (bool): True on successful removal. Failure will raise an exception. +**Returns** (bool): True on successful removal. Failure will raise a +`docker.errors.APIError` exception. ## rename From 8d3263163547fd9121ac13b01815b4c1740c9947 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 16 Sep 2015 12:29:06 -0700 Subject: [PATCH 6/6] Improve volume cleanup logic Signed-off-by: Joffrey F --- tests/integration_test.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index fc497975..b8101c1d 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -88,6 +88,7 @@ class BaseTestCase(unittest.TestCase): self.tmp_imgs = [] self.tmp_containers = [] self.tmp_folders = [] + self.tmp_volumes = [] def tearDown(self): for img in self.tmp_imgs: @@ -1391,8 +1392,8 @@ class TestImportFromURL(ImportTestCase): class TestVolumes(BaseTestCase): def test_create_volume(self): name = 'perfectcherryblossom' - result = self.client.create_volume(name) self.tmp_volumes.append(name) + result = self.client.create_volume(name) self.assertIn('Name', result) self.assertEqual(result['Name'], name) self.assertIn('Driver', result) @@ -1406,8 +1407,8 @@ class TestVolumes(BaseTestCase): def test_list_volumes(self): name = 'imperishablenight' - volume_info = self.client.create_volume(name) self.tmp_volumes.append(name) + volume_info = self.client.create_volume(name) result = self.client.volumes() self.assertIn('Volumes', result) volumes = result['Volumes'] @@ -1415,8 +1416,8 @@ class TestVolumes(BaseTestCase): def test_inspect_volume(self): name = 'embodimentofscarletdevil' - volume_info = self.client.create_volume(name) self.tmp_volumes.append(name) + volume_info = self.client.create_volume(name) result = self.client.inspect_volume(name) self.assertEqual(volume_info, result) @@ -1427,8 +1428,8 @@ class TestVolumes(BaseTestCase): def test_remove_volume(self): name = 'shootthebullet' - self.client.create_volume(name) self.tmp_volumes.append(name) + self.client.create_volume(name) result = self.client.remove_volume(name) self.assertTrue(result)