mirror of https://github.com/docker/docker-py.git
Add support for configs management
Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
parent
cd47a1f9f5
commit
b1301637cf
|
@ -9,6 +9,7 @@ import six
|
|||
import websocket
|
||||
|
||||
from .build import BuildApiMixin
|
||||
from .config import ConfigApiMixin
|
||||
from .container import ContainerApiMixin
|
||||
from .daemon import DaemonApiMixin
|
||||
from .exec_api import ExecApiMixin
|
||||
|
@ -43,6 +44,7 @@ except ImportError:
|
|||
class APIClient(
|
||||
requests.Session,
|
||||
BuildApiMixin,
|
||||
ConfigApiMixin,
|
||||
ContainerApiMixin,
|
||||
DaemonApiMixin,
|
||||
ExecApiMixin,
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import base64
|
||||
|
||||
import six
|
||||
|
||||
from .. import utils
|
||||
|
||||
|
||||
class ConfigApiMixin(object):
|
||||
@utils.minimum_version('1.25')
|
||||
def create_config(self, name, data, labels=None):
|
||||
"""
|
||||
Create a config
|
||||
|
||||
Args:
|
||||
name (string): Name of the config
|
||||
data (bytes): Config data to be stored
|
||||
labels (dict): A mapping of labels to assign to the config
|
||||
|
||||
Returns (dict): ID of the newly created config
|
||||
"""
|
||||
if not isinstance(data, bytes):
|
||||
data = data.encode('utf-8')
|
||||
|
||||
data = base64.b64encode(data)
|
||||
if six.PY3:
|
||||
data = data.decode('ascii')
|
||||
body = {
|
||||
'Data': data,
|
||||
'Name': name,
|
||||
'Labels': labels
|
||||
}
|
||||
|
||||
url = self._url('/configs/create')
|
||||
return self._result(
|
||||
self._post_json(url, data=body), True
|
||||
)
|
||||
|
||||
@utils.minimum_version('1.25')
|
||||
@utils.check_resource('id')
|
||||
def inspect_config(self, id):
|
||||
"""
|
||||
Retrieve config metadata
|
||||
|
||||
Args:
|
||||
id (string): Full ID of the config to remove
|
||||
|
||||
Returns (dict): A dictionary of metadata
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.NotFound`
|
||||
if no config with that ID exists
|
||||
"""
|
||||
url = self._url('/configs/{0}', id)
|
||||
return self._result(self._get(url), True)
|
||||
|
||||
@utils.minimum_version('1.25')
|
||||
@utils.check_resource('id')
|
||||
def remove_config(self, id):
|
||||
"""
|
||||
Remove a config
|
||||
|
||||
Args:
|
||||
id (string): Full ID of the config to remove
|
||||
|
||||
Returns (boolean): True if successful
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.NotFound`
|
||||
if no config with that ID exists
|
||||
"""
|
||||
url = self._url('/configs/{0}', id)
|
||||
res = self._delete(url)
|
||||
self._raise_for_status(res)
|
||||
return True
|
||||
|
||||
@utils.minimum_version('1.25')
|
||||
def configs(self, filters=None):
|
||||
"""
|
||||
List configs
|
||||
|
||||
Args:
|
||||
filters (dict): A map of filters to process on the configs
|
||||
list. Available filters: ``names``
|
||||
|
||||
Returns (list): A list of configs
|
||||
"""
|
||||
url = self._url('/configs')
|
||||
params = {}
|
||||
if filters:
|
||||
params['filters'] = utils.convert_filters(filters)
|
||||
return self._result(self._get(url, params=params), True)
|
|
@ -1,5 +1,6 @@
|
|||
from .api.client import APIClient
|
||||
from .constants import DEFAULT_TIMEOUT_SECONDS
|
||||
from .models.configs import ConfigCollection
|
||||
from .models.containers import ContainerCollection
|
||||
from .models.images import ImageCollection
|
||||
from .models.networks import NetworkCollection
|
||||
|
@ -80,6 +81,14 @@ class DockerClient(object):
|
|||
**kwargs_from_env(**kwargs))
|
||||
|
||||
# Resources
|
||||
@property
|
||||
def configs(self):
|
||||
"""
|
||||
An object for managing configs on the server. See the
|
||||
:doc:`configs documentation <configs>` for full details.
|
||||
"""
|
||||
return ConfigCollection(client=self)
|
||||
|
||||
@property
|
||||
def containers(self):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
from ..api import APIClient
|
||||
from .resource import Model, Collection
|
||||
|
||||
|
||||
class Config(Model):
|
||||
"""A config."""
|
||||
id_attribute = 'ID'
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.attrs['Spec']['Name']
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove this config.
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If config failed to remove.
|
||||
"""
|
||||
return self.client.api.remove_config(self.id)
|
||||
|
||||
|
||||
class ConfigCollection(Collection):
|
||||
"""Configs on the Docker server."""
|
||||
model = Config
|
||||
|
||||
def create(self, **kwargs):
|
||||
obj = self.client.api.create_config(**kwargs)
|
||||
return self.prepare_model(obj)
|
||||
create.__doc__ = APIClient.create_config.__doc__
|
||||
|
||||
def get(self, config_id):
|
||||
"""
|
||||
Get a config.
|
||||
|
||||
Args:
|
||||
config_id (str): Config ID.
|
||||
|
||||
Returns:
|
||||
(:py:class:`Config`): The config.
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.NotFound`
|
||||
If the config does not exist.
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
return self.prepare_model(self.client.api.inspect_config(config_id))
|
||||
|
||||
def list(self, **kwargs):
|
||||
"""
|
||||
List configs. Similar to the ``docker config ls`` command.
|
||||
|
||||
Args:
|
||||
filters (dict): Server-side list filtering options.
|
||||
|
||||
Returns:
|
||||
(list of :py:class:`Config`): The configs.
|
||||
|
||||
Raises:
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
resp = self.client.api.configs(**kwargs)
|
||||
return [self.prepare_model(obj) for obj in resp]
|
10
docs/api.rst
10
docs/api.rst
|
@ -9,6 +9,16 @@ It's possible to use :py:class:`APIClient` directly. Some basic things (e.g. run
|
|||
|
||||
.. autoclass:: docker.api.client.APIClient
|
||||
|
||||
Configs
|
||||
-------
|
||||
|
||||
.. py:module:: docker.api.config
|
||||
|
||||
.. rst-class:: hide-signature
|
||||
.. autoclass:: ConfigApiMixin
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
Containers
|
||||
----------
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ Client reference
|
|||
|
||||
.. autoclass:: DockerClient()
|
||||
|
||||
.. autoattribute:: configs
|
||||
.. autoattribute:: containers
|
||||
.. autoattribute:: images
|
||||
.. autoattribute:: networks
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
Configs
|
||||
=======
|
||||
|
||||
.. py:module:: docker.models.configs
|
||||
|
||||
Manage configs on the server.
|
||||
|
||||
Methods available on ``client.configs``:
|
||||
|
||||
.. rst-class:: hide-signature
|
||||
.. py:class:: ConfigCollection
|
||||
|
||||
.. automethod:: create
|
||||
.. automethod:: get
|
||||
.. automethod:: list
|
||||
|
||||
|
||||
Config objects
|
||||
--------------
|
||||
|
||||
.. autoclass:: Config()
|
||||
|
||||
.. autoattribute:: id
|
||||
.. autoattribute:: name
|
||||
.. py:attribute:: attrs
|
||||
|
||||
The raw representation of this object from the server.
|
||||
|
||||
.. automethod:: reload
|
||||
.. automethod:: remove
|
|
@ -80,6 +80,7 @@ That's just a taste of what you can do with the Docker SDK for Python. For more,
|
|||
:maxdepth: 2
|
||||
|
||||
client
|
||||
configs
|
||||
containers
|
||||
images
|
||||
networks
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import docker
|
||||
import pytest
|
||||
|
||||
from ..helpers import force_leave_swarm, requires_api_version
|
||||
from .base import BaseAPIIntegrationTest
|
||||
|
||||
|
||||
@requires_api_version('1.30')
|
||||
class ConfigAPITest(BaseAPIIntegrationTest):
|
||||
def setUp(self):
|
||||
super(ConfigAPITest, self).setUp()
|
||||
self.init_swarm()
|
||||
|
||||
def tearDown(self):
|
||||
super(ConfigAPITest, self).tearDown()
|
||||
force_leave_swarm(self.client)
|
||||
|
||||
def test_create_config(self):
|
||||
config_id = self.client.create_config(
|
||||
'favorite_character', 'sakuya izayoi'
|
||||
)
|
||||
self.tmp_configs.append(config_id)
|
||||
assert 'ID' in config_id
|
||||
data = self.client.inspect_config(config_id)
|
||||
assert data['Spec']['Name'] == 'favorite_character'
|
||||
|
||||
def test_create_config_unicode_data(self):
|
||||
config_id = self.client.create_config(
|
||||
'favorite_character', u'いざよいさくや'
|
||||
)
|
||||
self.tmp_configs.append(config_id)
|
||||
assert 'ID' in config_id
|
||||
data = self.client.inspect_config(config_id)
|
||||
assert data['Spec']['Name'] == 'favorite_character'
|
||||
|
||||
def test_inspect_config(self):
|
||||
config_name = 'favorite_character'
|
||||
config_id = self.client.create_config(
|
||||
config_name, 'sakuya izayoi'
|
||||
)
|
||||
self.tmp_configs.append(config_id)
|
||||
data = self.client.inspect_config(config_id)
|
||||
assert data['Spec']['Name'] == config_name
|
||||
assert 'ID' in data
|
||||
assert 'Version' in data
|
||||
|
||||
def test_remove_config(self):
|
||||
config_name = 'favorite_character'
|
||||
config_id = self.client.create_config(
|
||||
config_name, 'sakuya izayoi'
|
||||
)
|
||||
self.tmp_configs.append(config_id)
|
||||
|
||||
assert self.client.remove_config(config_id)
|
||||
with pytest.raises(docker.errors.NotFound):
|
||||
self.client.inspect_config(config_id)
|
||||
|
||||
def test_list_configs(self):
|
||||
config_name = 'favorite_character'
|
||||
config_id = self.client.create_config(
|
||||
config_name, 'sakuya izayoi'
|
||||
)
|
||||
self.tmp_configs.append(config_id)
|
||||
|
||||
data = self.client.configs(filters={'name': ['favorite_character']})
|
||||
assert len(data) == 1
|
||||
assert data[0]['ID'] == config_id['ID']
|
|
@ -473,7 +473,7 @@ class ServiceTest(BaseAPIIntegrationTest):
|
|||
secret_data = u'東方花映塚'
|
||||
secret_id = self.client.create_secret(secret_name, secret_data)
|
||||
self.tmp_secrets.append(secret_id)
|
||||
secret_ref = docker.types.SecretReference(secret_id, secret_name)
|
||||
secret_ref = docker.types.ConfigReference(secret_id, secret_name)
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
'busybox', ['sleep', '999'], secrets=[secret_ref]
|
||||
)
|
||||
|
@ -481,8 +481,8 @@ class ServiceTest(BaseAPIIntegrationTest):
|
|||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'Secrets' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
secrets = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Secrets']
|
||||
assert 'Configs' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
secrets = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Configs']
|
||||
assert secrets[0] == secret_ref
|
||||
|
||||
container = self.get_service_container(name)
|
||||
|
@ -493,3 +493,175 @@ class ServiceTest(BaseAPIIntegrationTest):
|
|||
container_secret = self.client.exec_start(exec_id)
|
||||
container_secret = container_secret.decode('utf-8')
|
||||
assert container_secret == secret_data
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_config(self):
|
||||
config_name = 'favorite_touhou'
|
||||
config_data = b'phantasmagoria of flower view'
|
||||
config_id = self.client.create_config(config_name, config_data)
|
||||
self.tmp_configs.append(config_id)
|
||||
config_ref = docker.types.ConfigReference(config_id, config_name)
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
'busybox', ['sleep', '999'], configs=[config_ref]
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'Configs' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
configs = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Configs']
|
||||
assert configs[0] == config_ref
|
||||
|
||||
container = self.get_service_container(name)
|
||||
assert container is not None
|
||||
exec_id = self.client.exec_create(
|
||||
container, 'cat /run/configs/{0}'.format(config_name)
|
||||
)
|
||||
assert self.client.exec_start(exec_id) == config_data
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_unicode_config(self):
|
||||
config_name = 'favorite_touhou'
|
||||
config_data = u'東方花映塚'
|
||||
config_id = self.client.create_config(config_name, config_data)
|
||||
self.tmp_configs.append(config_id)
|
||||
config_ref = docker.types.ConfigReference(config_id, config_name)
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
'busybox', ['sleep', '999'], configs=[config_ref]
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'Configs' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
configs = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Configs']
|
||||
assert configs[0] == config_ref
|
||||
|
||||
container = self.get_service_container(name)
|
||||
assert container is not None
|
||||
exec_id = self.client.exec_create(
|
||||
container, 'cat /run/configs/{0}'.format(config_name)
|
||||
)
|
||||
container_config = self.client.exec_start(exec_id)
|
||||
container_config = container_config.decode('utf-8')
|
||||
assert container_config == config_data
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_hosts(self):
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
'busybox', ['sleep', '999'], hosts={
|
||||
'foobar': '127.0.0.1',
|
||||
'baz': '8.8.8.8',
|
||||
}
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'Hosts' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
hosts = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Hosts']
|
||||
assert len(hosts) == 2
|
||||
assert 'foobar:127.0.0.1' in hosts
|
||||
assert 'baz:8.8.8.8' in hosts
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_hostname(self):
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
'busybox', ['sleep', '999'], hostname='foobar.baz.com'
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'Hostname' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
assert (
|
||||
svc_info['Spec']['TaskTemplate']['ContainerSpec']['Hostname'] ==
|
||||
'foobar.baz.com'
|
||||
)
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_groups(self):
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
'busybox', ['sleep', '999'], groups=['shrinemaidens', 'youkais']
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'Groups' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
groups = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Groups']
|
||||
assert len(groups) == 2
|
||||
assert 'shrinemaidens' in groups
|
||||
assert 'youkais' in groups
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_dns_config(self):
|
||||
dns_config = docker.types.DNSConfig(
|
||||
nameservers=['8.8.8.8', '8.8.4.4'],
|
||||
search=['local'], options=['debug']
|
||||
)
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
BUSYBOX, ['sleep', '999'], dns_config=dns_config
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert 'DNSConfig' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
assert (
|
||||
dns_config ==
|
||||
svc_info['Spec']['TaskTemplate']['ContainerSpec']['DNSConfig']
|
||||
)
|
||||
|
||||
@requires_api_version('1.25')
|
||||
def test_create_service_with_healthcheck(self):
|
||||
second = 1000000000
|
||||
hc = docker.types.Healthcheck(
|
||||
test='true', retries=3, timeout=1 * second,
|
||||
start_period=3 * second, interval=second / 2,
|
||||
)
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
BUSYBOX, ['sleep', '999'], healthcheck=hc
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert (
|
||||
'Healthcheck' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
)
|
||||
assert (
|
||||
hc ==
|
||||
svc_info['Spec']['TaskTemplate']['ContainerSpec']['Healthcheck']
|
||||
)
|
||||
|
||||
@requires_api_version('1.28')
|
||||
def test_create_service_with_readonly(self):
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
BUSYBOX, ['sleep', '999'], read_only=True
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert (
|
||||
'ReadOnly' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
)
|
||||
assert svc_info['Spec']['TaskTemplate']['ContainerSpec']['ReadOnly']
|
||||
|
||||
@requires_api_version('1.28')
|
||||
def test_create_service_with_stop_signal(self):
|
||||
container_spec = docker.types.ContainerSpec(
|
||||
BUSYBOX, ['sleep', '999'], stop_signal='SIGINT'
|
||||
)
|
||||
task_tmpl = docker.types.TaskTemplate(container_spec)
|
||||
name = self.get_service_name()
|
||||
svc_id = self.client.create_service(task_tmpl, name=name)
|
||||
svc_info = self.client.inspect_service(svc_id)
|
||||
assert (
|
||||
'StopSignal' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
|
||||
)
|
||||
assert (
|
||||
svc_info['Spec']['TaskTemplate']['ContainerSpec']['StopSignal'] ==
|
||||
'SIGINT'
|
||||
)
|
||||
|
|
|
@ -29,6 +29,7 @@ class BaseIntegrationTest(unittest.TestCase):
|
|||
self.tmp_networks = []
|
||||
self.tmp_plugins = []
|
||||
self.tmp_secrets = []
|
||||
self.tmp_configs = []
|
||||
|
||||
def tearDown(self):
|
||||
client = docker.from_env(version=TEST_API_VERSION)
|
||||
|
@ -59,6 +60,12 @@ class BaseIntegrationTest(unittest.TestCase):
|
|||
except docker.errors.APIError:
|
||||
pass
|
||||
|
||||
for config in self.tmp_configs:
|
||||
try:
|
||||
client.api.remove_config(config)
|
||||
except docker.errors.APIError:
|
||||
pass
|
||||
|
||||
for folder in self.tmp_folders:
|
||||
shutil.rmtree(folder)
|
||||
|
||||
|
|
Loading…
Reference in New Issue