From 842fc9a354fb64b0c6d416e4f61c183927068cf3 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Mon, 26 Aug 2019 14:51:14 +0200 Subject: [PATCH 1/7] add PersistentVolume & PersistentVolumeClaim objects & methods --- kubetest/client.py | 110 +++++++++++++++++++++- kubetest/manager.py | 4 + kubetest/objects/__init__.py | 2 + kubetest/objects/persistentvolume.py | 100 ++++++++++++++++++++ kubetest/objects/persistentvolumeclaim.py | 110 ++++++++++++++++++++++ 5 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 kubetest/objects/persistentvolume.py create mode 100644 kubetest/objects/persistentvolumeclaim.py diff --git a/kubetest/client.py b/kubetest/client.py index 01fd481..9b5f022 100644 --- a/kubetest/client.py +++ b/kubetest/client.py @@ -22,7 +22,8 @@ class TestClient: and provided to the TestClient during the test setup process. Args: - namespace (str): The namespace associated with the test client. + namespace (str): The namespace associated with the test + client. Each test case will have its own namespace assigned. """ @@ -215,6 +216,48 @@ def load_service(self, path, set_namespace=True): service.namespace = self.namespace return service + def load_PersistentVolume(self, path, set_namespace=False): + """Load a manifest YAML into a Service object. + + By default, this will augment the Service object with + the generated test case namespace. This behavior can be + disabled with the ``set_namespace`` flag. + + Args: + path (str): The path to the Service manifest. + set_namespace (bool): Enable/disable the automatic + augmentation of the Service namespace. + + Returns: + objects.Service: The Service for the specified manifest. + """ + log.info('loading persistentvolume from path: %s', path) + persistentvolume = objects.PersistentVolume.load(path) + if set_namespace: + persistentvolume.namespace = self.namespace + return persistentvolume + + def load_persistentvolumeclaim(self, path, set_namespace=True): + """Load a manifest YAML into a Service object. + + By default, this will augment the Service object with + the generated test case namespace. This behavior can be + disabled with the ``set_namespace`` flag. + + Args: + path (str): The path to the Service manifest. + set_namespace (bool): Enable/disable the automatic + augmentation of the Service namespace. + + Returns: + objects.Service: The Service for the specified manifest. + """ + log.info('loading persistentvolumeclaim from path: %s', path) + persistentvolumeclaim = objects.PersistentVolumeClaim.load(path) + if set_namespace: + persistentvolumeclaim.namespace = self.namespace + return persistentvolumeclaim + # ****** Generic Helpers on ApiObjects ****** def create(self, obj): @@ -505,6 +548,71 @@ def get_services(self, namespace=None, fields=None, labels=None): return services + def get_persistentvolume(self, fields=None, labels=None): + """Get PersistentVolume from the cluster. + + Args: + fields (dict[str, str]): A dictionary of fields used to restrict + the returned collection of PersistentVolume to only those which match + these field selectors. By default, no restricting is done. + labels (dict[str, str]): A dictionary of labels used to restrict + the returned collection of PersistentVolume to only those which match + these label selectors. By default, no restricting is done. + + Returns: + dict[str, objects.PersistentVolume]: A dictionary where the key is + the PersistentVolume name and the value is the PersistentVolume itself. + """ + selectors = utils.selector_kwargs(fields, labels) + + persistentvolume_list = client.CoreV1Api().list_persistent_volume( + **selectors + ) + + persistentvolumes = {} + for obj in persistentvolume_list.items: + persistentvolume = objects.PersistentVolume(obj) + persistentvolumes[persistentvolume.name] = persistentvolume + + return persistentvolumes + + def get_persistentvolumeclaim(self, namespace=None, fields=None, labels=None): + """Get PersistentVolumeClaim from the cluster. + + Args: + namespace (str): The namespace to get the PersistentVolumeClaim from. If not + specified, it will use the auto-generated test case namespace + by default. + fields (dict[str, str]): A dictionary of fields used to restrict + the returned collection of PersistentVolumeClaim to only those which match + these field selectors. By default, no restricting is done. + labels (dict[str, str]): A dictionary of labels used to restrict + the returned collection of PersistentVolumeClaim to only those which match + these label selectors. By default, no restricting is done. + + Returns: + dict[str, objects.PersistentVolumeClaim]: A dictionary where the key is + the PersistentVolumeClaim name and the value is the PersistentVolumeClaim + itself. + """ + if namespace is None: + namespace = self.namespace + + selectors = utils.selector_kwargs(fields, labels) + + persistentvolumeclaim_list = client.CoreV1Api().\ + list_namespaced_persistent_volume_claim( + namespace=namespace, + **selectors, + ) + + persistentvolumeclaims = {} + for obj in persistentvolumeclaim_list.items: + persistentvolumeclaim = objects.PersistentVolumeClaim(obj) + persistentvolumeclaims[persistentvolumeclaim.name] = persistentvolumeclaim + + return persistentvolumeclaims + @staticmethod def get_nodes(fields=None, labels=None): """Get the Nodes that make up the cluster. diff --git a/kubetest/manager.py b/kubetest/manager.py index 286a1b0..2f2cb4e 100644 --- a/kubetest/manager.py +++ b/kubetest/manager.py @@ -32,6 +32,8 @@ class ObjectManager: 'secret', 'service', 'configmap', + 'persistentvolume', + 'persistentvolumeclaim', 'daemonset', 'statefulset', 'deployment', @@ -103,6 +105,8 @@ def get_objects_in_apply_order(self): - Secret - Service - ConfigMap + - PersistentVolume + - PersistentVolumeClaim - DaemonSet - StatefulSet - Deployment diff --git a/kubetest/objects/__init__.py b/kubetest/objects/__init__.py index 76ef2bc..d371b52 100644 --- a/kubetest/objects/__init__.py +++ b/kubetest/objects/__init__.py @@ -12,6 +12,8 @@ from .event import Event from .namespace import Namespace from .node import Node +from .persistentvolume import PersistentVolume +from .persistentvolumeclaim import PersistentVolumeClaim from .pod import Pod from .rolebinding import RoleBinding from .secret import Secret diff --git a/kubetest/objects/persistentvolume.py b/kubetest/objects/persistentvolume.py new file mode 100644 index 0000000..0f4a928 --- /dev/null +++ b/kubetest/objects/persistentvolume.py @@ -0,0 +1,100 @@ +"""Kubetest wrapper for the Kubernetes ``PersistentVolume`` API Object.""" + +import logging + +from kubernetes import client + +from .api_object import ApiObject + +log = logging.getLogger('kubetest') + + +class PersistentVolume(ApiObject): + """Kubetest wrapper around a Kubernetes `PersistentVolume`_ API Object. + + The actual ``kubernetes.client.V1PersistentVolume`` instance that this + wraps can be accessed via the ``obj`` instance member. + + This wrapper provides some convenient functionality around the + API Object and provides some state management for the `PersistentVolume`_. + + .. _PersistentVolume: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#persistentvolume-v1-core + """ + + obj_type = client.V1PersistentVolume + + api_clients = { + 'preferred': client.CoreV1Api, + 'v1': client.CoreV1Api, + } + + def __str__(self): + return str(self.obj) + + def __repr__(self): + return self.__str__() + + def create(self): + """Create the PersistentVolume under the given namespace. + + Args: + namespace (str): The namespace to create the PersistentVolume under. + If the PersistentVolume was loaded via the kubetest client, the + namespace will already be set, so it is not needed here. + Otherwise, the namespace will need to be provided. + """ + log.info('creating persistentvolume "%s"', self.name) + log.debug('persistentvolume: %s', self.obj) + + self.obj = self.api_client.create_persistent_volume( + body=self.obj, + ) + + def delete(self, options): + """Delete the PersistentVolume. + + This method expects the PersistentVolume to have been loaded or otherwise + assigned a namespace already. If it has not, the namespace will need + to be set manually. + + Args: + options (client.V1DeleteOptions): Options for PersistentVolume deletion. + + Returns: + client.V1Status: The status of the delete operation. + """ + if options is None: + options = client.V1DeleteOptions() + + log.info('deleting persistentvolume "%s"', self.name) + log.debug('delete options: %s', options) + log.debug('persistentvolume: %s', self.obj) + + return self.api_client.delete_persistent_volume( + name=self.name, + body=options, + ) + + def refresh(self): + """Refresh the underlying Kubernetes PersistentVolume resource.""" + self.obj = self.api_client.read_persistent_volume( + name=self.name, + ) + + def is_ready(self): + """Check if the PersistentVolume is in the ready state. + + PersistentVolumes do not have a "status" field to check, so we will + measure their readiness status by whether or not they exist + on the cluster. + + Returns: + bool: True if in the ready state; False otherwise. + """ + try: + self.refresh() + except: # noqa + return False + else: + return True diff --git a/kubetest/objects/persistentvolumeclaim.py b/kubetest/objects/persistentvolumeclaim.py new file mode 100644 index 0000000..e7bc9a1 --- /dev/null +++ b/kubetest/objects/persistentvolumeclaim.py @@ -0,0 +1,110 @@ +"""Kubetest wrapper for the Kubernetes ``PersistentVolumeClaim`` API Object.""" + +import logging + +from kubernetes import client + +from .api_object import ApiObject + +log = logging.getLogger('kubetest') + + +class PersistentVolumeClaim(ApiObject): + """Kubetest wrapper around a Kubernetes `PersistentVolumeClaim`_ API Object. + + The actual ``kubernetes.client.V1PersistentVolumeClaim`` instance that this + wraps can be accessed via the ``obj`` instance member. + + This wrapper provides some convenient functionality around the + API Object and provides some state management for the `PersistentVolumeClaim`_. + + .. _PersistentVolumeClaim: + https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#persistentvolumeclaim-v1-core + """ + + obj_type = client.V1PersistentVolumeClaim + + api_clients = { + 'preferred': client.CoreV1Api, + 'v1': client.CoreV1Api, + } + + def __str__(self): + return str(self.obj) + + def __repr__(self): + return self.__str__() + + def create(self, namespace=None): + """Create the PersistentVolumeClaim under the given namespace. + + Args: + namespace (str): The namespace to create the PersistentVolumeClaim under. + If the PersistentVolumeClaim was loaded via the kubetest client, the + namespace will already be set, so it is not needed here. + Otherwise, the namespace will need to be provided. + """ + if namespace is None: + namespace = self.namespace + + log.info( + 'creating persistentvolumeclaim "%s" in namespace "%s"', + self.name, + self.namespace + ) + log.debug('persistentvolumeclaim: %s', self.obj) + + self.obj = self.api_client.create_namespaced_persistent_volume_claim( + namespace=namespace, + body=self.obj, + ) + + def delete(self, options): + """Delete the PersistentVolumeClaim. + + This method expects the PersistentVolumeClaim to have been loaded or otherwise + assigned a namespace already. If it has not, the namespace will need + to be set manually. + + Args: + options (client.V1DeleteOptions): Options for PersistentVolumeClaim deletion. + + Returns: + client.V1Status: The status of the delete operation. + """ + if options is None: + options = client.V1DeleteOptions() + + log.info('deleting persistentvolumeclaim "%s"', self.name) + log.debug('delete options: %s', options) + log.debug('persistentvolumeclaim: %s', self.obj) + + return self.api_client.delete_namespaced_persistent_volume_claim( + name=self.name, + namespace=self.namespace, + body=options, + ) + + def refresh(self): + """Refresh the underlying Kubernetes PersistentVolumeClaim resource.""" + self.obj = self.api_client.read_namespaced_persistent_volume_claim( + name=self.name, + namespace=self.namespace, + ) + + def is_ready(self): + """Check if the PersistentVolumeClaim is in the ready state. + + PersistentVolumeClaims do not have a "status" field to check, so we will + measure their readiness status by whether or not they exist + on the cluster. + + Returns: + bool: True if in the ready state; False otherwise. + """ + try: + self.refresh() + except: # noqa + return False + else: + return True From 37c78d6a21d415aafa263f254dd1643d92377558 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Thu, 29 Aug 2019 15:17:26 +0200 Subject: [PATCH 2/7] add tests for PV and PVC --- tests/data/simple-persistentvolume.yaml | 20 +++++++++++++++ tests/data/simple-persistentvolumeclaim.yaml | 10 ++++++++ tests/test_pv_pvc.py | 26 ++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 tests/data/simple-persistentvolume.yaml create mode 100644 tests/data/simple-persistentvolumeclaim.yaml create mode 100644 tests/test_pv_pvc.py diff --git a/tests/data/simple-persistentvolume.yaml b/tests/data/simple-persistentvolume.yaml new file mode 100644 index 0000000..3ea5151 --- /dev/null +++ b/tests/data/simple-persistentvolume.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: my-pv +spec: + capacity: + storage: 16Mi + accessModes: + - ReadWriteMany + local: + path: /tmp/vol1 + persistentVolumeReclaimPolicy: Recycle + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - my-node diff --git a/tests/data/simple-persistentvolumeclaim.yaml b/tests/data/simple-persistentvolumeclaim.yaml new file mode 100644 index 0000000..659739a --- /dev/null +++ b/tests/data/simple-persistentvolumeclaim.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: my-pvc +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 16Mi diff --git a/tests/test_pv_pvc.py b/tests/test_pv_pvc.py new file mode 100644 index 0000000..0b9b1e2 --- /dev/null +++ b/tests/test_pv_pvc.py @@ -0,0 +1,26 @@ +"""An example of using kubetest to manage a persistentvolume and persistentvolumeclaim.""" + +import os +import pytest +import time + +@pytest.mark.applymanifests('data', files=[ + 'simple-persistentvolume.yaml', + 'simple-persistentvolumeclaim.yaml', +]) + +def test_pv_pvc(kube): + + # Wait for the objects registered via marker to be ready. + kube.wait_for_registered(timeout=120) + time.sleep(10) + +## PERSISTENT VOLUME + # get persistent volume + pv = kube.get_persistentvolume() + assert "my-pv" in pv + +## PERSISTENT VOLUME CLAIM + # get persistent volume claim + pvc = kube.get_persistentvolumeclaim() + assert "my-pvc" in pvc From 58e6eaeed4874e9927f4433ebb6f3e82adc428d1 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Mon, 2 Sep 2019 09:08:03 +0200 Subject: [PATCH 3/7] fixing lint issue --- examples/test_deployment.py | 2 -- tests/test_pv_pvc.py | 12 ++++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/test_deployment.py b/examples/test_deployment.py index 1d2f88b..4c08de7 100644 --- a/examples/test_deployment.py +++ b/examples/test_deployment.py @@ -2,8 +2,6 @@ import os -import time - def test_deployment(kube): diff --git a/tests/test_pv_pvc.py b/tests/test_pv_pvc.py index 0b9b1e2..2ccf860 100644 --- a/tests/test_pv_pvc.py +++ b/tests/test_pv_pvc.py @@ -1,26 +1,22 @@ """An example of using kubetest to manage a persistentvolume and persistentvolumeclaim.""" -import os -import pytest import time +import pytest + + @pytest.mark.applymanifests('data', files=[ 'simple-persistentvolume.yaml', 'simple-persistentvolumeclaim.yaml', ]) - def test_pv_pvc(kube): + """Test pv and pvc methods""" - # Wait for the objects registered via marker to be ready. kube.wait_for_registered(timeout=120) time.sleep(10) -## PERSISTENT VOLUME - # get persistent volume pv = kube.get_persistentvolume() assert "my-pv" in pv -## PERSISTENT VOLUME CLAIM - # get persistent volume claim pvc = kube.get_persistentvolumeclaim() assert "my-pvc" in pvc From 287b30b54aab5c745f5976201ce170571fc6e028 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Mon, 2 Sep 2019 11:09:03 +0200 Subject: [PATCH 4/7] testing PV and PVC in test_manifest.py --- tests/test_manifest.py | 40 ++++++++++++++++++++++++++++++++++++++++ tests/test_pv_pvc.py | 22 ---------------------- 2 files changed, 40 insertions(+), 22 deletions(-) delete mode 100644 tests/test_pv_pvc.py diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 39d36b6..cacebdc 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -155,6 +155,46 @@ def test_simple_service_wrong_type(self, manifest_dir): os.path.join(manifest_dir, 'simple-service.yaml') ) + def test_simple_persistentvolume_ok(self, manifest_dir, simple_persistentvolume): + """Test loading the simple persistentvolume successfully.""" + obj = manifest.load_type( + client.V1PersistentVolume, + os.path.join(manifest_dir, 'simple-persistentvolume.yaml') + ) + assert obj == simple_persistentvolume + + def test_simple_persistentvolume_wrong_type(self, manifest_dir): + """Test loading the simple persistentvolume to the wrong type.""" + with pytest.raises(ValueError): + # The V1Container requires a name -- since the manifest has no name, + # it will cause V1Container construction to fail with ValueError. + manifest.load_type( + client.V1Container, + os.path.join(manifest_dir, 'simple-persistentvolume.yaml') + ) + + def test_simple_persistentvolumeclaim_ok( + self, + manifest_dir, + simple_persistentvolumeclaim + ): + """Test loading the simple persistentvolumeclaim successfully.""" + obj = manifest.load_type( + client.V1PersistentVolumeClaim, + os.path.join(manifest_dir, 'simple-persistentvolumeclaim.yaml') + ) + assert obj == simple_persistentvolumeclaim + + def test_simple_persistentvolumeclaim_wrong_type(self, manifest_dir): + """Test loading the simple persistentvolumeclaim to the wrong type.""" + with pytest.raises(ValueError): + # The V1Container requires a name -- since the manifest has no name, + # it will cause V1Container construction to fail with ValueError. + manifest.load_type( + client.V1Container, + os.path.join(manifest_dir, 'simple-persistentvolumeclaim.yaml') + ) + def test_bad_path(self, manifest_dir): """Test specifying an invalid manifest path.""" with pytest.raises(FileNotFoundError): diff --git a/tests/test_pv_pvc.py b/tests/test_pv_pvc.py deleted file mode 100644 index 2ccf860..0000000 --- a/tests/test_pv_pvc.py +++ /dev/null @@ -1,22 +0,0 @@ -"""An example of using kubetest to manage a persistentvolume and persistentvolumeclaim.""" - -import time - -import pytest - - -@pytest.mark.applymanifests('data', files=[ - 'simple-persistentvolume.yaml', - 'simple-persistentvolumeclaim.yaml', -]) -def test_pv_pvc(kube): - """Test pv and pvc methods""" - - kube.wait_for_registered(timeout=120) - time.sleep(10) - - pv = kube.get_persistentvolume() - assert "my-pv" in pv - - pvc = kube.get_persistentvolumeclaim() - assert "my-pvc" in pvc From e3e87586b470e071a3797c63bb597f87079741c9 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Mon, 2 Sep 2019 14:04:16 +0200 Subject: [PATCH 5/7] create fixture for testing pv/pvc --- kubetest/client.py | 25 ++++----- kubetest/objects/persistentvolume.py | 18 ++----- kubetest/objects/persistentvolumeclaim.py | 7 +-- tests/conftest.py | 64 +++++++++++++++++++++++ tests/data/simple-persistentvolume.yaml | 2 +- 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/kubetest/client.py b/kubetest/client.py index 9b5f022..fc9cb7e 100644 --- a/kubetest/client.py +++ b/kubetest/client.py @@ -216,20 +216,20 @@ def load_service(self, path, set_namespace=True): service.namespace = self.namespace return service - def load_PersistentVolume(self, path, set_namespace=False): - """Load a manifest YAML into a Service object. + def load_persistentvolume(self, path, set_namespace=False): + """Load a manifest YAML into a PersistentVolume object. - By default, this will augment the Service object with + By default, this will not augment the PersistentVolume object with the generated test case namespace. This behavior can be - disabled with the ``set_namespace`` flag. + enabled with the ``set_namespace`` flag. Args: - path (str): The path to the Service manifest. + path (str): The path to the PersistentVolume manifest. set_namespace (bool): Enable/disable the automatic - augmentation of the Service namespace. + augmentation of the PersistentVolume namespace. Returns: - objects.Service: The Service for the specified manifest. + objects.PersistentVolume: The PersistentVolume for the specified manifest. """ log.info('loading persistentvolume from path: %s', path) persistentvolume = objects.PersistentVolume.load(path) @@ -238,19 +238,20 @@ def load_PersistentVolume(self, path, set_namespace=False): return persistentvolume def load_persistentvolumeclaim(self, path, set_namespace=True): - """Load a manifest YAML into a Service object. + """Load a manifest YAML into a PersistentVolumeClaim object. - By default, this will augment the Service object with + By default, this will augment the PersistentVolumeClaim object with the generated test case namespace. This behavior can be disabled with the ``set_namespace`` flag. Args: - path (str): The path to the Service manifest. + path (str): The path to the PersistentVolumeClaim manifest. set_namespace (bool): Enable/disable the automatic - augmentation of the Service namespace. + augmentation of the PersistentVolumeClaim namespace. Returns: - objects.Service: The Service for the specified manifest. + objects.PersistentVolumeClaim: The PersistentVolumeClaim for the specified + manifest. """ log.info('loading persistentvolumeclaim from path: %s', path) persistentvolumeclaim = objects.PersistentVolumeClaim.load(path) diff --git a/kubetest/objects/persistentvolume.py b/kubetest/objects/persistentvolume.py index 0f4a928..5fec2de 100644 --- a/kubetest/objects/persistentvolume.py +++ b/kubetest/objects/persistentvolume.py @@ -36,13 +36,8 @@ def __repr__(self): return self.__str__() def create(self): - """Create the PersistentVolume under the given namespace. + """Create the PersistentVolume. - Args: - namespace (str): The namespace to create the PersistentVolume under. - If the PersistentVolume was loaded via the kubetest client, the - namespace will already be set, so it is not needed here. - Otherwise, the namespace will need to be provided. """ log.info('creating persistentvolume "%s"', self.name) log.debug('persistentvolume: %s', self.obj) @@ -54,10 +49,6 @@ def create(self): def delete(self, options): """Delete the PersistentVolume. - This method expects the PersistentVolume to have been loaded or otherwise - assigned a namespace already. If it has not, the namespace will need - to be set manually. - Args: options (client.V1DeleteOptions): Options for PersistentVolume deletion. @@ -85,9 +76,10 @@ def refresh(self): def is_ready(self): """Check if the PersistentVolume is in the ready state. - PersistentVolumes do not have a "status" field to check, so we will - measure their readiness status by whether or not they exist - on the cluster. + PersistentVolumes have a "status" field to check. However, as this + field may change from "available" to "bound" quickly if another + object is using the PersistentVolume, we will measure their + readiness status by whether or not they exist on the cluster. Returns: bool: True if in the ready state; False otherwise. diff --git a/kubetest/objects/persistentvolumeclaim.py b/kubetest/objects/persistentvolumeclaim.py index e7bc9a1..936b2c6 100644 --- a/kubetest/objects/persistentvolumeclaim.py +++ b/kubetest/objects/persistentvolumeclaim.py @@ -95,9 +95,10 @@ def refresh(self): def is_ready(self): """Check if the PersistentVolumeClaim is in the ready state. - PersistentVolumeClaims do not have a "status" field to check, so we will - measure their readiness status by whether or not they exist - on the cluster. + PersistentVolumeClaims have a "status" field to check. However, as this + field may change from "available" to "bound" quickly if another + object is using the PersistentVolumeClaim, we will measure their + readiness status by whether or not they exist on the cluster. Returns: bool: True if in the ready state; False otherwise. diff --git a/tests/conftest.py b/tests/conftest.py index 7a0ae74..7b39b1b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,3 +164,67 @@ def simple_service(): ] ) ) + + +@pytest.fixture() +def simple_persistentvolume(): + """Return the Kubernetes config matching the simple-persistentvolume.yaml manifest.""" + return client.V1PersistentVolume( + api_version='v1', + kind='PersistentVolume', + metadata=client.V1ObjectMeta( + name='my-pv' + ), + spec=client.V1PersistentVolumeSpec( + capacity={ + 'storage': '16Mi' + }, + access_modes=[ + 'ReadWriteMany' + ], + local=client.V1LocalVolumeSource( + path='/tmp/vol1' + ), + persistent_volume_reclaim_policy='Delete', + node_affinity=client.V1VolumeNodeAffinity( + required=client.V1NodeSelector( + node_selector_terms=[ + client.V1NodeSelectorTerm( + match_expressions=[ + client.V1NodeSelectorRequirement( + key='kubernetes.io/hostname', + operator='In', + values=[ + 'my-node' + ] + ) + ] + ) + ] + ) + ) + ) + ) + + +@pytest.fixture() +def simple_persistentvolumeclaim(): + """Return the Kubernetes config matching the simple-persistentvolumeclaim.yaml + manifest.""" + return client.V1PersistentVolumeClaim( + api_version='v1', + kind='PersistentVolumeClaim', + metadata=client.V1ObjectMeta( + name='my-pvc' + ), + spec=client.V1PersistentVolumeClaimSpec( + access_modes=[ + 'ReadWriteMany' + ], + resources=client.V1ResourceRequirements( + requests={ + 'storage': '16Mi' + } + ) + ) + ) diff --git a/tests/data/simple-persistentvolume.yaml b/tests/data/simple-persistentvolume.yaml index 3ea5151..fd1ea52 100644 --- a/tests/data/simple-persistentvolume.yaml +++ b/tests/data/simple-persistentvolume.yaml @@ -9,7 +9,7 @@ spec: - ReadWriteMany local: path: /tmp/vol1 - persistentVolumeReclaimPolicy: Recycle + persistentVolumeReclaimPolicy: Delete nodeAffinity: required: nodeSelectorTerms: From 71f14a18bd598bb475f8b36df6e0a60f05c2dd70 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Mon, 20 Jan 2020 09:26:40 +0100 Subject: [PATCH 6/7] delete pv after tests --- kubetest/manager.py | 19 +++++++++++++++++++ kubetest/objects/persistentvolume.py | 6 ++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/kubetest/manager.py b/kubetest/manager.py index 2f2cb4e..3ec8265 100644 --- a/kubetest/manager.py +++ b/kubetest/manager.py @@ -145,6 +145,7 @@ def __init__(self, name, node_id): self.rolebindings = [] self.clusterrolebindings = [] + self.persistentvolume = [] self.test_objects = ObjectManager() @@ -179,6 +180,10 @@ def setup(self): for crb in self.clusterrolebindings: self.client.create(crb) + # if there are any persistent volumes, create them. + for pv in self.persistentvolumes: + self.client.create(pv) + # if any objects were registered with the test case via the # `applymanifests` marker, register them to the test client # and add them to the cluster now @@ -211,6 +216,11 @@ def teardown(self): for crb in self.clusterrolebindings: self.client.delete(crb) + # PersistentVolume are not bound to a namespace, so we will need + # to delete them ourselves. + for pv in self.persistentvolume: + self.client.delete(pv) + def yield_container_logs(self, tail_lines=None): """Yield the container logs for the test case. @@ -286,6 +296,15 @@ def register_clusterrolebindings(self, *clusterrolebindings): """ self.clusterrolebindings.extend(clusterrolebindings) + def register_persistentvolumes(self, *persistentvolumes): + """Register a PersistentVolume requirement with the test case. + + Args: + *persistentvolumes (PersistentVolume): The PersistentVolumes + that are needed for the test case. + """ + self.persistentvolumes.extend(persistentvolumes) + def register_objects(self, api_objects): """Register the provided objects with the test case. diff --git a/kubetest/objects/persistentvolume.py b/kubetest/objects/persistentvolume.py index 5fec2de..48b04cb 100644 --- a/kubetest/objects/persistentvolume.py +++ b/kubetest/objects/persistentvolume.py @@ -35,9 +35,11 @@ def __str__(self): def __repr__(self): return self.__str__() - def create(self): - """Create the PersistentVolume. + def create(self, namespace=None): + """Create the PersistentVolume under the given namespace. + Args: + namespace (str): This argument is ignored for PersistentVolumes. """ log.info('creating persistentvolume "%s"', self.name) log.debug('persistentvolume: %s', self.obj) From deb6b1710c69cfb213eb88377ee262365a178916 Mon Sep 17 00:00:00 2001 From: Corentin JACQUEMET Date: Mon, 20 Jan 2020 14:46:48 +0100 Subject: [PATCH 7/7] add markers for persistentvolume --- kubetest/client.py | 12 ++++++------ kubetest/manager.py | 9 ++++++--- kubetest/markers.py | 42 +++++++++++++++++++++++++++++++++++++++++- kubetest/plugin.py | 3 +++ 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/kubetest/client.py b/kubetest/client.py index fda07b7..0696000 100644 --- a/kubetest/client.py +++ b/kubetest/client.py @@ -231,7 +231,7 @@ def load_service(self, path: str, set_namespace: bool = True) -> objects.Service service.namespace = self.namespace return service - def load_persistentvolume(self, path, set_namespace=False): + def load_persistentvolume(self, path, set_namespace=False) -> objects.PersistentVolume: """Load a manifest YAML into a PersistentVolume object. By default, this will not augment the PersistentVolume object with @@ -252,7 +252,7 @@ def load_persistentvolume(self, path, set_namespace=False): persistentvolume.namespace = self.namespace return persistentvolume - def load_persistentvolumeclaim(self, path, set_namespace=True): + def load_persistentvolumeclaim(self, path, set_namespace=True) -> objects.PersistentVolumeClaim: """Load a manifest YAML into a PersistentVolumeClaim object. By default, this will augment the PersistentVolumeClaim object with @@ -635,8 +635,8 @@ def get_secrets( return secrets - def get_persistentvolume(self, fields=None, labels=None): - """Get PersistentVolume from the cluster. + def get_persistentvolumes(self, fields=None, labels=None): + """Get PersistentVolumes from the cluster. Args: fields (dict[str, str]): A dictionary of fields used to restrict @@ -663,8 +663,8 @@ def get_persistentvolume(self, fields=None, labels=None): return persistentvolumes - def get_persistentvolumeclaim(self, namespace=None, fields=None, labels=None): - """Get PersistentVolumeClaim from the cluster. + def get_persistentvolumeclaims(self, namespace=None, fields=None, labels=None): + """Get PersistentVolumeClaims from the cluster. Args: namespace (str): The namespace to get the PersistentVolumeClaim from. If not diff --git a/kubetest/manager.py b/kubetest/manager.py index 5c0ec6d..9870b82 100644 --- a/kubetest/manager.py +++ b/kubetest/manager.py @@ -160,7 +160,7 @@ def __init__( self.namespace_create = namespace_create self.rolebindings = [] self.clusterrolebindings = [] - self.persistentvolume = [] + self.persistentvolumes = [] self.test_objects = ObjectManager() @@ -233,7 +233,7 @@ def teardown(self) -> None: # PersistentVolume are not bound to a namespace, so we will need # to delete them ourselves. - for pv in self.persistentvolume: + for pv in self.persistentvolumes: self.client.delete(pv) def yield_container_logs(self, tail_lines: int = None) -> Generator[str, None, None]: @@ -309,7 +309,10 @@ def register_clusterrolebindings( """ self.clusterrolebindings.extend(clusterrolebindings) - def register_persistentvolumes(self, *persistentvolumes): + def register_persistentvolumes( + self, + *persistentvolumes: objects.PersistentVolume + ) -> None: """Register a PersistentVolume requirement with the test case. Args: diff --git a/kubetest/markers.py b/kubetest/markers.py index d8287db..dad6491 100644 --- a/kubetest/markers.py +++ b/kubetest/markers.py @@ -8,7 +8,8 @@ from kubetest import manager from kubetest.manifest import load_file, load_path -from kubetest.objects import ApiObject, ClusterRoleBinding, RoleBinding +from kubetest.objects import (ApiObject, ClusterRoleBinding, PersistentVolume, + RoleBinding) APPLYMANIFEST_INI = ( 'applymanifest(path): ' @@ -63,6 +64,13 @@ 'see: https://kubernetes.io/docs/reference/access-authn-authz/rbac/' ) +PERSISTENTVOLUME_INI = ( + 'persistentvolume(name, subject_kind=None, subject_name=None): ' + 'create and use a Kubernetes PersistentVolume for the test case. The generated ' + 'persistent volume will be automatically created and removed for each marked ' + 'test. The name of the volume must be specified. Only existing PersistentVolume can be ' + 'used.' +) NAMESPACE_INI = ( 'namespace(create=True, name=None): ' @@ -85,6 +93,7 @@ def register(config) -> None: config.addinivalue_line('markers', APPLYMANIFESTS_INI) config.addinivalue_line('markers', CLUSTERROLEBINDING_INI) config.addinivalue_line('markers', ROLEBINDING_INI) + config.addinivalue_line('markers', PERSISTENTVOLUME_INI) config.addinivalue_line('markers', NAMESPACE_INI) @@ -260,6 +269,37 @@ def clusterrolebindings_from_marker(item: pytest.Item, namespace: str) -> List[C return clusterrolebindings +def persistentvolumes_from_marker(item: pytest.Item) -> List[PersistentVolume]: + """Create PersistentVolume for the test case if the test case is marked + with the `pytest.mark.persistentvolume` marker. + + Args: + item: The pytest test item. + + Return: + The PersistentVolumes which were generated from the test case markers. + """ + + persistentvolumes = [] + for mark in item.iter_markers(name='persistentvolume'): + name = mark.args[0] + subj_kind = mark.kwargs.get('subject_kind') + subj_name = mark.kwargs.get('subject_name') + + persistentvolumes.append(PersistentVolume(client.V1PersistentVolume( + metadata=client.V1ObjectMeta( + name=f'kubetest:{item.name}', + ), + spec=client.V1PersistentVolumeSpec( + api_group='storage.k8s.io', + kind='PersistentVolume', + name=name, + ) + ))) + + return persistentvolumes + + def get_custom_rbac_subject(namespace: str, kind: str, name: str) -> List[client.V1Subject]: """Create a custom RBAC subject for the given namespace. diff --git a/kubetest/plugin.py b/kubetest/plugin.py index 3f98217..1df0a78 100644 --- a/kubetest/plugin.py +++ b/kubetest/plugin.py @@ -247,6 +247,9 @@ def pytest_runtest_setup(item): test_case.register_clusterrolebindings( *markers.clusterrolebindings_from_marker(item, test_case.ns) ) + test_case.register_persistentvolumes( + *markers.persistentvolumes_from_marker(item) + ) # Apply manifests for the test case, if any are specified. markers.apply_manifests_from_marker(item, test_case)