Public API: Acknowledge & Resolve actions (#3108)

# What this PR does

Makes it possible to acknowledge/unacknowledge and resolve/unresolve
alert groups via public API, and makes sure these actions are reflected
properly in the alert group timeline.

## Demo

```bash
curl --request POST \
     --header "Authorization: TOKEN" \
     http://localhost:8080/api/v1/alert_groups/IQMHLV8INB24N/resolve
```

<img width="651" alt="Screenshot 2023-10-04 at 16 05 27"
src="https://github.com/grafana/oncall/assets/20116910/d4e66868-0132-4b6b-95c7-8424fced7c0b">

## Which issue(s) this PR fixes

https://github.com/grafana/oncall/issues/3051

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Vadim Stepanov 2023-10-05 09:46:48 +01:00 committed by GitHub
parent 6dcd443e89
commit a727450d49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 461 additions and 37 deletions

View file

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Make it possible to acknowledge/unacknowledge and resolve/unresolve alert groups via API by @vadimkerr ([#3108](https://github.com/grafana/oncall/pull/3108))
## v1.3.42 (2023-10-04)
### Added

View file

@ -54,6 +54,54 @@ These available filter parameters should be provided as `GET` arguments:
`GET {{API_URL}}/api/v1/alert_groups/`
# Acknowledge alert groups
```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/acknowledge" \
--request POST \
--header "Authorization: meowmeowmeow"
```
**HTTP request**
`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/acknowledge`
# Unacknowledge alert groups
```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unacknowledge" \
--request POST \
--header "Authorization: meowmeowmeow"
```
**HTTP request**
`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/unacknowledge`
# Resolve alert groups
```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/resolve" \
--request POST \
--header "Authorization: meowmeowmeow"
```
**HTTP request**
`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/resolve`
# Unresolve alert groups
```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unresolve" \
--request POST \
--header "Authorization: meowmeowmeow"
```
**HTTP request**
`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/unresolve`
# Delete alert groups
```shell

View file

@ -1,13 +1,14 @@
from enum import Enum
from django.db.models import IntegerChoices
class ActionSource:
(
SLACK,
WEB,
PHONE,
TELEGRAM,
) = range(4)
class ActionSource(IntegerChoices):
SLACK = 0, "Slack"
WEB = 1, "Web"
PHONE = 2, "Phone"
TELEGRAM = 3, "Telegram"
API = 4, "API"
TASK_DELAY_SECONDS = 1

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-10-04 10:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0032_remove_alertgroup_slack_message_state'),
]
operations = [
migrations.AddField(
model_name='alertgrouplogrecord',
name='action_source',
field=models.SmallIntegerField(default=None, null=True, verbose_name=[(0, 'Slack'), (1, 'Web'), (2, 'Phone'), (3, 'Telegram'), (4, 'API')]),
),
]

View file

@ -16,7 +16,7 @@ from django.dispatch import receiver
from django.utils import timezone
from django.utils.functional import cached_property
from apps.alerts.constants import AlertGroupState
from apps.alerts.constants import ActionSource, AlertGroupState
from apps.alerts.escalation_snapshot import EscalationSnapshotMixin
from apps.alerts.escalation_snapshot.escalation_snapshot_mixin import START_ESCALATION_DELAY
from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE
@ -550,7 +550,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
started_at=self.started_at,
)
def acknowledge_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def acknowledge_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord
initial_state = self.state
@ -564,10 +564,16 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
silence_delay=None,
reason="Acknowledge button",
action_source=action_source,
)
if self.resolved:
self.unresolve()
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, reason="Acknowledge button")
self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_RESOLVED,
author=user,
reason="Acknowledge button",
action_source=action_source,
)
self.acknowledge(acknowledged_by_user=user, acknowledged_by=AlertGroup.USER)
# Update alert group state and response time metrics cache
@ -576,7 +582,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
self.stop_escalation()
self.start_ack_reminder_if_needed()
log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_ACK, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_ACK, author=user, action_source=action_source
)
logger.debug(
f"send alert_group_action_triggered_signal for alert_group {self.pk}, "
@ -630,7 +638,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
for dependent_alert_group in self.dependent_alert_groups.all():
dependent_alert_group.acknowledge_by_source()
def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord
initial_state = self.state
@ -642,7 +650,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
if self.is_root_alert_group:
self.start_escalation_if_needed()
log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_ACK, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, action_source=action_source
)
logger.debug(
f"send alert_group_action_triggered_signal for alert_group {self.pk}, "
@ -659,7 +669,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
dependent_alert_group.un_acknowledge_by_user(user, action_source=action_source)
logger.debug(f"Finished un_acknowledge_by_user for alert_group {self.pk}")
def resolve_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def resolve_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord
initial_state = self.state
@ -672,12 +682,15 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
silence_delay=None,
reason="Resolve button",
action_source=action_source,
)
self.resolve(resolved_by=AlertGroup.USER, resolved_by_user=user)
# Update alert group state and response time metrics cache
self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state)
self.stop_escalation()
log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_RESOLVED, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_RESOLVED, author=user, action_source=action_source
)
logger.debug(
f"send alert_group_action_triggered_signal for alert_group {self.pk}, "
@ -777,7 +790,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
for dependent_alert_group in self.dependent_alert_groups.all():
dependent_alert_group.resolve_by_disable_maintenance()
def un_resolve_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_resolve_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord
if self.wiped_at is None:
@ -786,7 +799,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
# Update alert group state metric cache
self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state)
log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, action_source=action_source
)
if self.is_root_alert_group:
self.start_escalation_if_needed()
@ -807,7 +822,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
dependent_alert_group.un_resolve_by_user(user, action_source=action_source)
def attach_by_user(
self, user: User, root_alert_group: "AlertGroup", action_source: typing.Optional[str] = None
self, user: User, root_alert_group: "AlertGroup", action_source: typing.Optional[ActionSource] = None
) -> None:
from apps.alerts.models import AlertGroupLogRecord
@ -831,6 +846,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
root_alert_group=root_alert_group,
reason="Attach dropdown",
action_source=action_source,
)
logger.debug(
@ -850,6 +866,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
dependent_alert_group=self,
reason="Attach dropdown",
action_source=action_source,
)
logger.debug(
@ -870,6 +887,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
root_alert_group=root_alert_group,
reason="Failed to attach dropdown",
action_source=action_source,
)
logger.debug(
@ -884,7 +902,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
action_source=action_source,
)
def un_attach_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_attach_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord
root_alert_group: AlertGroup = self.root_alert_group
@ -898,6 +916,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
root_alert_group=root_alert_group,
reason="Unattach button",
action_source=action_source,
)
logger.debug(
@ -917,6 +936,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
dependent_alert_group=self,
reason="Unattach dropdown",
action_source=action_source,
)
logger.debug(
@ -957,7 +977,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
)
def silence_by_user(
self, user: User, silence_delay: typing.Optional[int], action_source: typing.Optional[str] = None
self, user: User, silence_delay: typing.Optional[int], action_source: typing.Optional[ActionSource] = None
) -> None:
from apps.alerts.models import AlertGroupLogRecord
@ -965,11 +985,18 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
if self.resolved:
self.unresolve()
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, reason="Silence button")
self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_RESOLVED,
author=user,
reason="Silence button",
action_source=action_source,
)
if self.acknowledged:
self.unacknowledge()
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, reason="Silence button")
self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, reason="Silence button", action_source=action_source
)
if self.silenced:
self.un_silence()
@ -978,6 +1005,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
silence_delay=None,
reason="Silence button",
action_source=action_source,
)
now = timezone.now()
@ -1006,6 +1034,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
author=user,
silence_delay=silence_delay_timedelta,
reason="Silence button",
action_source=action_source,
)
logger.debug(
@ -1022,7 +1051,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
for dependent_alert_group in self.dependent_alert_groups.all():
dependent_alert_group.silence_by_user(user, silence_delay, action_source)
def un_silence_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_silence_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord
initial_state = self.state
@ -1040,6 +1069,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
silence_delay=None,
# 2.Look like some time ago there was no TYPE_UN_SILENCE
reason="Unsilence button",
action_source=action_source,
)
logger.debug(

View file

@ -10,6 +10,7 @@ from django.dispatch import receiver
from rest_framework.fields import DateTimeField
from apps.alerts import tasks
from apps.alerts.constants import ActionSource
from apps.alerts.utils import render_relative_timeline
from apps.slack.slack_formatter import SlackFormatter
from common.utils import clean_markup
@ -155,6 +156,9 @@ class AlertGroupLogRecord(models.Model):
type = models.IntegerField(choices=TYPE_CHOICES)
# Where the action was performed (e.g. web UI, Slack, API, etc.)
action_source = models.SmallIntegerField(ActionSource.choices, null=True, default=None)
author = models.ForeignKey(
"user_management.User",
on_delete=models.SET_NULL,
@ -248,7 +252,6 @@ class AlertGroupLogRecord(models.Model):
from apps.alerts.models import EscalationPolicy
result = ""
author_name = None
invitee_name = None
escalation_policy_step = None
step_specific_info = self.get_step_specific_info()
@ -258,13 +261,18 @@ class AlertGroupLogRecord(models.Model):
elif self.escalation_policy is not None:
escalation_policy_step = self.escalation_policy.step
if self.author is not None:
if self.action_source == ActionSource.API:
author_name = "API"
elif self.author:
if substitute_author_with_tag:
author_name = "{{author}}"
elif for_slack:
author_name = self.author.get_username_with_slack_verbal()
else:
author_name = self.author.username
else:
author_name = None
if self.invitation is not None:
if for_slack:
invitee_name = self.invitation.invitee.get_username_with_slack_verbal()
@ -479,7 +487,7 @@ class AlertGroupLogRecord(models.Model):
f"because it is already attached or resolved."
)
elif self.type == AlertGroupLogRecord.TYPE_RESOLVED:
result += f"alert group resolved {f'by {author_name}'if author_name else ''}"
result += f"resolved {f'by {author_name}'if author_name else ''}"
elif self.type == AlertGroupLogRecord.TYPE_UN_RESOLVED:
result += f"unresolved by {author_name}"
elif self.type == AlertGroupLogRecord.TYPE_WIPED:

View file

@ -2,6 +2,7 @@ from unittest.mock import call, patch
import pytest
from apps.alerts.constants import ActionSource
from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertGroupPhoneCallRenderer
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
from apps.alerts.tasks.delete_alert_group import delete_alert_group
@ -403,3 +404,59 @@ def test_bulk_silence_forever(
assert alert_group.silenced
assert alert_group.raw_escalation_snapshot["next_step_eta"] == raw_next_step_eta
assert not mocked_start_unsilence_task.called
@pytest.mark.parametrize("action_source", ActionSource)
@pytest.mark.django_db
def test_alert_group_log_record_action_source(
make_organization_and_user,
make_alert_receive_channel,
make_alert_group,
action_source,
):
"""Test that action source is saved in alert group log record"""
organization, user = make_organization_and_user()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
root_alert_group = make_alert_group(alert_receive_channel)
# Silence alert group
alert_group.silence_by_user(user, 42, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_SILENCE, action_source)
# Unsilence alert group
alert_group.un_silence_by_user(user, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_SILENCE, action_source)
# Acknowledge alert group
alert_group.acknowledge_by_user(user, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_ACK, action_source)
# Unacknowledge alert group
alert_group.un_acknowledge_by_user(user, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_ACK, action_source)
# Resolve alert group
alert_group.resolve_by_user(user, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_RESOLVED, action_source)
# Unresolve alert group
alert_group.un_resolve_by_user(user, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_RESOLVED, action_source)
# Attach alert group
alert_group.attach_by_user(user, root_alert_group, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_ATTACHED, action_source)
# Unattach alert group
alert_group.un_attach_by_user(user, action_source=action_source)
log_record = alert_group.log_records.last()
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UNATTACHED, action_source)

View file

@ -8,6 +8,7 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient
from apps.alerts.constants import ActionSource
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
from apps.api.errors import AlertGroupAPIError
from apps.api.permissions import LegacyAccessControlRole
@ -1862,3 +1863,31 @@ def test_alert_group_resolve_resolution_note(
assert new_alert_group.has_resolution_notes
assert mock_signal.called
@pytest.mark.django_db
def test_timeline_api_action(
make_organization_and_user_with_plugin_token,
make_alert_receive_channel,
make_channel_filter,
make_alert_group,
make_alert,
make_user_auth_headers,
):
"""Check that the timeline API returns the correct actions when using AlertSource.WEB vs ActionSource.API"""
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
channel_filter = make_channel_filter(alert_receive_channel, is_default=True)
alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter)
make_alert(alert_group=alert_group, raw_request_data=alert_raw_request_data)
alert_group.acknowledge_by_user(user, action_source=ActionSource.WEB)
alert_group.resolve_by_user(user, action_source=ActionSource.API)
client = APIClient()
url = reverse("api-internal:alertgroup-detail", kwargs={"pk": alert_group.public_primary_key})
response = client.get(url, **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json()["render_after_resolve_report_json"][0]["action"] == "acknowledged by {{author}}"
assert response.json()["render_after_resolve_report_json"][1]["action"] == "resolved by API"

View file

@ -6,6 +6,7 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.alerts.constants import ActionSource
from apps.alerts.models import AlertGroup, AlertReceiveChannel
@ -291,14 +292,167 @@ def test_pagination(settings, incident_public_api_setup):
assert result["next"].startswith("https://test.com/test/prefixed/urls")
# This is test from old django-based tests
# TODO: uncomment with date checking in delete mode
# def test_delete_incident_invalid_date(self):
# not_valid_creation_date = VALID_DATE_FOR_DELETE_INCIDENT - timezone.timedelta(days=1)
# self.grafana_second_alert_group.started_at = not_valid_creation_date
# self.grafana_second_alert_group.save()
#
# url = reverse("api-public:alert_groups-detail", kwargs={'pk': self.grafana_second_alert_group.public_primary_key})
# data = {"mode": "delete"}
# response = self.client.delete(url, data=data, format="json", HTTP_AUTHORIZATION=f"{self.token}")
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@pytest.mark.parametrize(
"acknowledged,resolved,attached,maintenance,status_code",
[
(False, False, False, False, status.HTTP_200_OK),
(True, False, False, False, status.HTTP_400_BAD_REQUEST),
(False, True, False, False, status.HTTP_400_BAD_REQUEST),
(False, False, True, False, status.HTTP_400_BAD_REQUEST),
(False, False, False, True, status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_alert_group_acknowledge(
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
acknowledged,
resolved,
attached,
maintenance,
status_code,
):
organization, _, token = make_organization_and_user_with_token()
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel)
alert_group = make_alert_group(
alert_receive_channel,
acknowledged=acknowledged,
resolved=resolved,
root_alert_group=root_alert_group if attached else None,
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
)
client = APIClient()
url = reverse("api-public:alert_groups-acknowledge", kwargs={"pk": alert_group.public_primary_key})
response = client.post(url, HTTP_AUTHORIZATION=token)
assert response.status_code == status_code
if status_code == status.HTTP_200_OK:
alert_group.refresh_from_db()
assert alert_group.acknowledged is True
assert alert_group.log_records.last().action_source == ActionSource.API
@pytest.mark.parametrize(
"acknowledged,resolved,attached,maintenance,status_code",
[
(True, False, False, False, status.HTTP_200_OK),
(True, True, False, False, status.HTTP_400_BAD_REQUEST),
(True, False, True, False, status.HTTP_400_BAD_REQUEST),
(True, False, False, True, status.HTTP_400_BAD_REQUEST),
(False, False, False, False, status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_alert_group_unacknowledge(
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
acknowledged,
resolved,
attached,
maintenance,
status_code,
):
organization, _, token = make_organization_and_user_with_token()
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel)
alert_group = make_alert_group(
alert_receive_channel,
acknowledged=acknowledged,
resolved=resolved,
root_alert_group=root_alert_group if attached else None,
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
)
client = APIClient()
url = reverse("api-public:alert_groups-unacknowledge", kwargs={"pk": alert_group.public_primary_key})
response = client.post(url, HTTP_AUTHORIZATION=token)
assert response.status_code == status_code
if status_code == status.HTTP_200_OK:
alert_group.refresh_from_db()
assert alert_group.acknowledged is False
assert alert_group.log_records.last().action_source == ActionSource.API
@pytest.mark.parametrize(
"resolved,attached,maintenance,status_code",
[
(False, False, False, status.HTTP_200_OK),
(False, False, True, status.HTTP_200_OK),
(True, False, False, status.HTTP_400_BAD_REQUEST),
(False, True, False, status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_alert_group_resolve(
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
resolved,
attached,
maintenance,
status_code,
):
organization, _, token = make_organization_and_user_with_token()
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel)
alert_group = make_alert_group(
alert_receive_channel,
resolved=resolved,
root_alert_group=root_alert_group if attached else None,
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
)
client = APIClient()
url = reverse("api-public:alert_groups-resolve", kwargs={"pk": alert_group.public_primary_key})
response = client.post(url, HTTP_AUTHORIZATION=token)
assert response.status_code == status_code
if status_code == status.HTTP_200_OK and not maintenance:
alert_group.refresh_from_db()
assert alert_group.resolved is True
assert alert_group.log_records.last().action_source == ActionSource.API
@pytest.mark.parametrize(
"resolved,attached,maintenance,status_code",
[
(True, False, False, status.HTTP_200_OK),
(True, True, False, status.HTTP_400_BAD_REQUEST),
(True, False, True, status.HTTP_400_BAD_REQUEST),
(False, False, False, status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_alert_group_unresolve(
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
resolved,
attached,
maintenance,
status_code,
):
organization, _, token = make_organization_and_user_with_token()
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel)
alert_group = make_alert_group(
alert_receive_channel,
resolved=resolved,
root_alert_group=root_alert_group if attached else None,
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
)
client = APIClient()
url = reverse("api-public:alert_groups-unresolve", kwargs={"pk": alert_group.public_primary_key})
response = client.post(url, HTTP_AUTHORIZATION=token)
assert response.status_code == status_code
if status_code == status.HTTP_200_OK:
alert_group.refresh_from_db()
assert alert_group.resolved is False
assert alert_group.log_records.last().action_source == ActionSource.API

View file

@ -1,11 +1,13 @@
from django.db.models import Q
from django_filters import rest_framework as filters
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from apps.alerts.constants import ActionSource
from apps.alerts.models import AlertGroup
from apps.alerts.tasks import delete_alert_group, wipe
from apps.auth_token.auth import ApiTokenAuthentication
@ -112,3 +114,74 @@ class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyM
wipe.apply_async((instance.pk, request.user.pk))
return Response(status=status.HTTP_204_NO_CONTENT)
@action(methods=["post"], detail=True)
def acknowledge(self, request, pk):
alert_group = self.get_object()
if alert_group.acknowledged:
raise BadRequest(detail="Can't acknowledge an acknowledged alert group")
if alert_group.resolved:
raise BadRequest(detail="Can't acknowledge a resolved alert group")
if alert_group.root_alert_group:
raise BadRequest(detail="Can't acknowledge an attached alert group")
if alert_group.is_maintenance_incident:
raise BadRequest(detail="Can't acknowledge a maintenance alert group")
alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.API)
return Response(status=status.HTTP_200_OK)
@action(methods=["post"], detail=True)
def unacknowledge(self, request, pk):
alert_group = self.get_object()
if not alert_group.acknowledged:
raise BadRequest(detail="Can't unacknowledge an unacknowledged alert group")
if alert_group.resolved:
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
if alert_group.root_alert_group:
raise BadRequest(detail="Can't unacknowledge an attached alert group")
if alert_group.is_maintenance_incident:
raise BadRequest(detail="Can't unacknowledge a maintenance alert group")
alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.API)
return Response(status=status.HTTP_200_OK)
@action(methods=["post"], detail=True)
def resolve(self, request, pk):
alert_group = self.get_object()
if alert_group.resolved:
raise BadRequest(detail="Can't resolve a resolved alert group")
if alert_group.root_alert_group:
raise BadRequest(detail="Can't resolve an attached alert group")
if alert_group.is_maintenance_incident:
alert_group.stop_maintenance(self.request.user)
else:
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.API)
return Response(status=status.HTTP_200_OK)
@action(methods=["post"], detail=True)
def unresolve(self, request, pk):
alert_group = self.get_object()
if not alert_group.resolved:
raise BadRequest(detail="Can't unresolve an unresolved alert group")
if alert_group.root_alert_group:
raise BadRequest(detail="Can't unresolve an attached alert group")
if alert_group.is_maintenance_incident:
raise BadRequest(detail="Can't unresolve a maintenance alert group")
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.API)
return Response(status=status.HTTP_200_OK)