|
8 | 8 | import re |
9 | 9 | import sys |
10 | 10 | import threading |
| 11 | +from abc import abstractmethod, ABC |
11 | 12 | from collections.abc import Callable, Generator |
12 | 13 | from io import StringIO |
13 | 14 | from signal import SIGINT, signal |
14 | 15 | from types import TracebackType |
15 | | -from typing import Any |
| 16 | +from typing import Any, Type |
16 | 17 | from urllib.parse import parse_qs, urlencode, urlparse |
17 | 18 | from warnings import warn |
18 | 19 |
|
|
59 | 60 | from ocp_resources.utils.resource_constants import ResourceConstants |
60 | 61 | from ocp_resources.utils.utils import skip_existing_resource_creation_teardown |
61 | 62 |
|
| 63 | + |
62 | 64 | LOGGER = get_logger(name=__name__) |
63 | 65 | MAX_SUPPORTED_API_VERSION = "v2" |
64 | 66 |
|
@@ -736,7 +738,7 @@ def _sigint_handler(self, signal_received: int, frame: Any) -> None: |
736 | 738 | self.__exit__() |
737 | 739 | sys.exit(signal_received) |
738 | 740 |
|
739 | | - def deploy(self, wait: bool = False) -> Any: |
| 741 | + def deploy(self, wait: bool = False) -> Resource | NamespacedResource: |
740 | 742 | """ |
741 | 743 | For debug, export REUSE_IF_RESOURCE_EXISTS to skip resource create. |
742 | 744 | Spaces are important in the export dict |
@@ -1756,3 +1758,160 @@ def _apply_patches_sampler(self, patches: dict[Any, Any], action_text: str, acti |
1756 | 1758 | timeout=TIMEOUT_30SEC, |
1757 | 1759 | sleep_time=TIMEOUT_5SEC, |
1758 | 1760 | ) |
| 1761 | + |
| 1762 | + |
| 1763 | +class BaseResourceList(ABC): |
| 1764 | + """ |
| 1765 | + Abstract base class for managing collections of resources. |
| 1766 | +
|
| 1767 | + Provides common functionality for resource lists including context management, |
| 1768 | + iteration, indexing, deployment, and cleanup operations. |
| 1769 | + """ |
| 1770 | + |
| 1771 | + def __init__(self, client: DynamicClient): |
| 1772 | + self.resources: list[Resource] = [] |
| 1773 | + self.client = client |
| 1774 | + |
| 1775 | + def __enter__(self): |
| 1776 | + """Enters the runtime context and deploys all resources.""" |
| 1777 | + self.deploy() |
| 1778 | + return self |
| 1779 | + |
| 1780 | + def __exit__( |
| 1781 | + self, |
| 1782 | + exc_type: type[BaseException] | None, |
| 1783 | + exc_val: BaseException | None, |
| 1784 | + exc_tb: TracebackType | None, |
| 1785 | + ) -> None: |
| 1786 | + """Exits the runtime context and cleans up all resources.""" |
| 1787 | + self.clean_up() |
| 1788 | + |
| 1789 | + def __iter__(self) -> Generator[Resource | NamespacedResource, None, None]: |
| 1790 | + """Allows iteration over the resources in the list.""" |
| 1791 | + yield from self.resources |
| 1792 | + |
| 1793 | + def __getitem__(self, index: int) -> Resource | NamespacedResource: |
| 1794 | + """Retrieves a resource from the list by its index.""" |
| 1795 | + return self.resources[index] |
| 1796 | + |
| 1797 | + def __len__(self) -> int: |
| 1798 | + """Returns the number of resources in the list.""" |
| 1799 | + return len(self.resources) |
| 1800 | + |
| 1801 | + def deploy(self, wait: bool = False) -> list[Resource | NamespacedResource]: |
| 1802 | + """ |
| 1803 | + Deploys all resources in the list. |
| 1804 | +
|
| 1805 | + Args: |
| 1806 | + wait (bool): If True, wait for each resource to be ready. |
| 1807 | +
|
| 1808 | + Returns: |
| 1809 | + List[Any]: A list of the results from each resource's deploy() call. |
| 1810 | + """ |
| 1811 | + return [resource.deploy(wait=wait) for resource in self.resources] |
| 1812 | + |
| 1813 | + def clean_up(self, wait: bool = True) -> bool: |
| 1814 | + """ |
| 1815 | + Deletes all resources in the list. |
| 1816 | +
|
| 1817 | + Args: |
| 1818 | + wait (bool): If True, wait for each resource to be deleted. |
| 1819 | +
|
| 1820 | + Returns: |
| 1821 | + bool: Returns True if all resources are cleaned up correclty. |
| 1822 | + """ |
| 1823 | + # Deleting in reverse order to resolve dependencies correctly. |
| 1824 | + return all(resource.clean_up(wait=wait) for resource in reversed(self.resources)) |
| 1825 | + |
| 1826 | + @abstractmethod |
| 1827 | + def _create_resources(self, resource_class: Type, **kwargs: Any) -> None: |
| 1828 | + """Abstract method to create resources based on specific logic.""" |
| 1829 | + pass |
| 1830 | + |
| 1831 | + |
| 1832 | +class ResourceList(BaseResourceList): |
| 1833 | + """ |
| 1834 | + A class to manage a collection of a specific resource type. |
| 1835 | +
|
| 1836 | + This class creates and manages N copies of a given resource, |
| 1837 | + each with a unique name derived from a base name. |
| 1838 | + """ |
| 1839 | + |
| 1840 | + def __init__( |
| 1841 | + self, |
| 1842 | + resource_class: Type[Resource], |
| 1843 | + num_resources: int, |
| 1844 | + client: DynamicClient, |
| 1845 | + **kwargs: Any, |
| 1846 | + ) -> None: |
| 1847 | + """ |
| 1848 | + Initializes a list of N resource objects. |
| 1849 | +
|
| 1850 | + Args: |
| 1851 | + resource_class (Type[Resource]): The resource class to instantiate (e.g., Namespace). |
| 1852 | + num_resources (int): The number of resource copies to create. |
| 1853 | + client (DynamicClient): The dynamic client to use. Defaults to None. |
| 1854 | + **kwargs (Any): Arguments to be passed to the constructor of the resource_class. |
| 1855 | + A 'name' key is required in kwargs to serve as the base name for the resources. |
| 1856 | + """ |
| 1857 | + super().__init__(client) |
| 1858 | + |
| 1859 | + self.num_resources = num_resources |
| 1860 | + self._create_resources(resource_class, **kwargs) |
| 1861 | + |
| 1862 | + def _create_resources(self, resource_class: Type[Resource], **kwargs: Any) -> None: |
| 1863 | + """Creates N resources with indexed names.""" |
| 1864 | + base_name = kwargs["name"] |
| 1865 | + |
| 1866 | + for i in range(1, self.num_resources + 1): |
| 1867 | + resource_name = f"{base_name}-{i}" |
| 1868 | + resource_kwargs = kwargs.copy() |
| 1869 | + resource_kwargs["name"] = resource_name |
| 1870 | + |
| 1871 | + instance = resource_class(client=self.client, **resource_kwargs) |
| 1872 | + self.resources.append(instance) |
| 1873 | + |
| 1874 | + |
| 1875 | +class NamespacedResourceList(BaseResourceList): |
| 1876 | + """ |
| 1877 | + Manages a collection of a specific namespaced resource (e.g., Pod, Service, etc), creating one instance per provided namespace. |
| 1878 | +
|
| 1879 | + This class creates one copy of a given namespaced resource in each of the |
| 1880 | + namespaces provided in a list. |
| 1881 | + """ |
| 1882 | + |
| 1883 | + def __init__( |
| 1884 | + self, |
| 1885 | + resource_class: Type[NamespacedResource], |
| 1886 | + namespaces: ResourceList, |
| 1887 | + client: DynamicClient, |
| 1888 | + **kwargs: Any, |
| 1889 | + ) -> None: |
| 1890 | + """ |
| 1891 | + Initializes a list of resource objects, one for each specified namespace. |
| 1892 | +
|
| 1893 | + Args: |
| 1894 | + resource_class (Type[NamespacedResource]): The namespaced resource class to instantiate (e.g., Pod). |
| 1895 | + namespaces (ResourceList): A ResourceList containing namespaces where the resources will be created. |
| 1896 | + client (DynamicClient): The dynamic client to use for cluster communication. |
| 1897 | + **kwargs (Any): Additional arguments to be passed to the resource_class constructor. |
| 1898 | + A 'name' key is required in kwargs to serve as the base name for the resources. |
| 1899 | + """ |
| 1900 | + for ns in namespaces: |
| 1901 | + if ns.kind != "Namespace": |
| 1902 | + raise TypeError("All the resources in namespaces should be namespaces.") |
| 1903 | + |
| 1904 | + super().__init__(client) |
| 1905 | + |
| 1906 | + self.namespaces = namespaces |
| 1907 | + self._create_resources(resource_class, **kwargs) |
| 1908 | + |
| 1909 | + def _create_resources(self, resource_class: Type[NamespacedResource], **kwargs: Any) -> None: |
| 1910 | + """Creates one resource per namespace.""" |
| 1911 | + for ns in self.namespaces: |
| 1912 | + instance = resource_class( |
| 1913 | + namespace=ns.name, |
| 1914 | + client=self.client, |
| 1915 | + **kwargs, |
| 1916 | + ) |
| 1917 | + self.resources.append(instance) |
0 commit comments