From 5803ed021a4e5e46914ffecaf82aedb8b9b623c8 Mon Sep 17 00:00:00 2001 From: Jenia Peimer Date: Tue, 10 Jun 2025 12:14:08 +0300 Subject: [PATCH 1/6] Update DataVolume resource --- ocp_resources/datavolume.py | 173 ++++++++++++++++++++++-------------- 1 file changed, 108 insertions(+), 65 deletions(-) diff --git a/ocp_resources/datavolume.py b/ocp_resources/datavolume.py index bbf82ec1a4..9348206d6a 100644 --- a/ocp_resources/datavolume.py +++ b/ocp_resources/datavolume.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ocp_resources.utils.constants import ( TIMEOUT_1MINUTE, TIMEOUT_2MINUTES, @@ -7,9 +9,12 @@ ) from ocp_resources.persistent_volume_claim import PersistentVolumeClaim from ocp_resources.resource import NamespacedResource, Resource +from ocp_resources.secret import Secret from timeout_sampler import TimeoutExpiredError, TimeoutSampler from warnings import warn +from typing import Any + class DataVolume(NamespacedResource): """ @@ -70,37 +75,37 @@ class Status(Resource.Condition.Status): def __init__( self, - name=None, - namespace=None, - source=None, - size=None, - storage_class=None, - url=None, - content_type=ContentType.KUBEVIRT, - access_modes=None, - cert_configmap=None, - secret=None, - client=None, - volume_mode=None, - hostpath_node=None, - source_pvc=None, - source_namespace=None, - multus_annotation=None, - bind_immediate_annotation=None, - preallocation=None, - teardown=True, - yaml_file=None, - delete_timeout=TIMEOUT_4MINUTES, - api_name="pvc", - delete_after_completion=None, - **kwargs, - ): + source: str | None = None, + source_dict: dict[str, Any] | None = None, + size: str | None = None, + storage_class: str | None = None, + url: str | None = None, + content_type: str | None = None, + access_modes: str | None = None, + volume_mode: str | None = None, + cert_configmap: str | None = None, + secret: Secret | None = None, + hostpath_node: str | None = None, + source_pvc: str | None = None, + source_namespace: str | None = None, + source_ref_dict: dict[str, Any] | None = None, + source_ref_kind: str | None = None, + source_ref_name: str | None = None, + source_ref_namespace: str | None = None, + multus_annotation: str | None = None, + bind_immediate_annotation: bool | None = None, + preallocation: bool | None = None, + api_name: str = "pvc", + delete_after_completion: str | None = None, + checkpoints: list[Any] | None = None, + final_checkpoint: bool | None = None, + priority_class_name: str | None = None, + **kwargs: Any, + ) -> None: """ DataVolume object Args: - name (str): DataVolume name. - namespace (str): DataVolume namespace. source (str): source of DV - upload/http/pvc/registry. size (str): DataVolume size - format size+size unit, for example: "5Gi". storage_class (str, default: None): storage class name for DataVolume. @@ -124,15 +129,7 @@ def __init__( api_name (str, default: "pvc"): api used for DV, pvc/storage delete_after_completion (str, default: None): annotation for garbage collector - "true"/"false" """ - super().__init__( - name=name, - namespace=namespace, - client=client, - teardown=teardown, - yaml_file=yaml_file, - delete_timeout=delete_timeout, - **kwargs, - ) + super().__init__(**kwargs) self.source = source self.url = url self.cert_configmap = cert_configmap @@ -145,58 +142,104 @@ def __init__( self.hostpath_node = hostpath_node self.source_pvc = source_pvc self.source_namespace = source_namespace + self.source_dict = source_dict + self.source_ref_dict = source_ref_dict + self.source_ref_kind = source_ref_kind + self.source_ref_name = source_ref_name + self.source_ref_namespace = source_ref_namespace self.multus_annotation = multus_annotation self.bind_immediate_annotation = bind_immediate_annotation self.preallocation = preallocation self.api_name = api_name self.delete_after_completion = delete_after_completion + self.checkpoints = checkpoints + self.final_checkpoint = final_checkpoint + self.priority_class_name = priority_class_name def to_dict(self) -> None: super().to_dict() if not self.kind_dict and not self.yaml_file: - self.res.update({ - "spec": { - "source": {self.source: {"url": self.url}}, - self.api_name: { - "resources": {"requests": {"storage": self.size}}, - }, - } - }) - if self.access_modes: - self.res["spec"][self.api_name]["accessModes"] = [self.access_modes] - if self.content_type: - self.res["spec"]["contentType"] = self.content_type - if self.storage_class: - self.res["spec"][self.api_name]["storageClassName"] = self.storage_class - if self.secret: - self.res["spec"]["source"][self.source]["secretRef"] = self.secret.name - if self.volume_mode: - self.res["spec"][self.api_name]["volumeMode"] = self.volume_mode + self.res["spec"] = {} + _spec = self.res["spec"] + + if self.checkpoints is not None: + _spec["checkpoints"] = self.checkpoints + + if self.content_type is not None: + _spec["contentType"] = self.content_type + + if self.final_checkpoint is not None: + _spec["finalCheckpoint"] = self.final_checkpoint + + if self.preallocation is not None: + _spec["preallocation"] = self.preallocation + + if self.priority_class_name is not None: + _spec["priorityClassName"] = self.priority_class_name + + if self.access_modes is not None: + _spec[self.api_name]["accessModes"] = [self.access_modes] + + if self.volume_mode is not None: + _spec[self.api_name]["volumeMode"] = self.volume_mode + + if self.storage_class is not None: + _spec[self.api_name]["storageClassName"] = self.storage_class + + if self.size is not None: + _spec[self.api_name]["resources"]["requests"]["storage"] = self.storage_class + + if self.content_type is not None: + _spec["contentType"] = self.content_type + + if self.source_dict is not None: + _spec["source"] = self.source_dict + if self.source == "http" or "registry": - self.res["spec"]["source"][self.source]["url"] = self.url - if self.cert_configmap: - self.res["spec"]["source"][self.source]["certConfigMap"] = self.cert_configmap + _spec["source"][self.source]["url"] = self.url + if self.source == "upload" or self.source == "blank": - self.res["spec"]["source"][self.source] = {} + _spec["source"][self.source] = {} + + if self.source == "pvc": + _spec["source"][self.source] = { + "name": self.source_pvc, + "namespace": self.source_namespace, + } + + if self.secret is not None: + _spec["source"][self.source]["secretRef"] = self.secret.name + + if self.cert_configmap is not None: + _spec["source"][self.source]["certConfigMap"] = self.cert_configmap + + if self.source_ref_dict is not None: + _spec["sourceRef"] = self.source_ref_dict + + if self.source_ref_kind is not None: + _spec["sourceRef"]["kind"] = self.source_ref_kind + + if self.source_ref_name is not None: + _spec["sourceRef"]["name"] = self.source_ref_name + + if self.source_ref_namespace is not None: + _spec["sourceRef"]["namespace"] = self.source_ref_namespace + if self.hostpath_node: self.res["metadata"].setdefault("annotations", {}).update({ f"{NamespacedResource.ApiGroup.KUBEVIRT_IO}/provisionOnNode": (self.hostpath_node) }) + if self.multus_annotation: self.res["metadata"].setdefault("annotations", {}).update({ f"{NamespacedResource.ApiGroup.K8S_V1_CNI_CNCF_IO}/networks": (self.multus_annotation) }) + if self.bind_immediate_annotation: self.res["metadata"].setdefault("annotations", {}).update({ f"{self.api_group}/storage.bind.immediate.requested": "true" }) - if self.source == "pvc": - self.res["spec"]["source"]["pvc"] = { - "name": self.source_pvc or "dv-source", - "namespace": self.source_namespace or self.namespace, - } - if self.preallocation is not None: - self.res["spec"]["preallocation"] = self.preallocation + if self.delete_after_completion: self.res["metadata"].setdefault("annotations", {}).update({ f"{self.api_group}/storage.deleteAfterCompletion": (self.delete_after_completion) From 154fe5df969a9d19dd764c50e88afac4cf57b84c Mon Sep 17 00:00:00 2001 From: Jenia Peimer Date: Sun, 3 Aug 2025 15:03:28 +0300 Subject: [PATCH 2/6] keep only source_ref --- ocp_resources/datavolume.py | 59 ++++++++++++------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/ocp_resources/datavolume.py b/ocp_resources/datavolume.py index 9348206d6a..bc07aad991 100644 --- a/ocp_resources/datavolume.py +++ b/ocp_resources/datavolume.py @@ -88,15 +88,11 @@ def __init__( hostpath_node: str | None = None, source_pvc: str | None = None, source_namespace: str | None = None, - source_ref_dict: dict[str, Any] | None = None, - source_ref_kind: str | None = None, - source_ref_name: str | None = None, - source_ref_namespace: str | None = None, + source_ref: dict[str, Any] | None = None, multus_annotation: str | None = None, bind_immediate_annotation: bool | None = None, preallocation: bool | None = None, api_name: str = "pvc", - delete_after_completion: str | None = None, checkpoints: list[Any] | None = None, final_checkpoint: bool | None = None, priority_class_name: str | None = None, @@ -106,28 +102,29 @@ def __init__( DataVolume object Args: - source (str): source of DV - upload/http/pvc/registry. - size (str): DataVolume size - format size+size unit, for example: "5Gi". + source (str, default: None): source of DV - upload/http/pvc/registry/blank. + source_dict (dict[str, Any], default: None): DataVolume.source dictionary. + size (str, default: None): DataVolume size - format size+size unit, for example: "5Gi". storage_class (str, default: None): storage class name for DataVolume. url (str, default: None): url for importing DV, when source is http/registry. - content_type (str, default: "kubevirt"): DataVolume content type. - access_modes (str, default: None): DataVolume access mode. + content_type (str, default: None): DataVolume content type (e.g., "kubevirt", "archive"). + access_modes (str, default: None): DataVolume access mode (e.g., "ReadWriteOnce", "ReadWriteMany"). + volume_mode (str, default: None): DataVolume volume mode (e.g., "Filesystem", "Block"). cert_configmap (str, default: None): name of config map for TLS certificates. secret (Secret, default: None): to be set as secretRef. - client (DynamicClient): DynamicClient to use. - volume_mode (str, default: None): DataVolume volume mode. hostpath_node (str, default: None): Node name to provision the DV on. source_pvc (str, default: None): PVC name for when cloning the DV. source_namespace (str, default: None): PVC namespace for when cloning the DV. + source_ref (dict[str, Any], default: None): SourceRef is an indirect reference to the source of data for the + requested DataVolume. Currently only "DataSource" is supported. Fields: kind (str), name (str), namespace (str) multus_annotation (str, default: None): network nad name. - bind_immediate_annotation (bool, default: None): when WaitForFirstConsumer is set in StorageClass and DV - should be bound immediately. + bind_immediate_annotation (bool, default: None): when WaitForFirstConsumer is set in StorageClass and DV + should be bound immediately. preallocation (bool, default: None): preallocate disk space. - teardown (bool, default: True): Indicates if this resource would need to be deleted. - yaml_file (yaml, default: None): yaml file for the resource. - delete_timeout (int, default: 4 minutes): timeout associated with delete action. - api_name (str, default: "pvc"): api used for DV, pvc/storage - delete_after_completion (str, default: None): annotation for garbage collector - "true"/"false" + api_name (str, default: "pvc"): api used for DV (e.g., "storage", "pvc"). + checkpoints (list[Any], default: None): list of DataVolumeCheckpoints for snapshot operations. + final_checkpoint (bool, default: None): indicates whether the current DataVolumeCheckpoint is the final one. + priority_class_name (str, default: None): priority class name for the DataVolume pod. """ super().__init__(**kwargs) self.source = source @@ -143,15 +140,11 @@ def __init__( self.source_pvc = source_pvc self.source_namespace = source_namespace self.source_dict = source_dict - self.source_ref_dict = source_ref_dict - self.source_ref_kind = source_ref_kind - self.source_ref_name = source_ref_name - self.source_ref_namespace = source_ref_namespace + self.source_ref = source_ref self.multus_annotation = multus_annotation self.bind_immediate_annotation = bind_immediate_annotation self.preallocation = preallocation self.api_name = api_name - self.delete_after_completion = delete_after_completion self.checkpoints = checkpoints self.final_checkpoint = final_checkpoint self.priority_class_name = priority_class_name @@ -187,7 +180,7 @@ def to_dict(self) -> None: _spec[self.api_name]["storageClassName"] = self.storage_class if self.size is not None: - _spec[self.api_name]["resources"]["requests"]["storage"] = self.storage_class + _spec[self.api_name]["resources"]["requests"]["storage"] = self.size if self.content_type is not None: _spec["contentType"] = self.content_type @@ -213,17 +206,8 @@ def to_dict(self) -> None: if self.cert_configmap is not None: _spec["source"][self.source]["certConfigMap"] = self.cert_configmap - if self.source_ref_dict is not None: - _spec["sourceRef"] = self.source_ref_dict - - if self.source_ref_kind is not None: - _spec["sourceRef"]["kind"] = self.source_ref_kind - - if self.source_ref_name is not None: - _spec["sourceRef"]["name"] = self.source_ref_name - - if self.source_ref_namespace is not None: - _spec["sourceRef"]["namespace"] = self.source_ref_namespace + if self.source_ref is not None: + _spec["sourceRef"] = self.source_ref if self.hostpath_node: self.res["metadata"].setdefault("annotations", {}).update({ @@ -240,11 +224,6 @@ def to_dict(self) -> None: f"{self.api_group}/storage.bind.immediate.requested": "true" }) - if self.delete_after_completion: - self.res["metadata"].setdefault("annotations", {}).update({ - f"{self.api_group}/storage.deleteAfterCompletion": (self.delete_after_completion) - }) - def wait_deleted(self, timeout=TIMEOUT_4MINUTES): """ Wait until DataVolume and the PVC created by it are deleted From 657517a6a61b746107bd4b5d57acd4faed2fe9b0 Mon Sep 17 00:00:00 2001 From: Jenia Peimer Date: Sun, 3 Aug 2025 16:07:03 +0300 Subject: [PATCH 3/6] address comments --- ocp_resources/datavolume.py | 60 +++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/ocp_resources/datavolume.py b/ocp_resources/datavolume.py index bc07aad991..14a7bf1108 100644 --- a/ocp_resources/datavolume.py +++ b/ocp_resources/datavolume.py @@ -92,7 +92,7 @@ def __init__( multus_annotation: str | None = None, bind_immediate_annotation: bool | None = None, preallocation: bool | None = None, - api_name: str = "pvc", + api_name: str | None = "pvc", checkpoints: list[Any] | None = None, final_checkpoint: bool | None = None, priority_class_name: str | None = None, @@ -170,41 +170,43 @@ def to_dict(self) -> None: if self.priority_class_name is not None: _spec["priorityClassName"] = self.priority_class_name - if self.access_modes is not None: - _spec[self.api_name]["accessModes"] = [self.access_modes] + # Set api_name spec fields (pvc/storage) + if self.api_name is not None: + _spec[self.api_name] = {} - if self.volume_mode is not None: - _spec[self.api_name]["volumeMode"] = self.volume_mode + if self.access_modes is not None: + _spec[self.api_name]["accessModes"] = [self.access_modes] - if self.storage_class is not None: - _spec[self.api_name]["storageClassName"] = self.storage_class + if self.volume_mode is not None: + _spec[self.api_name]["volumeMode"] = self.volume_mode - if self.size is not None: - _spec[self.api_name]["resources"]["requests"]["storage"] = self.size + if self.storage_class is not None: + _spec[self.api_name]["storageClassName"] = self.storage_class - if self.content_type is not None: - _spec["contentType"] = self.content_type + if self.size is not None: + _spec[self.api_name]["resources"] = {"requests": {"storage": self.size}} + # Handle source configuration if self.source_dict is not None: _spec["source"] = self.source_dict - - if self.source == "http" or "registry": - _spec["source"][self.source]["url"] = self.url - - if self.source == "upload" or self.source == "blank": - _spec["source"][self.source] = {} - - if self.source == "pvc": - _spec["source"][self.source] = { - "name": self.source_pvc, - "namespace": self.source_namespace, - } - - if self.secret is not None: - _spec["source"][self.source]["secretRef"] = self.secret.name - - if self.cert_configmap is not None: - _spec["source"][self.source]["certConfigMap"] = self.cert_configmap + elif self.source is not None: + _spec["source"] = {} + source_spec = _spec["source"] + + if self.source in ["http", "registry"]: + source_spec[self.source] = {"url": self.url} + elif self.source in ["upload", "blank"]: + source_spec[self.source] = {} + elif self.source == "pvc": + source_spec[self.source] = { + "name": self.source_pvc, + "namespace": self.source_namespace, + } + + if self.secret is not None: + source_spec[self.source]["secretRef"] = self.secret.name + if self.cert_configmap is not None: + source_spec[self.source]["certConfigMap"] = self.cert_configmap if self.source_ref is not None: _spec["sourceRef"] = self.source_ref From 2b68eb489c0de6e3bc423dc736691541641e65a5 Mon Sep 17 00:00:00 2001 From: Jenia Peimer Date: Sun, 10 Aug 2025 15:26:18 +0300 Subject: [PATCH 4/6] bring back self.namespace to pvc source --- ocp_resources/datavolume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocp_resources/datavolume.py b/ocp_resources/datavolume.py index 14a7bf1108..f474409d0b 100644 --- a/ocp_resources/datavolume.py +++ b/ocp_resources/datavolume.py @@ -200,7 +200,7 @@ def to_dict(self) -> None: elif self.source == "pvc": source_spec[self.source] = { "name": self.source_pvc, - "namespace": self.source_namespace, + "namespace": self.source_namespace or self.namespace, } if self.secret is not None: From 0f88c06d97aaca19c4c4d5aeeb84619d5a4666aa Mon Sep 17 00:00:00 2001 From: Jenia Peimer Date: Thu, 14 Aug 2025 12:22:26 +0300 Subject: [PATCH 5/6] fix import secret --- ocp_resources/datavolume.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ocp_resources/datavolume.py b/ocp_resources/datavolume.py index f474409d0b..6a346de14e 100644 --- a/ocp_resources/datavolume.py +++ b/ocp_resources/datavolume.py @@ -9,11 +9,13 @@ ) from ocp_resources.persistent_volume_claim import PersistentVolumeClaim from ocp_resources.resource import NamespacedResource, Resource -from ocp_resources.secret import Secret from timeout_sampler import TimeoutExpiredError, TimeoutSampler from warnings import warn -from typing import Any +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from ocp_resources.secret import Secret class DataVolume(NamespacedResource): From a4344631b0055f4efa2027a3d6405caa774d7130 Mon Sep 17 00:00:00 2001 From: Jenia Peimer Date: Thu, 14 Aug 2025 12:40:56 +0300 Subject: [PATCH 6/6] add deprecation warning --- ocp_resources/datavolume.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ocp_resources/datavolume.py b/ocp_resources/datavolume.py index 6a346de14e..8e0dbb9be0 100644 --- a/ocp_resources/datavolume.py +++ b/ocp_resources/datavolume.py @@ -192,6 +192,12 @@ def to_dict(self) -> None: if self.source_dict is not None: _spec["source"] = self.source_dict elif self.source is not None: + warn( + "source is deprecated and will be removed in the next version. Use source_dict instead.", + DeprecationWarning, + stacklevel=2, + ) + _spec["source"] = {} source_spec = _spec["source"]