diff options
author | Joffrey F <joffrey@docker.com> | 2017-10-26 16:13:05 -0700 |
---|---|---|
committer | Joffrey F <joffrey@docker.com> | 2017-11-06 19:21:03 -0800 |
commit | 57a891f4f5ee48edcfbf3319d02c45d6a30d7f18 (patch) | |
tree | 4f8a950872cb8110c26016b3412012246420052d | |
parent | 2043131cd613acb64c492aab446bce5132dbb303 (diff) | |
download | docker-py-57a891f4f5ee48edcfbf3319d02c45d6a30d7f18.tar.gz |
Add support for configs management
Signed-off-by: Joffrey F <joffrey@docker.com>
-rw-r--r-- | docker/api/client.py | 2 | ||||
-rw-r--r-- | docker/api/config.py | 91 | ||||
-rw-r--r-- | docker/client.py | 9 | ||||
-rw-r--r-- | docker/models/configs.py | 69 | ||||
-rw-r--r-- | docs/api.rst | 10 | ||||
-rw-r--r-- | docs/client.rst | 1 | ||||
-rw-r--r-- | docs/configs.rst | 30 | ||||
-rw-r--r-- | docs/index.rst | 1 | ||||
-rw-r--r-- | tests/integration/api_config_test.py | 69 | ||||
-rw-r--r-- | tests/integration/api_service_test.py | 178 | ||||
-rw-r--r-- | tests/integration/base.py | 7 |
11 files changed, 464 insertions, 3 deletions
diff --git a/docker/api/client.py b/docker/api/client.py index 1de10c7..cbe74b9 100644 --- a/docker/api/client.py +++ b/docker/api/client.py @@ -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, diff --git a/docker/api/config.py b/docker/api/config.py new file mode 100644 index 0000000..b46b09c --- /dev/null +++ b/docker/api/config.py @@ -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) diff --git a/docker/client.py b/docker/client.py index ee361bb..29968c1 100644 --- a/docker/client.py +++ b/docker/client.py @@ -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 @@ -81,6 +82,14 @@ class DockerClient(object): # 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): """ An object for managing containers on the server. See the diff --git a/docker/models/configs.py b/docker/models/configs.py new file mode 100644 index 0000000..7f23f65 --- /dev/null +++ b/docker/models/configs.py @@ -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] diff --git a/docs/api.rst b/docs/api.rst index 2fce0a7..18993ad 100644 --- a/docs/api.rst +++ b/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 ---------- diff --git a/docs/client.rst b/docs/client.rst index ac7a256..43d7c63 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -15,6 +15,7 @@ Client reference .. autoclass:: DockerClient() + .. autoattribute:: configs .. autoattribute:: containers .. autoattribute:: images .. autoattribute:: networks diff --git a/docs/configs.rst b/docs/configs.rst new file mode 100644 index 0000000..d907ad4 --- /dev/null +++ b/docs/configs.rst @@ -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 diff --git a/docs/index.rst b/docs/index.rst index 9113bff..39426b6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/tests/integration/api_config_test.py b/tests/integration/api_config_test.py new file mode 100644 index 0000000..fb6002a --- /dev/null +++ b/tests/integration/api_config_test.py @@ -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'] diff --git a/tests/integration/api_service_test.py b/tests/integration/api_service_test.py index c966916..56c3e68 100644 --- a/tests/integration/api_service_test.py +++ b/tests/integration/api_service_test.py @@ -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' + ) diff --git a/tests/integration/base.py b/tests/integration/base.py index 0c0cd06..701e7fc 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -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) |