Skip to content

Commit 5a960e1

Browse files
authored
Added support for webhook_url dynamic overriding using annotations (issue 1083) (#1476)
* Created MsTeamsWebhookUrlTransformer to enable overriding the webhook_url using annotations from workload or from environment variable
1 parent 0b63f9d commit 5a960e1

File tree

7 files changed

+324
-35
lines changed

7 files changed

+324
-35
lines changed

docs/configuration/sinks/ms-teams.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Configuring the MS Teams sink
2020
- ms_teams_sink:
2121
name: main_ms_teams_sink
2222
webhook_url: teams-incoming-webhook # see instructions below
23+
webhook_override: DYNAMIC MS TEAMS WEBHOOK URL OVERRIDE (Optional)
2324
2425
Then do a :ref:`Helm Upgrade <Simple Upgrade>`.
2526

@@ -35,3 +36,40 @@ Obtaining a webhook URL
3536
.. image:: /images/msteams_sink/msteam_get_webhook_url.gif
3637
:width: 1024
3738
:align: center
39+
40+
41+
Dynamically Route MS Teams Alerts
42+
-------------------------------------------------------------------
43+
44+
You can set the MS Teams webhook url value dynamically, based on the value of a specific ``annotation`` and environmental variable passed to runner.
45+
46+
This can be done using the optional ``webhook_override`` sink parameter.
47+
48+
As for now, the ``webhook_override`` parameter supports retrieving values specifically from annotations. You can specify an annotation key to retrieve the MS Teams webhook URL using the format ``annotations.<annotation_key>``. For example, if you use ``annotations.ms-team-alerts-sink``, the webhook URL will be taken from an annotation with the key ``ms-team-alerts-sink``.
49+
50+
If the specified annotation does not exist, the default webhook URL from the ``webhook_url`` parameter will be used. If the annotation exists but does not contain a URL, the system will look for an environmental variable with the name matching the ``annotation`` value.
51+
52+
.. code-block:: yaml
53+
54+
sinksConfig:
55+
# MS Teams integration params
56+
- ms_teams_sink:
57+
name: main_ms_teams_sink
58+
webhook_url: teams-incoming-webhook # see instructions below
59+
webhook_override: "annotations.ms-team-alerts-sink"
60+
61+
A replacement pattern is also allowed, using ``$`` sign, before the variable.
62+
For cases where labels or annotations include special characters, such as ``${annotations.kubernetes.io/service-name}``, you can use the `${}` replacement pattern to represent the entire key, including special characters.
63+
For example, if you want to dynamically set the MS Teams webhook url based on the annotation ``kubernetes.io/service-name``, you can use the following syntax:
64+
65+
- ``webhook_override: "${annotations.kubernetes.io/service-name}"``
66+
67+
Example:
68+
69+
.. code-block:: yaml
70+
71+
sinksConfig:
72+
- ms_teams_sink:
73+
name: main_ms_teams_sink
74+
webhook_url: teams-incoming-webhook # see instructions below
75+
webhook_override: ${annotations.kubernetes.io/service-name}

src/robusta/core/sinks/common/channel_transformer.py

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import defaultdict
22
from string import Template
3-
from typing import Dict, Optional, Union
3+
from typing import Dict, Optional
44

55
import regex
66

@@ -18,21 +18,7 @@
1818
MISSING = "<missing>"
1919

2020

21-
class ChannelTransformer:
22-
@classmethod
23-
def validate_channel_override(cls, v: Union[str, None]):
24-
if v:
25-
if regex.match(ONLY_VALUE_PATTERN, v):
26-
return "$" + v
27-
if not regex.match(COMPOSITE_PATTERN, v):
28-
err_msg = (
29-
f"channel_override must be '{CLUSTER_PREF}' or '{LABELS_PREF}foo' or '{ANNOTATIONS_PREF}foo' "
30-
f"or contain patters like: '${CLUSTER_PREF}'/'${LABELS_PREF}foo'/"
31-
f"'${ANNOTATIONS_PREF}foo'"
32-
)
33-
raise ValueError(err_msg)
34-
return v
35-
21+
class BaseChannelTransformer:
3622
@classmethod
3723
def normalize_key_string(cls, s: str) -> str:
3824
return s.replace("/", "_").replace(".", "_").replace("-", "_")
@@ -47,7 +33,7 @@ def normalize_dict_keys(cls, metadata: Dict) -> Dict:
4733
# else, if found, return replacement else return MISSING
4834
@classmethod
4935
def get_replacement(cls, prefix: str, value: str, normalized_replacements: Dict) -> str:
50-
if prefix in value: # value is in the format of "$prefix" or "prefix"
36+
if prefix in value:
5137
value = cls.normalize_key_string(value.replace(prefix, ""))
5238
if "$" in value:
5339
return Template(value).safe_substitute(normalized_replacements)
@@ -56,13 +42,7 @@ def get_replacement(cls, prefix: str, value: str, normalized_replacements: Dict)
5642
return ""
5743

5844
@classmethod
59-
def replace_token(
60-
cls,
61-
pattern: regex.Pattern,
62-
prefix: str,
63-
channel: str,
64-
replacements: Dict[str, str],
65-
) -> str:
45+
def replace_token(cls, pattern: regex.Pattern, prefix: str, channel: str, replacements: Dict[str, str]) -> str:
6646
tokens = pattern.findall(channel)
6747
for token in tokens:
6848
clean_token = token.replace("{", "").replace("}", "")
@@ -71,6 +51,30 @@ def replace_token(
7151
channel = channel.replace(token, replacement)
7252
return channel
7353

54+
@classmethod
55+
def process_template_annotations(cls, channel: str, annotations: Dict[str, str]) -> str:
56+
if ANNOTATIONS_PREF in channel:
57+
normalized_annotations = cls.normalize_dict_keys(annotations)
58+
channel = cls.replace_token(BRACKETS_PATTERN, ANNOTATIONS_PREF, channel, normalized_annotations)
59+
channel = cls.replace_token(ANNOTATIONS_PREF_PATTERN, ANNOTATIONS_PREF, channel, normalized_annotations)
60+
return channel
61+
62+
63+
class ChannelTransformer(BaseChannelTransformer):
64+
@classmethod
65+
def validate_channel_override(cls, v: Optional[str]) -> str:
66+
if v:
67+
if regex.match(ONLY_VALUE_PATTERN, v):
68+
return "$" + v
69+
if not regex.match(COMPOSITE_PATTERN, v):
70+
err_msg = (
71+
f"channel_override must be '{CLUSTER_PREF}' or '{LABELS_PREF}foo' or '{ANNOTATIONS_PREF}foo' "
72+
f"or contain patters like: '${CLUSTER_PREF}'/'${LABELS_PREF}foo'/"
73+
f"'${ANNOTATIONS_PREF}foo'"
74+
)
75+
raise ValueError(err_msg)
76+
return v
77+
7478
@classmethod
7579
def template(
7680
cls,
@@ -93,14 +97,6 @@ def template(
9397
channel = cls.replace_token(BRACKETS_PATTERN, LABELS_PREF, channel, normalized_labels)
9498
channel = cls.replace_token(LABEL_PREF_PATTERN, LABELS_PREF, channel, normalized_labels)
9599

96-
if ANNOTATIONS_PREF in channel:
97-
normalized_annotations = cls.normalize_dict_keys(annotations)
98-
channel = cls.replace_token(BRACKETS_PATTERN, ANNOTATIONS_PREF, channel, normalized_annotations)
99-
channel = cls.replace_token(
100-
ANNOTATIONS_PREF_PATTERN,
101-
ANNOTATIONS_PREF,
102-
channel,
103-
normalized_annotations,
104-
)
100+
channel = cls.process_template_annotations(channel, annotations)
105101

106102
return channel if MISSING not in channel else default_channel

src/robusta/core/sinks/msteams/msteams_sink.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ class MsTeamsSink(SinkBase):
88
def __init__(self, sink_config: MsTeamsSinkConfigWrapper, registry):
99
super().__init__(sink_config.ms_teams_sink, registry)
1010
self.webhook_url = sink_config.ms_teams_sink.webhook_url
11+
self.webhook_override = sink_config.ms_teams_sink.webhook_override
1112

1213
def write_finding(self, finding: Finding, platform_enabled: bool):
1314
MsTeamsSender.send_finding_to_ms_teams(
14-
self.webhook_url, finding, platform_enabled, self.cluster_name, self.account_id
15+
self.webhook_url, finding, platform_enabled, self.cluster_name, self.account_id, self.webhook_override
1516
)

src/robusta/core/sinks/msteams/msteams_sink_params.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
from typing import Optional
2+
3+
from pydantic import validator
4+
5+
from robusta.core.sinks.msteams.msteams_webhook_tranformer import MsTeamsWebhookUrlTransformer
16
from robusta.core.sinks.sink_base_params import SinkBaseParams
27
from robusta.core.sinks.sink_config import SinkConfigBase
38

49

510
class MsTeamsSinkParams(SinkBaseParams):
611
webhook_url: str
12+
webhook_override: Optional[str] = None
713

814
@classmethod
915
def _get_sink_type(cls):
1016
return "msteams"
1117

18+
@validator("webhook_override")
19+
def validate_webhook_override(cls, v: str):
20+
return MsTeamsWebhookUrlTransformer.validate_webhook_override(v)
21+
1222

1323
class MsTeamsSinkConfigWrapper(SinkConfigBase):
1424
ms_teams_sink: MsTeamsSinkParams
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import logging
2+
import os
3+
from typing import Dict, Optional
4+
5+
import regex
6+
7+
from robusta.core.sinks.common.channel_transformer import ANNOTATIONS_PREF, MISSING, BaseChannelTransformer
8+
9+
ANNOTATIONS_COMPOSITE_PATTERN = r".*\$({?annotations.[^$]+).*"
10+
ANNOTATIONS_ONLY_VALUE_PATTERN = r"^(annotations.[^$]+)$"
11+
URL_PATTERN = regex.compile(
12+
r"^(https?)://"
13+
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|"
14+
r"localhost|"
15+
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|"
16+
r"\[?[A-F0-9]*:[A-F0-9:]+\]?)"
17+
r"(?::\d+)?"
18+
r"(?:/?|[/?]\S+)$",
19+
regex.IGNORECASE,
20+
)
21+
22+
23+
# This class supports overriding the webhook_url only using annotations from yaml files.
24+
# Annotations are used instead of labels because urls can be passed to annotations contrary to labels.
25+
# Labels must be an empty string or consist of alphanumeric characters, '-', '_', or '.',
26+
# and must start and end with an alphanumeric character (e.g., 'MyValue', 'my_value', or '12345').
27+
# The regex used for label validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?'.
28+
class MsTeamsWebhookUrlTransformer(BaseChannelTransformer):
29+
@classmethod
30+
def validate_webhook_override(cls, v: Optional[str]) -> Optional[str]:
31+
if v:
32+
if regex.match(ANNOTATIONS_ONLY_VALUE_PATTERN, v):
33+
return "$" + v
34+
if not regex.match(ANNOTATIONS_COMPOSITE_PATTERN, v):
35+
err_msg = f"webhook_override must be '{ANNOTATIONS_PREF}foo' or contain patterns like: '${ANNOTATIONS_PREF}foo'"
36+
raise ValueError(err_msg)
37+
return v
38+
39+
@classmethod
40+
def validate_url_or_get_env(cls, webhook_url: str, default_webhook_url: str) -> str:
41+
if URL_PATTERN.match(webhook_url):
42+
return webhook_url
43+
logging.info(f"URL matching failed for: {webhook_url}. Trying to get environment variable.")
44+
45+
env_value = os.getenv(webhook_url)
46+
if env_value:
47+
return env_value
48+
logging.info(f"Environment variable not found for: {webhook_url}. Using default webhook URL.")
49+
50+
return default_webhook_url
51+
52+
@classmethod
53+
def template(
54+
cls,
55+
webhook_override: Optional[str],
56+
default_webhook_url: str,
57+
annotations: Dict[str, str],
58+
) -> str:
59+
if not webhook_override:
60+
return default_webhook_url
61+
62+
webhook_url = webhook_override
63+
64+
webhook_url = cls.process_template_annotations(webhook_url, annotations)
65+
if MISSING in webhook_url:
66+
return default_webhook_url
67+
webhook_url = cls.validate_url_or_get_env(webhook_url, default_webhook_url)
68+
69+
return webhook_url

src/robusta/integrations/msteams/sender.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
MarkdownBlock,
1414
TableBlock,
1515
)
16+
from robusta.core.sinks.msteams.msteams_webhook_tranformer import MsTeamsWebhookUrlTransformer
1617
from robusta.integrations.msteams.msteams_msg import MsTeamsMsg
1718

1819

@@ -50,8 +51,17 @@ def __split_block_to_files_and_all_the_rest(cls, enrichment: Enrichment):
5051

5152
@classmethod
5253
def send_finding_to_ms_teams(
53-
cls, webhook_url: str, finding: Finding, platform_enabled: bool, cluster_name: str, account_id: str
54+
cls,
55+
webhook_url: str,
56+
finding: Finding,
57+
platform_enabled: bool,
58+
cluster_name: str,
59+
account_id: str,
60+
webhook_override: str,
5461
):
62+
webhook_url = MsTeamsWebhookUrlTransformer.template(
63+
webhook_override=webhook_override, default_webhook_url=webhook_url, annotations=finding.subject.annotations
64+
)
5565
msg = MsTeamsMsg(webhook_url)
5666
msg.write_title_and_desc(platform_enabled, finding, cluster_name, account_id)
5767

0 commit comments

Comments
 (0)