Skip to content

Add support for ClusterVolumeSpec in create_volume api #3318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 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
7 changes: 7 additions & 0 deletions docker/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@
UpdateConfig,
)
from .swarm import SwarmExternalCA, SwarmSpec
from .volumes import (
AccessibilityRequirement,
AccessMode,
CapacityRange,
ClusterVolumeSpec,
Secret,
)
189 changes: 189 additions & 0 deletions docker/types/volumes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
from ..types import Mount
from .base import DictType


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
Loading