Skip to content

Support cloudnative-pg (CNPG) clusters#473

Open
saifulmuhajir wants to merge 8 commits intorobusta-dev:mainfrom
saifulmuhajir:main
Open

Support cloudnative-pg (CNPG) clusters#473
saifulmuhajir wants to merge 8 commits intorobusta-dev:mainfrom
saifulmuhajir:main

Conversation

@saifulmuhajir
Copy link

Related to #139

@coderabbitai
Copy link

coderabbitai bot commented Sep 23, 2025

Walkthrough

Adds CNPGCluster support to Kubernetes integration (discovery via CustomObjects API, CNPG-aware pod selection and allocation extraction), extends KindLiteral, updates CLI help text, switches formatters to safe dict lookups, and adds RBAC rules for CNPG clusters.

Changes

Cohort / File(s) Summary
Kubernetes integration: CNPG support
robusta_krr/core/integrations/kubernetes/__init__.py
Adds _list_cnpg_clusters, integrates CNPGCluster into list_scannable_objects and list_pods, updates __build_scannable_object to extract allocations from item.spec.resources and set container_name="postgres", and handles CNPG CRD-not-available like other CRDs.
Model kind expansion
robusta_krr/core/models/objects.py
Extends KindLiteral to include "CNPGCluster".
Formatters: safe resource lookups
robusta_krr/formatters/csv.py, robusta_krr/formatters/csv_raw.py, robusta_krr/formatters/table.py
Replace direct dict indexing with safe .get(..., None) for allocation/recommendation access; CSV total diff adds guard when recommended is None.
CLI help text
robusta_krr/main.py
Updates --resource help string to list CNPGCluster.
RBAC for CNPG
tests/single_namespace_permissions.yaml
Adds Role rule granting get and list on clusters in API group postgresql.cnpg.io.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant ClusterLoader
  participant K8sCore as "K8s Core APIs"
  participant COAPI as "CustomObjects API"

  User->>ClusterLoader: list_scannable_objects()
  ClusterLoader->>K8sCore: List Deployments/StatefulSets/...
  ClusterLoader->>COAPI: List CNPGCluster (cluster & namespaced)
  Note over ClusterLoader: Build K8sObjectData for each item\n(CNPG: allocations from item.spec.resources,\ncontainer_name="postgres")
  ClusterLoader-->>User: [K8sObjectData...]
Loading
sequenceDiagram
  autonumber
  participant Consumer
  participant ClusterLoader
  participant K8sCore as "K8s Core APIs"

  Consumer->>ClusterLoader: list_pods(object)
  alt object.kind == "CNPGCluster"
    ClusterLoader->>K8sCore: List Pods selector: cnpg.io/cluster={name}
  else
    ClusterLoader->>K8sCore: List Pods via standard selectors
  end
  ClusterLoader-->>Consumer: [PodData...]
Loading
sequenceDiagram
  autonumber
  participant Builder as "__build_scannable_object"
  participant Item as "K8s Item"

  Item-->>Builder: item, container, kind
  alt kind == "CNPGCluster"
    Note right of Builder: allocations <- item.spec.resources\ncontainer_name = "postgres"
  else
    Note right of Builder: allocations <- container.resources\ncontainer_name = container.name
  end
  Builder-->>Item: K8sObjectData
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Support cloudnative-pg (CNPG) clusters' directly and clearly describes the main objective of the PR, which is to add CNPGCluster support throughout the codebase.
Description check ✅ Passed The description 'Related to #139' is minimal but appropriately references the related issue, indicating it is related to the changeset of adding CNPG support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
robusta_krr/formatters/table.py (2)

20-26: Fix None-deref when recommendations are missing.

recommended can now be None; accessing recommended.severity/recommended.value will raise. Short-circuit and return NONE_LITERAL.

Apply this diff:

 def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str:
-    allocated = getattr(item.object.allocations, selector).get(resource, None)
+    allocated = getattr(item.object.allocations, selector).get(resource, None)
     info = item.recommended.info.get(resource)
-    recommended = getattr(item.recommended, selector).get(resource, None)
-    severity = recommended.severity
+    recommended = getattr(item.recommended, selector).get(resource, None)
+    if recommended is None:
+        return f"{NONE_LITERAL}"
+    severity = recommended.severity

50-61: Guard format_diff call when recommendation is absent.

Avoid passing None into format_diff.

Apply this diff:

 def _format_total_diff(item: ResourceScan, resource: ResourceType, pods_current: int) -> str:
     selector = "requests"
-    allocated = getattr(item.object.allocations, selector).get(resource, None)
-    recommended = getattr(item.recommended, selector).get(resource, None)
+    allocated = getattr(item.object.allocations, selector).get(resource, None)
+    recommended = getattr(item.recommended, selector).get(resource, None)
+    if recommended is None:
+        return ""
robusta_krr/formatters/csv.py (1)

29-41: Prevent AttributeError when recommendation is missing.

recommended may be None now; recommended.value access will crash. Short-circuit.

Apply this diff:

 def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str:
-    allocated = getattr(item.object.allocations, selector).get(resource, None)
-    recommended = getattr(item.recommended, selector).get(resource, None)
+    allocated = getattr(item.object.allocations, selector).get(resource, None)
+    recommended = getattr(item.recommended, selector).get(resource, None)
+    if recommended is None:
+        return f"{NONE_LITERAL}"
 
-    if allocated is None and recommended.value is None:
+    if allocated is None and recommended.value is None:
         return f"{NONE_LITERAL}"
 
     diff = format_diff(allocated, recommended, selector)
     if diff != "":
         diff = f"({diff}) "
 
     return diff + format_recommendation_value(allocated) + " -> " + format_recommendation_value(recommended.value)
🧹 Nitpick comments (1)
robusta_krr/core/integrations/kubernetes/__init__.py (1)

443-466: Remove unused and incorrect CNPG pods helper.

This method isn’t referenced, uses kind="Cluster" (not in KindLiteral), and an extract_containers shape that doesn’t apply to CNPG. Prefer deleting to avoid confusion.

Apply this diff to remove it:

-    def _list_cnpgpods(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
-        return self._list_scannable_objects(
-            kind="Cluster",
-            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.spec.pods[0].spec.containers,
-        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baaffc6 and a07d54d.

📒 Files selected for processing (7)
  • robusta_krr/core/integrations/kubernetes/__init__.py (7 hunks)
  • robusta_krr/core/models/objects.py (1 hunks)
  • robusta_krr/formatters/csv.py (2 hunks)
  • robusta_krr/formatters/csv_raw.py (1 hunks)
  • robusta_krr/formatters/table.py (2 hunks)
  • robusta_krr/main.py (1 hunks)
  • tests/single_namespace_permissions.yaml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
robusta_krr/formatters/csv_raw.py (2)
robusta_krr/core/models/result.py (1)
  • ResourceScan (25-54)
robusta_krr/core/models/allocations.py (1)
  • ResourceType (13-20)
robusta_krr/core/integrations/kubernetes/__init__.py (3)
robusta_krr/core/models/objects.py (2)
  • selector (76-83)
  • K8sObjectData (38-107)
robusta_krr/utils/object_like_dict.py (2)
  • get (25-26)
  • ObjectLikeDict (1-29)
robusta_krr/core/models/allocations.py (2)
  • ResourceAllocations (52-106)
  • from_container (79-106)
🔇 Additional comments (9)
tests/single_namespace_permissions.yaml (1)

29-31: RBAC addition for CNPG clusters looks good.

Correct API group and plural. Matches how CNPG clusters are discovered in code.

robusta_krr/main.py (1)

101-103: Help text update LGTM.

Including CNPGCluster in --resource help keeps CLI aligned with new kind.

robusta_krr/formatters/csv_raw.py (2)

44-47: Safer access: good change.

Using .get(...) avoids KeyError and aligns with other formatters.


51-55: Safer access: good change.

Consistent with the project-wide guarded lookups.

robusta_krr/core/models/objects.py (1)

11-11: CNPGCluster kind addition LGTM.

Extending KindLiteral to include CNPGCluster matches the integration changes.

robusta_krr/core/integrations/kubernetes/__init__.py (4)

150-152: Confirm CNPG pod selector label.

Selector cnpg.io/cluster= is assumed. Please confirm this label is stable across your supported CNPG versions.

If needed, I can adjust to a configurable label.


331-335: Graceful handling for CNPG API absence looks good.

CNPGCluster added to the “API not available” skip list.


341-363: Name/type correctness of CNPG listing helper.

new helper is good, but ensure import of ResourceType (suggested above) to fix allocations mapping. No other issues with using CustomObjectsApi + ObjectLikeDict.

Would you like me to add a minimal e2e test that stubs a CNPG cluster object and validates parsed allocations?


23-25: ResourceAllocations import is valid — no change needed. result.py imports ResourceAllocations from core.models.allocations (from robusta_krr.core.models.allocations import RecommendationValue, ResourceAllocations, ResourceType), so from robusta_krr.core.models.result import ResourceAllocations will succeed at runtime.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
robusta_krr/core/integrations/kubernetes/__init__.py (1)

193-199: CRD selector: matchExpressions branch iterates .items() (list) — will break.

getattr(selector, "matchExpressions") is typically a list; .items() will raise. Iterate directly.

Apply this diff:

-        if getattr(selector, "matchExpressions", None):
-            label_filters += [
-                ClusterLoader._get_match_expression_filter(expression) for expression in getattr(selector, "matchExpressions").items()
-            ]
+        if getattr(selector, "matchExpressions", None):
+            label_filters += [
+                ClusterLoader._get_match_expression_filter(expression)
+                for expression in getattr(selector, "matchExpressions")
+            ]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a07d54d and b79d7fe.

📒 Files selected for processing (1)
  • robusta_krr/core/integrations/kubernetes/__init__.py (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
robusta_krr/core/integrations/kubernetes/__init__.py (3)
robusta_krr/core/models/objects.py (2)
  • selector (76-83)
  • K8sObjectData (38-107)
robusta_krr/utils/object_like_dict.py (2)
  • get (25-26)
  • ObjectLikeDict (1-29)
robusta_krr/core/models/allocations.py (2)
  • ResourceAllocations (52-106)
  • from_container (79-106)
🔇 Additional comments (5)
robusta_krr/core/integrations/kubernetes/__init__.py (5)

110-111: CNPG discovery wired into scan flow — LGTM.

Including CNPG clusters in the gathered workloads aligns with the other resource paths.


331-335: Graceful handling when CNPG CRD is unavailable — LGTM.

Adding CNPGCluster to the non-fatal API-not-available list is consistent with other CRDs.


341-363: CNPG listing via CustomObjectsApi — LGTM.

The wrapper with ObjectLikeDict matches existing CRD patterns; passing the item through for CNPG-specific allocation handling is appropriate.


218-226: CNPG allocations likely incorrect: map string keys to ResourceType.

spec.resources uses "cpu"/"memory" keys, but downstream expects ResourceType keys; otherwise allocations resolve to None in reports.

Apply this diff to map keys properly:

         # Special handling for CNPGCluster
         if kind == "CNPGCluster":
-            resources = getattr(item.spec, "resources", None) or item.get("spec", {}).get("resources", {})
-            allocations = ResourceAllocations(
-                requests=resources.get("requests", {}),
-                limits=resources.get("limits", {}),
-            )
+            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)

Also add the missing import:

-from robusta_krr.core.models.result import ResourceAllocations
+from robusta_krr.core.models.result import ResourceAllocations, ResourceType
#!/bin/bash
# Check call sites expect ResourceType keys (to validate why mapping is needed)
rg -nP --type=py -C2 '\ballocations\.(requests|limits)\s*\[\s*ResourceType\.' 

Also applies to: 249-251


150-152: No change required — selector cnpg.io/cluster is correct

CloudNativePG pods use the label key cnpg.io/cluster to associate pods with a cluster (confirmed in CloudNativePG docs/quickstart).

@JohnRMacGowan
Copy link

was hoping to check in on the status of this, and if we might see some movement on it. Thanks!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@robusta_krr/core/integrations/kubernetes/__init__.py`:
- Around line 176-177: The CNPGCluster selector currently only uses the cluster
label (selector = f"cnpg.io/cluster={object.name}") which matches both database
and pooler pods while the code assumes a postgres container; update the selector
in the CNPGCluster branch so it includes the component label to restrict to the
database pods (e.g. add ",app.kubernetes.io/component=database") or choose
component dynamically when you need pooler vs database; modify the code that
builds selector for object.kind == "CNPGCluster" to include the appropriate
app.kubernetes.io/component value so downstream references to the postgres
container are safe.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ad6f0bde-0ef3-4b2f-b752-e7128e19c6f3

📥 Commits

Reviewing files that changed from the base of the PR and between d955dd9 and ddc4e6f.

📒 Files selected for processing (7)
  • robusta_krr/core/integrations/kubernetes/__init__.py
  • robusta_krr/core/models/objects.py
  • robusta_krr/formatters/csv.py
  • robusta_krr/formatters/csv_raw.py
  • robusta_krr/formatters/table.py
  • robusta_krr/main.py
  • tests/single_namespace_permissions.yaml
🚧 Files skipped from review as they are similar to previous changes (5)
  • robusta_krr/main.py
  • robusta_krr/formatters/csv_raw.py
  • robusta_krr/formatters/table.py
  • robusta_krr/formatters/csv.py
  • robusta_krr/core/models/objects.py

Comment on lines +176 to +177
elif object.kind == "CNPGCluster":
selector = f"cnpg.io/cluster={object.name}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

CloudNativePG cnpg.io/cluster label pooler database app.kubernetes.io/component

💡 Result:

In CloudNativePG, the operator applies standard Kubernetes recommended labels to the resources it generates:

  • cnpg.io/cluster: <CLUSTER_NAME> → identifies the owning Cluster name. [1]
  • app.kubernetes.io/component: database|pooler|... → identifies the resource “component”; for PostgreSQL instance resources it’s typically database, and for PgBouncer resources it’s pooler. [1]
  • (Pooler-specific) cnpg.io/poolerName: <POOLER_NAME> → identifies the PgBouncer Pooler name. [1]

Example selectors:

  • Database pods: -l cnpg.io/cluster=<cluster>,app.kubernetes.io/component=database [1]
  • Pooler pods: -l cnpg.io/cluster=<cluster>,app.kubernetes.io/component=pooler (optionally add ,cnpg.io/poolerName=<pooler>). [1]

Sources: [1]


🏁 Script executed:

# First, let's examine the full context of the code around the flagged selector
cd /tmp && find . -type f -name "__init__.py" -path "*/kubernetes/*" 2>/dev/null | head -5

Repository: robusta-dev/krr

Length of output: 41


🏁 Script executed:

# Look at the file structure to locate the file mentioned in the review
find . -type f -name "__init__.py" -path "*robusta_krr*kubernetes*" 2>/dev/null

Repository: robusta-dev/krr

Length of output: 113


🏁 Script executed:

# Examine the ClusterLoader class and surrounding context, especially the pod discovery and container lookup logic
cat -n robusta_krr/core/integrations/kubernetes/__init__.py | sed -n '1,200p'

Repository: robusta-dev/krr

Length of output: 9205


🏁 Script executed:

# Search for postgres container name in the file
rg -n "postgres" robusta_krr/core/integrations/kubernetes/__init__.py

Repository: robusta-dev/krr

Length of output: 206


🏁 Script executed:

# Look for __build_scannable_object method and related container lookup logic
rg -n "__build_scannable_object|_get_container_from_pod" robusta_krr/core/integrations/kubernetes/__init__.py -A 3

Repository: robusta-dev/krr

Length of output: 1039


🏁 Script executed:

# Get more context around the container discovery, especially for CNPGCluster
cat -n robusta_krr/core/integrations/kubernetes/__init__.py | sed -n '240,350p'

Repository: robusta-dev/krr

Length of output: 5069


🏁 Script executed:

# Search for any verification of container existence or pooler-related filtering
rg -n "container|pooler|Pooler" robusta_krr/core/integrations/kubernetes/__init__.py | head -20

Repository: robusta-dev/krr

Length of output: 1561


🏁 Script executed:

# Check if there's error handling when a container is not found in a pod
rg -n "get_container_from_pod|_get_container" robusta_krr/core/integrations/kubernetes/__init__.py -A 5

Repository: robusta-dev/krr

Length of output: 41


🏁 Script executed:

# Search for any validation or filtering of pods after they're discovered
rg -n "list_pods|PodData" robusta_krr/core/integrations/kubernetes/__init__.py -B 2 -A 2 | head -40

Repository: robusta-dev/krr

Length of output: 1298


Add component selector to CNPG pod discovery.

CloudNativePG applies cnpg.io/cluster to both database and pooler pods, but distinguishes them with app.kubernetes.io/component=database for database instances and app.kubernetes.io/component=pooler for PgBouncer poolers. The current selector uses only the cluster label and will match both pod types, but the code hard-codes the container name to postgres, which only exists on database pods. This causes an inconsistency that will fail when downstream code attempts to reference the container.

🔧 Suggested fix
         elif object.kind == "CNPGCluster":
-            selector = f"cnpg.io/cluster={object.name}"
+            selector = f"cnpg.io/cluster={object.name},app.kubernetes.io/component=database"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@robusta_krr/core/integrations/kubernetes/__init__.py` around lines 176 - 177,
The CNPGCluster selector currently only uses the cluster label (selector =
f"cnpg.io/cluster={object.name}") which matches both database and pooler pods
while the code assumes a postgres container; update the selector in the
CNPGCluster branch so it includes the component label to restrict to the
database pods (e.g. add ",app.kubernetes.io/component=database") or choose
component dynamically when you need pooler vs database; modify the code that
builds selector for object.kind == "CNPGCluster" to include the appropriate
app.kubernetes.io/component value so downstream references to the postgres
container are safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants