diff --git a/robusta_krr/core/integrations/kubernetes/__init__.py b/robusta_krr/core/integrations/kubernetes/__init__.py index 8fd5ab96..e5e0bdde 100644 --- a/robusta_krr/core/integrations/kubernetes/__init__.py +++ b/robusta_krr/core/integrations/kubernetes/__init__.py @@ -21,6 +21,7 @@ from robusta_krr.core.models.config import settings from robusta_krr.core.models.objects import HPAData, K8sObjectData, KindLiteral, PodData from robusta_krr.core.models.result import ResourceAllocations +from robusta_krr.core.models.result import ResourceType from robusta_krr.utils.object_like_dict import ObjectLikeDict from . import config_patch as _ @@ -107,6 +108,7 @@ async def list_scannable_objects(self) -> list[K8sObjectData]: self._list_all_daemon_set(), self._list_all_jobs(), self._list_all_cronjobs(), + self._list_cnpg_clusters(), ) return [ @@ -146,6 +148,9 @@ async def list_pods(self, object: K8sObjectData) -> list[PodData]: ] selector = f"batch.kubernetes.io/controller-uid in ({','.join(ownered_jobs_uids)})" + elif object.kind == "CNPGCluster": + selector = f"cnpg.io/cluster={object.name}" + else: if object.selector is None: return [] @@ -211,6 +216,26 @@ def __build_scannable_object( namespace = item.metadata.namespace kind = kind or item.__class__.__name__[2:] + # Special handling for CNPGCluster + if kind == "CNPGCluster": + resources = getattr(item.spec, "resources", None) or item.get("spec", {}).get("resources", {}) or {} + req = resources.get("requests", {}) or {} + lim = resources.get("limits", {}) or {} + allocations = ResourceAllocations( + requests={ + ResourceType.CPU: req.get("cpu"), + ResourceType.Memory: req.get("memory"), + }, + limits={ + ResourceType.CPU: lim.get("cpu"), + ResourceType.Memory: lim.get("memory"), + }, + ) + container_name = "postgres" + else: + allocations = ResourceAllocations.from_container(container) + container_name = getattr(container, "name", None) + labels = {} annotations = {} if item.metadata.labels: @@ -230,8 +255,8 @@ def __build_scannable_object( namespace=namespace, name=name, kind=kind, - container=container.name, - allocations=ResourceAllocations.from_container(container), + container=container_name, + allocations=allocations, hpa=self.__hpa_list.get((namespace, kind, name)), labels=labels, annotations= annotations @@ -312,7 +337,7 @@ async def _list_scannable_objects( result.extend(self.__build_scannable_object(item, container, kind) for container in containers) except ApiException as e: - if kind in ("Rollout", "DeploymentConfig", "StrimziPodSet") and e.status in [400, 401, 403, 404]: + if kind in ("Rollout", "DeploymentConfig", "StrimziPodSet", "CNPGCluster") and e.status in [400, 401, 403, 404]: if self.__kind_available[kind]: logger.debug(f"{kind} API not available in {self.cluster}") self.__kind_available[kind] = False @@ -322,6 +347,29 @@ async def _list_scannable_objects( return result + def _list_cnpg_clusters(self) -> list[K8sObjectData]: + # List CNPGCluster resources + return self._list_scannable_objects( + kind="CNPGCluster", + all_namespaces_request=lambda **kwargs: ObjectLikeDict( + self.custom_objects.list_cluster_custom_object( + group="postgresql.cnpg.io", + version="v1", + plural="clusters", + **kwargs, + ) + ), + namespaced_request=lambda **kwargs: ObjectLikeDict( + self.custom_objects.list_namespaced_custom_object( + group="postgresql.cnpg.io", + version="v1", + plural="clusters", + **kwargs, + ) + ), + extract_containers=lambda item: [item], # Pass the item itself for allocation extraction + ) + def _list_deployments(self) -> list[K8sObjectData]: return self._list_scannable_objects( kind="Deployment", @@ -401,6 +449,7 @@ def _list_strimzipodsets(self) -> list[K8sObjectData]: extract_containers=lambda item: item.spec.pods[0].spec.containers, ) + def _list_deploymentconfig(self) -> list[K8sObjectData]: # NOTE: Using custom objects API returns dicts, but all other APIs return objects # We need to handle this difference using a small wrapper diff --git a/robusta_krr/core/models/objects.py b/robusta_krr/core/models/objects.py index 71ba0ca2..b82551aa 100644 --- a/robusta_krr/core/models/objects.py +++ b/robusta_krr/core/models/objects.py @@ -8,7 +8,7 @@ from robusta_krr.utils.batched import batched from kubernetes.client.models import V1LabelSelector -KindLiteral = Literal["Deployment", "DaemonSet", "StatefulSet", "Job", "CronJob", "Rollout", "DeploymentConfig", "StrimziPodSet"] +KindLiteral = Literal["Deployment", "DaemonSet", "StatefulSet", "Job", "CronJob", "Rollout", "DeploymentConfig", "StrimziPodSet", "CNPGCluster"] class PodData(pd.BaseModel): diff --git a/robusta_krr/formatters/csv.py b/robusta_krr/formatters/csv.py index d32f5f94..3559a7be 100644 --- a/robusta_krr/formatters/csv.py +++ b/robusta_krr/formatters/csv.py @@ -27,8 +27,8 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str: - allocated = getattr(item.object.allocations, selector)[resource] - recommended = getattr(item.recommended, selector)[resource] + allocated = getattr(item.object.allocations, selector).get(resource, None) + recommended = getattr(item.recommended, selector).get(resource, None) if allocated is None and recommended.value is None: return f"{NONE_LITERAL}" @@ -42,8 +42,10 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: int) -> str: selector = "requests" - allocated = getattr(item.object.allocations, selector)[resource] - recommended = getattr(item.recommended, selector)[resource] + allocated = getattr(item.object.allocations, selector).get(resource, None) + recommended = getattr(item.recommended, selector).get(resource, None) + if recommended is None: + return "" return format_diff(allocated, recommended, selector, pods_current) diff --git a/robusta_krr/formatters/csv_raw.py b/robusta_krr/formatters/csv_raw.py index c88d1fa7..744abfe6 100644 --- a/robusta_krr/formatters/csv_raw.py +++ b/robusta_krr/formatters/csv_raw.py @@ -41,14 +41,14 @@ def _format_value(val: Union[float, int]) -> str: def _format_request_current(item: ResourceScan, resource: ResourceType, selector: str) -> str: - allocated = getattr(item.object.allocations, selector)[resource] + allocated = getattr(item.object.allocations, selector).get(resource, None) if allocated is None: return NONE_LITERAL return _format_value(allocated) def _format_request_recommend(item: ResourceScan, resource: ResourceType, selector: str) -> str: - recommended = getattr(item.recommended, selector)[resource] + recommended = getattr(item.recommended, selector).get(resource, None) if recommended is None: return NONE_LITERAL return _format_value(recommended.value) diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index 7d027541..1587bd01 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -17,9 +17,9 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str: - allocated = getattr(item.object.allocations, selector)[resource] + allocated = getattr(item.object.allocations, selector).get(resource, None) info = item.recommended.info.get(resource) - recommended = getattr(item.recommended, selector)[resource] + recommended = getattr(item.recommended, selector).get(resource, None) severity = recommended.severity if allocated is None and recommended.value is None: @@ -48,8 +48,8 @@ def _format_request_str(item: ResourceScan, resource: ResourceType, selector: st def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: int) -> str: selector = "requests" - allocated = getattr(item.object.allocations, selector)[resource] - recommended = getattr(item.recommended, selector)[resource] + allocated = getattr(item.object.allocations, selector).get(resource, None) + recommended = getattr(item.recommended, selector).get(resource, None) # if we have more than one pod, say so (this explains to the user why the total is different than the recommendation) if pods_current == 1: diff --git a/robusta_krr/main.py b/robusta_krr/main.py index b29d7e10..1320718f 100644 --- a/robusta_krr/main.py +++ b/robusta_krr/main.py @@ -98,7 +98,7 @@ def run_strategy( None, "--resource", "-r", - help="List of resources to run on (Deployment, StatefulSet, DaemonSet, Job, Rollout, StrimziPodSet). By default, will run on all resources. Case insensitive.", + help="List of resources to run on (Deployment, StatefulSet, DaemonSet, Job, Rollout, StrimziPodSet, CNPGCluster). By default, will run on all resources. Case insensitive.", rich_help_panel="Kubernetes Settings", ), selector: Optional[str] = typer.Option( diff --git a/tests/single_namespace_permissions.yaml b/tests/single_namespace_permissions.yaml index f6e324d8..f719ef86 100644 --- a/tests/single_namespace_permissions.yaml +++ b/tests/single_namespace_permissions.yaml @@ -26,6 +26,9 @@ rules: - apiGroups: ["autoscaling"] resources: ["horizontalpodautoscalers"] verbs: ["get", "list", "watch"] +- apiGroups: ["postgresql.cnpg.io"] + resources: ["clusters"] + verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding