commit
c60074c07b
57 changed files with 346 additions and 1463 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
## v1.3.12 (2023-07-14)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `page_size`, `current_page_number`, and `total_pages` attributes to paginated API responses by @joeyorlando ([#2471](https://github.com/grafana/oncall/pull/2471))
|
||||
|
||||
### Fixed
|
||||
|
||||
- New webhooks incorrectly masking authorization header by @mderynck ([#2541](https://github.com/grafana/oncall/pull/2541))
|
||||
|
||||
## v1.3.11 (2023-07-13)
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ The above command returns JSON structured in the following way:
|
|||
"telegram": "https://t.me/c/5354/1234?thread=1234"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@ The above command returns JSON structured in the following way:
|
|||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,10 @@ The above command returns JSON structured in the following way:
|
|||
"name": "default",
|
||||
"team_id": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,10 @@ The above command returns JSON structured in the following way:
|
|||
"type": "notify_person_next_each_time",
|
||||
"persons_to_notify_next_each_time": ["U4DNY931HHJS5"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ The above command returns JSON structured in the following way:
|
|||
"channel_id": "CH23212D"
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"templates": {
|
||||
"grouping_key": null,
|
||||
"resolve_signal": null,
|
||||
"acknowledge_signal": null,
|
||||
|
|
@ -219,7 +219,10 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,10 @@ The above command returns JSON structured in the following way:
|
|||
"by_monthday": null,
|
||||
"users": ["U4DNY931HHJS5"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ The above command returns JSON structured in the following way:
|
|||
"id": "KGEFG74LU1D8L",
|
||||
"name": "Publish alert group notification to JIRA"
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,10 @@ The above command returns JSON structured in the following ways:
|
|||
"important": true,
|
||||
"type": "notify_by_phone_call"
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ The above command returns JSON structured in the following way:
|
|||
}
|
||||
```
|
||||
|
||||
| Parameter | Required | Description |
|
||||
| --------------- | :------: | :--------------------- |
|
||||
| `alert_group_id`| Yes | Alert group ID | |
|
||||
| `text` | Yes | Resolution note text |
|
||||
| Parameter | Required | Description |
|
||||
| ---------------- | :------: | :------------------- | --- |
|
||||
| `alert_group_id` | Yes | Alert group ID | |
|
||||
| `text` | Yes | Resolution note text |
|
||||
|
||||
**HTTP request**
|
||||
|
||||
|
|
@ -90,7 +90,10 @@ The above command returns JSON structured in the following way:
|
|||
"created_at": "2020-06-19T12:40:01.429805Z",
|
||||
"text": "Demo resolution note"
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,10 @@ The above command returns JSON structured in the following way:
|
|||
"channel_id": "CH23212D"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 25,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,10 @@ The above command returns JSON structured in the following way:
|
|||
"user_group_id": "MEOW_SLACK_ID"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -294,7 +297,10 @@ The above command returns JSON structured in the following way:
|
|||
"shift_start": "2023-01-27T09:00:00Z",
|
||||
"shift_end": "2023-01-27T17:00:00Z"
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ The above command returns JSON structured in the following way:
|
|||
"name": "meow_channel",
|
||||
"slack_id": "MEOW_SLACK_ID"
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ The above command returns JSON structured in the following way:
|
|||
"handle": "meow_group"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,10 @@ The above command returns JSON structured in the following way:
|
|||
"username": "alex",
|
||||
"role": "admin"
|
||||
}
|
||||
]
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 100,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -527,8 +527,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return getattr(heartbeat, self.INTEGRATIONS_TO_REVERSE_URL_MAP[self.integration], None)
|
||||
|
||||
# Demo alerts
|
||||
def send_demo_alert(self, force_route_id=None, payload=None):
|
||||
logger.info(f"send_demo_alert integration={self.pk} force_route_id={force_route_id}")
|
||||
def send_demo_alert(self, payload=None):
|
||||
logger.info(f"send_demo_alert integration={self.pk}")
|
||||
|
||||
if not self.is_demo_alert_enabled:
|
||||
raise UnableToSendDemoAlert("Unable to send demo alert for this integration.")
|
||||
|
|
@ -543,9 +543,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
"Unable to send demo alert as payload has no 'alerts' key, it is not array, or it is empty."
|
||||
)
|
||||
for alert in alerts:
|
||||
create_alertmanager_alerts.delay(
|
||||
alert_receive_channel_pk=self.pk, alert=alert, is_demo=True, force_route_id=force_route_id
|
||||
)
|
||||
create_alertmanager_alerts.delay(alert_receive_channel_pk=self.pk, alert=alert, is_demo=True)
|
||||
else:
|
||||
create_alert.delay(
|
||||
title="Demo alert",
|
||||
|
|
@ -556,7 +554,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
integration_unique_data=None,
|
||||
raw_request_data=payload,
|
||||
is_demo=True,
|
||||
force_route_id=force_route_id,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -165,11 +165,6 @@ class ChannelFilter(OrderedModel):
|
|||
return str(self.filtering_term).replace("`", "")
|
||||
raise Exception("Unknown filtering term")
|
||||
|
||||
def send_demo_alert(self):
|
||||
"""Deprecated. May be used in the older versions of the plugin"""
|
||||
integration = self.alert_receive_channel
|
||||
integration.send_demo_alert(force_route_id=self.pk)
|
||||
|
||||
# Insight logs
|
||||
@property
|
||||
def insight_logs_type_verbal(self):
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
from .terraform_file_renderer import TerraformFileRenderer # noqa: F401
|
||||
from .terraform_state_renderer import TerraformStateRenderer # noqa: F401
|
||||
|
|
@ -1,781 +0,0 @@
|
|||
from django.apps import apps
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.alerts.models import EscalationPolicy
|
||||
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal
|
||||
|
||||
|
||||
class TerraformFileRenderer:
|
||||
AMIXR_USER_DATA_TEMPLATE = '\ndata "amixr_user" "{}" {{\n username = "{}"\n}}\n'
|
||||
|
||||
TEAM_DATA_TEMPLATE = '\ndata "amixr_team" "{}" {{\n name = "{}"\n}}\n'
|
||||
|
||||
CUSTOM_ACTION_DATA_TEMPLATE = (
|
||||
'\ndata "amixr_action" "{}" {{\n name = "{}"\n integration_id = amixr_integration.{}.id\n}}\n'
|
||||
)
|
||||
|
||||
SCHEDULE_DATA_TEMPLATE = '\ndata "amixr_schedule" "{}" {{\n name = "{}"\n}}\n'
|
||||
|
||||
USER_GROUP_DATA_TEMPLATE = '\ndata "amixr_user_group" "{}" {{\n slack_handle = "{}"\n}}\n'
|
||||
|
||||
SLACK_CHANNEL_DATA_TEMPLATE = '\ndata "amixr_slack_channel" "{}" {{\n name = "{}"\n}}\n'
|
||||
|
||||
INTEGRATION_RESOURCE_TEMPLATE = (
|
||||
'\nresource "amixr_integration" "{}" {{\n name = "{}"\n type = "{}"\n team_id = {}\n}}\n'
|
||||
)
|
||||
INTEGRATION_RESOURCE_TEMPLATE_WITH_TEMPLATES = (
|
||||
'\nresource "amixr_integration" "{}" {{\n'
|
||||
' name = "{}"\n'
|
||||
' type = "{}"\n'
|
||||
" team_id = {}\n"
|
||||
" templates {}\n"
|
||||
"}}\n"
|
||||
)
|
||||
ROUTE_RESOURCES_TEMPLATE = (
|
||||
'\nresource "amixr_route" "{}" {{\n'
|
||||
" integration_id = amixr_integration.{}.id\n"
|
||||
" escalation_chain_id = {}.id\n"
|
||||
' routing_regex = "{}"\n'
|
||||
" position = {}\n"
|
||||
"}}\n"
|
||||
)
|
||||
ROUTE_RESOURCES_TEMPLATE_WITH_SLACK = (
|
||||
'\nresource "amixr_route" "{}" {{\n'
|
||||
" integration_id = amixr_integration.{}.id\n"
|
||||
" escalation_chain_id = {}.id\n"
|
||||
' routing_regex = "{}"\n'
|
||||
" position = {}\n"
|
||||
" slack {{\n"
|
||||
" channel_id = {}\n"
|
||||
" }}\n"
|
||||
"}}\n"
|
||||
)
|
||||
|
||||
ESCALATION_CHAIN_DATA_TEMPLATE = '\ndata "amixr_escalation_chain" "{}" {{\n name = "{}"\n}}\n'
|
||||
|
||||
ESCALATION_CHAIN_RESOURCE_TEMPLATE = (
|
||||
'\nresource "amixr_escalation_chain" "{}" {{\n name = "{}"\n team_id = {}\n}}\n'
|
||||
)
|
||||
|
||||
ESCALATION_POLICY_TEMPLATES = {
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[EscalationPolicy.STEP_WAIT]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" duration = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" important = {}\n"
|
||||
" persons_to_notify = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" persons_to_notify_next_each_time = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_NOTIFY_SCHEDULE
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" important = {}\n"
|
||||
" notify_on_call_from_schedule = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_NOTIFY_GROUP
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" important = {}\n"
|
||||
" group_to_notify = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" action_to_trigger = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_NOTIFY_IF_TIME
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" notify_if_time_from = {}\n"
|
||||
" notify_if_time_to = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
|
||||
EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW
|
||||
]: '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" num_alerts_in_window = {}\n"
|
||||
" num_minutes_in_window = {}\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
"step_is_none": '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
" type = null\n"
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
"other_steps": '\nresource "amixr_escalation" "{}" {{\n'
|
||||
" escalation_chain_id = {}\n"
|
||||
' type = "{}"\n'
|
||||
" position = {}\n"
|
||||
"}}\n",
|
||||
}
|
||||
|
||||
AMIXR_USERS_LIST_TEMPLATE = "[\n {}\n ]"
|
||||
AMIXR_USERS_LIST_TEMPLATE_EMPTY = "[]"
|
||||
ROLLING_USERS_TEMPLATE = " [{}],\n"
|
||||
ROLLING_USERS_LIST_TEMPLATE = "[\n{} ]"
|
||||
|
||||
SCHEDULE_RESOURCE_TEMPLATE_ICAL = (
|
||||
'\nresource "amixr_schedule" "{}" {{\n'
|
||||
' name = "{}"\n'
|
||||
' type = "ical"\n'
|
||||
" team_id = {}\n"
|
||||
" ical_url_primary = {}\n"
|
||||
" ical_url_overrides = {}\n"
|
||||
"{}"
|
||||
"}}\n"
|
||||
)
|
||||
|
||||
SCHEDULE_RESOURCE_TEMPLATE_CALENDAR = (
|
||||
'\nresource "amixr_schedule" "{}" {{\n'
|
||||
' name = "{}"\n'
|
||||
' type = "calendar"\n'
|
||||
" team_id = {}\n"
|
||||
' time_zone = "{}"\n'
|
||||
"{}"
|
||||
"}}\n"
|
||||
)
|
||||
|
||||
SCHEDULE_RESOURCE_SLACK_TEMPLATE = " slack {{\n channel_id = {}\n }}\n"
|
||||
|
||||
ON_CALL_SHIFT_RESOURCE_TEMPLATE_RECURRENT_EVENT = (
|
||||
'\nresource "amixr_on_call_shift" "{}" {{\n'
|
||||
' name = "{}"\n'
|
||||
' type = "{}"\n'
|
||||
" team_id = {}\n"
|
||||
' start = "{}"\n'
|
||||
" duration = {}\n"
|
||||
" level = {}\n"
|
||||
' frequency = "{}"\n'
|
||||
" interval = {}\n"
|
||||
' week_start = "{}"\n'
|
||||
" by_day = {}\n"
|
||||
" by_month = {}\n"
|
||||
" by_monthday = {}\n"
|
||||
" users = {}\n"
|
||||
"}}\n"
|
||||
)
|
||||
|
||||
ON_CALL_SHIFT_RESOURCE_TEMPLATE_ROLLING_USERS = (
|
||||
'\nresource "amixr_on_call_shift" "{}" {{\n'
|
||||
' name = "{}"\n'
|
||||
' type = "{}"\n'
|
||||
" team_id = {}\n"
|
||||
' start = "{}"\n'
|
||||
" duration = {}\n"
|
||||
" level = {}\n"
|
||||
' frequency = "{}"\n'
|
||||
" interval = {}\n"
|
||||
' week_start = "{}"\n'
|
||||
" by_day = {}\n"
|
||||
" by_month = {}\n"
|
||||
" by_monthday = {}\n"
|
||||
" rolling_users = {}\n"
|
||||
"}}\n"
|
||||
)
|
||||
|
||||
ON_CALL_SHIFT_RESOURCE_TEMPLATE_SINGLE_EVENT = (
|
||||
'\nresource "amixr_on_call_shift" "{}" {{\n'
|
||||
' name = "{}"\n'
|
||||
' type = "{}"\n'
|
||||
" team_id = {}\n"
|
||||
' start = "{}"\n'
|
||||
" duration = {}\n"
|
||||
" level = {}\n"
|
||||
" users = {}\n"
|
||||
"}}\n"
|
||||
)
|
||||
|
||||
def __init__(self, organization):
|
||||
self.organization = organization
|
||||
self.data = {}
|
||||
self.used_names = {}
|
||||
|
||||
def render_terraform_file(self):
|
||||
result = self.render_resource_text()
|
||||
data_result = self.render_data_text()
|
||||
result = data_result + result
|
||||
if len(result) == 0:
|
||||
result += "There is nothing here yet. Check Settings to add integration and come back!"
|
||||
return result
|
||||
|
||||
def render_resource_text(self):
|
||||
result = ""
|
||||
|
||||
result += self.render_escalation_chains_related_resources_text()
|
||||
|
||||
integrations_related_resources_text = self.render_integrations_related_resources_text()
|
||||
result += integrations_related_resources_text
|
||||
|
||||
shifts_related_resources_text = self.render_on_call_shift_resource_text()
|
||||
result += shifts_related_resources_text
|
||||
|
||||
schedules_related_resources_text = self.render_schedules_related_resources_text()
|
||||
result += schedules_related_resources_text
|
||||
|
||||
return result
|
||||
|
||||
def render_escalation_chains_related_resources_text(self):
|
||||
result = ""
|
||||
escalation_chains = self.organization.escalation_chains.all()
|
||||
|
||||
for escalation_chain in escalation_chains:
|
||||
resource_name = self.escape_string_for_terraform(escalation_chain.name)
|
||||
team_name = self.render_team_name(escalation_chain.team)
|
||||
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
|
||||
result += self.ESCALATION_CHAIN_RESOURCE_TEMPLATE.format(
|
||||
resource_name, escalation_chain.name, team_name_text
|
||||
)
|
||||
result += self.render_escalation_policy_resource_text(escalation_chain, resource_name)
|
||||
|
||||
return result
|
||||
|
||||
def render_escalation_policy_resource_text(self, escalation_chain, escalation_chain_resource_name):
|
||||
result = ""
|
||||
escalation_policies = escalation_chain.escalation_policies.all()
|
||||
|
||||
escalation_chain_id = f"amixr_escalation_chain.{escalation_chain_resource_name}.id"
|
||||
|
||||
for num, escalation_policy in enumerate(escalation_policies, start=1):
|
||||
escalation_name = f"escalation-{num}-{escalation_chain_resource_name}"
|
||||
step_type = None
|
||||
|
||||
if escalation_policy.step is None:
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES["step_is_none"].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
escalation_policy.order,
|
||||
)
|
||||
else:
|
||||
step_type = EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[escalation_policy.step]
|
||||
|
||||
if escalation_policy.step == EscalationPolicy.STEP_WAIT:
|
||||
wait_delay = escalation_policy.wait_delay
|
||||
delay = int(wait_delay.total_seconds()) if wait_delay is not None else "null"
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
delay,
|
||||
escalation_policy.order,
|
||||
)
|
||||
elif escalation_policy.step in [
|
||||
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
||||
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
|
||||
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE,
|
||||
]:
|
||||
persons_to_notify = escalation_policy.sorted_users_queue
|
||||
important = escalation_policy.step == EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT
|
||||
important = "true" if important else "false"
|
||||
rendered_persons_to_notify = self.render_amixr_users_list_text(persons_to_notify)
|
||||
|
||||
if escalation_policy.step == EscalationPolicy.STEP_NOTIFY_USERS_QUEUE:
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
rendered_persons_to_notify,
|
||||
escalation_policy.order,
|
||||
)
|
||||
else:
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
important,
|
||||
rendered_persons_to_notify,
|
||||
escalation_policy.order,
|
||||
)
|
||||
elif escalation_policy.step in [
|
||||
EscalationPolicy.STEP_NOTIFY_SCHEDULE,
|
||||
EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT,
|
||||
]:
|
||||
schedule = escalation_policy.notify_schedule
|
||||
schedule_name = self.render_name(schedule, "schedules", "name")
|
||||
schedule_text = "null"
|
||||
if schedule is not None:
|
||||
schedule_text = f"amixr_schedule.{schedule_name}.id"
|
||||
|
||||
important = escalation_policy.step == EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT
|
||||
important = "true" if important else "false"
|
||||
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
important,
|
||||
schedule_text,
|
||||
escalation_policy.order,
|
||||
)
|
||||
elif escalation_policy.step in [
|
||||
EscalationPolicy.STEP_NOTIFY_GROUP,
|
||||
EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT,
|
||||
]:
|
||||
user_group = escalation_policy.notify_to_group
|
||||
user_group_name = self.render_user_group_name(user_group)
|
||||
important = escalation_policy.step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT
|
||||
important = "true" if important else "false"
|
||||
user_group_text = f"data.amixr_user_group.{user_group_name}.id" if user_group else "null"
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
important,
|
||||
user_group_text,
|
||||
escalation_policy.order,
|
||||
)
|
||||
# TODO: uncomment after custom actions refactoring
|
||||
# elif escalation_policy.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
|
||||
# custom_action = escalation_policy.custom_button_trigger
|
||||
# custom_action_name = self.render_name(custom_action, "custom_actions", "name")
|
||||
# self.render_custom_action_data_source(custom_action, custom_action_name, integration_resource_name)
|
||||
# custom_action_text = f"data.amixr_action.{custom_action_name}.id" if custom_action else "null"
|
||||
# result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
# escalation_name, escalation_chain_id, step_type, custom_action_text, escalation_policy.order,
|
||||
# )
|
||||
elif escalation_policy.step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
||||
from_time = self.render_time_string(escalation_policy.from_time)
|
||||
to_time = self.render_time_string(escalation_policy.to_time)
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
from_time,
|
||||
to_time,
|
||||
escalation_policy.order,
|
||||
)
|
||||
elif escalation_policy.step == EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW:
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
escalation_policy.num_alerts_in_window,
|
||||
escalation_policy.num_minutes_in_window,
|
||||
escalation_policy.order,
|
||||
)
|
||||
elif escalation_policy.step is not None:
|
||||
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES["other_steps"].format(
|
||||
escalation_name,
|
||||
escalation_chain_id,
|
||||
step_type,
|
||||
escalation_policy.order,
|
||||
)
|
||||
return result
|
||||
|
||||
def render_integrations_related_resources_text(self):
|
||||
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
|
||||
result = ""
|
||||
integrations = self.organization.alert_receive_channels.all().order_by("created_at")
|
||||
for integration in integrations:
|
||||
integration_resource_name = self.render_name(integration, "integrations", "verbal_name")
|
||||
team_name = self.render_team_name(integration.team)
|
||||
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
|
||||
formatted_integration_name = self.escape_string_for_terraform(integration.verbal_name)
|
||||
templates = self.render_integration_template(integration)
|
||||
if templates is not None:
|
||||
result += TerraformFileRenderer.INTEGRATION_RESOURCE_TEMPLATE_WITH_TEMPLATES.format(
|
||||
integration_resource_name,
|
||||
formatted_integration_name,
|
||||
AlertReceiveChannel.INTEGRATIONS_TO_REVERSE_URL_MAP[integration.integration],
|
||||
team_name_text,
|
||||
templates,
|
||||
)
|
||||
else:
|
||||
result += TerraformFileRenderer.INTEGRATION_RESOURCE_TEMPLATE.format(
|
||||
integration_resource_name,
|
||||
formatted_integration_name,
|
||||
AlertReceiveChannel.INTEGRATIONS_TO_REVERSE_URL_MAP[integration.integration],
|
||||
team_name_text,
|
||||
)
|
||||
route_text = self.render_route_resource_text(integration, integration_resource_name)
|
||||
# render data sources for custom actions just after integration resource
|
||||
actions_data_text = self.render_action_data_text()
|
||||
result += actions_data_text
|
||||
|
||||
result += route_text
|
||||
return result
|
||||
|
||||
def render_route_resource_text(self, integration, integration_resource_name):
|
||||
SlackChannel = apps.get_model("slack", "SlackChannel")
|
||||
slack_team_identity = self.organization.slack_team_identity
|
||||
slack_channels_q = SlackChannel.objects.filter(
|
||||
slack_id=OuterRef("slack_channel_id"),
|
||||
slack_team_identity=slack_team_identity,
|
||||
)
|
||||
routes = integration.channel_filters.all().annotate(
|
||||
slack_channel_name=Subquery(slack_channels_q.values("name")[:1])
|
||||
)
|
||||
result = ""
|
||||
for num, route in enumerate(routes, start=1):
|
||||
if route.is_default:
|
||||
continue
|
||||
route_name = f"route-{num}-{integration_resource_name}"
|
||||
|
||||
escalation_chain_resource_name = "amixr_escalation_chain." + self.escape_string_for_terraform(
|
||||
route.escalation_chain.name
|
||||
)
|
||||
|
||||
routing_regex = self.escape_string_for_terraform(route.filtering_term)
|
||||
if route.slack_channel_id is not None:
|
||||
if route.slack_channel_name is not None:
|
||||
slack_channel_data_name = f"slack-channel-{route.slack_channel_name}"
|
||||
slack_channel_id = f"data.amixr_slack_channel.{slack_channel_data_name}.slack_id"
|
||||
if route.slack_channel_name not in self.data.setdefault("slack_channels", {}):
|
||||
data_result = TerraformFileRenderer.SLACK_CHANNEL_DATA_TEMPLATE.format(
|
||||
slack_channel_data_name,
|
||||
route.slack_channel_name,
|
||||
)
|
||||
self.data["slack_channels"][route.slack_channel_name] = data_result
|
||||
else:
|
||||
slack_channel_id = f'"{route.slack_channel_id}"'
|
||||
result += TerraformFileRenderer.ROUTE_RESOURCES_TEMPLATE_WITH_SLACK.format(
|
||||
route_name,
|
||||
integration_resource_name,
|
||||
escalation_chain_resource_name,
|
||||
routing_regex,
|
||||
route.order,
|
||||
slack_channel_id,
|
||||
)
|
||||
else:
|
||||
result += TerraformFileRenderer.ROUTE_RESOURCES_TEMPLATE.format(
|
||||
route_name,
|
||||
integration_resource_name,
|
||||
escalation_chain_resource_name,
|
||||
routing_regex,
|
||||
route.order,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def render_schedules_related_resources_text(self):
|
||||
SlackChannel = apps.get_model("slack", "SlackChannel")
|
||||
slack_team_identity = self.organization.slack_team_identity
|
||||
slack_channels_q = SlackChannel.objects.filter(
|
||||
slack_id=OuterRef("channel"),
|
||||
slack_team_identity=slack_team_identity,
|
||||
)
|
||||
schedules = self.organization.oncall_schedules.annotate(
|
||||
slack_channel_name=Subquery(slack_channels_q.values("name")[:1])
|
||||
).order_by("pk")
|
||||
result = ""
|
||||
for schedule in schedules:
|
||||
schedule_name = self.render_name(schedule, "schedules", "name")
|
||||
formatted_schedule_name = self.escape_string_for_terraform(schedule.name)
|
||||
team_name = self.render_team_name(schedule.team)
|
||||
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
|
||||
slack_channel_text = ""
|
||||
if schedule.channel is not None:
|
||||
if schedule.slack_channel_name is not None:
|
||||
slack_channel_data_name = f"slack-channel-{schedule.slack_channel_name}"
|
||||
slack_channel_id = f"data.amixr_slack_channel.{slack_channel_data_name}.slack_id"
|
||||
if schedule.slack_channel_name not in self.data.setdefault("slack_channels", {}):
|
||||
data_result = TerraformFileRenderer.SLACK_CHANNEL_DATA_TEMPLATE.format(
|
||||
slack_channel_data_name,
|
||||
schedule.slack_channel_name,
|
||||
)
|
||||
self.data["slack_channels"][schedule.slack_channel_name] = data_result
|
||||
else:
|
||||
slack_channel_id = f'"{schedule.channel}"'
|
||||
|
||||
slack_channel_text = TerraformFileRenderer.SCHEDULE_RESOURCE_SLACK_TEMPLATE.format(slack_channel_id)
|
||||
|
||||
if isinstance(schedule, OnCallScheduleICal):
|
||||
ical_url_primary = f'"{schedule.ical_url_primary}"' if schedule.ical_url_primary else "null"
|
||||
ical_url_overrides = f'"{schedule.ical_url_overrides}"' if schedule.ical_url_overrides else "null"
|
||||
result += TerraformFileRenderer.SCHEDULE_RESOURCE_TEMPLATE_ICAL.format(
|
||||
schedule_name,
|
||||
formatted_schedule_name,
|
||||
team_name_text,
|
||||
ical_url_primary,
|
||||
ical_url_overrides,
|
||||
slack_channel_text,
|
||||
)
|
||||
|
||||
elif isinstance(schedule, OnCallScheduleCalendar):
|
||||
result += TerraformFileRenderer.SCHEDULE_RESOURCE_TEMPLATE_CALENDAR.format(
|
||||
schedule_name, formatted_schedule_name, team_name_text, schedule.time_zone, slack_channel_text
|
||||
)
|
||||
return result
|
||||
|
||||
def render_on_call_shift_resource_text(self):
|
||||
CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift")
|
||||
result = ""
|
||||
on_call_shifts = self.organization.custom_on_call_shifts.all().order_by("pk")
|
||||
|
||||
for shift in on_call_shifts:
|
||||
shift_name = self.render_name(shift, "on_call_shifts", "name")
|
||||
team_name = self.render_team_name(shift.team)
|
||||
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
|
||||
formatted_integration_name = self.escape_string_for_terraform(shift.name)
|
||||
shift_type = CustomOnCallShift.PUBLIC_TYPE_CHOICES_MAP[shift.type]
|
||||
start = shift.start.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
duration = int(shift.duration.total_seconds())
|
||||
level = shift.priority_level
|
||||
frequency = CustomOnCallShift.PUBLIC_FREQUENCY_CHOICES_MAP.get(shift.frequency)
|
||||
interval = shift.interval or "null"
|
||||
week_start = CustomOnCallShift.ICAL_WEEKDAY_MAP[shift.week_start]
|
||||
by_day = self.replace_quotes(f"{shift.by_day}") if shift.by_day else "null"
|
||||
by_month = self.replace_quotes(f"{shift.by_day}") if shift.by_month else "null"
|
||||
by_monthday = self.replace_quotes(f"{shift.by_day}") if shift.by_monthday else "null"
|
||||
|
||||
if shift.type == CustomOnCallShift.TYPE_ROLLING_USERS_EVENT:
|
||||
rolling_amixr_users = shift.get_rolling_users()
|
||||
rendered_amixr_users = self.render_rolling_users_list_text(rolling_amixr_users)
|
||||
result += TerraformFileRenderer.ON_CALL_SHIFT_RESOURCE_TEMPLATE_ROLLING_USERS.format(
|
||||
shift_name,
|
||||
formatted_integration_name,
|
||||
shift_type,
|
||||
team_name_text,
|
||||
start,
|
||||
duration,
|
||||
level,
|
||||
frequency,
|
||||
interval,
|
||||
week_start,
|
||||
by_day,
|
||||
by_month,
|
||||
by_monthday,
|
||||
rendered_amixr_users,
|
||||
)
|
||||
else:
|
||||
amixr_users = shift.users.all()
|
||||
rendered_amixr_users = self.render_amixr_users_list_text(amixr_users)
|
||||
|
||||
if shift.type == CustomOnCallShift.TYPE_SINGLE_EVENT:
|
||||
result += TerraformFileRenderer.ON_CALL_SHIFT_RESOURCE_TEMPLATE_SINGLE_EVENT.format(
|
||||
shift_name,
|
||||
formatted_integration_name,
|
||||
shift_type,
|
||||
team_name_text,
|
||||
start,
|
||||
duration,
|
||||
level,
|
||||
rendered_amixr_users,
|
||||
)
|
||||
elif shift.type == CustomOnCallShift.TYPE_RECURRENT_EVENT:
|
||||
result += TerraformFileRenderer.ON_CALL_SHIFT_RESOURCE_TEMPLATE_RECURRENT_EVENT.format(
|
||||
shift_name,
|
||||
formatted_integration_name,
|
||||
shift_type,
|
||||
team_name_text,
|
||||
start,
|
||||
duration,
|
||||
level,
|
||||
frequency,
|
||||
interval,
|
||||
week_start,
|
||||
by_day,
|
||||
by_month,
|
||||
by_monthday,
|
||||
rendered_amixr_users,
|
||||
)
|
||||
return result
|
||||
|
||||
def render_data_text(self):
|
||||
result = ""
|
||||
for data_type, data_source in sorted(self.data.items(), key=lambda x: x[0], reverse=True):
|
||||
for data_result in data_source:
|
||||
result += data_source[data_result]
|
||||
return result
|
||||
|
||||
def render_action_data_text(self):
|
||||
result = ""
|
||||
actions = self.data.pop("custom_actions", {})
|
||||
for action, action_text in actions.items():
|
||||
result += action_text
|
||||
return result
|
||||
|
||||
def render_names_and_data_for_amixr_users(self, amixr_users):
|
||||
amixr_users_names = []
|
||||
for user in amixr_users:
|
||||
user_data_name = self.render_name(user, "users", "username")
|
||||
amixr_users_names.append(user_data_name)
|
||||
if user.public_primary_key not in self.data.setdefault("users", {}):
|
||||
data_result = TerraformFileRenderer.AMIXR_USER_DATA_TEMPLATE.format(user_data_name, user.username)
|
||||
self.data["users"][user.public_primary_key] = data_result
|
||||
return amixr_users_names
|
||||
|
||||
def render_rolling_users_list_text(self, rolling_amixr_users):
|
||||
if rolling_amixr_users:
|
||||
rolling_users_text = ""
|
||||
for amixr_users in rolling_amixr_users:
|
||||
if amixr_users:
|
||||
amixr_users_names = self.render_names_and_data_for_amixr_users(amixr_users)
|
||||
rendered_amixr_users = ", ".join(
|
||||
[f"data.amixr_user.{user_verbal}.id" for user_verbal in amixr_users_names]
|
||||
)
|
||||
else:
|
||||
rendered_amixr_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE_EMPTY
|
||||
rolling_users_text += TerraformFileRenderer.ROLLING_USERS_TEMPLATE.format(rendered_amixr_users)
|
||||
rendered_rolling_users = TerraformFileRenderer.ROLLING_USERS_LIST_TEMPLATE.format(rolling_users_text)
|
||||
else:
|
||||
rendered_rolling_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE_EMPTY
|
||||
return rendered_rolling_users
|
||||
|
||||
def render_amixr_users_list_text(self, amixr_users):
|
||||
if amixr_users:
|
||||
amixr_users_names = self.render_names_and_data_for_amixr_users(amixr_users)
|
||||
rendered_amixr_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE.format(
|
||||
", ".join([f"data.amixr_user.{user_verbal}.id" for user_verbal in amixr_users_names])
|
||||
)
|
||||
else:
|
||||
rendered_amixr_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE_EMPTY
|
||||
return rendered_amixr_users
|
||||
|
||||
def render_name(self, obj, obj_verbal, name_attr):
|
||||
if obj is None:
|
||||
return None
|
||||
obj_name = slugify(getattr(obj, name_attr))
|
||||
obj_data_name = obj_name
|
||||
counter = 1
|
||||
while (
|
||||
obj_data_name in self.used_names.setdefault(obj_verbal, {})
|
||||
and self.used_names[obj_verbal][obj_data_name] != obj.pk
|
||||
):
|
||||
counter += 1
|
||||
obj_data_name = f"{obj_name}-{counter}"
|
||||
self.used_names[obj_verbal][obj_data_name] = obj.pk
|
||||
return obj_data_name
|
||||
|
||||
def render_schedule_data_source(self, schedule, schedule_data_name):
|
||||
if schedule is None:
|
||||
return None
|
||||
if schedule.pk not in self.data.setdefault("schedules", {}):
|
||||
formatted_schedule_name = self.escape_string_for_terraform(schedule.name)
|
||||
data_result = TerraformFileRenderer.SCHEDULE_DATA_TEMPLATE.format(
|
||||
schedule_data_name, formatted_schedule_name
|
||||
)
|
||||
self.data["schedules"][schedule.pk] = data_result
|
||||
|
||||
def render_user_group_name(self, user_group):
|
||||
if user_group is None:
|
||||
return None
|
||||
user_group_data_name = slugify(user_group.handle)
|
||||
if user_group.pk not in self.data.setdefault("user_groups", {}):
|
||||
data_result = TerraformFileRenderer.USER_GROUP_DATA_TEMPLATE.format(user_group_data_name, user_group.handle)
|
||||
self.data["user_groups"][user_group.pk] = data_result
|
||||
return user_group_data_name
|
||||
|
||||
def render_team_name(self, team):
|
||||
if team is None:
|
||||
return None
|
||||
|
||||
team_data_name = slugify(team.name)
|
||||
if team.pk not in self.data.setdefault("teams", {}):
|
||||
data_result = TerraformFileRenderer.TEAM_DATA_TEMPLATE.format(team_data_name, team.name)
|
||||
self.data["teams"][team.pk] = data_result
|
||||
return team_data_name
|
||||
|
||||
def render_custom_action_data_source(self, custom_action, custom_action_data_name, integration_resource_name):
|
||||
if custom_action is None:
|
||||
return None
|
||||
if custom_action.pk not in self.data.setdefault("custom_actions", {}):
|
||||
formatted_action_name = self.escape_string_for_terraform(custom_action.name)
|
||||
data_result = TerraformFileRenderer.CUSTOM_ACTION_DATA_TEMPLATE.format(
|
||||
custom_action_data_name,
|
||||
formatted_action_name,
|
||||
integration_resource_name,
|
||||
)
|
||||
self.data["custom_actions"][custom_action.pk] = data_result
|
||||
|
||||
def render_integration_template(self, integration):
|
||||
result_template = None
|
||||
slack_template = self.render_integration_slack_template(integration)
|
||||
template_fields = {
|
||||
"resolve_signal": integration.resolve_condition_template,
|
||||
"grouping_key": integration.grouping_id_template,
|
||||
}
|
||||
|
||||
templates = {}
|
||||
for template_type, template in template_fields.items():
|
||||
if template is not None:
|
||||
if "\n" in template:
|
||||
result = ""
|
||||
template_lines = template.split("\n")
|
||||
for line in template_lines:
|
||||
result += f" {line}\n"
|
||||
result = "<<-EOT\n" "{}" " EOT".format(result)
|
||||
templates[template_type] = result
|
||||
else:
|
||||
template = self.escape_string_for_terraform(template)
|
||||
templates[template_type] = f'"{template}"'
|
||||
|
||||
if len(templates) > 0 or slack_template:
|
||||
result_template = "{\n"
|
||||
for template_type, template in templates.items():
|
||||
result_template += f" {template_type} = {template}\n"
|
||||
if slack_template:
|
||||
result_template += f" slack {slack_template}"
|
||||
result_template += " }"
|
||||
|
||||
return result_template
|
||||
|
||||
def render_integration_slack_template(self, integration):
|
||||
slack_template = None
|
||||
slack_fields = {
|
||||
"title": integration.slack_title_template,
|
||||
"message": integration.slack_message_template,
|
||||
"image_url": integration.slack_image_url_template,
|
||||
}
|
||||
slack_templates = {}
|
||||
for template_type, template in slack_fields.items():
|
||||
if template is not None:
|
||||
if "\n" in template:
|
||||
result = ""
|
||||
template_lines = template.split("\n")
|
||||
for line in template_lines:
|
||||
result += f" {line}\n"
|
||||
result = "<<-EOT\n" "{}" " EOT".format(result)
|
||||
slack_templates[template_type] = result
|
||||
else:
|
||||
template = self.escape_string_for_terraform(template)
|
||||
slack_templates[template_type] = f'"{template}"'
|
||||
|
||||
if len(slack_templates) > 0:
|
||||
slack_template = "{\n"
|
||||
for template_type, template in slack_templates.items():
|
||||
slack_template += f" {template_type} = {template}\n"
|
||||
slack_template += " }\n"
|
||||
|
||||
return slack_template
|
||||
|
||||
def render_time_string(self, time_obj):
|
||||
result = f"\"{time_obj.strftime('%H:%M:%SZ')}\"" if time_obj is not None else "null"
|
||||
return result
|
||||
|
||||
def escape_string_for_terraform(self, template_line):
|
||||
template_line = template_line.replace("\\", "\\\\")
|
||||
template_line = template_line.replace('"', r"\"")
|
||||
return template_line
|
||||
|
||||
def replace_quotes(self, template_line):
|
||||
template_line = template_line.replace("'", r'"')
|
||||
return template_line
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
from django.utils.text import slugify
|
||||
|
||||
from apps.schedules.models import OnCallScheduleCalendar
|
||||
|
||||
|
||||
class TerraformStateRenderer:
|
||||
STATE_INTEGRATION_TEMPLATE = "terraform import amixr_integration.{} {}\n"
|
||||
STATE_ROUTE_TEMPLATE = "terraform import amixr_route.{} {}\n"
|
||||
STATE_ESCALATION_CHAIN_TEMPLATE = "terraform import amixr_escalation_chain.{} {}\n"
|
||||
STATE_ESCALATION_TEMPLATE = "terraform import amixr_escalation.{} {}\n"
|
||||
STATE_SCHEDULE_TEMPLATE = "terraform import amixr_schedule.{} {}\n"
|
||||
STATE_ON_CALL_SHIFT_TEMPLATE = "terraform import amixr_on_call_shift.{} {}\n"
|
||||
|
||||
def __init__(self, organization):
|
||||
self.organization = organization
|
||||
self.used_names = {}
|
||||
|
||||
def render_state(self):
|
||||
result = self.render_state_text()
|
||||
if len(result) == 0:
|
||||
result += "There is nothing here yet. Check Settings to add integration and come back!"
|
||||
return result
|
||||
|
||||
def render_state_text(self):
|
||||
result = ""
|
||||
|
||||
result += self.render_escalation_chains_related_states_text()
|
||||
|
||||
integrations_related_states_text = self.render_integrations_related_states_text()
|
||||
result += integrations_related_states_text
|
||||
|
||||
schedules_related_states_text = self.render_schedule_related_states_text()
|
||||
result += schedules_related_states_text
|
||||
|
||||
return result
|
||||
|
||||
def render_escalation_chains_related_states_text(self):
|
||||
result = ""
|
||||
escalation_chains = self.organization.escalation_chains.all()
|
||||
for escalation_chain in escalation_chains:
|
||||
resource_name = self.render_name(escalation_chain, "escalation_chains", "name")
|
||||
result += self.STATE_ESCALATION_CHAIN_TEMPLATE.format(resource_name, escalation_chain.public_primary_key)
|
||||
|
||||
result += self.render_escalation_policy_state_text(escalation_chain, resource_name)
|
||||
return result
|
||||
|
||||
def render_integrations_related_states_text(self):
|
||||
result = ""
|
||||
integrations = self.organization.alert_receive_channels.all().order_by("created_at")
|
||||
for integration in integrations:
|
||||
integration_resource_name = self.render_name(integration, "integrations", "verbal_name")
|
||||
result += TerraformStateRenderer.STATE_INTEGRATION_TEMPLATE.format(
|
||||
integration_resource_name,
|
||||
integration.public_primary_key,
|
||||
)
|
||||
route_text = self.render_route_state_text(integration, integration_resource_name)
|
||||
result += route_text
|
||||
return result
|
||||
|
||||
def render_schedule_related_states_text(self):
|
||||
result = ""
|
||||
ical_schedules = self.organization.oncall_schedules.order_by("pk")
|
||||
for schedule in ical_schedules:
|
||||
schedule_resource_name = self.render_name(schedule, "schedules", "name")
|
||||
result += TerraformStateRenderer.STATE_SCHEDULE_TEMPLATE.format(
|
||||
schedule_resource_name,
|
||||
schedule.public_primary_key,
|
||||
)
|
||||
if isinstance(schedule, OnCallScheduleCalendar):
|
||||
on_call_shifts_text = self.render_on_call_shift_state_text(schedule)
|
||||
result += on_call_shifts_text
|
||||
return result
|
||||
|
||||
def render_route_state_text(self, integration, integration_resource_name):
|
||||
routes = integration.channel_filters.all()
|
||||
result = ""
|
||||
for num, route in enumerate(routes, start=1):
|
||||
if route.is_default:
|
||||
continue
|
||||
route_name = f"route-{num}-{integration_resource_name}"
|
||||
result += TerraformStateRenderer.STATE_ROUTE_TEMPLATE.format(
|
||||
route_name,
|
||||
route.public_primary_key,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def render_escalation_policy_state_text(self, escalation_chain, escalation_chain_resource_name):
|
||||
result = ""
|
||||
escalation_policies = escalation_chain.escalation_policies.all()
|
||||
for num, escalation_policy in enumerate(escalation_policies, start=1):
|
||||
escalation_name = f"escalation-{num}-{escalation_chain_resource_name}"
|
||||
result += TerraformStateRenderer.STATE_ESCALATION_TEMPLATE.format(
|
||||
escalation_name,
|
||||
escalation_policy.public_primary_key,
|
||||
)
|
||||
return result
|
||||
|
||||
def render_on_call_shift_state_text(self, schedule):
|
||||
result = ""
|
||||
on_call_shifts = schedule.custom_on_call_shifts.all().order_by("pk")
|
||||
for shift in on_call_shifts:
|
||||
shift_name = self.render_name(shift, "on_call_shifts", "name")
|
||||
result += TerraformStateRenderer.STATE_ON_CALL_SHIFT_TEMPLATE.format(
|
||||
shift_name,
|
||||
shift.public_primary_key,
|
||||
)
|
||||
return result
|
||||
|
||||
def render_name(self, obj, obj_verbal, name_attr):
|
||||
obj_name = slugify(getattr(obj, name_attr))
|
||||
obj_data_name = obj_name
|
||||
counter = 1
|
||||
while (
|
||||
obj_data_name in self.used_names.setdefault(obj_verbal, {})
|
||||
and self.used_names[obj_verbal][obj_data_name] != obj.pk
|
||||
):
|
||||
counter += 1
|
||||
obj_data_name = f"{obj_name}-{counter}"
|
||||
self.used_names[obj_verbal][obj_data_name] = obj.pk
|
||||
return obj_data_name
|
||||
|
|
@ -110,7 +110,6 @@ def test_send_demo_alert(mocked_create_alert, make_organization, make_alert_rece
|
|||
mocked_create_alert.call_args.args[1]["raw_request_data"] == payload
|
||||
or alert_receive_channel.config.example_payload
|
||||
)
|
||||
assert mocked_create_alert.call_args.args[1]["force_route_id"] is None
|
||||
|
||||
|
||||
@mock.patch("apps.integrations.tasks.create_alertmanager_alerts.apply_async", return_value=None)
|
||||
|
|
@ -143,7 +142,6 @@ def test_send_demo_alert_alertmanager_payload_shape(
|
|||
if payload
|
||||
else alert_receive_channel.config.example_payload["alerts"][0]
|
||||
)
|
||||
assert mocked_create_alert.call_args.args[1]["force_route_id"] is None
|
||||
|
||||
|
||||
@mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel, ChannelFilter
|
||||
from apps.alerts.models import ChannelFilter
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -93,48 +91,3 @@ def test_channel_filter_select_filter_jinja2(make_organization, make_alert_recei
|
|||
alert_receive_channel, raw_request_data, force_route_id=channel_filter.pk
|
||||
)
|
||||
assert satisfied_filter == channel_filter
|
||||
|
||||
|
||||
@mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_send_demo_alert(
|
||||
mocked_create_alert,
|
||||
make_organization,
|
||||
make_alert_receive_channel,
|
||||
make_channel_filter,
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_WEBHOOK
|
||||
)
|
||||
filtering_term = "test alert"
|
||||
channel_filter = make_channel_filter(alert_receive_channel, filtering_term=filtering_term, is_default=False)
|
||||
|
||||
channel_filter.send_demo_alert()
|
||||
assert mocked_create_alert.called
|
||||
assert mocked_create_alert.call_args.args[1]["is_demo"]
|
||||
assert mocked_create_alert.call_args.args[1]["force_route_id"] == channel_filter.id
|
||||
|
||||
|
||||
@mock.patch("apps.integrations.tasks.create_alertmanager_alerts.apply_async", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"integration",
|
||||
[
|
||||
AlertReceiveChannel.INTEGRATION_ALERTMANAGER,
|
||||
AlertReceiveChannel.INTEGRATION_GRAFANA,
|
||||
AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING,
|
||||
],
|
||||
)
|
||||
def test_send_demo_alert_alertmanager_payload_shape(
|
||||
mocked_create_alert, make_organization, make_alert_receive_channel, make_channel_filter, integration
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
filtering_term = "test alert"
|
||||
channel_filter = make_channel_filter(alert_receive_channel, filtering_term=filtering_term, is_default=False)
|
||||
|
||||
channel_filter.send_demo_alert()
|
||||
assert mocked_create_alert.called
|
||||
assert mocked_create_alert.call_args.args[1]["is_demo"]
|
||||
assert mocked_create_alert.call_args.args[1]["force_route_id"] == channel_filter.pk
|
||||
|
|
|
|||
|
|
@ -1,151 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.alerts.models import EscalationPolicy
|
||||
from apps.alerts.terraform_renderer import TerraformFileRenderer, TerraformStateRenderer
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar
|
||||
|
||||
terraform_file_renderer_data = {
|
||||
"filtering_term": "\\[test\\]",
|
||||
"escaped_filtering_term": "\\\\[test\\\\]",
|
||||
}
|
||||
|
||||
rendered_terraform_file_template = """
|
||||
data "amixr_user" "{user_name}" {{
|
||||
username = "{user_name}"
|
||||
}}
|
||||
|
||||
resource "amixr_escalation_chain" "{escalation_chain_name}" {{
|
||||
name = "{escalation_chain_name}"
|
||||
team_id = null
|
||||
}}
|
||||
|
||||
resource "amixr_escalation" "escalation-1-{escalation_chain_name}" {{
|
||||
escalation_chain_id = amixr_escalation_chain.{escalation_chain_name}.id
|
||||
type = "notify_persons"
|
||||
important = false
|
||||
persons_to_notify = [
|
||||
data.amixr_user.{user_name}.id
|
||||
]
|
||||
position = 0
|
||||
}}
|
||||
|
||||
resource "amixr_integration" "{integration_name}" {{
|
||||
name = "{integration_verbal_name}"
|
||||
type = "grafana"
|
||||
team_id = null
|
||||
}}
|
||||
|
||||
resource "amixr_on_call_shift" "{shift_name}" {{
|
||||
name = "{shift_name}"
|
||||
type = "rolling_users"
|
||||
team_id = null
|
||||
start = "2021-08-16T17:00:00"
|
||||
duration = 3600
|
||||
level = 0
|
||||
frequency = "weekly"
|
||||
interval = 1
|
||||
week_start = "MO"
|
||||
by_day = ["MO", "SA"]
|
||||
by_month = null
|
||||
by_monthday = null
|
||||
rolling_users = [
|
||||
[data.amixr_user.{user_name}.id],
|
||||
]
|
||||
}}
|
||||
|
||||
resource "amixr_schedule" "{schedule_name}" {{
|
||||
name = "{schedule_name}"
|
||||
type = "calendar"
|
||||
team_id = null
|
||||
time_zone = "UTC"
|
||||
}}
|
||||
"""
|
||||
|
||||
rendered_terraform_imports_template = """terraform import amixr_escalation_chain.{escalation_chain_name} {escalation_chain_public_primary_key}
|
||||
terraform import amixr_escalation.escalation-1-{escalation_chain_name} {escalation_1_public_primary_key}
|
||||
terraform import amixr_integration.{integration_name} {integration_public_primary_key}
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_render_terraform_file(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_integration_escalation_chain_route_escalation_policy,
|
||||
make_escalation_chain,
|
||||
make_escalation_policy,
|
||||
make_on_call_shift,
|
||||
make_schedule,
|
||||
):
|
||||
organization, user, _, _ = make_organization_and_user_with_slack_identities()
|
||||
(integration, escalation_chain, _, escalation_policy) = make_integration_escalation_chain_route_escalation_policy(
|
||||
organization,
|
||||
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
||||
)
|
||||
escalation_policy.notify_to_users_queue.add(user)
|
||||
|
||||
schedule = make_schedule(
|
||||
organization=organization,
|
||||
schedule_class=OnCallScheduleCalendar,
|
||||
name="test_calendar_schedule",
|
||||
)
|
||||
|
||||
start = datetime.datetime.fromisoformat("2021-08-16T17:00:00Z")
|
||||
|
||||
shift = make_on_call_shift(
|
||||
organization=organization,
|
||||
name="test_shift",
|
||||
shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT,
|
||||
frequency=CustomOnCallShift.FREQUENCY_WEEKLY,
|
||||
interval=1,
|
||||
week_start=CustomOnCallShift.MONDAY,
|
||||
start=start,
|
||||
rotation_start=start,
|
||||
duration=datetime.timedelta(seconds=3600),
|
||||
by_day=["MO", "SA"],
|
||||
rolling_users=[{user.pk: user.public_primary_key}],
|
||||
)
|
||||
|
||||
renderer = TerraformFileRenderer(organization)
|
||||
result = renderer.render_terraform_file()
|
||||
|
||||
expected_result = rendered_terraform_file_template.format(
|
||||
user_name=slugify(user.username),
|
||||
escalation_chain_name=escalation_chain.name,
|
||||
integration_name=slugify(integration.verbal_name),
|
||||
integration_verbal_name=integration.verbal_name,
|
||||
routing_regex=terraform_file_renderer_data["escaped_filtering_term"],
|
||||
schedule_name=schedule.name,
|
||||
shift_name=shift.name,
|
||||
)
|
||||
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_render_terraform_imports(
|
||||
make_organization_and_user_with_slack_identities,
|
||||
make_integration_escalation_chain_route_escalation_policy,
|
||||
make_escalation_chain,
|
||||
make_escalation_policy,
|
||||
):
|
||||
organization, user, _, _ = make_organization_and_user_with_slack_identities()
|
||||
integration, escalation_chain, _, escalation_policy = make_integration_escalation_chain_route_escalation_policy(
|
||||
organization,
|
||||
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
||||
)
|
||||
|
||||
renderer = TerraformStateRenderer(organization)
|
||||
result = renderer.render_state()
|
||||
|
||||
expected_result = rendered_terraform_imports_template.format(
|
||||
escalation_chain_name=slugify(escalation_chain.name),
|
||||
escalation_chain_public_primary_key=escalation_chain.public_primary_key,
|
||||
integration_name=slugify(integration.verbal_name),
|
||||
integration_public_primary_key=integration.public_primary_key,
|
||||
escalation_1_public_primary_key=escalation_policy.public_primary_key,
|
||||
)
|
||||
|
||||
assert result == expected_result
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from rest_framework import renderers
|
||||
|
||||
|
||||
class PlainTextRenderer(renderers.BaseRenderer):
|
||||
media_type = "text/plain"
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
if type(data) == dict:
|
||||
result = ""
|
||||
for k, v in data.items():
|
||||
result += f"{k}: {v}\n"
|
||||
return result
|
||||
return data.encode(self.charset)
|
||||
|
|
@ -219,41 +219,6 @@ def test_channel_filter_move_to_position_permissions(
|
|||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_alert_receive_channel_send_demo_alert_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_user_auth_headers,
|
||||
make_alert_receive_channel,
|
||||
make_channel_filter,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
channel_filter = make_channel_filter(alert_receive_channel, is_default=True)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:channel_filter-send-demo-alert", kwargs={"pk": channel_filter.public_primary_key})
|
||||
|
||||
with patch(
|
||||
"apps.api.views.channel_filter.ChannelFilterView.send_demo_alert",
|
||||
return_value=Response(
|
||||
status=status.HTTP_200_OK,
|
||||
),
|
||||
):
|
||||
response = client.post(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_channel_filter_create_with_order(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
],
|
||||
)
|
||||
def test_terraform_gitops_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_escalation_chain,
|
||||
make_user_auth_headers,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
|
||||
make_escalation_chain(organization)
|
||||
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:terraform_file")
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
],
|
||||
)
|
||||
def test_terraform_state_permissions(
|
||||
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status
|
||||
):
|
||||
_, user, token = make_organization_and_user_with_plugin_token(role=role)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:terraform_imports")
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
|
@ -222,6 +222,9 @@ def test_list_on_call_shift(
|
|||
"updated_shift": None,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -280,6 +283,9 @@ def test_list_on_call_shift_filter_schedule_id(
|
|||
"updated_shift": None,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -290,6 +296,9 @@ def test_list_on_call_shift_filter_schedule_id(
|
|||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
response = client.get(
|
||||
|
|
|
|||
|
|
@ -137,6 +137,9 @@ def test_get_list_schedules(
|
|||
"enable_web_overrides": True,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 15,
|
||||
"total_pages": 1,
|
||||
}
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -247,6 +250,9 @@ def test_get_list_schedules_pagination(
|
|||
"next": next_url,
|
||||
"previous": previous_url,
|
||||
"results": [schedule],
|
||||
"current_page_number": p,
|
||||
"page_size": 1,
|
||||
"total_pages": 3,
|
||||
}
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
|
@ -334,6 +340,9 @@ def test_get_list_schedules_by_type(
|
|||
"next": None,
|
||||
"previous": None,
|
||||
"results": [available_schedules[schedule_type]],
|
||||
"current_page_number": 1,
|
||||
"page_size": 15,
|
||||
"total_pages": 1,
|
||||
}
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
|
@ -346,6 +355,9 @@ def test_get_list_schedules_by_type(
|
|||
"next": None,
|
||||
"previous": None,
|
||||
"results": [available_schedules[0], available_schedules[1]],
|
||||
"current_page_number": 1,
|
||||
"page_size": 15,
|
||||
"total_pages": 1,
|
||||
}
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_terraform_file(
|
||||
make_organization_and_user_with_plugin_token, make_user_auth_headers, make_escalation_chain
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
make_escalation_chain(organization)
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:terraform_file")
|
||||
response = client.get(url, format="text/plain", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_terraform_imports(make_organization_and_user_with_plugin_token, make_user_auth_headers):
|
||||
_, user, token = make_organization_and_user_with_plugin_token()
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:terraform_imports")
|
||||
response = client.get(url, format="text/plain", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -173,6 +173,9 @@ def test_list_users(
|
|||
"cloud_connection_status": None,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 100,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(admin, token))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from .views.custom_button import CustomButtonView
|
|||
from .views.escalation_chain import EscalationChainViewSet
|
||||
from .views.escalation_policy import EscalationPolicyView
|
||||
from .views.features import FeaturesAPIView
|
||||
from .views.gitops import TerraformGitOpsView, TerraformStateView
|
||||
from .views.integration_heartbeat import IntegrationHeartBeatView
|
||||
from .views.live_setting import LiveSettingViewSet
|
||||
from .views.on_call_shifts import OnCallShiftView
|
||||
|
|
@ -81,8 +80,6 @@ urlpatterns = [
|
|||
GetChannelVerificationCode.as_view(),
|
||||
name="api-get-channel-verification-code",
|
||||
),
|
||||
optional_slash_path("terraform_file", TerraformGitOpsView.as_view(), name="terraform_file"),
|
||||
optional_slash_path("terraform_imports", TerraformStateView.as_view(), name="terraform_imports"),
|
||||
optional_slash_path("slack_settings", SlackTeamSettingsAPIView.as_view(), name="slack-settings"),
|
||||
optional_slash_path(
|
||||
"slack_settings/acknowledge_remind_options",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ from apps.api.serializers.channel_filter import (
|
|||
ChannelFilterSerializer,
|
||||
ChannelFilterUpdateSerializer,
|
||||
)
|
||||
from apps.api.throttlers import DemoAlertThrottler
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.slack.models import SlackChannel
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
|
|
@ -23,7 +22,6 @@ from common.api_helpers.mixins import (
|
|||
UpdateSerializerMixin,
|
||||
)
|
||||
from common.api_helpers.serializers import get_move_to_position_param
|
||||
from common.exceptions import UnableToSendDemoAlert
|
||||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
|
||||
|
||||
|
|
@ -41,7 +39,6 @@ class ChannelFilterView(
|
|||
"partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"move_to_position": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"send_demo_alert": [RBACPermission.Permissions.INTEGRATIONS_TEST],
|
||||
"convert_from_regex_to_jinja2": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
}
|
||||
|
||||
|
|
@ -131,16 +128,6 @@ class ChannelFilterView(
|
|||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler])
|
||||
def send_demo_alert(self, request, pk):
|
||||
"""Deprecated action. May be used in the older version of the plugin."""
|
||||
instance = self.get_object()
|
||||
try:
|
||||
instance.send_demo_alert()
|
||||
except UnableToSendDemoAlert as e:
|
||||
raise BadRequest(detail=str(e))
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def convert_from_regex_to_jinja2(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.alerts.terraform_renderer import TerraformFileRenderer, TerraformStateRenderer
|
||||
from apps.api.response_renderers import PlainTextRenderer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
|
||||
|
||||
class TerraformGitOpsView(APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
renderer_classes = [PlainTextRenderer]
|
||||
|
||||
def get(self, request):
|
||||
organization = self.request.auth.organization
|
||||
renderer = TerraformFileRenderer(organization)
|
||||
terraform_file = renderer.render_terraform_file()
|
||||
return Response(terraform_file)
|
||||
|
||||
|
||||
class TerraformStateView(APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
renderer_classes = (PlainTextRenderer,)
|
||||
|
||||
def get(self, request):
|
||||
organization = self.request.auth.organization
|
||||
renderer = TerraformStateRenderer(organization)
|
||||
terraform_state = renderer.render_state()
|
||||
return Response(terraform_state)
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
|
@ -13,12 +11,24 @@ from apps.oss_installation.serializers import CloudUserSerializer
|
|||
from apps.oss_installation.utils import cloud_user_identity_status
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin
|
||||
from common.api_helpers.paginators import HundredPageSizePaginator
|
||||
from common.api_helpers.paginators import HundredPageSizePaginator, PaginatedData
|
||||
|
||||
PERMISSIONS = [RBACPermission.Permissions.OTHER_SETTINGS_WRITE]
|
||||
|
||||
|
||||
class CloudUsersView(HundredPageSizePaginator, APIView):
|
||||
class CloudUsersPagination(HundredPageSizePaginator):
|
||||
# the override ignore here is expected. The parent classes' get_paginated_response method does not
|
||||
# take a matched_users_count argument. This is fine in this case
|
||||
def get_paginated_response(self, data: PaginatedData, matched_users_count: int) -> Response: # type: ignore[override]
|
||||
return Response(
|
||||
{
|
||||
**self._get_paginated_response_data(data),
|
||||
"matched_users_count": matched_users_count,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CloudUsersView(CloudUsersPagination, APIView):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
|
|
@ -44,14 +54,14 @@ class CloudUsersView(HundredPageSizePaginator, APIView):
|
|||
cloud_identities = list(CloudUserIdentity.objects.filter(email__in=emails))
|
||||
cloud_identities = {cloud_identity.email: cloud_identity for cloud_identity in cloud_identities}
|
||||
|
||||
response = []
|
||||
data = []
|
||||
|
||||
connector = CloudConnector.objects.first()
|
||||
|
||||
for user in results:
|
||||
cloud_identity = cloud_identities.get(user.email, None)
|
||||
status, link = cloud_user_identity_status(connector, cloud_identity)
|
||||
response.append(
|
||||
data.append(
|
||||
{
|
||||
"id": user.public_primary_key,
|
||||
"email": user.email,
|
||||
|
|
@ -60,20 +70,7 @@ class CloudUsersView(HundredPageSizePaginator, APIView):
|
|||
}
|
||||
)
|
||||
|
||||
return self.get_paginated_response_with_matched_users_count(response, len(cloud_identities))
|
||||
|
||||
def get_paginated_response_with_matched_users_count(self, data, matched_users_count):
|
||||
return Response(
|
||||
OrderedDict(
|
||||
[
|
||||
("count", self.page.paginator.count),
|
||||
("matched_users_count", matched_users_count),
|
||||
("next", self.get_next_link()),
|
||||
("previous", self.get_previous_link()),
|
||||
("results", data),
|
||||
]
|
||||
)
|
||||
)
|
||||
return self.get_paginated_response(data, len(cloud_identities))
|
||||
|
||||
def post(self, request):
|
||||
connector = CloudConnector.objects.first()
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ def test_get_list_alerts(
|
|||
},
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == expected_response
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ def test_get_custom_actions(
|
|||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -74,6 +77,9 @@ def test_get_custom_actions_filter_by_name(
|
|||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -94,7 +100,15 @@ def test_get_custom_actions_filter_by_name_empty_result(
|
|||
|
||||
response = client.get(f"{url}?name=NonExistentName", format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = {"count": 0, "next": None, "previous": None, "results": []}
|
||||
expected_payload = {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ def test_get_escalation_chains(make_organization_and_user_with_token):
|
|||
"name": "test",
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ def escalation_policies_setup():
|
|||
escalation_policy_wait_payload,
|
||||
escalation_policy_notify_persons_empty_payload,
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
return (
|
||||
escalation_chain,
|
||||
|
|
|
|||
|
|
@ -46,8 +46,15 @@ def construct_expected_response_from_incidents(incidents):
|
|||
},
|
||||
}
|
||||
)
|
||||
expected_response = {"count": incidents.count(), "next": None, "previous": None, "results": results}
|
||||
return expected_response
|
||||
return {
|
||||
"count": incidents.count(),
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": results,
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
@ -275,7 +282,7 @@ def test_pagination(settings, incident_public_api_setup):
|
|||
|
||||
url = reverse("api-public:alert_groups-list")
|
||||
|
||||
with patch("common.api_helpers.paginators.PathPrefixedPagination.get_page_size", return_value=1):
|
||||
with patch("common.api_helpers.paginators.PathPrefixedPagePagination.get_page_size", return_value=1):
|
||||
response = client.get(url, HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ def test_get_list_integrations(
|
|||
"maintenance_end_at": None,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
url = reverse("api-public:integrations-list")
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ def test_get_personal_notification_rules_list(personal_notification_rule_public_
|
|||
"important": True,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -126,6 +129,9 @@ def test_get_personal_notification_rules_list_important(personal_notification_ru
|
|||
"important": True,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -169,6 +175,9 @@ def test_get_personal_notification_rules_list_non_important(personal_notificatio
|
|||
"important": False,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ def test_get_resolution_notes(
|
|||
"text": resolution_note_1.text,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ def test_get_routes_list(
|
|||
TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False},
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 25,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -123,6 +126,9 @@ def test_get_routes_filter_by_integration_id(
|
|||
TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False},
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 25,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -702,6 +702,9 @@ def test_get_schedule_list(
|
|||
"slack": {"channel_id": slack_channel_id, "user_group_id": user_group_id},
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -912,6 +915,12 @@ def test_oncall_shifts_export(
|
|||
assert total_time_on_call[user2_public_primary_key] == expected_time_on_call
|
||||
|
||||
# pagination parameters are mocked out for now
|
||||
assert response_json["next"] is None
|
||||
assert response_json["previous"] is None
|
||||
assert response_json["count"] == len(shifts)
|
||||
del response_json["results"]
|
||||
assert response_json == {
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"count": len(shifts),
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ def test_get_slack_channels_list(
|
|||
"next": None,
|
||||
"previous": None,
|
||||
"results": [{"name": slack_channel.name, "slack_id": slack_channel.slack_id}],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ def test_get_teams_list(team_public_api_setup):
|
|||
"avatar_url": team.avatar_url,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ def test_get_user_groups(
|
|||
},
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -80,6 +83,9 @@ def test_get_user_groups_filter_by_handle(
|
|||
},
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -98,7 +104,15 @@ def test_get_user_groups_filter_by_handle_empty_result(
|
|||
|
||||
response = client.get(f"{url}?slack_handle=NonExistentSlackHandle", format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = {"count": 0, "next": None, "previous": None, "results": []}
|
||||
expected_payload = {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ def test_get_users_list(
|
|||
"is_phone_number_verified": False,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 100,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -121,6 +124,9 @@ def test_get_users_list_short(
|
|||
"is_phone_number_verified": False,
|
||||
},
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 100,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -170,6 +176,9 @@ def test_get_users_list_all_role_users(user_public_api_setup, make_user_for_orga
|
|||
}
|
||||
for user, role in expected_users
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 100,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
|
|||
|
|
@ -175,5 +175,8 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo
|
|||
"next": None,
|
||||
"previous": None,
|
||||
"results": data,
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -96,9 +96,10 @@ def _build_payload(webhook, alert_group, user):
|
|||
|
||||
|
||||
def mask_authorization_header(headers):
|
||||
if "Authorization" in headers:
|
||||
headers["Authorization"] = WEBHOOK_FIELD_PLACEHOLDER
|
||||
return headers
|
||||
masked_headers = headers.copy()
|
||||
if "Authorization" in masked_headers:
|
||||
masked_headers["Authorization"] = WEBHOOK_FIELD_PLACEHOLDER
|
||||
return masked_headers
|
||||
|
||||
|
||||
def make_request(webhook, alert_group, data):
|
||||
|
|
@ -123,8 +124,8 @@ def make_request(webhook, alert_group, data):
|
|||
if triggered:
|
||||
status["url"] = webhook.build_url(data)
|
||||
request_kwargs = webhook.build_request_kwargs(data, raise_data_errors=True)
|
||||
headers = mask_authorization_header(request_kwargs.get("headers", {}))
|
||||
status["request_headers"] = json.dumps(headers)
|
||||
display_headers = mask_authorization_header(request_kwargs.get("headers", {}))
|
||||
status["request_headers"] = json.dumps(display_headers)
|
||||
if "json" in request_kwargs:
|
||||
status["request_data"] = json.dumps(request_kwargs["json"])
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,45 +1,82 @@
|
|||
from rest_framework.pagination import CursorPagination, PageNumberPagination
|
||||
import typing
|
||||
|
||||
from rest_framework.pagination import BasePagination, CursorPagination, PageNumberPagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
|
||||
MAX_PAGE_SIZE = 100
|
||||
PAGE_QUERY_PARAM = "page"
|
||||
PAGE_SIZE_QUERY_PARAM = "perpage"
|
||||
PaginatedData = typing.List[typing.Any]
|
||||
|
||||
|
||||
class PathPrefixedPagination(PageNumberPagination):
|
||||
max_page_size = MAX_PAGE_SIZE
|
||||
page_query_param = PAGE_QUERY_PARAM
|
||||
page_size_query_param = PAGE_SIZE_QUERY_PARAM
|
||||
class BasePaginatedResponseData(typing.TypedDict):
|
||||
next: str | None
|
||||
previous: str | None
|
||||
results: PaginatedData
|
||||
page_size: int
|
||||
|
||||
|
||||
class PageBasedPaginationResponseData(BasePaginatedResponseData):
|
||||
count: int
|
||||
current_page_number: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
class BasePathPrefixedPagination(BasePagination):
|
||||
max_page_size = 100
|
||||
page_query_param = "page"
|
||||
page_size_query_param = "perpage"
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
|
||||
|
||||
# we're setting the request object explicitly here because the way the paginate_quersey works
|
||||
# between PageNumberPagination and CursorPagination is slightly different. In the latter class,
|
||||
# it does not set self.request in the paginate_queryset method, whereas in the former it does.
|
||||
# this leads to an issue in _get_base_paginated_response_data where the self.request would not be set
|
||||
self.request = request
|
||||
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
|
||||
class PathPrefixedCursorPagination(CursorPagination):
|
||||
max_page_size = MAX_PAGE_SIZE
|
||||
page_query_param = PAGE_QUERY_PARAM
|
||||
page_size_query_param = PAGE_SIZE_QUERY_PARAM
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
def _get_base_paginated_response_data(self, data: PaginatedData) -> BasePaginatedResponseData:
|
||||
return {
|
||||
"next": self.get_next_link(),
|
||||
"previous": self.get_previous_link(),
|
||||
"results": data,
|
||||
"page_size": self.get_page_size(self.request),
|
||||
}
|
||||
|
||||
|
||||
class HundredPageSizePaginator(PathPrefixedPagination):
|
||||
class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPagination):
|
||||
def _get_paginated_response_data(self, data: PaginatedData) -> PageBasedPaginationResponseData:
|
||||
return {
|
||||
**self._get_base_paginated_response_data(data),
|
||||
"count": self.page.paginator.count,
|
||||
"current_page_number": self.page.number,
|
||||
"total_pages": self.page.paginator.num_pages,
|
||||
}
|
||||
|
||||
def get_paginated_response(self, data: PaginatedData) -> Response:
|
||||
return Response(self._get_paginated_response_data(data))
|
||||
|
||||
|
||||
class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination):
|
||||
def get_paginated_response(self, data: PaginatedData) -> Response:
|
||||
return Response(self._get_base_paginated_response_data(data))
|
||||
|
||||
|
||||
class HundredPageSizePaginator(PathPrefixedPagePagination):
|
||||
page_size = 100
|
||||
|
||||
|
||||
class FiftyPageSizePaginator(PathPrefixedPagination):
|
||||
class FiftyPageSizePaginator(PathPrefixedPagePagination):
|
||||
page_size = 50
|
||||
|
||||
|
||||
class TwentyFivePageSizePaginator(PathPrefixedPagination):
|
||||
class TwentyFivePageSizePaginator(PathPrefixedPagePagination):
|
||||
page_size = 25
|
||||
|
||||
|
||||
class FifteenPageSizePaginator(PathPrefixedPagination):
|
||||
class FifteenPageSizePaginator(PathPrefixedPagePagination):
|
||||
page_size = 15
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ disable_error_code = [
|
|||
"call-arg",
|
||||
"call-overload",
|
||||
"has-type",
|
||||
"import",
|
||||
"index",
|
||||
"misc",
|
||||
"name-defined",
|
||||
|
|
@ -54,6 +53,7 @@ module = [
|
|||
# we can slowly either start to add library stubs ourselves, or try and upgrade these libraries to see if
|
||||
# a newer version includes type stubs
|
||||
"anymail.*",
|
||||
"celery.utils.debug",
|
||||
"debug_toolbar.*",
|
||||
"django_deprecate_fields.*",
|
||||
"django_sns_view.*",
|
||||
|
|
@ -78,6 +78,7 @@ module = [
|
|||
"twilio.*",
|
||||
"uwsgidecorators.*",
|
||||
"whitenoise.*",
|
||||
"uwsgi.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
|
|
|||
|
|
@ -12,17 +12,5 @@
|
|||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
background: var(--background-secondary);
|
||||
|
||||
& > li .hover-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,20 +141,19 @@ const ManualAlertGroup: FC<ManualAlertGroupProps> = (props) => {
|
|||
<HorizontalGroup>
|
||||
{chatOpsAvailableChannels && (
|
||||
<>
|
||||
<Text type="secondary">ChatOps:</Text>{' '}
|
||||
{chatOpsAvailableChannels.map(
|
||||
(chatOpsChannel: { name: string; icon: IconName }, chatOpsIndex) => (
|
||||
<div
|
||||
key={`${chatOpsChannel.name}-${chatOpsIndex}`}
|
||||
className={cx({
|
||||
'u-margin-right-xs': chatOpsIndex !== chatOpsAvailableChannels.length,
|
||||
})}
|
||||
>
|
||||
{chatOpsChannel.icon && <Icon name={chatOpsChannel.icon} className={cx('icon')} />}
|
||||
<div key={`${chatOpsChannel.name}-${chatOpsIndex}`}>
|
||||
{chatOpsChannel.icon && <Icon name={chatOpsChannel.icon} />}
|
||||
<Text type="primary">{chatOpsChannel.name || ''}</Text>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{chatOpsAvailableChannels && (
|
||||
<Tooltip content="Alert group will be posted to these chatops channels according to integration configuration">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
|
|
|
|||
|
|
@ -27,16 +27,16 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
|
|||
const { heartbeatStore, alertReceiveChannelStore } = useStore();
|
||||
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[alertReceveChannelId];
|
||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||
|
||||
useEffect(() => {
|
||||
heartbeatStore.updateTimeoutOptions();
|
||||
}, [heartbeatStore]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
setInterval(alertReceiveChannel.heartbeat.timeout_seconds);
|
||||
}
|
||||
}, [alertReceiveChannel]);
|
||||
setInterval(heartbeat.timeout_seconds);
|
||||
}, [heartbeat]);
|
||||
|
||||
const timeoutOptions = heartbeatStore.timeoutOptions;
|
||||
|
||||
|
|
@ -66,22 +66,30 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
|
|||
</WithPermissionControlTooltip>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className={cx('u-width-100')}>
|
||||
<Field label="Endpoint" description="Use the following unique Grafana link to send GET and POST requests">
|
||||
<IntegrationInputField value={alertReceiveChannel?.integration_url} showEye={false} isMasked={false} />
|
||||
<IntegrationInputField value={heartbeat?.link} showEye={false} isMasked={false} />
|
||||
</Field>
|
||||
</div>
|
||||
{/* <p>
|
||||
To send periodic heartbeat alerts from <Emoji text={alertReceiveChannel?.verbal_name || ''} /> to OnCall, do
|
||||
the following:
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: heartbeat.instruction,
|
||||
}}
|
||||
/>
|
||||
</p> */}
|
||||
</VerticalGroup>
|
||||
|
||||
<VerticalGroup style={{ marginTop: 'auto' }}>
|
||||
<HorizontalGroup className={cx('buttons')} justify="flex-end">
|
||||
<Button variant={'secondary'} onClick={onClose}>
|
||||
Cancel
|
||||
{heartbeat ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
|
||||
<Button variant="primary" onClick={onSave}>
|
||||
{alertReceiveChannel.heartbeat ? 'Save' : 'Create'}
|
||||
{heartbeat ? 'Save' : 'Create'}
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
|
|
@ -91,24 +99,14 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
|
|||
);
|
||||
|
||||
async function onSave() {
|
||||
const heartbeat = alertReceiveChannel.heartbeat;
|
||||
await heartbeatStore.saveHeartbeat(heartbeat.id, {
|
||||
alert_receive_channel: heartbeat.alert_receive_channel,
|
||||
timeout_seconds: interval,
|
||||
});
|
||||
|
||||
if (heartbeat) {
|
||||
await heartbeatStore.saveHeartbeat(heartbeat.id, {
|
||||
alert_receive_channel: heartbeat.alert_receive_channel,
|
||||
timeout_seconds: interval,
|
||||
});
|
||||
onClose();
|
||||
|
||||
onClose();
|
||||
} else {
|
||||
await heartbeatStore.createHeartbeat(alertReceveChannelId, {
|
||||
timeout_seconds: interval,
|
||||
});
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
await alertReceiveChannelStore.updateItem(alertReceveChannelId);
|
||||
await alertReceiveChannelStore.loadItem(alertReceveChannelId);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -91,11 +91,14 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
|
||||
const alertReceiveChannel = await this.getById(id, skipErrorHandling);
|
||||
|
||||
// @ts-ignore
|
||||
this.items = {
|
||||
...this.items,
|
||||
[id]: alertReceiveChannel,
|
||||
[id]: omit(alertReceiveChannel, 'heartbeat'),
|
||||
};
|
||||
|
||||
this.populateHearbeats([alertReceiveChannel]);
|
||||
|
||||
return alertReceiveChannel;
|
||||
}
|
||||
|
||||
|
|
@ -118,31 +121,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
|
||||
this.searchResult = results.map((item: AlertReceiveChannel) => item.id);
|
||||
|
||||
const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.rootStore.heartbeatStore.items = {
|
||||
...this.rootStore.heartbeatStore.items,
|
||||
...heartbeats,
|
||||
};
|
||||
|
||||
const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.alertReceiveChannelToHeartbeat = {
|
||||
...this.alertReceiveChannelToHeartbeat,
|
||||
...alertReceiveChannelToHeartbeat,
|
||||
};
|
||||
this.populateHearbeats(results);
|
||||
|
||||
this.updateCounters();
|
||||
|
||||
|
|
@ -170,7 +149,15 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
results: results.map((item: AlertReceiveChannel) => item.id),
|
||||
};
|
||||
|
||||
const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
this.populateHearbeats(results);
|
||||
|
||||
this.updateCounters();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
populateHearbeats(alertReceiveChannels: AlertReceiveChannel[]) {
|
||||
const heartbeats = alertReceiveChannels.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
|
||||
}
|
||||
|
|
@ -183,22 +170,21 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
...heartbeats,
|
||||
};
|
||||
|
||||
const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
|
||||
}
|
||||
const alertReceiveChannelToHeartbeat = alertReceiveChannels.reduce(
|
||||
(acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
this.alertReceiveChannelToHeartbeat = {
|
||||
...this.alertReceiveChannelToHeartbeat,
|
||||
...alertReceiveChannelToHeartbeat,
|
||||
};
|
||||
|
||||
this.updateCounters();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
|||
|
|
@ -726,7 +726,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
alertReceiveChannel,
|
||||
changeIsTemplateSettingsOpen,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore, heartbeatStore } = useStore();
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
|
@ -927,9 +927,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
);
|
||||
|
||||
function showHeartbeatSettings() {
|
||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||
return !!heartbeat?.last_heartbeat_time_verbal;
|
||||
return alertReceiveChannel.is_available_for_integration_heartbeat;
|
||||
}
|
||||
|
||||
function deleteIntegration() {
|
||||
|
|
@ -1159,22 +1157,19 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
|||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
|
||||
if (
|
||||
!alertReceiveChannel.is_available_for_integration_heartbeat ||
|
||||
!alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal
|
||||
) {
|
||||
if (!alertReceiveChannel.is_available_for_integration_heartbeat || !heartbeat?.last_heartbeat_time_verbal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
|
||||
return (
|
||||
<TooltipBadge
|
||||
text={undefined}
|
||||
className={cx('heartbeat-badge')}
|
||||
borderType={heartbeatStatus ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal}`}
|
||||
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal}`}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue