summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoffrey F <joffrey@docker.com>2017-10-26 16:13:05 -0700
committerJoffrey F <joffrey@docker.com>2017-11-06 19:21:03 -0800
commit57a891f4f5ee48edcfbf3319d02c45d6a30d7f18 (patch)
tree4f8a950872cb8110c26016b3412012246420052d
parent2043131cd613acb64c492aab446bce5132dbb303 (diff)
downloaddocker-py-57a891f4f5ee48edcfbf3319d02c45d6a30d7f18.tar.gz
Add support for configs management
Signed-off-by: Joffrey F <joffrey@docker.com>
-rw-r--r--docker/api/client.py2
-rw-r--r--docker/api/config.py91
-rw-r--r--docker/client.py9
-rw-r--r--docker/models/configs.py69
-rw-r--r--docs/api.rst10
-rw-r--r--docs/client.rst1
-rw-r--r--docs/configs.rst30
-rw-r--r--docs/index.rst1
-rw-r--r--tests/integration/api_config_test.py69
-rw-r--r--tests/integration/api_service_test.py178
-rw-r--r--tests/integration/base.py7
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)