Skip to content

Commit 392c319

Browse files
committed
chore: add ClusterVolumeSpec--class (closes #3254)
1 parent db7f8b8 commit 392c319

File tree

5 files changed

+423
-5
lines changed

5 files changed

+423
-5
lines changed

docker/api/volume.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@ def volumes(self, filters=None):
3131

3232
params = {
3333
'filters': utils.convert_filters(filters) if filters else None
34-
}
34+
}
3535
url = self._url('/volumes')
3636
return self._result(self._get(url, params=params), True)
3737

38-
def create_volume(self, name=None, driver=None, driver_opts=None,
39-
labels=None):
38+
def create_volume(self, name=None, driver=None, driver_opts=None, labels=None, cluster_volume_spec=None):
4039
"""
4140
Create and register a named volume
4241
@@ -83,11 +82,18 @@ def create_volume(self, name=None, driver=None, driver_opts=None,
8382
if utils.compare_version('1.23', self._version) < 0:
8483
raise errors.InvalidVersion(
8584
'volume labels were introduced in API 1.23'
86-
)
85+
)
8786
if not isinstance(labels, dict):
8887
raise TypeError('labels must be a dictionary')
8988
data["Labels"] = labels
9089

90+
if cluster_volume_spec is not None:
91+
if utils.compare_version("1.42", self._version) < 0:
92+
raise errors.InvalidVersion(
93+
"cluster volume spec was introduced in API 1.45"
94+
)
95+
data["ClusterVolumeSpec"] = cluster_volume_spec
96+
9197
return self._result(self._post_json(url, data=data), True)
9298

9399
def inspect_volume(self, name):

docker/types/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
UpdateConfig,
2323
)
2424
from .swarm import SwarmExternalCA, SwarmSpec
25+
from .volumes import ClusterVolumeSpec, AccessMode, Secret, CapacityRange, AccessibilityRequirement

docker/types/volumes.py

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
from .base import DictType
2+
from ..types import Mount
3+
4+
def access_mode_type_error(param, param_value, expected):
5+
return TypeError(
6+
f"Invalid type for {param} param: expected {expected} "
7+
f"but found {type(param_value)}"
8+
)
9+
10+
11+
class CapacityRange(DictType):
12+
def __init__(self, **kwargs):
13+
limit_bytes = kwargs.get("limit_bytes", kwargs.get("LimitBytes"))
14+
required_bytes = kwargs.get("required_bytes", kwargs.get("RequiredBytes"))
15+
16+
if limit_bytes is not None:
17+
if not isinstance(limit_bytes, int):
18+
raise access_mode_type_error("limit_bytes", limit_bytes, "int")
19+
if required_bytes is not None:
20+
if not isinstance(required_bytes, int):
21+
raise access_mode_type_error("required_bytes", required_bytes, "int")
22+
23+
super().__init__({"RequiredBytes": required_bytes, "LimitBytes": limit_bytes})
24+
25+
@property
26+
def limit_bytes(self):
27+
return self["LimitBytes"]
28+
29+
@property
30+
def required_bytes(self):
31+
return self["RequiredBytes"]
32+
33+
@limit_bytes.setter
34+
def limit_bytes(self, value):
35+
if not isinstance(value, int):
36+
raise access_mode_type_error("limit_bytes", value, "int")
37+
self["LimitBytes"] = value
38+
39+
@required_bytes.setter
40+
def required_bytes(self, value):
41+
if not isinstance(value, int):
42+
raise access_mode_type_error("required_bytes", value, "int")
43+
self["RequiredBytes"]
44+
45+
46+
class Secret(DictType):
47+
def __init__(self, **kwargs):
48+
key = kwargs.get("key", kwargs.get("Key"))
49+
secret = kwargs.get("secret", kwargs.get("Secret"))
50+
51+
if key is not None:
52+
if not isinstance(key, str):
53+
raise access_mode_type_error("key", key, "str")
54+
if secret is not None:
55+
if not isinstance(secret, str):
56+
raise access_mode_type_error("secret", secret, "str")
57+
58+
super().__init__({"Key": key, "Secret": secret})
59+
60+
@property
61+
def key(self):
62+
return self["Key"]
63+
64+
@property
65+
def secret(self):
66+
return self["Secret"]
67+
68+
@key.setter
69+
def key(self, value):
70+
if not isinstance(value, str):
71+
raise access_mode_type_error("key", value, "str")
72+
self["Key"] = value
73+
74+
@secret.setter
75+
def secret(self, value):
76+
if not isinstance(value, str):
77+
raise access_mode_type_error("secret", value, "str")
78+
self["Secret"]
79+
80+
81+
class AccessibilityRequirement(DictType):
82+
def __init__(self, **kwargs):
83+
requisite = kwargs.get("requisite", kwargs.get("Requisite"))
84+
preferred = kwargs.get("preferred", kwargs.get("Preferred"))
85+
86+
if requisite is not None:
87+
if not isinstance(requisite, list):
88+
raise access_mode_type_error("requisite", requisite, "list")
89+
self["Requisite"] = requisite
90+
91+
if preferred is not None:
92+
if not isinstance(preferred, list):
93+
raise access_mode_type_error("preferred", preferred, "list")
94+
self["Preferred"] = preferred
95+
96+
super().__init__({"Requisite": requisite, "Preferred": preferred})
97+
98+
@property
99+
def requisite(self):
100+
return self["Requisite"]
101+
102+
@property
103+
def preferred(self):
104+
return self["Preferred"]
105+
106+
@requisite.setter
107+
def requisite(self, value):
108+
if not isinstance(value, list):
109+
raise access_mode_type_error("requisite", value, "list")
110+
self["Requisite"] = value
111+
112+
@preferred.setter
113+
def preferred(self, value):
114+
if not isinstance(value, list):
115+
raise access_mode_type_error("preferred", value, "list")
116+
self["Preferred"] = value
117+
118+
119+
class AccessMode(dict):
120+
def __init__(
121+
self,
122+
scope=None,
123+
sharing=None,
124+
mount_volume=None,
125+
availabilty=None,
126+
secrets=None,
127+
accessibility_requirements=None,
128+
capacity_range=None,
129+
):
130+
if scope is not None:
131+
if not isinstance(scope, str):
132+
raise access_mode_type_error("scope", scope, "str")
133+
self["Scope"] = scope
134+
135+
if sharing is not None:
136+
if not isinstance(sharing, str):
137+
raise access_mode_type_error("sharing", sharing, "str")
138+
self["Sharing"] = sharing
139+
140+
if mount_volume is not None:
141+
if not isinstance(mount_volume, str):
142+
raise access_mode_type_error("mount_volume", mount_volume, "str")
143+
self["MountVolume"] = Mount.parse_mount_string(mount_volume)
144+
145+
if availabilty is not None:
146+
if not isinstance(availabilty, str):
147+
raise access_mode_type_error("availabilty", availabilty, "str")
148+
self["Availabilty"] = availabilty
149+
150+
if secrets is not None:
151+
if not isinstance(secrets, list):
152+
raise access_mode_type_error("secrets", secrets, "list")
153+
self["Secrets"] = []
154+
for secret in secrets:
155+
if not isinstance(secret, Secret):
156+
secret = Secret(**secret)
157+
self["Secrets"].append(secret)
158+
159+
if capacity_range is not None:
160+
if not isinstance(capacity_range, CapacityRange):
161+
capacity_range = CapacityRange(**capacity_range)
162+
self["CapacityRange"] = capacity_range
163+
164+
if accessibility_requirements is not None:
165+
if not isinstance(accessibility_requirements, AccessibilityRequirement):
166+
accessibility_requirements = AccessibilityRequirement(
167+
**accessibility_requirements
168+
)
169+
self["AccessibilityRequirements"] = accessibility_requirements
170+
171+
172+
class ClusterVolumeSpec(dict):
173+
def __init__(self, group=None, access_mode=None):
174+
if group:
175+
self["Group"] = group
176+
177+
if access_mode:
178+
if not isinstance(access_mode, AccessMode):
179+
raise TypeError("access_mode must be a AccessMode")
180+
self["AccessMode"] = access_mode
181+
182+
@property
183+
def group(self):
184+
return self["Group"]
185+
186+
@property
187+
def access_mode(self):
188+
return self["AccessMode"]

tests/integration/api_volume_test.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@
22

33
import docker
44

5-
from ..helpers import requires_api_version
5+
from ..helpers import force_leave_swarm, requires_api_version
66
from .base import BaseAPIIntegrationTest
77

88

99
class TestVolumes(BaseAPIIntegrationTest):
10+
11+
def setUp(self):
12+
super().setUp()
13+
force_leave_swarm(self.client)
14+
self._unlock_key = None
15+
16+
def tearDown(self):
17+
try:
18+
if self._unlock_key:
19+
self.client.unlock_swarm(self._unlock_key)
20+
except docker.errors.APIError:
21+
pass
22+
force_leave_swarm(self.client)
23+
super().tearDown()
24+
25+
1026
def test_create_volume(self):
1127
name = 'perfectcherryblossom'
1228
self.tmp_volumes.append(name)
@@ -73,3 +89,31 @@ def test_remove_nonexistent_volume(self):
7389
name = 'shootthebullet'
7490
with pytest.raises(docker.errors.NotFound):
7591
self.client.remove_volume(name)
92+
93+
def test_create_volume_with_cluster_volume(self):
94+
name = "perfectcherryblossom"
95+
self.init_swarm()
96+
97+
spec = docker.types.ClusterVolumeSpec(
98+
group="group_test",
99+
access_mode=docker.types.AccessMode(
100+
scope="multi",
101+
sharing="readonly",
102+
mount_volume="mount_volume",
103+
availabilty="active",
104+
secrets=[],
105+
accessibility_requirements={},
106+
capacity_range={},
107+
),
108+
)
109+
110+
result = self.client.create_volume(
111+
name, driver="local", cluster_volume_spec=spec
112+
)
113+
assert "Name" in result
114+
assert result["Name"] == name
115+
assert "Driver" in result
116+
assert result["Driver"] == "local"
117+
assert "ClusterVolume" in result
118+
assert result["ClusterVolume"]["Spec"]["Group"] == "group_test"
119+
assert "AccessMode" in result["ClusterVolume"]["Spec"]

0 commit comments

Comments
 (0)