Skip to content

Commit

Permalink
chore: add ClusterVolumeSpec--class (closes docker#3254)
Browse files Browse the repository at this point in the history
  • Loading branch information
Khushiyant committed Mar 7, 2025
1 parent db7f8b8 commit 25a14c7
Show file tree
Hide file tree
Showing 5 changed files with 440 additions and 5 deletions.
31 changes: 27 additions & 4 deletions docker/api/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ def volumes(self, filters=None):

params = {
'filters': utils.convert_filters(filters) if filters else None
}
}
url = self._url('/volumes')
return self._result(self._get(url, params=params), True)

def create_volume(self, name=None, driver=None, driver_opts=None,
labels=None):
def create_volume(self, name=None, driver=None, driver_opts=None, labels=None, cluster_volume_spec=None):
"""
Create and register a named volume
Expand Down Expand Up @@ -83,11 +82,18 @@ def create_volume(self, name=None, driver=None, driver_opts=None,
if utils.compare_version('1.23', self._version) < 0:
raise errors.InvalidVersion(
'volume labels were introduced in API 1.23'
)
)
if not isinstance(labels, dict):
raise TypeError('labels must be a dictionary')
data["Labels"] = labels

if cluster_volume_spec is not None:
if utils.compare_version("1.42", self._version) < 0:
raise errors.InvalidVersion(
"cluster volume spec was introduced in API 1.45"
)
data["ClusterVolumeSpec"] = cluster_volume_spec

return self._result(self._post_json(url, data=data), True)

def inspect_volume(self, name):
Expand Down Expand Up @@ -161,3 +167,20 @@ def remove_volume(self, name, force=False):
url = self._url('/volumes/{0}', name, params=params)
resp = self._delete(url)
self._raise_for_status(resp)

@utils.minimum_version("1.42")
def update_volume(self, name, version, spec):
"""
Update a volume with a new spec. Valid only for Swarm cluster volumes
Args:
name (str): The volume's name
version (int): The version of the volume to update
spec (ClusterVolumeSpec): The new spec for the volume
"""
url = self._url("/volumes/{0}", name)
data = {
"Spec": spec,
}
return self._result(self._post_json(url, data=data), True)
1 change: 1 addition & 0 deletions docker/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
UpdateConfig,
)
from .swarm import SwarmExternalCA, SwarmSpec
from .volumes import ClusterVolumeSpec, AccessMode, Secret, CapacityRange, AccessibilityRequirement
188 changes: 188 additions & 0 deletions docker/types/volumes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
from .base import DictType
from ..types import Mount

def access_mode_type_error(param, param_value, expected):
return TypeError(
f"Invalid type for {param} param: expected {expected} "
f"but found {type(param_value)}"
)


class CapacityRange(DictType):
def __init__(self, **kwargs):
limit_bytes = kwargs.get("limit_bytes", kwargs.get("LimitBytes"))
required_bytes = kwargs.get("required_bytes", kwargs.get("RequiredBytes"))

if limit_bytes is not None:
if not isinstance(limit_bytes, int):
raise access_mode_type_error("limit_bytes", limit_bytes, "int")
if required_bytes is not None:
if not isinstance(required_bytes, int):
raise access_mode_type_error("required_bytes", required_bytes, "int")

super().__init__({"RequiredBytes": required_bytes, "LimitBytes": limit_bytes})

@property
def limit_bytes(self):
return self["LimitBytes"]

@property
def required_bytes(self):
return self["RequiredBytes"]

@limit_bytes.setter
def limit_bytes(self, value):
if not isinstance(value, int):
raise access_mode_type_error("limit_bytes", value, "int")
self["LimitBytes"] = value

@required_bytes.setter
def required_bytes(self, value):
if not isinstance(value, int):
raise access_mode_type_error("required_bytes", value, "int")
self["RequiredBytes"]


class Secret(DictType):
def __init__(self, **kwargs):
key = kwargs.get("key", kwargs.get("Key"))
secret = kwargs.get("secret", kwargs.get("Secret"))

if key is not None:
if not isinstance(key, str):
raise access_mode_type_error("key", key, "str")
if secret is not None:
if not isinstance(secret, str):
raise access_mode_type_error("secret", secret, "str")

super().__init__({"Key": key, "Secret": secret})

@property
def key(self):
return self["Key"]

@property
def secret(self):
return self["Secret"]

@key.setter
def key(self, value):
if not isinstance(value, str):
raise access_mode_type_error("key", value, "str")
self["Key"] = value

@secret.setter
def secret(self, value):
if not isinstance(value, str):
raise access_mode_type_error("secret", value, "str")
self["Secret"]


class AccessibilityRequirement(DictType):
def __init__(self, **kwargs):
requisite = kwargs.get("requisite", kwargs.get("Requisite"))
preferred = kwargs.get("preferred", kwargs.get("Preferred"))

if requisite is not None:
if not isinstance(requisite, list):
raise access_mode_type_error("requisite", requisite, "list")
self["Requisite"] = requisite

if preferred is not None:
if not isinstance(preferred, list):
raise access_mode_type_error("preferred", preferred, "list")
self["Preferred"] = preferred

super().__init__({"Requisite": requisite, "Preferred": preferred})

@property
def requisite(self):
return self["Requisite"]

@property
def preferred(self):
return self["Preferred"]

@requisite.setter
def requisite(self, value):
if not isinstance(value, list):
raise access_mode_type_error("requisite", value, "list")
self["Requisite"] = value

@preferred.setter
def preferred(self, value):
if not isinstance(value, list):
raise access_mode_type_error("preferred", value, "list")
self["Preferred"] = value


class AccessMode(dict):
def __init__(
self,
scope=None,
sharing=None,
mount_volume=None,
availabilty=None,
secrets=None,
accessibility_requirements=None,
capacity_range=None,
):
if scope is not None:
if not isinstance(scope, str):
raise access_mode_type_error("scope", scope, "str")
self["Scope"] = scope

if sharing is not None:
if not isinstance(sharing, str):
raise access_mode_type_error("sharing", sharing, "str")
self["Sharing"] = sharing

if mount_volume is not None:
if not isinstance(mount_volume, str):
raise access_mode_type_error("mount_volume", mount_volume, "str")
self["MountVolume"] = Mount.parse_mount_string(mount_volume)

if availabilty is not None:
if not isinstance(availabilty, str):
raise access_mode_type_error("availabilty", availabilty, "str")
self["Availabilty"] = availabilty

if secrets is not None:
if not isinstance(secrets, list):
raise access_mode_type_error("secrets", secrets, "list")
self["Secrets"] = []
for secret in secrets:
if not isinstance(secret, Secret):
secret = Secret(**secret)
self["Secrets"].append(secret)

if capacity_range is not None:
if not isinstance(capacity_range, CapacityRange):
capacity_range = CapacityRange(**capacity_range)
self["CapacityRange"] = capacity_range

if accessibility_requirements is not None:
if not isinstance(accessibility_requirements, AccessibilityRequirement):
accessibility_requirements = AccessibilityRequirement(
**accessibility_requirements
)
self["AccessibilityRequirements"] = accessibility_requirements


class ClusterVolumeSpec(dict):
def __init__(self, group=None, access_mode=None):
if group:
self["Group"] = group

if access_mode:
if not isinstance(access_mode, AccessMode):
raise TypeError("access_mode must be a AccessMode")
self["AccessMode"] = access_mode

@property
def group(self):
return self["Group"]

@property
def access_mode(self):
return self["AccessMode"]
46 changes: 45 additions & 1 deletion tests/integration/api_volume_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@

import docker

from ..helpers import requires_api_version
from ..helpers import force_leave_swarm, requires_api_version
from .base import BaseAPIIntegrationTest


class TestVolumes(BaseAPIIntegrationTest):

def setUp(self):
super().setUp()
force_leave_swarm(self.client)
self._unlock_key = None

def tearDown(self):
try:
if self._unlock_key:
self.client.unlock_swarm(self._unlock_key)
except docker.errors.APIError:
pass
force_leave_swarm(self.client)
super().tearDown()


def test_create_volume(self):
name = 'perfectcherryblossom'
self.tmp_volumes.append(name)
Expand Down Expand Up @@ -73,3 +89,31 @@ def test_remove_nonexistent_volume(self):
name = 'shootthebullet'
with pytest.raises(docker.errors.NotFound):
self.client.remove_volume(name)

def test_create_volume_with_cluster_volume(self):
name = "perfectcherryblossom"
self.init_swarm()

spec = docker.types.ClusterVolumeSpec(
group="group_test",
access_mode=docker.types.AccessMode(
scope="multi",
sharing="readonly",
mount_volume="mount_volume",
availabilty="active",
secrets=[],
accessibility_requirements={},
capacity_range={},
),
)

result = self.client.create_volume(
name, driver="local", cluster_volume_spec=spec
)
assert "Name" in result
assert result["Name"] == name
assert "Driver" in result
assert result["Driver"] == "local"
assert "ClusterVolume" in result
assert result["ClusterVolume"]["Spec"]["Group"] == "group_test"
assert "AccessMode" in result["ClusterVolume"]["Spec"]
Loading

0 comments on commit 25a14c7

Please sign in to comment.