mirror of https://github.com/docker/docker-py.git
commit
35f37a0936
|
|
@ -5,3 +5,4 @@ include README.rst
|
||||||
include LICENSE
|
include LICENSE
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
recursive-include tests/unit/testdata *
|
recursive-include tests/unit/testdata *
|
||||||
|
recursive-include tests/integration/testdata *
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from .daemon import DaemonApiMixin
|
||||||
from .exec_api import ExecApiMixin
|
from .exec_api import ExecApiMixin
|
||||||
from .image import ImageApiMixin
|
from .image import ImageApiMixin
|
||||||
from .network import NetworkApiMixin
|
from .network import NetworkApiMixin
|
||||||
|
from .plugin import PluginApiMixin
|
||||||
from .service import ServiceApiMixin
|
from .service import ServiceApiMixin
|
||||||
from .swarm import SwarmApiMixin
|
from .swarm import SwarmApiMixin
|
||||||
from .volume import VolumeApiMixin
|
from .volume import VolumeApiMixin
|
||||||
|
|
@ -46,6 +47,7 @@ class APIClient(
|
||||||
ExecApiMixin,
|
ExecApiMixin,
|
||||||
ImageApiMixin,
|
ImageApiMixin,
|
||||||
NetworkApiMixin,
|
NetworkApiMixin,
|
||||||
|
PluginApiMixin,
|
||||||
ServiceApiMixin,
|
ServiceApiMixin,
|
||||||
SwarmApiMixin,
|
SwarmApiMixin,
|
||||||
VolumeApiMixin):
|
VolumeApiMixin):
|
||||||
|
|
@ -225,10 +227,12 @@ class APIClient(
|
||||||
# Go <1.1 can't unserialize null to a string
|
# Go <1.1 can't unserialize null to a string
|
||||||
# so we do this disgusting thing here.
|
# so we do this disgusting thing here.
|
||||||
data2 = {}
|
data2 = {}
|
||||||
if data is not None:
|
if data is not None and isinstance(data, dict):
|
||||||
for k, v in six.iteritems(data):
|
for k, v in six.iteritems(data):
|
||||||
if v is not None:
|
if v is not None:
|
||||||
data2[k] = v
|
data2[k] = v
|
||||||
|
elif data is not None:
|
||||||
|
data2 = data
|
||||||
|
|
||||||
if 'headers' not in kwargs:
|
if 'headers' not in kwargs:
|
||||||
kwargs['headers'] = {}
|
kwargs['headers'] = {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .. import auth, utils
|
||||||
|
|
||||||
|
|
||||||
|
class PluginApiMixin(object):
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
@utils.check_resource
|
||||||
|
def configure_plugin(self, name, options):
|
||||||
|
"""
|
||||||
|
Configure a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): The name of the plugin. The ``:latest`` tag is
|
||||||
|
optional, and is the default if omitted.
|
||||||
|
options (dict): A key-value mapping of options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``True`` if successful
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/{0}/set', name)
|
||||||
|
data = options
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = ['{0}={1}'.format(k, v) for k, v in six.iteritems(data)]
|
||||||
|
res = self._post_json(url, data=data)
|
||||||
|
self._raise_for_status(res)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@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.
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Disable an installed plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): The name of the plugin. The ``:latest`` tag is
|
||||||
|
optional, and is the default if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``True`` if successful
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/{0}/disable', name)
|
||||||
|
res = self._post(url)
|
||||||
|
self._raise_for_status(res)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
def enable_plugin(self, name, timeout=0):
|
||||||
|
"""
|
||||||
|
Enable an installed plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): The name of the plugin. The ``:latest`` tag is
|
||||||
|
optional, and is the default if omitted.
|
||||||
|
timeout (int): Operation timeout (in seconds). Default: 0
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``True`` if successful
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/{0}/enable', name)
|
||||||
|
params = {'timeout': timeout}
|
||||||
|
res = self._post(url, params=params)
|
||||||
|
self._raise_for_status(res)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
def inspect_plugin(self, name):
|
||||||
|
"""
|
||||||
|
Retrieve plugin metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): The name of the plugin. The ``:latest`` tag is
|
||||||
|
optional, and is the default if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict containing plugin info
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/{0}/json', name)
|
||||||
|
return self._result(self._get(url), True)
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
def pull_plugin(self, remote, privileges, name=None):
|
||||||
|
"""
|
||||||
|
Pull and install a plugin. After the plugin is installed, it can be
|
||||||
|
enabled using :py:meth:`~enable_plugin`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote (string): Remote reference for the plugin to install.
|
||||||
|
The ``:latest`` tag is optional, and is the default if
|
||||||
|
omitted.
|
||||||
|
privileges (list): A list of privileges the user consents to
|
||||||
|
grant to the plugin. Can be retrieved using
|
||||||
|
:py:meth:`~plugin_privileges`.
|
||||||
|
name (string): Local name for the pulled plugin. The
|
||||||
|
``:latest`` tag is optional, and is the default if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An iterable object streaming the decoded API logs
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/pull')
|
||||||
|
params = {
|
||||||
|
'remote': remote,
|
||||||
|
}
|
||||||
|
if name:
|
||||||
|
params['name'] = name
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
registry, repo_name = auth.resolve_repository_name(remote)
|
||||||
|
header = auth.get_config_header(self, registry)
|
||||||
|
if header:
|
||||||
|
headers['X-Registry-Auth'] = header
|
||||||
|
response = self._post_json(
|
||||||
|
url, params=params, headers=headers, data=privileges,
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
self._raise_for_status(response)
|
||||||
|
return self._stream_helper(response, decode=True)
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
def plugins(self):
|
||||||
|
"""
|
||||||
|
Retrieve a list of installed plugins.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of dicts, one per plugin
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins')
|
||||||
|
return self._result(self._get(url), True)
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
def plugin_privileges(self, name):
|
||||||
|
"""
|
||||||
|
Retrieve list of privileges to be granted to a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): Name of the remote plugin to examine. The
|
||||||
|
``:latest`` tag is optional, and is the default if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of dictionaries representing the plugin's
|
||||||
|
permissions
|
||||||
|
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
'remote': name,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = self._url('/plugins/privileges')
|
||||||
|
return self._result(self._get(url, params=params), True)
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
@utils.check_resource
|
||||||
|
def push_plugin(self, name):
|
||||||
|
"""
|
||||||
|
Push a plugin to the registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): Name of the plugin to upload. The ``:latest``
|
||||||
|
tag is optional, and is the default if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``True`` if successful
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/{0}/pull', name)
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
registry, repo_name = auth.resolve_repository_name(name)
|
||||||
|
header = auth.get_config_header(self, registry)
|
||||||
|
if header:
|
||||||
|
headers['X-Registry-Auth'] = header
|
||||||
|
res = self._post(url, headers=headers)
|
||||||
|
self._raise_for_status(res)
|
||||||
|
return self._stream_helper(res, decode=True)
|
||||||
|
|
||||||
|
@utils.minimum_version('1.25')
|
||||||
|
def remove_plugin(self, name, force=False):
|
||||||
|
"""
|
||||||
|
Remove an installed plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (string): Name of the plugin to remove. The ``:latest``
|
||||||
|
tag is optional, and is the default if omitted.
|
||||||
|
force (bool): Disable the plugin before removing. This may
|
||||||
|
result in issues if the plugin is in use by a container.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``True`` if successful
|
||||||
|
"""
|
||||||
|
url = self._url('/plugins/{0}', name)
|
||||||
|
res = self._delete(url, params={'force': force})
|
||||||
|
self._raise_for_status(res)
|
||||||
|
return True
|
||||||
|
|
@ -3,6 +3,7 @@ from .models.containers import ContainerCollection
|
||||||
from .models.images import ImageCollection
|
from .models.images import ImageCollection
|
||||||
from .models.networks import NetworkCollection
|
from .models.networks import NetworkCollection
|
||||||
from .models.nodes import NodeCollection
|
from .models.nodes import NodeCollection
|
||||||
|
from .models.plugins import PluginCollection
|
||||||
from .models.services import ServiceCollection
|
from .models.services import ServiceCollection
|
||||||
from .models.swarm import Swarm
|
from .models.swarm import Swarm
|
||||||
from .models.volumes import VolumeCollection
|
from .models.volumes import VolumeCollection
|
||||||
|
|
@ -109,6 +110,14 @@ class DockerClient(object):
|
||||||
"""
|
"""
|
||||||
return NodeCollection(client=self)
|
return NodeCollection(client=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugins(self):
|
||||||
|
"""
|
||||||
|
An object for managing plugins on the server. See the
|
||||||
|
:doc:`plugins documentation <plugins>` for full details.
|
||||||
|
"""
|
||||||
|
return PluginCollection(client=self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self):
|
def services(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
from .resource import Collection, Model
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(Model):
|
||||||
|
"""
|
||||||
|
A plugin on the server.
|
||||||
|
"""
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""
|
||||||
|
The plugin's name.
|
||||||
|
"""
|
||||||
|
return self.attrs.get('Name')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self):
|
||||||
|
"""
|
||||||
|
Whether the plugin is enabled.
|
||||||
|
"""
|
||||||
|
return self.attrs.get('Enabled')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def settings(self):
|
||||||
|
"""
|
||||||
|
A dictionary representing the plugin's configuration.
|
||||||
|
"""
|
||||||
|
return self.attrs.get('Settings')
|
||||||
|
|
||||||
|
def configure(self, options):
|
||||||
|
"""
|
||||||
|
Update the plugin's settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options (dict): A key-value mapping of options.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
self.client.api.configure_plugin(self.name, options)
|
||||||
|
self.reload()
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
"""
|
||||||
|
Disable the plugin.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.client.api.disable_plugin(self.name)
|
||||||
|
self.reload()
|
||||||
|
|
||||||
|
def enable(self, timeout=0):
|
||||||
|
"""
|
||||||
|
Enable the plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout (int): Timeout in seconds. Default: 0
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
self.client.api.enable_plugin(self.name, timeout)
|
||||||
|
self.reload()
|
||||||
|
|
||||||
|
def push(self):
|
||||||
|
"""
|
||||||
|
Push the plugin to a remote registry.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict iterator streaming the status of the upload.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
return self.client.api.push_plugin(self.name)
|
||||||
|
|
||||||
|
def remove(self, force=False):
|
||||||
|
"""
|
||||||
|
Remove the plugin from the server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force (bool): Remove even if the plugin is enabled.
|
||||||
|
Default: False
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
return self.client.api.remove_plugin(self.name, force=force)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginCollection(Collection):
|
||||||
|
model = Plugin
|
||||||
|
|
||||||
|
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.
|
||||||
|
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, plugin_data_dir, gzip)
|
||||||
|
return self.get(name)
|
||||||
|
|
||||||
|
def get(self, name):
|
||||||
|
"""
|
||||||
|
Gets a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the plugin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(:py:class:`Plugin`): The plugin.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.NotFound` If the plugin does not
|
||||||
|
exist.
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
return self.prepare_model(self.client.api.inspect_plugin(name))
|
||||||
|
|
||||||
|
def install(self, remote_name, local_name=None):
|
||||||
|
"""
|
||||||
|
Pull and install a plugin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote_name (string): Remote reference for the plugin to
|
||||||
|
install. The ``:latest`` tag is optional, and is the
|
||||||
|
default if omitted.
|
||||||
|
local_name (string): Local name for the pulled plugin.
|
||||||
|
The ``:latest`` tag is optional, and is the default if
|
||||||
|
omitted. Optional.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(:py:class:`Plugin`): The installed plugin
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
privileges = self.client.api.plugin_privileges(remote_name)
|
||||||
|
it = self.client.api.pull_plugin(remote_name, privileges, local_name)
|
||||||
|
for data in it:
|
||||||
|
pass
|
||||||
|
return self.get(local_name or remote_name)
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
"""
|
||||||
|
List plugins installed on the server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list of :py:class:`Plugin`): The plugins.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:class:`docker.errors.APIError`
|
||||||
|
If the server returns an error.
|
||||||
|
"""
|
||||||
|
resp = self.client.api.plugins()
|
||||||
|
return [self.prepare_model(r) for r in resp]
|
||||||
|
|
@ -6,7 +6,7 @@ from .utils import (
|
||||||
create_host_config, parse_bytes, ping_registry, parse_env_file, version_lt,
|
create_host_config, parse_bytes, ping_registry, parse_env_file, version_lt,
|
||||||
version_gte, decode_json_header, split_command, create_ipam_config,
|
version_gte, decode_json_header, split_command, create_ipam_config,
|
||||||
create_ipam_pool, parse_devices, normalize_links, convert_service_networks,
|
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
|
from .decorators import check_resource, minimum_version, update_headers
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,35 @@ def decode_json_header(header):
|
||||||
|
|
||||||
|
|
||||||
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
|
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)
|
root = os.path.abspath(path)
|
||||||
exclude = exclude or []
|
exclude = exclude or []
|
||||||
|
|
||||||
for path in sorted(exclude_paths(root, exclude, dockerfile=dockerfile)):
|
return create_archive(
|
||||||
i = t.gettarinfo(os.path.join(root, path), arcname=path)
|
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:
|
if i is None:
|
||||||
# This happens when we encounter a socket file. We can safely
|
# This happens when we encounter a socket file. We can safely
|
||||||
# ignore it and proceed.
|
# ignore it and proceed.
|
||||||
|
|
@ -102,13 +121,11 @@ def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We open the file object in binary mode for Windows support.
|
# 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:
|
except IOError:
|
||||||
# When we encounter a directory the file object is set to None.
|
# When we encounter a directory the file object is set to None.
|
||||||
f = None
|
t.addfile(i, None)
|
||||||
|
|
||||||
t.addfile(i, f)
|
|
||||||
|
|
||||||
t.close()
|
t.close()
|
||||||
fileobj.seek(0)
|
fileobj.seek(0)
|
||||||
return fileobj
|
return fileobj
|
||||||
|
|
|
||||||
11
docs/api.rst
11
docs/api.rst
|
|
@ -87,6 +87,17 @@ Services
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. py:module:: docker.api.plugin
|
||||||
|
|
||||||
|
.. rst-class:: hide-signature
|
||||||
|
.. autoclass:: PluginApiMixin
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
|
||||||
|
|
||||||
The Docker daemon
|
The Docker daemon
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ Client reference
|
||||||
.. autoattribute:: images
|
.. autoattribute:: images
|
||||||
.. autoattribute:: networks
|
.. autoattribute:: networks
|
||||||
.. autoattribute:: nodes
|
.. autoattribute:: nodes
|
||||||
|
.. autoattribute:: plugins
|
||||||
.. autoattribute:: services
|
.. autoattribute:: services
|
||||||
.. autoattribute:: swarm
|
.. autoattribute:: swarm
|
||||||
.. autoattribute:: volumes
|
.. autoattribute:: volumes
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ That's just a taste of what you can do with the Docker SDK for Python. For more,
|
||||||
images
|
images
|
||||||
networks
|
networks
|
||||||
nodes
|
nodes
|
||||||
|
plugins
|
||||||
services
|
services
|
||||||
swarm
|
swarm
|
||||||
volumes
|
volumes
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
Plugins
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. py:module:: docker.models.plugins
|
||||||
|
|
||||||
|
Manage plugins on the server.
|
||||||
|
|
||||||
|
Methods available on ``client.plugins``:
|
||||||
|
|
||||||
|
.. rst-class:: hide-signature
|
||||||
|
.. py:class:: PluginCollection
|
||||||
|
|
||||||
|
.. automethod:: get
|
||||||
|
.. automethod:: install
|
||||||
|
.. automethod:: list
|
||||||
|
|
||||||
|
|
||||||
|
Plugin objects
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. autoclass:: Plugin()
|
||||||
|
|
||||||
|
.. autoattribute:: id
|
||||||
|
.. autoattribute:: short_id
|
||||||
|
.. autoattribute:: name
|
||||||
|
.. autoattribute:: enabled
|
||||||
|
.. autoattribute:: settings
|
||||||
|
.. py:attribute:: attrs
|
||||||
|
|
||||||
|
The raw representation of this object from the server.
|
||||||
|
|
||||||
|
.. automethod:: configure
|
||||||
|
.. automethod:: disable
|
||||||
|
.. automethod:: enable
|
||||||
|
.. automethod:: reload
|
||||||
|
.. automethod:: push
|
||||||
|
.. automethod:: remove
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
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):
|
||||||
|
c = docker.APIClient(
|
||||||
|
version=TEST_API_VERSION, timeout=60,
|
||||||
|
**docker.utils.kwargs_from_env()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
c.remove_plugin(SSHFS, force=True)
|
||||||
|
except docker.errors.APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def teardown_method(self, method):
|
||||||
|
try:
|
||||||
|
self.client.disable_plugin(SSHFS)
|
||||||
|
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)
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
prv = self.client.plugin_privileges(plugin_name)
|
||||||
|
for d in self.client.pull_plugin(plugin_name, prv):
|
||||||
|
pass
|
||||||
|
return self.client.inspect_plugin(plugin_name)
|
||||||
|
|
||||||
|
def test_enable_plugin(self):
|
||||||
|
pl_data = self.ensure_plugin_installed(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is False
|
||||||
|
assert self.client.enable_plugin(SSHFS)
|
||||||
|
pl_data = self.client.inspect_plugin(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is True
|
||||||
|
with pytest.raises(docker.errors.APIError):
|
||||||
|
self.client.enable_plugin(SSHFS)
|
||||||
|
|
||||||
|
def test_disable_plugin(self):
|
||||||
|
pl_data = self.ensure_plugin_installed(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is False
|
||||||
|
assert self.client.enable_plugin(SSHFS)
|
||||||
|
pl_data = self.client.inspect_plugin(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is True
|
||||||
|
self.client.disable_plugin(SSHFS)
|
||||||
|
pl_data = self.client.inspect_plugin(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is False
|
||||||
|
with pytest.raises(docker.errors.APIError):
|
||||||
|
self.client.disable_plugin(SSHFS)
|
||||||
|
|
||||||
|
def test_inspect_plugin(self):
|
||||||
|
self.ensure_plugin_installed(SSHFS)
|
||||||
|
data = self.client.inspect_plugin(SSHFS)
|
||||||
|
assert 'Config' in data
|
||||||
|
assert 'Name' in data
|
||||||
|
assert data['Name'] == SSHFS
|
||||||
|
|
||||||
|
def test_plugin_privileges(self):
|
||||||
|
prv = self.client.plugin_privileges(SSHFS)
|
||||||
|
assert isinstance(prv, list)
|
||||||
|
for item in prv:
|
||||||
|
assert 'Name' in item
|
||||||
|
assert 'Value' in item
|
||||||
|
assert 'Description' in item
|
||||||
|
|
||||||
|
def test_list_plugins(self):
|
||||||
|
self.ensure_plugin_installed(SSHFS)
|
||||||
|
data = self.client.plugins()
|
||||||
|
assert len(data) > 0
|
||||||
|
plugin = [p for p in data if p['Name'] == SSHFS][0]
|
||||||
|
assert 'Config' in plugin
|
||||||
|
|
||||||
|
def test_configure_plugin(self):
|
||||||
|
pl_data = self.ensure_plugin_installed(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is False
|
||||||
|
self.client.configure_plugin(SSHFS, {
|
||||||
|
'DEBUG': '1'
|
||||||
|
})
|
||||||
|
pl_data = self.client.inspect_plugin(SSHFS)
|
||||||
|
assert 'Env' in pl_data['Settings']
|
||||||
|
assert 'DEBUG=1' in pl_data['Settings']['Env']
|
||||||
|
|
||||||
|
self.client.configure_plugin(SSHFS, ['DEBUG=0'])
|
||||||
|
pl_data = self.client.inspect_plugin(SSHFS)
|
||||||
|
assert 'DEBUG=0' in pl_data['Settings']['Env']
|
||||||
|
|
||||||
|
def test_remove_plugin(self):
|
||||||
|
pl_data = self.ensure_plugin_installed(SSHFS)
|
||||||
|
assert pl_data['Enabled'] is False
|
||||||
|
assert self.client.remove_plugin(SSHFS) is True
|
||||||
|
|
||||||
|
def test_force_remove_plugin(self):
|
||||||
|
self.ensure_plugin_installed(SSHFS)
|
||||||
|
self.client.enable_plugin(SSHFS)
|
||||||
|
assert self.client.inspect_plugin(SSHFS)['Enabled'] is True
|
||||||
|
assert self.client.remove_plugin(SSHFS, force=True) is True
|
||||||
|
|
||||||
|
def test_install_plugin(self):
|
||||||
|
try:
|
||||||
|
self.client.remove_plugin(SSHFS, force=True)
|
||||||
|
except docker.errors.APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
prv = self.client.plugin_privileges(SSHFS)
|
||||||
|
logs = [d for d in self.client.pull_plugin(SSHFS, prv)]
|
||||||
|
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']
|
||||||
|
|
@ -27,6 +27,7 @@ class BaseIntegrationTest(unittest.TestCase):
|
||||||
self.tmp_folders = []
|
self.tmp_folders = []
|
||||||
self.tmp_volumes = []
|
self.tmp_volumes = []
|
||||||
self.tmp_networks = []
|
self.tmp_networks = []
|
||||||
|
self.tmp_plugins = []
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
client = docker.from_env(version=TEST_API_VERSION)
|
client = docker.from_env(version=TEST_API_VERSION)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue