Skip to content

Commit 4b636c3

Browse files
committed
Add create_plugin implementation
Signed-off-by: Joffrey F <[email protected]>
1 parent 4e55628 commit 4b636c3

File tree

9 files changed

+89
-21
lines changed

9 files changed

+89
-21
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ include README.rst
55
include LICENSE
66
recursive-include tests *.py
77
recursive-include tests/unit/testdata *
8+
recursive-include tests/integration/testdata *

docker/api/plugin.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,28 @@ def configure_plugin(self, name, options):
2626
self._raise_for_status(res)
2727
return True
2828

29-
def create_plugin(self, name, rootfs, manifest):
29+
@utils.minimum_version('1.25')
30+
def create_plugin(self, name, plugin_data_dir, gzip=False):
3031
"""
3132
Create a new plugin.
3233
3334
Args:
3435
name (string): The name of the plugin. The ``:latest`` tag is
3536
optional, and is the default if omitted.
36-
rootfs (string): Path to the plugin's ``rootfs``
37-
manifest (string): Path to the plugin's manifest file
37+
plugin_data_dir (string): Path to the plugin data directory.
38+
Plugin data directory must contain the ``config.json``
39+
manifest file and the ``rootfs`` directory.
40+
gzip (bool): Compress the context using gzip. Default: False
3841
3942
Returns:
4043
``True`` if successful
4144
"""
42-
# FIXME: Needs implementation
43-
raise NotImplementedError()
45+
url = self._url('/plugins/create')
46+
47+
with utils.create_archive(root=plugin_data_dir, gzip=gzip) as archv:
48+
res = self._post(url, params={'name': name}, data=archv)
49+
self._raise_for_status(res)
50+
return True
4451

4552
@utils.minimum_version('1.25')
4653
def disable_plugin(self, name):

docker/models/plugins.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,22 @@ def remove(self, force=False):
100100
class PluginCollection(Collection):
101101
model = Plugin
102102

103-
def create(self, name, rootfs, manifest):
103+
def create(self, name, plugin_data_dir, gzip=False):
104104
"""
105105
Create a new plugin.
106106
107107
Args:
108108
name (string): The name of the plugin. The ``:latest`` tag is
109109
optional, and is the default if omitted.
110-
rootfs (string): Path to the plugin's ``rootfs``
111-
manifest (string): Path to the plugin's manifest file
110+
plugin_data_dir (string): Path to the plugin data directory.
111+
Plugin data directory must contain the ``config.json``
112+
manifest file and the ``rootfs`` directory.
113+
gzip (bool): Compress the context using gzip. Default: False
112114
113115
Returns:
114116
(:py:class:`Plugin`): The newly created plugin.
115117
"""
116-
self.client.api.create_plugin(name, rootfs, manifest)
118+
self.client.api.create_plugin(name, plugin_data_dir, gzip)
117119
return self.get(name)
118120

119121
def get(self, name):

docker/utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
create_host_config, parse_bytes, ping_registry, parse_env_file, version_lt,
77
version_gte, decode_json_header, split_command, create_ipam_config,
88
create_ipam_pool, parse_devices, normalize_links, convert_service_networks,
9-
format_environment,
9+
format_environment, create_archive
1010
)
1111

1212
from .decorators import check_resource, minimum_version, update_headers

docker/utils/utils.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,35 @@ def decode_json_header(header):
8080

8181

8282
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
83-
if not fileobj:
84-
fileobj = tempfile.NamedTemporaryFile()
85-
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
86-
8783
root = os.path.abspath(path)
8884
exclude = exclude or []
8985

90-
for path in sorted(exclude_paths(root, exclude, dockerfile=dockerfile)):
91-
i = t.gettarinfo(os.path.join(root, path), arcname=path)
86+
return create_archive(
87+
files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile)),
88+
root=root, fileobj=fileobj, gzip=gzip
89+
)
90+
91+
92+
def build_file_list(root):
93+
files = []
94+
for dirname, dirnames, fnames in os.walk(root):
95+
for filename in fnames + dirnames:
96+
longpath = os.path.join(dirname, filename)
97+
files.append(
98+
longpath.replace(root, '', 1).lstrip('/')
99+
)
92100

101+
return files
102+
103+
104+
def create_archive(root, files=None, fileobj=None, gzip=False):
105+
if not fileobj:
106+
fileobj = tempfile.NamedTemporaryFile()
107+
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
108+
if files is None:
109+
files = build_file_list(root)
110+
for path in files:
111+
i = t.gettarinfo(os.path.join(root, path), arcname=path)
93112
if i is None:
94113
# This happens when we encounter a socket file. We can safely
95114
# ignore it and proceed.
@@ -102,13 +121,11 @@ def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
102121

103122
try:
104123
# We open the file object in binary mode for Windows support.
105-
f = open(os.path.join(root, path), 'rb')
124+
with open(os.path.join(root, path), 'rb') as f:
125+
t.addfile(i, f)
106126
except IOError:
107127
# When we encounter a directory the file object is set to None.
108-
f = None
109-
110-
t.addfile(i, f)
111-
128+
t.addfile(i, None)
112129
t.close()
113130
fileobj.seek(0)
114131
return fileobj

tests/integration/api_plugin_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import os
2+
13
import docker
24
import pytest
35

46
from .base import BaseAPIIntegrationTest, TEST_API_VERSION
7+
from ..helpers import requires_api_version
58

69
SSHFS = 'vieux/sshfs:latest'
710

811

12+
@requires_api_version('1.25')
913
class PluginTest(BaseAPIIntegrationTest):
1014
@classmethod
1115
def teardown_class(cls):
@@ -24,6 +28,12 @@ def teardown_method(self, method):
2428
except docker.errors.APIError:
2529
pass
2630

31+
for p in self.tmp_plugins:
32+
try:
33+
self.client.remove_plugin(p, force=True)
34+
except docker.errors.APIError:
35+
pass
36+
2737
def ensure_plugin_installed(self, plugin_name):
2838
try:
2939
return self.client.inspect_plugin(plugin_name)
@@ -112,3 +122,14 @@ def test_install_plugin(self):
112122
assert filter(lambda x: x['status'] == 'Download complete', logs)
113123
assert self.client.inspect_plugin(SSHFS)
114124
assert self.client.enable_plugin(SSHFS)
125+
126+
def test_create_plugin(self):
127+
plugin_data_dir = os.path.join(
128+
os.path.dirname(__file__), 'testdata/dummy-plugin'
129+
)
130+
assert self.client.create_plugin(
131+
'docker-sdk-py/dummy', plugin_data_dir
132+
)
133+
self.tmp_plugins.append('docker-sdk-py/dummy')
134+
data = self.client.inspect_plugin('docker-sdk-py/dummy')
135+
assert data['Config']['Entrypoint'] == ['/dummy']

tests/integration/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def setUp(self):
2727
self.tmp_folders = []
2828
self.tmp_volumes = []
2929
self.tmp_networks = []
30+
self.tmp_plugins = []
3031

3132
def tearDown(self):
3233
client = docker.from_env(version=TEST_API_VERSION)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"description": "Dummy test plugin for docker python SDK",
3+
"documentation": "https://github.com/docker/docker-py",
4+
"entrypoint": ["/dummy"],
5+
"network": {
6+
"type": "host"
7+
},
8+
"interface" : {
9+
"types": ["docker.volumedriver/1.0"],
10+
"socket": "dummy.sock"
11+
},
12+
"env": [
13+
{
14+
"name":"DEBUG",
15+
"settable":["value"],
16+
"value":"0"
17+
}
18+
]
19+
}

tests/integration/testdata/dummy-plugin/rootfs/dummy/file.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)