Skip to content

Commit 4ce82d5

Browse files
committed
chore: add ClusterVolumeSpec--class (closes #3254)
Signed-off-by: Khushiyant <[email protected]>
1 parent db7f8b8 commit 4ce82d5

File tree

5 files changed

+430
-5
lines changed

5 files changed

+430
-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

+7
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@
2222
UpdateConfig,
2323
)
2424
from .swarm import SwarmExternalCA, SwarmSpec
25+
from .volumes import (
26+
AccessibilityRequirement,
27+
AccessMode,
28+
CapacityRange,
29+
ClusterVolumeSpec,
30+
Secret,
31+
)

docker/types/volumes.py

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