Skip to content
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
55 changes: 52 additions & 3 deletions robusta_krr/core/integrations/kubernetes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _
Expand Down Expand Up @@ -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 [
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion robusta_krr/core/models/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 6 additions & 4 deletions robusta_krr/formatters/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions robusta_krr/formatters/csv_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions robusta_krr/formatters/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion robusta_krr/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions tests/single_namespace_permissions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down