Skip to content

Commit 856414b

Browse files
committed
Add support for configs management
Signed-off-by: Joffrey F <[email protected]>
1 parent 601d6be commit 856414b

File tree

11 files changed

+464
-3
lines changed

11 files changed

+464
-3
lines changed

docker/api/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import websocket
1010

1111
from .build import BuildApiMixin
12+
from .config import ConfigApiMixin
1213
from .container import ContainerApiMixin
1314
from .daemon import DaemonApiMixin
1415
from .exec_api import ExecApiMixin
@@ -43,6 +44,7 @@
4344
class APIClient(
4445
requests.Session,
4546
BuildApiMixin,
47+
ConfigApiMixin,
4648
ContainerApiMixin,
4749
DaemonApiMixin,
4850
ExecApiMixin,

docker/api/config.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import base64
2+
3+
import six
4+
5+
from .. import utils
6+
7+
8+
class ConfigApiMixin(object):
9+
@utils.minimum_version('1.25')
10+
def create_config(self, name, data, labels=None):
11+
"""
12+
Create a config
13+
14+
Args:
15+
name (string): Name of the config
16+
data (bytes): Config data to be stored
17+
labels (dict): A mapping of labels to assign to the config
18+
19+
Returns (dict): ID of the newly created config
20+
"""
21+
if not isinstance(data, bytes):
22+
data = data.encode('utf-8')
23+
24+
data = base64.b64encode(data)
25+
if six.PY3:
26+
data = data.decode('ascii')
27+
body = {
28+
'Data': data,
29+
'Name': name,
30+
'Labels': labels
31+
}
32+
33+
url = self._url('/configs/create')
34+
return self._result(
35+
self._post_json(url, data=body), True
36+
)
37+
38+
@utils.minimum_version('1.25')
39+
@utils.check_resource('id')
40+
def inspect_config(self, id):
41+
"""
42+
Retrieve config metadata
43+
44+
Args:
45+
id (string): Full ID of the config to remove
46+
47+
Returns (dict): A dictionary of metadata
48+
49+
Raises:
50+
:py:class:`docker.errors.NotFound`
51+
if no config with that ID exists
52+
"""
53+
url = self._url('/configs/{0}', id)
54+
return self._result(self._get(url), True)
55+
56+
@utils.minimum_version('1.25')
57+
@utils.check_resource('id')
58+
def remove_config(self, id):
59+
"""
60+
Remove a config
61+
62+
Args:
63+
id (string): Full ID of the config to remove
64+
65+
Returns (boolean): True if successful
66+
67+
Raises:
68+
:py:class:`docker.errors.NotFound`
69+
if no config with that ID exists
70+
"""
71+
url = self._url('/configs/{0}', id)
72+
res = self._delete(url)
73+
self._raise_for_status(res)
74+
return True
75+
76+
@utils.minimum_version('1.25')
77+
def configs(self, filters=None):
78+
"""
79+
List configs
80+
81+
Args:
82+
filters (dict): A map of filters to process on the configs
83+
list. Available filters: ``names``
84+
85+
Returns (list): A list of configs
86+
"""
87+
url = self._url('/configs')
88+
params = {}
89+
if filters:
90+
params['filters'] = utils.convert_filters(filters)
91+
return self._result(self._get(url, params=params), True)

docker/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .api.client import APIClient
22
from .constants import DEFAULT_TIMEOUT_SECONDS
3+
from .models.configs import ConfigCollection
34
from .models.containers import ContainerCollection
45
from .models.images import ImageCollection
56
from .models.networks import NetworkCollection
@@ -80,6 +81,14 @@ def from_env(cls, **kwargs):
8081
**kwargs_from_env(**kwargs))
8182

8283
# Resources
84+
@property
85+
def configs(self):
86+
"""
87+
An object for managing configs on the server. See the
88+
:doc:`configs documentation <configs>` for full details.
89+
"""
90+
return ConfigCollection(client=self)
91+
8392
@property
8493
def containers(self):
8594
"""

docker/models/configs.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from ..api import APIClient
2+
from .resource import Model, Collection
3+
4+
5+
class Config(Model):
6+
"""A config."""
7+
id_attribute = 'ID'
8+
9+
def __repr__(self):
10+
return "<%s: '%s'>" % (self.__class__.__name__, self.name)
11+
12+
@property
13+
def name(self):
14+
return self.attrs['Spec']['Name']
15+
16+
def remove(self):
17+
"""
18+
Remove this config.
19+
20+
Raises:
21+
:py:class:`docker.errors.APIError`
22+
If config failed to remove.
23+
"""
24+
return self.client.api.remove_config(self.id)
25+
26+
27+
class ConfigCollection(Collection):
28+
"""Configs on the Docker server."""
29+
model = Config
30+
31+
def create(self, **kwargs):
32+
obj = self.client.api.create_config(**kwargs)
33+
return self.prepare_model(obj)
34+
create.__doc__ = APIClient.create_config.__doc__
35+
36+
def get(self, config_id):
37+
"""
38+
Get a config.
39+
40+
Args:
41+
config_id (str): Config ID.
42+
43+
Returns:
44+
(:py:class:`Config`): The config.
45+
46+
Raises:
47+
:py:class:`docker.errors.NotFound`
48+
If the config does not exist.
49+
:py:class:`docker.errors.APIError`
50+
If the server returns an error.
51+
"""
52+
return self.prepare_model(self.client.api.inspect_config(config_id))
53+
54+
def list(self, **kwargs):
55+
"""
56+
List configs. Similar to the ``docker config ls`` command.
57+
58+
Args:
59+
filters (dict): Server-side list filtering options.
60+
61+
Returns:
62+
(list of :py:class:`Config`): The configs.
63+
64+
Raises:
65+
:py:class:`docker.errors.APIError`
66+
If the server returns an error.
67+
"""
68+
resp = self.client.api.configs(**kwargs)
69+
return [self.prepare_model(obj) for obj in resp]

docs/api.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ It's possible to use :py:class:`APIClient` directly. Some basic things (e.g. run
99
1010
.. autoclass:: docker.api.client.APIClient
1111

12+
Configs
13+
-------
14+
15+
.. py:module:: docker.api.config
16+
17+
.. rst-class:: hide-signature
18+
.. autoclass:: ConfigApiMixin
19+
:members:
20+
:undoc-members:
21+
1222
Containers
1323
----------
1424

docs/client.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Client reference
1515

1616
.. autoclass:: DockerClient()
1717

18+
.. autoattribute:: configs
1819
.. autoattribute:: containers
1920
.. autoattribute:: images
2021
.. autoattribute:: networks

docs/configs.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Configs
2+
=======
3+
4+
.. py:module:: docker.models.configs
5+
6+
Manage configs on the server.
7+
8+
Methods available on ``client.configs``:
9+
10+
.. rst-class:: hide-signature
11+
.. py:class:: ConfigCollection
12+
13+
.. automethod:: create
14+
.. automethod:: get
15+
.. automethod:: list
16+
17+
18+
Config objects
19+
--------------
20+
21+
.. autoclass:: Config()
22+
23+
.. autoattribute:: id
24+
.. autoattribute:: name
25+
.. py:attribute:: attrs
26+
27+
The raw representation of this object from the server.
28+
29+
.. automethod:: reload
30+
.. automethod:: remove

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ That's just a taste of what you can do with the Docker SDK for Python. For more,
8080
:maxdepth: 2
8181

8282
client
83+
configs
8384
containers
8485
images
8586
networks
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import docker
4+
import pytest
5+
6+
from ..helpers import force_leave_swarm, requires_api_version
7+
from .base import BaseAPIIntegrationTest
8+
9+
10+
@requires_api_version('1.30')
11+
class ConfigAPITest(BaseAPIIntegrationTest):
12+
def setUp(self):
13+
super(ConfigAPITest, self).setUp()
14+
self.init_swarm()
15+
16+
def tearDown(self):
17+
super(ConfigAPITest, self).tearDown()
18+
force_leave_swarm(self.client)
19+
20+
def test_create_config(self):
21+
config_id = self.client.create_config(
22+
'favorite_character', 'sakuya izayoi'
23+
)
24+
self.tmp_configs.append(config_id)
25+
assert 'ID' in config_id
26+
data = self.client.inspect_config(config_id)
27+
assert data['Spec']['Name'] == 'favorite_character'
28+
29+
def test_create_config_unicode_data(self):
30+
config_id = self.client.create_config(
31+
'favorite_character', u'いざよいさくや'
32+
)
33+
self.tmp_configs.append(config_id)
34+
assert 'ID' in config_id
35+
data = self.client.inspect_config(config_id)
36+
assert data['Spec']['Name'] == 'favorite_character'
37+
38+
def test_inspect_config(self):
39+
config_name = 'favorite_character'
40+
config_id = self.client.create_config(
41+
config_name, 'sakuya izayoi'
42+
)
43+
self.tmp_configs.append(config_id)
44+
data = self.client.inspect_config(config_id)
45+
assert data['Spec']['Name'] == config_name
46+
assert 'ID' in data
47+
assert 'Version' in data
48+
49+
def test_remove_config(self):
50+
config_name = 'favorite_character'
51+
config_id = self.client.create_config(
52+
config_name, 'sakuya izayoi'
53+
)
54+
self.tmp_configs.append(config_id)
55+
56+
assert self.client.remove_config(config_id)
57+
with pytest.raises(docker.errors.NotFound):
58+
self.client.inspect_config(config_id)
59+
60+
def test_list_configs(self):
61+
config_name = 'favorite_character'
62+
config_id = self.client.create_config(
63+
config_name, 'sakuya izayoi'
64+
)
65+
self.tmp_configs.append(config_id)
66+
67+
data = self.client.configs(filters={'name': ['favorite_character']})
68+
assert len(data) == 1
69+
assert data[0]['ID'] == config_id['ID']

0 commit comments

Comments
 (0)