From e1ad3186ef86c0a91f1051da96049facae7a0325 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 8 Feb 2017 18:05:24 -0800 Subject: [PATCH] Add create_plugin implementation Signed-off-by: Joffrey F --- MANIFEST.in | 1 + docker/api/plugin.py | 17 +++++--- docker/models/plugins.py | 10 +++-- docker/utils/__init__.py | 2 +- docker/utils/utils.py | 39 +++++++++++++------ tests/integration/api_plugin_test.py | 21 ++++++++++ tests/integration/base.py | 1 + .../testdata/dummy-plugin/config.json | 19 +++++++++ .../dummy-plugin/rootfs/dummy/file.txt | 0 9 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 tests/integration/testdata/dummy-plugin/config.json create mode 100644 tests/integration/testdata/dummy-plugin/rootfs/dummy/file.txt diff --git a/MANIFEST.in b/MANIFEST.in index ee6cdbbd..41b3fa9f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,3 +5,4 @@ include README.rst include LICENSE recursive-include tests *.py recursive-include tests/unit/testdata * +recursive-include tests/integration/testdata * diff --git a/docker/api/plugin.py b/docker/api/plugin.py index 0a800349..772d2633 100644 --- a/docker/api/plugin.py +++ b/docker/api/plugin.py @@ -26,21 +26,28 @@ class PluginApiMixin(object): self._raise_for_status(res) return True - def create_plugin(self, name, rootfs, manifest): + @utils.minimum_version('1.25') + def create_plugin(self, name, plugin_data_dir, gzip=False): """ Create a new plugin. Args: name (string): The name of the plugin. The ``:latest`` tag is optional, and is the default if omitted. - rootfs (string): Path to the plugin's ``rootfs`` - manifest (string): Path to the plugin's manifest file + plugin_data_dir (string): Path to the plugin data directory. + Plugin data directory must contain the ``config.json`` + manifest file and the ``rootfs`` directory. + gzip (bool): Compress the context using gzip. Default: False Returns: ``True`` if successful """ - # FIXME: Needs implementation - raise NotImplementedError() + url = self._url('/plugins/create') + + with utils.create_archive(root=plugin_data_dir, gzip=gzip) as archv: + res = self._post(url, params={'name': name}, data=archv) + self._raise_for_status(res) + return True @utils.minimum_version('1.25') def disable_plugin(self, name): diff --git a/docker/models/plugins.py b/docker/models/plugins.py index 04c8bde5..8b6ede95 100644 --- a/docker/models/plugins.py +++ b/docker/models/plugins.py @@ -100,20 +100,22 @@ class Plugin(Model): class PluginCollection(Collection): model = Plugin - def create(self, name, rootfs, manifest): + def create(self, name, plugin_data_dir, gzip=False): """ Create a new plugin. Args: name (string): The name of the plugin. The ``:latest`` tag is optional, and is the default if omitted. - rootfs (string): Path to the plugin's ``rootfs`` - manifest (string): Path to the plugin's manifest file + plugin_data_dir (string): Path to the plugin data directory. + Plugin data directory must contain the ``config.json`` + manifest file and the ``rootfs`` directory. + gzip (bool): Compress the context using gzip. Default: False Returns: (:py:class:`Plugin`): The newly created plugin. """ - self.client.api.create_plugin(name, rootfs, manifest) + self.client.api.create_plugin(name, plugin_data_dir, gzip) return self.get(name) def get(self, name): diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 747743cc..8f8eb270 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -6,7 +6,7 @@ from .utils import ( create_host_config, parse_bytes, ping_registry, parse_env_file, version_lt, version_gte, decode_json_header, split_command, create_ipam_config, create_ipam_pool, parse_devices, normalize_links, convert_service_networks, - format_environment + format_environment, create_archive ) from .decorators import check_resource, minimum_version, update_headers diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 8026c4df..01eb16c3 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -80,16 +80,35 @@ def decode_json_header(header): def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False): - if not fileobj: - fileobj = tempfile.NamedTemporaryFile() - t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj) - root = os.path.abspath(path) exclude = exclude or [] - for path in sorted(exclude_paths(root, exclude, dockerfile=dockerfile)): - i = t.gettarinfo(os.path.join(root, path), arcname=path) + return create_archive( + files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile)), + root=root, fileobj=fileobj, gzip=gzip + ) + +def build_file_list(root): + files = [] + for dirname, dirnames, fnames in os.walk(root): + for filename in fnames + dirnames: + longpath = os.path.join(dirname, filename) + files.append( + longpath.replace(root, '', 1).lstrip('/') + ) + + return files + + +def create_archive(root, files=None, fileobj=None, gzip=False): + if not fileobj: + fileobj = tempfile.NamedTemporaryFile() + t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj) + if files is None: + files = build_file_list(root) + for path in files: + i = t.gettarinfo(os.path.join(root, path), arcname=path) if i is None: # This happens when we encounter a socket file. We can safely # ignore it and proceed. @@ -102,13 +121,11 @@ def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False): try: # We open the file object in binary mode for Windows support. - f = open(os.path.join(root, path), 'rb') + with open(os.path.join(root, path), 'rb') as f: + t.addfile(i, f) except IOError: # When we encounter a directory the file object is set to None. - f = None - - t.addfile(i, f) - + t.addfile(i, None) t.close() fileobj.seek(0) return fileobj diff --git a/tests/integration/api_plugin_test.py b/tests/integration/api_plugin_test.py index 29e1fa18..e90a1088 100644 --- a/tests/integration/api_plugin_test.py +++ b/tests/integration/api_plugin_test.py @@ -1,11 +1,15 @@ +import os + import docker import pytest from .base import BaseAPIIntegrationTest, TEST_API_VERSION +from ..helpers import requires_api_version SSHFS = 'vieux/sshfs:latest' +@requires_api_version('1.25') class PluginTest(BaseAPIIntegrationTest): @classmethod def teardown_class(cls): @@ -24,6 +28,12 @@ class PluginTest(BaseAPIIntegrationTest): except docker.errors.APIError: pass + for p in self.tmp_plugins: + try: + self.client.remove_plugin(p, force=True) + except docker.errors.APIError: + pass + def ensure_plugin_installed(self, plugin_name): try: return self.client.inspect_plugin(plugin_name) @@ -112,3 +122,14 @@ class PluginTest(BaseAPIIntegrationTest): assert filter(lambda x: x['status'] == 'Download complete', logs) assert self.client.inspect_plugin(SSHFS) assert self.client.enable_plugin(SSHFS) + + def test_create_plugin(self): + plugin_data_dir = os.path.join( + os.path.dirname(__file__), 'testdata/dummy-plugin' + ) + assert self.client.create_plugin( + 'docker-sdk-py/dummy', plugin_data_dir + ) + self.tmp_plugins.append('docker-sdk-py/dummy') + data = self.client.inspect_plugin('docker-sdk-py/dummy') + assert data['Config']['Entrypoint'] == ['/dummy'] diff --git a/tests/integration/base.py b/tests/integration/base.py index f0f5a910..8b75acc9 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -27,6 +27,7 @@ class BaseIntegrationTest(unittest.TestCase): self.tmp_folders = [] self.tmp_volumes = [] self.tmp_networks = [] + self.tmp_plugins = [] def tearDown(self): client = docker.from_env(version=TEST_API_VERSION) diff --git a/tests/integration/testdata/dummy-plugin/config.json b/tests/integration/testdata/dummy-plugin/config.json new file mode 100644 index 00000000..53b4e7aa --- /dev/null +++ b/tests/integration/testdata/dummy-plugin/config.json @@ -0,0 +1,19 @@ +{ + "description": "Dummy test plugin for docker python SDK", + "documentation": "https://github.com/docker/docker-py", + "entrypoint": ["/dummy"], + "network": { + "type": "host" + }, + "interface" : { + "types": ["docker.volumedriver/1.0"], + "socket": "dummy.sock" + }, + "env": [ + { + "name":"DEBUG", + "settable":["value"], + "value":"0" + } + ] +} diff --git a/tests/integration/testdata/dummy-plugin/rootfs/dummy/file.txt b/tests/integration/testdata/dummy-plugin/rootfs/dummy/file.txt new file mode 100644 index 00000000..e69de29b