Add support for configs management

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2017-10-26 16:13:05 -07:00 committed by Joffrey F
parent cd47a1f9f5
commit b1301637cf
11 changed files with 464 additions and 3 deletions

View File

@ -9,6 +9,7 @@ import six
import websocket import websocket
from .build import BuildApiMixin from .build import BuildApiMixin
from .config import ConfigApiMixin
from .container import ContainerApiMixin from .container import ContainerApiMixin
from .daemon import DaemonApiMixin from .daemon import DaemonApiMixin
from .exec_api import ExecApiMixin from .exec_api import ExecApiMixin
@ -43,6 +44,7 @@ except ImportError:
class APIClient( class APIClient(
requests.Session, requests.Session,
BuildApiMixin, BuildApiMixin,
ConfigApiMixin,
ContainerApiMixin, ContainerApiMixin,
DaemonApiMixin, DaemonApiMixin,
ExecApiMixin, ExecApiMixin,

91
docker/api/config.py Normal file
View File

@ -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)

View File

@ -1,5 +1,6 @@
from .api.client import APIClient from .api.client import APIClient
from .constants import DEFAULT_TIMEOUT_SECONDS from .constants import DEFAULT_TIMEOUT_SECONDS
from .models.configs import ConfigCollection
from .models.containers import ContainerCollection 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
@ -80,6 +81,14 @@ class DockerClient(object):
**kwargs_from_env(**kwargs)) **kwargs_from_env(**kwargs))
# Resources # 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 @property
def containers(self): def containers(self):
""" """

69
docker/models/configs.py Normal file
View File

@ -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]

View File

@ -9,6 +9,16 @@ It's possible to use :py:class:`APIClient` directly. Some basic things (e.g. run
.. autoclass:: docker.api.client.APIClient .. autoclass:: docker.api.client.APIClient
Configs
-------
.. py:module:: docker.api.config
.. rst-class:: hide-signature
.. autoclass:: ConfigApiMixin
:members:
:undoc-members:
Containers Containers
---------- ----------

View File

@ -15,6 +15,7 @@ Client reference
.. autoclass:: DockerClient() .. autoclass:: DockerClient()
.. autoattribute:: configs
.. autoattribute:: containers .. autoattribute:: containers
.. autoattribute:: images .. autoattribute:: images
.. autoattribute:: networks .. autoattribute:: networks

30
docs/configs.rst Normal file
View File

@ -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

View File

@ -80,6 +80,7 @@ That's just a taste of what you can do with the Docker SDK for Python. For more,
:maxdepth: 2 :maxdepth: 2
client client
configs
containers containers
images images
networks networks

View File

@ -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']

View File

@ -473,7 +473,7 @@ class ServiceTest(BaseAPIIntegrationTest):
secret_data = u'東方花映塚' secret_data = u'東方花映塚'
secret_id = self.client.create_secret(secret_name, secret_data) secret_id = self.client.create_secret(secret_name, secret_data)
self.tmp_secrets.append(secret_id) 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( container_spec = docker.types.ContainerSpec(
'busybox', ['sleep', '999'], secrets=[secret_ref] 'busybox', ['sleep', '999'], secrets=[secret_ref]
) )
@ -481,8 +481,8 @@ class ServiceTest(BaseAPIIntegrationTest):
name = self.get_service_name() name = self.get_service_name()
svc_id = self.client.create_service(task_tmpl, name=name) svc_id = self.client.create_service(task_tmpl, name=name)
svc_info = self.client.inspect_service(svc_id) svc_info = self.client.inspect_service(svc_id)
assert 'Secrets' in svc_info['Spec']['TaskTemplate']['ContainerSpec'] assert 'Configs' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
secrets = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Secrets'] secrets = svc_info['Spec']['TaskTemplate']['ContainerSpec']['Configs']
assert secrets[0] == secret_ref assert secrets[0] == secret_ref
container = self.get_service_container(name) container = self.get_service_container(name)
@ -493,3 +493,175 @@ class ServiceTest(BaseAPIIntegrationTest):
container_secret = self.client.exec_start(exec_id) container_secret = self.client.exec_start(exec_id)
container_secret = container_secret.decode('utf-8') container_secret = container_secret.decode('utf-8')
assert container_secret == secret_data 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'
)

View File

@ -29,6 +29,7 @@ class BaseIntegrationTest(unittest.TestCase):
self.tmp_networks = [] self.tmp_networks = []
self.tmp_plugins = [] self.tmp_plugins = []
self.tmp_secrets = [] self.tmp_secrets = []
self.tmp_configs = []
def tearDown(self): def tearDown(self):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)
@ -59,6 +60,12 @@ class BaseIntegrationTest(unittest.TestCase):
except docker.errors.APIError: except docker.errors.APIError:
pass pass
for config in self.tmp_configs:
try:
client.api.remove_config(config)
except docker.errors.APIError:
pass
for folder in self.tmp_folders: for folder in self.tmp_folders:
shutil.rmtree(folder) shutil.rmtree(folder)