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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Generic resource tracking
===============
Comment on lines +1 to +2
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

RST underline should match or exceed title length.

The underline =============== (15 characters) is shorter than the title "Generic resource tracking" (25 characters). In reStructuredText, the underline should be at least as long as the title text.

📝 Suggested fix
 Generic resource tracking
-===============
+=========================
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Generic resource tracking
===============
Generic resource tracking
=========================
🤖 Prompt for AI Agents
In `@docs/playbook-reference/kubernetes-examples/kubernetes-generic-example.rst`
around lines 1 - 2, The RST title underline is shorter than the title text
"Generic resource tracking"; update the underline row (the line currently
containing `===============`) so it matches or exceeds the title length (at
least 25 characters) by extending the sequence of `=` characters to equal the
title length, ensuring the underline and title use the same adornment character.


Track changes to any Kubernetes resource

Track HTTPRoute Change
--------------------------------

For example, get notified when an HTTPRoute resource changes.

**Setup Steps**:

1. **Grant Permissions to Robusta**: By default, Robusta does not have permission to read it
2. **Configure Kubewatch**: Set up Kubewatch to monitor HTTPRoute resources
3. **Create Custom Playbook**: Define notification rules

**1. Grant Permissions to Robusta**

Create a YAML file named ``kubewatch-httproute-permissions.yaml`` with the following content:

.. code-block:: yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kubewatch-custom-role
rules:
- apiGroups:
- "gateway.networking.k8s.io"
resources:
- httproutes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubewatch-custom-role-binding
roleRef:
kind: ClusterRole
name: kubewatch-custom-role
subjects:
- kind: ServiceAccount
name: robusta-forwarder-service-account
namespace: default
Comment on lines +44 to +47
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded namespace may not match actual deployment.

The ServiceAccount namespace is hardcoded to default, but Robusta is typically installed in its own namespace (e.g., robusta). Consider noting that users should adjust this to match their Robusta installation namespace.

📝 Suggested fix
     subjects:
     - kind: ServiceAccount
       name: robusta-forwarder-service-account
-      namespace: default
+      namespace: robusta  # Adjust to match your Robusta installation namespace
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subjects:
- kind: ServiceAccount
name: robusta-forwarder-service-account
namespace: default
subjects:
- kind: ServiceAccount
name: robusta-forwarder-service-account
namespace: robusta # Adjust to match your Robusta installation namespace
🤖 Prompt for AI Agents
In `@docs/playbook-reference/kubernetes-examples/kubernetes-generic-example.rst`
around lines 44 - 47, The ServiceAccount entry hardcodes namespace: default
which may not match users' Robusta installation; update the documentation
snippet around the subjects -> kind: ServiceAccount / name:
robusta-forwarder-service-account to either remove the hardcoded namespace or
add a clear note and example showing the namespace should be set to the Robusta
namespace (e.g., robusta) or replaced by a placeholder (e.g.,
<ROBUSTA_NAMESPACE>) so users adjust it to their deployment.

Apply the permissions:

.. code-block:: bash
kubectl apply -f kubewatch-httproute-permissions.yaml
**2. Configure Kubewatch to Monitor HTTPRoute**

Add the following to the ``kubewatch`` section in your ``generated_values.yaml``:

.. code-block:: yaml
kubewatch:
config:
customresources:
- group: gateway.networking.k8s.io
version: v1
resource: httproutes
**3. Create Custom Playbook**

Add the following to the ``customPlaybooks`` section in your ``generated_values.yaml``:

.. code-block:: yaml
customPlaybooks:
- triggers:
- on_kubernetes_resource_operation:
namespace_prefix: "monitoring"
name_prefix: "grafana-"
actions:
- create_finding: #
title: "resource $name in namespace $namespace was modified"
aggregation_key: "resource_modified"
Comment on lines +74 to +82
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Playbook example filters don't align with HTTPRoute context.

The example uses namespace_prefix: "monitoring" and name_prefix: "grafana-", which doesn't relate to the HTTPRoute scenario described earlier. This could confuse users following the guide. Also, the trailing # comment on line 80 appears incomplete.

📝 Suggested fix
   customPlaybooks:
   - triggers:
     - on_kubernetes_resource_operation:
-        namespace_prefix: "monitoring"
-        name_prefix: "grafana-"
+        # Adjust these filters to match your HTTPRoute resources
+        namespace_prefix: ""
+        name_prefix: ""
     actions:
-    - create_finding: # 
+    - create_finding:
         title: "resource $name in namespace $namespace was modified"
         aggregation_key: "resource_modified"
🤖 Prompt for AI Agents
In `@docs/playbook-reference/kubernetes-examples/kubernetes-generic-example.rst`
around lines 74 - 82, The example's Kubernetes filters don't match the HTTPRoute
scenario and include a stray "#" comment; update the customPlaybooks entry for
on_kubernetes_resource_operation so namespace_prefix and name_prefix reflect the
HTTPRoute example (e.g., set namespace_prefix to the HTTPRoute namespace used
earlier and name_prefix to a prefix like "httproute-" or the actual HTTPRoute
name prefix), ensure the create_finding action uses the intended title,
aggregation_key, etc., and remove the trailing "#" after create_finding to
eliminate the incomplete comment.

Then perform a :ref:`Helm Upgrade <Simple Upgrade>`.
1 change: 1 addition & 0 deletions src/robusta/core/playbooks/playbooks_event_handler_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def handle_trigger(self, trigger_event: TriggerEvent) -> Optional[Dict[str, Any]
f"Failed to build execution event for {trigger_event.get_event_description()}, Event: {trigger_event}",
exc_info=True,
)
execution_event = None

if execution_event: # might not exist for unsupported k8s types
execution_event.named_sinks = (
Expand Down
38 changes: 37 additions & 1 deletion src/robusta/integrations/kubernetes/base_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from robusta.core.model.events import ExecutionBaseEvent
from robusta.core.model.k8s_operation_type import K8sOperationType
from robusta.core.reporting import Finding, FindingSource
from robusta.core.reporting import Finding, FindingSource, FindingSubject, FindingSubjectType


@dataclass
Expand All @@ -31,3 +31,39 @@ def create_default_finding(self) -> Finding:
@classmethod
def get_source(cls) -> FindingSource:
return FindingSource.KUBERNETES_API_SERVER

def get_subject(self) -> FindingSubject:
"""Get subject information from the Kubernetes object, including custom resources."""
if not self.obj:
return FindingSubject(name="Unresolved", subject_type=FindingSubjectType.TYPE_NONE)

# Handle both Hikaru objects and GenericKubernetesObject
if hasattr(self.obj, 'metadata') and self.obj.metadata:
metadata = self.obj.metadata
name = getattr(metadata, 'name', None) or 'Unresolved'
namespace = getattr(metadata, 'namespace', None)
labels = getattr(metadata, 'labels', {}) or {}
annotations = getattr(metadata, 'annotations', {}) or {}
else:
name = 'Unresolved'
namespace = None
labels = {}
annotations = {}

# Get the kind from the object
kind = getattr(self.obj, 'kind', None) or 'Unknown'

# Determine the subject type from the kind
try:
subject_type = FindingSubjectType.from_kind(kind)
except:
# Fallback for unknown kinds
subject_type = FindingSubjectType.TYPE_NONE
Comment on lines +57 to +61
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid bare except clause.

The bare except: catches all exceptions including KeyboardInterrupt and SystemExit, which can mask critical issues. Based on the from_kind implementation in the relevant snippets, it returns TYPE_NONE for unknown kinds and shouldn't raise exceptions. This try-except may be unnecessary, but if kept, catch a specific exception.

📝 Suggested fix
         # Determine the subject type from the kind
-        try:
-            subject_type = FindingSubjectType.from_kind(kind)
-        except:
-            # Fallback for unknown kinds
-            subject_type = FindingSubjectType.TYPE_NONE
+        subject_type = FindingSubjectType.from_kind(kind)

If defensive handling is preferred:

-        except:
+        except Exception:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
subject_type = FindingSubjectType.from_kind(kind)
except:
# Fallback for unknown kinds
subject_type = FindingSubjectType.TYPE_NONE
# Determine the subject type from the kind
subject_type = FindingSubjectType.from_kind(kind)
🧰 Tools
🪛 Flake8 (7.3.0)

[error] 59-59: do not use bare 'except'

(E722)

🪛 Ruff (0.14.11)

59-59: Do not use bare except

(E722)

🤖 Prompt for AI Agents
In `@src/robusta/integrations/kubernetes/base_event.py` around lines 57 - 61,
Replace the bare except around FindingSubjectType.from_kind by calling it
directly (subject_type = FindingSubjectType.from_kind(kind)) since from_kind
already returns TYPE_NONE for unknown kinds; if you prefer defensive handling,
catch only specific exceptions like (ValueError, TypeError) and handle or log
them rather than using a bare except, ensuring subject_type falls back to
FindingSubjectType.TYPE_NONE.


return FindingSubject(
name=name,
subject_type=subject_type,
namespace=namespace,
labels=labels,
annotations=annotations,
)
50 changes: 44 additions & 6 deletions src/robusta/integrations/kubernetes/base_triggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@
from robusta.utils.common import duplicate_without_fields, is_matching_diff
from robusta.utils.scope import BaseScopeMatcher, ScopeParams


class GenericKubernetesObject:
"""Wrapper for unsupported Kubernetes resources that provides basic metadata access."""

def __init__(self, raw_data: Dict[str, Any]):
self._raw_data = raw_data
self.kind = raw_data.get('kind', 'Unknown')
# Create a simple metadata object for compatibility
metadata_data = raw_data.get('metadata', {})
self.metadata = type('Metadata', (), {
'name': metadata_data.get('name'),
'namespace': metadata_data.get('namespace'),
'labels': metadata_data.get('labels', {}),
'annotations': metadata_data.get('annotations', {}),
})()

def __getattr__(self, name):
"""Fallback to raw data for any missing attributes."""
return self._raw_data.get(name)

OBJ = "obj"
OLD_OBJ = "old_obj"

Expand Down Expand Up @@ -219,13 +239,20 @@ def __load_hikaru_obj(cls, obj: Dict[Any, Any], model_class: type) -> HikaruBase

@classmethod
def __parse_kubernetes_objs(cls, k8s_payload: IncomingK8sEventPayload):
model_class = get_api_version(k8s_payload.apiVersion).get(k8s_payload.kind)
api_version_models = get_api_version(k8s_payload.apiVersion)
model_class = None
if api_version_models:
model_class = api_version_models.get(k8s_payload.kind)

if model_class is None:
msg = (
f"classes for kind {k8s_payload.kind} cannot be found. skipping. description {k8s_payload.description}"
f"classes for kind {k8s_payload.kind} cannot be found. using generic model. description {k8s_payload.description}"
)
logging.error(msg)
raise ModelNotFoundException(msg)
logging.warning(msg)
# For unsupported kinds, we'll create a generic wrapper object from the raw data
obj = GenericKubernetesObject(k8s_payload.obj)
old_obj = GenericKubernetesObject(k8s_payload.oldObj) if k8s_payload.oldObj is not None else None
return obj, old_obj

obj = cls.__load_hikaru_obj(k8s_payload.obj, model_class)

Expand All @@ -247,9 +274,9 @@ def build_execution_event(
event_class = KIND_TO_EVENT_CLASS.get(event.k8s_payload.kind.lower())
if event_class is None:
logging.info(
f"classes for kind {event.k8s_payload.kind} cannot be found. skipping. description {event.k8s_payload.description}"
f"classes for kind {event.k8s_payload.kind} cannot be found. using KubernetesAnyChangeEvent. description {event.k8s_payload.description}"
)
return None
event_class = KubernetesAnyChangeEvent
if build_context and OBJ in build_context.keys() and OLD_OBJ in build_context.keys():
obj = build_context.get(OBJ)
old_obj = build_context.get(OLD_OBJ)
Expand Down Expand Up @@ -285,6 +312,17 @@ def check_change_filters(self, execution_event: ExecutionBaseEvent):

result = True
filtered_diffs = []

# For GenericKubernetesObject, we can't use the duplicate_without_fields method
# so we just set the filtered objects to be the same as the originals
if isinstance(execution_event.obj, GenericKubernetesObject):
obj_filtered = execution_event.obj
old_obj_filtered = execution_event.old_obj
execution_event.obj_filtered = obj_filtered
execution_event.old_obj_filtered = old_obj_filtered
execution_event.filtered_diffs = []
return True

obj_filtered = duplicate_without_fields(execution_event.obj, self.change_filters.ignore)
old_obj_filtered = duplicate_without_fields(execution_event.old_obj, self.change_filters.ignore)

Expand Down