Skip to content

Commit 34eec39

Browse files
joeyorlandomatiasbactions-usergrafana-irm-app[bot]mderynck
authored
main -> dev (#5458)
# What this PR does ## Which issue(s) this PR closes Related to [issue link here] <!-- *Note*: If you want the issue to be auto-closed once the PR is merged, change "Related to" to "Closes" in the line above. If you have more than one GitHub issue that this PR closes, be sure to preface each issue link with a [closing keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). This ensures that the issue(s) are auto-closed once the PR has been merged. --> ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --------- Co-authored-by: Matias Bordese <[email protected]> Co-authored-by: GitHub Actions <[email protected]> Co-authored-by: grafana-irm-app[bot] <165293418+grafana-irm-app[bot]@users.noreply.github.com> Co-authored-by: Michael Derynck <[email protected]>
2 parents 576fecf + aaae31a commit 34eec39

File tree

10 files changed

+747
-103
lines changed

10 files changed

+747
-103
lines changed

helm/oncall/Chart.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ apiVersion: v2
22
name: oncall
33
description: Developer-friendly incident response with brilliant Slack integration
44
type: application
5-
version: 1.14.1
6-
appVersion: v1.14.1
5+
version: 1.14.4
6+
appVersion: v1.14.4
77
dependencies:
88
- name: cert-manager
99
version: v1.8.0

tools/migrators/README.md

+53-29
Large diffs are not rendered by default.

tools/migrators/lib/pagerduty/config.py

+23
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"Zabbix Webhook (for 5.0 and 5.2)": "zabbix",
2121
"Elastic Alerts": "elastalert",
2222
"Firebase": "fabric",
23+
"Amazon CloudWatch": "amazon_sns",
2324
}
2425

2526
# Experimental feature to migrate PD rulesets to OnCall integrations
@@ -38,3 +39,25 @@
3839
)
3940

4041
MIGRATE_USERS = os.getenv("MIGRATE_USERS", "true").lower() == "true"
42+
43+
# Filter resources by team
44+
PAGERDUTY_FILTER_TEAM = os.getenv("PAGERDUTY_FILTER_TEAM")
45+
46+
# Filter resources by users (comma-separated list of PagerDuty user IDs)
47+
PAGERDUTY_FILTER_USERS = [
48+
user_id.strip()
49+
for user_id in os.getenv("PAGERDUTY_FILTER_USERS", "").split(",")
50+
if user_id.strip()
51+
]
52+
53+
# Filter resources by name regex patterns
54+
PAGERDUTY_FILTER_SCHEDULE_REGEX = os.getenv("PAGERDUTY_FILTER_SCHEDULE_REGEX")
55+
PAGERDUTY_FILTER_ESCALATION_POLICY_REGEX = os.getenv(
56+
"PAGERDUTY_FILTER_ESCALATION_POLICY_REGEX"
57+
)
58+
PAGERDUTY_FILTER_INTEGRATION_REGEX = os.getenv("PAGERDUTY_FILTER_INTEGRATION_REGEX")
59+
60+
# Whether to preserve existing notification rules when migrating users
61+
PRESERVE_EXISTING_USER_NOTIFICATION_RULES = (
62+
os.getenv("PRESERVE_EXISTING_USER_NOTIFICATION_RULES", "true").lower() == "true"
63+
)

tools/migrators/lib/pagerduty/migrate.py

+154-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import re
23

34
from pdpyras import APISession
45

@@ -11,6 +12,11 @@
1112
MODE,
1213
MODE_PLAN,
1314
PAGERDUTY_API_TOKEN,
15+
PAGERDUTY_FILTER_ESCALATION_POLICY_REGEX,
16+
PAGERDUTY_FILTER_INTEGRATION_REGEX,
17+
PAGERDUTY_FILTER_SCHEDULE_REGEX,
18+
PAGERDUTY_FILTER_TEAM,
19+
PAGERDUTY_FILTER_USERS,
1420
)
1521
from lib.pagerduty.report import (
1622
escalation_policy_report,
@@ -43,6 +49,136 @@
4349
)
4450

4551

52+
def filter_schedules(schedules):
53+
"""Filter schedules based on configured filters"""
54+
filtered_schedules = []
55+
filtered_out = 0
56+
57+
for schedule in schedules:
58+
should_include = True
59+
reason = None
60+
61+
# Filter by team
62+
if PAGERDUTY_FILTER_TEAM:
63+
teams = schedule.get("teams", [])
64+
if not any(team["summary"] == PAGERDUTY_FILTER_TEAM for team in teams):
65+
should_include = False
66+
reason = f"No teams found for team filter: {PAGERDUTY_FILTER_TEAM}"
67+
68+
# Filter by users
69+
if should_include and PAGERDUTY_FILTER_USERS:
70+
schedule_users = set()
71+
for layer in schedule.get("schedule_layers", []):
72+
for user in layer.get("users", []):
73+
schedule_users.add(user["user"]["id"])
74+
75+
if not any(user_id in schedule_users for user_id in PAGERDUTY_FILTER_USERS):
76+
should_include = False
77+
reason = f"No users found for user filter: {','.join(PAGERDUTY_FILTER_USERS)}"
78+
79+
# Filter by name regex
80+
if should_include and PAGERDUTY_FILTER_SCHEDULE_REGEX:
81+
if not re.match(PAGERDUTY_FILTER_SCHEDULE_REGEX, schedule["name"]):
82+
should_include = False
83+
reason = f"Schedule regex filter: {PAGERDUTY_FILTER_SCHEDULE_REGEX}"
84+
85+
if should_include:
86+
filtered_schedules.append(schedule)
87+
else:
88+
filtered_out += 1
89+
print(f"{TAB}Schedule {schedule['id']}: {reason}")
90+
91+
if filtered_out > 0:
92+
print(f"Filtered out {filtered_out} schedules")
93+
94+
return filtered_schedules
95+
96+
97+
def filter_escalation_policies(policies):
98+
"""Filter escalation policies based on configured filters"""
99+
filtered_policies = []
100+
filtered_out = 0
101+
102+
for policy in policies:
103+
should_include = True
104+
reason = None
105+
106+
# Filter by team
107+
if PAGERDUTY_FILTER_TEAM:
108+
teams = policy.get("teams", [])
109+
if not any(team["summary"] == PAGERDUTY_FILTER_TEAM for team in teams):
110+
should_include = False
111+
reason = f"No teams found for team filter: {PAGERDUTY_FILTER_TEAM}"
112+
113+
# Filter by users
114+
if should_include and PAGERDUTY_FILTER_USERS:
115+
policy_users = set()
116+
for rule in policy.get("escalation_rules", []):
117+
for target in rule.get("targets", []):
118+
if target["type"] == "user":
119+
policy_users.add(target["id"])
120+
121+
if not any(user_id in policy_users for user_id in PAGERDUTY_FILTER_USERS):
122+
should_include = False
123+
reason = f"No users found for user filter: {','.join(PAGERDUTY_FILTER_USERS)}"
124+
125+
# Filter by name regex
126+
if should_include and PAGERDUTY_FILTER_ESCALATION_POLICY_REGEX:
127+
if not re.match(PAGERDUTY_FILTER_ESCALATION_POLICY_REGEX, policy["name"]):
128+
should_include = False
129+
reason = f"Escalation policy regex filter: {PAGERDUTY_FILTER_ESCALATION_POLICY_REGEX}"
130+
131+
if should_include:
132+
filtered_policies.append(policy)
133+
else:
134+
filtered_out += 1
135+
print(f"{TAB}Policy {policy['id']}: {reason}")
136+
137+
if filtered_out > 0:
138+
print(f"Filtered out {filtered_out} escalation policies")
139+
140+
return filtered_policies
141+
142+
143+
def filter_integrations(integrations):
144+
"""Filter integrations based on configured filters"""
145+
filtered_integrations = []
146+
filtered_out = 0
147+
148+
for integration in integrations:
149+
should_include = True
150+
reason = None
151+
152+
# Filter by team
153+
if PAGERDUTY_FILTER_TEAM:
154+
teams = integration["service"].get("teams", [])
155+
if not any(team["summary"] == PAGERDUTY_FILTER_TEAM for team in teams):
156+
should_include = False
157+
reason = f"No teams found for team filter: {PAGERDUTY_FILTER_TEAM}"
158+
159+
# Filter by name regex
160+
if should_include and PAGERDUTY_FILTER_INTEGRATION_REGEX:
161+
integration_name = (
162+
f"{integration['service']['name']} - {integration['name']}"
163+
)
164+
if not re.match(PAGERDUTY_FILTER_INTEGRATION_REGEX, integration_name):
165+
should_include = False
166+
reason = (
167+
f"Integration regex filter: {PAGERDUTY_FILTER_INTEGRATION_REGEX}"
168+
)
169+
170+
if should_include:
171+
filtered_integrations.append(integration)
172+
else:
173+
filtered_out += 1
174+
print(f"{TAB}Integration {integration['id']}: {reason}")
175+
176+
if filtered_out > 0:
177+
print(f"Filtered out {filtered_out} integrations")
178+
179+
return filtered_integrations
180+
181+
46182
def migrate() -> None:
47183
session = APISession(PAGERDUTY_API_TOKEN)
48184
session.timeout = 20
@@ -59,9 +195,13 @@ def migrate() -> None:
59195
print("▶ Fetching schedules...")
60196
# Fetch schedules from PagerDuty
61197
schedules = session.list_all(
62-
"schedules", params={"include[]": "schedule_layers", "time_zone": "UTC"}
198+
"schedules",
199+
params={"include[]": ["schedule_layers", "teams"], "time_zone": "UTC"},
63200
)
64201

202+
# Apply filters to schedules
203+
schedules = filter_schedules(schedules)
204+
65205
# Fetch overrides from PagerDuty
66206
since = datetime.datetime.now(datetime.timezone.utc)
67207
until = since + datetime.timedelta(
@@ -78,11 +218,19 @@ def migrate() -> None:
78218
oncall_schedules = OnCallAPIClient.list_all("schedules")
79219

80220
print("▶ Fetching escalation policies...")
81-
escalation_policies = session.list_all("escalation_policies")
221+
escalation_policies = session.list_all(
222+
"escalation_policies", params={"include[]": "teams"}
223+
)
224+
225+
# Apply filters to escalation policies
226+
escalation_policies = filter_escalation_policies(escalation_policies)
227+
82228
oncall_escalation_chains = OnCallAPIClient.list_all("escalation_chains")
83229

84230
print("▶ Fetching integrations...")
85-
services = session.list_all("services", params={"include[]": "integrations"})
231+
services = session.list_all(
232+
"services", params={"include[]": ["integrations", "teams"]}
233+
)
86234
vendors = session.list_all("vendors")
87235

88236
integrations = []
@@ -92,6 +240,9 @@ def migrate() -> None:
92240
integration["service"] = service
93241
integrations.append(integration)
94242

243+
# Apply filters to integrations
244+
integrations = filter_integrations(integrations)
245+
95246
oncall_integrations = OnCallAPIClient.list_all("integrations")
96247

97248
rulesets = None

tools/migrators/lib/pagerduty/report.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from lib.common.report import ERROR_SIGN, SUCCESS_SIGN, TAB, WARNING_SIGN
2+
from lib.pagerduty.config import PRESERVE_EXISTING_USER_NOTIFICATION_RULES
23

34

45
def format_user(user: dict) -> str:
@@ -88,8 +89,22 @@ def user_report(users: list[dict]) -> str:
8889
for user in sorted(users, key=lambda u: bool(u["oncall_user"]), reverse=True):
8990
result += "\n" + TAB + format_user(user)
9091

91-
if user["oncall_user"] and user["notification_rules"]:
92-
result += " (existing notification rules will be deleted)"
92+
if user["oncall_user"]:
93+
if (
94+
user["oncall_user"]["notification_rules"]
95+
and PRESERVE_EXISTING_USER_NOTIFICATION_RULES
96+
):
97+
# already has user notification rules defined in OnCall.. we won't touch these
98+
result += " (existing notification rules will be preserved due to the PRESERVE_EXISTING_USER_NOTIFICATION_RULES being set to True and this user already having notification rules defined in OnCall)"
99+
elif (
100+
user["oncall_user"]["notification_rules"]
101+
and not PRESERVE_EXISTING_USER_NOTIFICATION_RULES
102+
):
103+
# already has user notification rules defined in OnCall.. we will overwrite these
104+
result += " (existing notification rules will be overwritten due to the PRESERVE_EXISTING_USER_NOTIFICATION_RULES being set to False)"
105+
elif user["notification_rules"]:
106+
# user has notification rules defined in PagerDuty, but none defined in OnCall, we will migrate these
107+
result += " (existing PagerDuty notification rules will be migrated due to this user not having any notification rules defined in OnCall)"
93108

94109
return result
95110

tools/migrators/lib/pagerduty/resources/escalation_policies.py

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def match_escalation_policy_for_integration(
1717
policy_id = integration["service"]["escalation_policy"]["id"]
1818
policy = find_by_id(escalation_policies, policy_id)
1919

20+
if policy is None:
21+
integration["is_escalation_policy_flawed"] = True
22+
return
23+
2024
integration["is_escalation_policy_flawed"] = bool(
2125
policy["unmatched_users"] or policy["flawed_schedules"]
2226
)

tools/migrators/lib/pagerduty/resources/notification_rules.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import copy
22

33
from lib.oncall.api_client import OnCallAPIClient
4-
from lib.pagerduty.config import PAGERDUTY_TO_ONCALL_CONTACT_METHOD_MAP
4+
from lib.pagerduty.config import (
5+
PAGERDUTY_TO_ONCALL_CONTACT_METHOD_MAP,
6+
PRESERVE_EXISTING_USER_NOTIFICATION_RULES,
7+
)
58
from lib.utils import remove_duplicates, transform_wait_delay
69

710

@@ -23,6 +26,13 @@ def remove_duplicate_rules_between_waits(rules: list[dict]) -> list[dict]:
2326

2427

2528
def migrate_notification_rules(user: dict) -> None:
29+
if (
30+
PRESERVE_EXISTING_USER_NOTIFICATION_RULES
31+
and user["oncall_user"]["notification_rules"]
32+
):
33+
print(f"Preserving existing notification rules for {user['email']}")
34+
return
35+
2636
notification_rules = [
2737
rule for rule in user["notification_rules"] if rule["urgency"] == "high"
2838
]

tools/migrators/lib/tests/pagerduty/test_matching.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1330,7 +1330,7 @@
13301330
"scheduled_actions": [],
13311331
},
13321332
"oncall_integration": None,
1333-
"oncall_type": None,
1333+
"oncall_type": "amazon_sns",
13341334
"is_escalation_policy_flawed": False,
13351335
},
13361336
{
@@ -1420,7 +1420,7 @@
14201420
"scheduled_actions": [],
14211421
},
14221422
"oncall_integration": None,
1423-
"oncall_type": None,
1423+
"oncall_type": "amazon_sns",
14241424
"is_escalation_policy_flawed": True,
14251425
},
14261426
{
@@ -1510,7 +1510,7 @@
15101510
"scheduled_actions": [],
15111511
},
15121512
"oncall_integration": None,
1513-
"oncall_type": None,
1513+
"oncall_type": "amazon_sns",
15141514
"is_escalation_policy_flawed": True,
15151515
},
15161516
{

0 commit comments

Comments
 (0)