diff --git a/CHANGELOG.md b/CHANGELOG.md index 0999423e..37d39e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). +## v1.3.57 (2023-11-10) + +### Fixed + +- Fix AmazonSNS integration to handle exceptions the same as other integrations @mderynck ([#3315](https://github.com/grafana/oncall/pull/3315)) + ## v1.3.56 (2023-11-10) ## v1.3.55 (2023-11-07) diff --git a/engine/apps/integrations/views.py b/engine/apps/integrations/views.py index 359b692b..61e51bea 100644 --- a/engine/apps/integrations/views.py +++ b/engine/apps/integrations/views.py @@ -21,6 +21,7 @@ from apps.integrations.mixins import ( is_ratelimit_ignored, ) from apps.integrations.tasks import create_alert, create_alertmanager_alerts +from apps.user_management.exceptions import OrganizationDeletedException, OrganizationMovedException from common.api_helpers.utils import create_engine_url logger = logging.getLogger(__name__) @@ -31,8 +32,10 @@ class AmazonSNS(BrowsableInstructionMixin, AlertChannelDefiningMixin, Integratio def dispatch(self, *args, **kwargs): try: return super().dispatch(*args, **kwargs) + except (OrganizationMovedException, OrganizationDeletedException, PermissionDenied) as oe: + raise oe except Exception as e: - print(e) + logger.error(f"AmazonSNS - Bad Request (400) {str(e)}") return JsonResponse(status=400, data={}) def handle_message(self, message, payload): diff --git a/engine/apps/user_management/tests/test_region.py b/engine/apps/user_management/tests/test_region.py index 414396d1..6331327c 100644 --- a/engine/apps/user_management/tests/test_region.py +++ b/engine/apps/user_management/tests/test_region.py @@ -9,7 +9,7 @@ from rest_framework.test import APIClient from apps.alerts.models import AlertReceiveChannel from apps.auth_token.auth import ApiTokenAuthentication, ScheduleExportAuthentication, UserScheduleExportAuthentication from apps.auth_token.models import ScheduleExportAuthToken, UserScheduleExportAuthToken -from apps.integrations.views import AlertManagerAPIView +from apps.integrations.views import AlertManagerAPIView, AmazonSNS from apps.schedules.models import OnCallScheduleWeb from apps.user_management.exceptions import OrganizationMovedException @@ -30,19 +30,27 @@ def test_organization_region_delete( @pytest.mark.django_db +@pytest.mark.parametrize( + "integration_type,integration_view", + [ + (AlertReceiveChannel.INTEGRATION_ALERTMANAGER, AlertManagerAPIView()), + ("amazon_sns", AmazonSNS()), + ], +) def test_integration_does_not_raise_exception_organization_moved( make_organization, make_alert_receive_channel, + integration_type, + integration_view, ): organization = make_organization() alert_receive_channel = make_alert_receive_channel( organization=organization, - integration=AlertReceiveChannel.INTEGRATION_ALERTMANAGER, + integration=integration_type, ) try: - am = AlertManagerAPIView() - am.dispatch(alert_channel_key=alert_receive_channel.token) + integration_view.dispatch(alert_channel_key=alert_receive_channel.token) assert False except OrganizationMovedException: assert False @@ -51,21 +59,29 @@ def test_integration_does_not_raise_exception_organization_moved( @pytest.mark.django_db +@pytest.mark.parametrize( + "integration_type,integration_view", + [ + (AlertReceiveChannel.INTEGRATION_ALERTMANAGER, AlertManagerAPIView()), + ("amazon_sns", AmazonSNS()), + ], +) def test_integration_raises_exception_organization_moved( make_organization_and_region, make_alert_receive_channel, + integration_type, + integration_view, ): organization, region = make_organization_and_region() organization.save() alert_receive_channel = make_alert_receive_channel( organization=organization, - integration=AlertReceiveChannel.INTEGRATION_ALERTMANAGER, + integration=integration_type, ) try: - am = AlertManagerAPIView() - am.dispatch(alert_channel_key=alert_receive_channel.token) + integration_view.dispatch(alert_channel_key=alert_receive_channel.token) assert False except OrganizationMovedException as e: assert e.organization == organization @@ -73,24 +89,29 @@ def test_integration_raises_exception_organization_moved( @patch("apps.user_management.middlewares.OrganizationMovedMiddleware.make_request") @pytest.mark.django_db +@pytest.mark.parametrize( + "integration_type", + [ + AlertReceiveChannel.INTEGRATION_ALERTMANAGER, + "amazon_sns", + ], +) def test_organization_moved_middleware( - mocked_make_request, - make_organization_and_region, - make_alert_receive_channel, + mocked_make_request, make_organization_and_region, make_alert_receive_channel, integration_type ): organization, region = make_organization_and_region() organization.save() alert_receive_channel = make_alert_receive_channel( organization=organization, - integration=AlertReceiveChannel.INTEGRATION_ALERTMANAGER, + integration=integration_type, ) expected_message = bytes(f"Redirected to {region.oncall_backend_url}", "utf-8") mocked_make_request.return_value = HttpResponse(expected_message, status=status.HTTP_200_OK) client = APIClient() - url = reverse("integrations:alertmanager", kwargs={"alert_channel_key": alert_receive_channel.token}) + url = reverse(f"integrations:{integration_type}", kwargs={"alert_channel_key": alert_receive_channel.token}) data = {"value": "test"} response = client.post(url, data, format="json") diff --git a/engine/engine/management/commands/restart_acknowledge_reminder.py b/engine/engine/management/commands/restart_acknowledge_reminder.py new file mode 100644 index 00000000..69622053 --- /dev/null +++ b/engine/engine/management/commands/restart_acknowledge_reminder.py @@ -0,0 +1,73 @@ +from celery import uuid as celery_uuid +from django.core.management import BaseCommand + +from apps.alerts.models import AlertGroup +from apps.alerts.tasks import acknowledge_reminder_task +from apps.user_management.models import Organization + + +class Command(BaseCommand): + """ + Restart acknowledge_reminder_task for organization. Used for migrated organizations. + + Usage example: + `python manage.py restart_acknowledge_reminder -ppk "organization ppk"` - restart task for alert groups from + organizations with this public pk + """ + + def add_arguments(self, parser): + group = parser.add_mutually_exclusive_group(required=True) + + group.add_argument( + "-ppk", "--organization_ppk", type=str, help="Organization public pks to restart reminder for." + ) + + def handle(self, *args, **options): + organization_ppk = options["organization_ppk"] + organization = Organization.objects.get(public_primary_key=organization_ppk) + self.stdout.write( + f"Processing restart acknowledge reminder for alert groups from organization " + f"(id: {organization.id}, ppk: {organization.public_primary_key})..." + ) + if organization.acknowledge_remind_timeout == 0: + self.stdout.write("Organization doesn't have acknowledge reminder setting set") + return + + alert_groups = AlertGroup.objects.filter( + acknowledged=True, + resolved=False, + silenced=False, + maintenance_uuid__isnull=True, + root_alert_group=None, + channel__organization=organization, + ) + if not alert_groups: + self.stdout.write("No affected alert groups.") + return + + self.stdout.write(f"Affected alert groups count: {alert_groups.count()}.") + + tasks = [] + alert_groups_to_update = [] + + for alert_group in alert_groups: + task_id = celery_uuid() + countdown = Organization.ACKNOWLEDGE_REMIND_DELAY[organization.acknowledge_remind_timeout] + alert_group.last_unique_unacknowledge_process_id = task_id + alert_groups_to_update.append(alert_group) + tasks.append( + acknowledge_reminder_task.signature( + args=(alert_group.pk, task_id), immutable=True, task_id=task_id, countdown=countdown + ) + ) + + AlertGroup.objects.bulk_update( + alert_groups_to_update, + ["last_unique_unacknowledge_process_id"], + batch_size=5000, + ) + + for task in tasks: + task.apply_async() + + self.stdout.write("Acknowledge reminder has been restarted for affected alert groups")