Skip to content

Commit c262dfe

Browse files
committed
APIClient implementation of plugin methods
Signed-off-by: Joffrey F <[email protected]>
1 parent 11c2d29 commit c262dfe

File tree

4 files changed

+337
-1
lines changed

4 files changed

+337
-1
lines changed

docker/api/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .exec_api import ExecApiMixin
1515
from .image import ImageApiMixin
1616
from .network import NetworkApiMixin
17+
from .plugin import PluginApiMixin
1718
from .service import ServiceApiMixin
1819
from .swarm import SwarmApiMixin
1920
from .volume import VolumeApiMixin
@@ -46,6 +47,7 @@ class APIClient(
4647
ExecApiMixin,
4748
ImageApiMixin,
4849
NetworkApiMixin,
50+
PluginApiMixin,
4951
ServiceApiMixin,
5052
SwarmApiMixin,
5153
VolumeApiMixin):
@@ -225,10 +227,12 @@ def _post_json(self, url, data, **kwargs):
225227
# Go <1.1 can't unserialize null to a string
226228
# so we do this disgusting thing here.
227229
data2 = {}
228-
if data is not None:
230+
if data is not None and isinstance(data, dict):
229231
for k, v in six.iteritems(data):
230232
if v is not None:
231233
data2[k] = v
234+
else:
235+
data2 = data
232236

233237
if 'headers' not in kwargs:
234238
kwargs['headers'] = {}

docker/api/plugin.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import six
2+
3+
from .. import auth, utils
4+
5+
6+
class PluginApiMixin(object):
7+
@utils.minimum_version('1.25')
8+
@utils.check_resource
9+
def configure_plugin(self, name, options):
10+
"""
11+
Configure a plugin.
12+
13+
Args:
14+
name (string): The name of the plugin. The ``:latest`` tag is
15+
optional, and is the default if omitted.
16+
options (dict): A key-value mapping of options
17+
18+
Returns:
19+
``True`` if successful
20+
"""
21+
url = self._url('/plugins/{0}/set', name)
22+
data = options
23+
if isinstance(data, dict):
24+
data = ['{0}={1}'.format(k, v) for k, v in six.iteritems(data)]
25+
res = self._post_json(url, data=data)
26+
self._raise_for_status(res)
27+
return True
28+
29+
def create_plugin(self, name, rootfs, manifest):
30+
"""
31+
Create a new plugin.
32+
33+
Args:
34+
name (string): The name of the plugin. The ``:latest`` tag is
35+
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
38+
39+
Returns:
40+
``True`` if successful
41+
"""
42+
# FIXME: Needs implementation
43+
raise NotImplementedError()
44+
45+
@utils.minimum_version('1.25')
46+
def disable_plugin(self, name):
47+
"""
48+
Disable an installed plugin.
49+
50+
Args:
51+
name (string): The name of the plugin. The ``:latest`` tag is
52+
optional, and is the default if omitted.
53+
54+
Returns:
55+
``True`` if successful
56+
"""
57+
url = self._url('/plugins/{0}/disable', name)
58+
res = self._post(url)
59+
self._raise_for_status(res)
60+
return True
61+
62+
@utils.minimum_version('1.25')
63+
def enable_plugin(self, name, timeout=0):
64+
"""
65+
Enable an installed plugin.
66+
67+
Args:
68+
name (string): The name of the plugin. The ``:latest`` tag is
69+
optional, and is the default if omitted.
70+
timeout (int): Operation timeout (in seconds). Default: 0
71+
72+
Returns:
73+
``True`` if successful
74+
"""
75+
url = self._url('/plugins/{0}/enable', name)
76+
params = {'timeout': timeout}
77+
res = self._post(url, params=params)
78+
self._raise_for_status(res)
79+
return True
80+
81+
@utils.minimum_version('1.25')
82+
def inspect_plugin(self, name):
83+
"""
84+
Retrieve plugin metadata.
85+
86+
Args:
87+
name (string): The name of the plugin. The ``:latest`` tag is
88+
optional, and is the default if omitted.
89+
90+
Returns:
91+
A dict containing plugin info
92+
"""
93+
url = self._url('/plugins/{0}/json', name)
94+
return self._result(self._get(url), True)
95+
96+
@utils.minimum_version('1.25')
97+
def pull_plugin(self, remote, privileges, name=None):
98+
"""
99+
Pull and install a plugin. After the plugin is installed, it can be
100+
enabled using :py:meth:`~enable_plugin`.
101+
102+
Args:
103+
remote (string): Remote reference for the plugin to install.
104+
The ``:latest`` tag is optional, and is the default if
105+
omitted.
106+
privileges (list): A list of privileges the user consents to
107+
grant to the plugin. Can be retrieved using
108+
:py:meth:`~plugin_privileges`.
109+
name (string): Local name for the pulled plugin. The
110+
``:latest`` tag is optional, and is the default if omitted.
111+
112+
Returns:
113+
An iterable object streaming the decoded API logs
114+
"""
115+
url = self._url('/plugins/pull')
116+
params = {
117+
'remote': remote,
118+
}
119+
if name:
120+
params['name'] = name
121+
122+
headers = {}
123+
registry, repo_name = auth.resolve_repository_name(remote)
124+
header = auth.get_config_header(self, registry)
125+
if header:
126+
headers['X-Registry-Auth'] = header
127+
response = self._post_json(
128+
url, params=params, headers=headers, data=privileges,
129+
stream=True
130+
)
131+
self._raise_for_status(response)
132+
return self._stream_helper(response, decode=True)
133+
134+
@utils.minimum_version('1.25')
135+
def plugins(self):
136+
"""
137+
Retrieve a list of installed plugins.
138+
139+
Returns:
140+
A list of dicts, one per plugin
141+
"""
142+
url = self._url('/plugins')
143+
return self._result(self._get(url), True)
144+
145+
@utils.minimum_version('1.25')
146+
def plugin_privileges(self, name):
147+
"""
148+
Retrieve list of privileges to be granted to a plugin.
149+
150+
Args:
151+
name (string): Name of the remote plugin to examine. The
152+
``:latest`` tag is optional, and is the default if omitted.
153+
154+
Returns:
155+
A list of dictionaries representing the plugin's
156+
permissions
157+
158+
"""
159+
params = {
160+
'remote': name,
161+
}
162+
163+
url = self._url('/plugins/privileges')
164+
return self._result(self._get(url, params=params), True)
165+
166+
@utils.minimum_version('1.25')
167+
@utils.check_resource
168+
def push_plugin(self, name):
169+
"""
170+
Push a plugin to the registry.
171+
172+
Args:
173+
name (string): Name of the plugin to upload. The ``:latest``
174+
tag is optional, and is the default if omitted.
175+
176+
Returns:
177+
``True`` if successful
178+
"""
179+
url = self._url('/plugins/{0}/pull', name)
180+
181+
headers = {}
182+
registry, repo_name = auth.resolve_repository_name(name)
183+
header = auth.get_config_header(self, registry)
184+
if header:
185+
headers['X-Registry-Auth'] = header
186+
res = self._post(url, headers=headers)
187+
self._raise_for_status(res)
188+
return self._stream_helper(res, decode=True)
189+
190+
@utils.minimum_version('1.25')
191+
def remove_plugin(self, name, force=False):
192+
"""
193+
Remove an installed plugin.
194+
195+
Args:
196+
name (string): Name of the plugin to remove. The ``:latest``
197+
tag is optional, and is the default if omitted.
198+
force (bool): Disable the plugin before removing. This may
199+
result in issues if the plugin is in use by a container.
200+
201+
Returns:
202+
``True`` if successful
203+
"""
204+
url = self._url('/plugins/{0}', name)
205+
res = self._delete(url, params={'force': force})
206+
self._raise_for_status(res)
207+
return True

docs/api.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ Services
8787
:members:
8888
:undoc-members:
8989

90+
Plugins
91+
-------
92+
93+
.. py:module:: docker.api.plugin
94+
95+
.. rst-class:: hide-signature
96+
.. autoclass:: PluginApiMixin
97+
:members:
98+
:undoc-members:
99+
100+
90101
The Docker daemon
91102
-----------------
92103

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import docker
2+
import pytest
3+
4+
from .base import BaseAPIIntegrationTest, TEST_API_VERSION
5+
6+
SSHFS = 'vieux/sshfs:latest'
7+
8+
9+
class PluginTest(BaseAPIIntegrationTest):
10+
@classmethod
11+
def teardown_class(cls):
12+
c = docker.APIClient(
13+
version=TEST_API_VERSION, timeout=60,
14+
**docker.utils.kwargs_from_env()
15+
)
16+
try:
17+
c.remove_plugin(SSHFS, force=True)
18+
except docker.errors.APIError:
19+
pass
20+
21+
def teardown_method(self, method):
22+
try:
23+
self.client.disable_plugin(SSHFS)
24+
except docker.errors.APIError:
25+
pass
26+
27+
def ensure_plugin_installed(self, plugin_name):
28+
try:
29+
return self.client.inspect_plugin(plugin_name)
30+
except docker.errors.NotFound:
31+
prv = self.client.plugin_privileges(plugin_name)
32+
for d in self.client.pull_plugin(plugin_name, prv):
33+
pass
34+
return self.client.inspect_plugin(plugin_name)
35+
36+
def test_enable_plugin(self):
37+
pl_data = self.ensure_plugin_installed(SSHFS)
38+
assert pl_data['Enabled'] is False
39+
assert self.client.enable_plugin(SSHFS)
40+
pl_data = self.client.inspect_plugin(SSHFS)
41+
assert pl_data['Enabled'] is True
42+
with pytest.raises(docker.errors.APIError):
43+
self.client.enable_plugin(SSHFS)
44+
45+
def test_disable_plugin(self):
46+
pl_data = self.ensure_plugin_installed(SSHFS)
47+
assert pl_data['Enabled'] is False
48+
assert self.client.enable_plugin(SSHFS)
49+
pl_data = self.client.inspect_plugin(SSHFS)
50+
assert pl_data['Enabled'] is True
51+
self.client.disable_plugin(SSHFS)
52+
pl_data = self.client.inspect_plugin(SSHFS)
53+
assert pl_data['Enabled'] is False
54+
with pytest.raises(docker.errors.APIError):
55+
self.client.disable_plugin(SSHFS)
56+
57+
def test_inspect_plugin(self):
58+
self.ensure_plugin_installed(SSHFS)
59+
data = self.client.inspect_plugin(SSHFS)
60+
assert 'Config' in data
61+
assert 'Name' in data
62+
assert data['Name'] == SSHFS
63+
64+
def test_plugin_privileges(self):
65+
prv = self.client.plugin_privileges(SSHFS)
66+
assert isinstance(prv, list)
67+
for item in prv:
68+
assert 'Name' in item
69+
assert 'Value' in item
70+
assert 'Description' in item
71+
72+
def test_list_plugins(self):
73+
self.ensure_plugin_installed(SSHFS)
74+
data = self.client.plugins()
75+
assert len(data) > 0
76+
plugin = [p for p in data if p['Name'] == SSHFS][0]
77+
assert 'Config' in plugin
78+
79+
def test_configure_plugin(self):
80+
pl_data = self.ensure_plugin_installed(SSHFS)
81+
assert pl_data['Enabled'] is False
82+
self.client.configure_plugin(SSHFS, {
83+
'DEBUG': '1'
84+
})
85+
pl_data = self.client.inspect_plugin(SSHFS)
86+
assert 'Env' in pl_data['Settings']
87+
assert 'DEBUG=1' in pl_data['Settings']['Env']
88+
89+
self.client.configure_plugin(SSHFS, ['DEBUG=0'])
90+
pl_data = self.client.inspect_plugin(SSHFS)
91+
assert 'DEBUG=0' in pl_data['Settings']['Env']
92+
93+
def test_remove_plugin(self):
94+
pl_data = self.ensure_plugin_installed(SSHFS)
95+
assert pl_data['Enabled'] is False
96+
assert self.client.remove_plugin(SSHFS) is True
97+
98+
def test_force_remove_plugin(self):
99+
self.ensure_plugin_installed(SSHFS)
100+
self.client.enable_plugin(SSHFS)
101+
assert self.client.inspect_plugin(SSHFS)['Enabled'] is True
102+
assert self.client.remove_plugin(SSHFS, force=True) is True
103+
104+
def test_install_plugin(self):
105+
try:
106+
self.client.remove_plugin(SSHFS, force=True)
107+
except docker.errors.APIError:
108+
pass
109+
110+
prv = self.client.plugin_privileges(SSHFS)
111+
logs = [d for d in self.client.pull_plugin(SSHFS, prv)]
112+
assert filter(lambda x: x['status'] == 'Download complete', logs)
113+
assert self.client.inspect_plugin(SSHFS)
114+
assert self.client.enable_plugin(SSHFS)

0 commit comments

Comments
 (0)