Delete duplicate direct paging integrations (#3412)
# What this PR does Deletes duplicate direct paging integrations (i.e. keeps only the first direct paging integration per team). Also adds a unique constraint that will make such duplicates impossible at the DB level. ## Which issue(s) this PR fixes Related to https://github.com/grafana/oncall-private/issues/2302 ## 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:
parent
efe274b465
commit
381a9ecf54
5 changed files with 96 additions and 30 deletions
|
|
@ -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
|
||||
|
||||
### Fixed
|
||||
|
||||
- Delete duplicate direct paging integrations by @vadimkerr ([#3412](https://github.com/grafana/oncall/pull/3412))
|
||||
|
||||
## v1.3.65 (2023-11-29)
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# Generated by Django 4.2.7 on 2023-11-28 10:45
|
||||
import logging
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def delete_duplicate_direct_paging_integrations(apps, schema_editor):
|
||||
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
|
||||
|
||||
# get (organization_id, team_id) pairs for teams that have more than one direct paging integration
|
||||
duplicate_rows = AlertReceiveChannel.objects.values_list(
|
||||
"organization_id", "team_id"
|
||||
).annotate(count=models.Count("id")).filter(integration="direct_paging", deleted_at__isnull=True, count__gt=1)
|
||||
|
||||
for organization_id, team_id, _ in duplicate_rows:
|
||||
# get the first direct paging integration for the team (the one we want to keep)
|
||||
first_direct_paging_integration = AlertReceiveChannel.objects.filter(
|
||||
organization_id=organization_id,
|
||||
team_id=team_id,
|
||||
integration="direct_paging",
|
||||
deleted_at__isnull=True,
|
||||
).order_by("id").first()
|
||||
|
||||
if first_direct_paging_integration is None:
|
||||
continue
|
||||
|
||||
# delete all other direct paging integrations for the team (except the first one)
|
||||
num_deleted = AlertReceiveChannel.objects.filter(
|
||||
organization_id=organization_id,
|
||||
team_id=team_id,
|
||||
integration="direct_paging",
|
||||
deleted_at__isnull=True,
|
||||
).exclude(id=first_direct_paging_integration.id).update(deleted_at=timezone.now())
|
||||
|
||||
logger.info(
|
||||
f"Deleted {num_deleted} duplicate direct paging integrations for team ({organization_id}, {team_id}), "
|
||||
f"keeping only one direct paging integration for team: {first_direct_paging_integration.id}."
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0040_alertreceivechannel_alert_group_labels_custom_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(delete_duplicate_direct_paging_integrations, migrations.RunPython.noop),
|
||||
migrations.AddConstraint(
|
||||
model_name='alertreceivechannel',
|
||||
constraint=models.UniqueConstraint(models.F('organization'), models.Case(models.When(team=None, then=0), default=models.F('team'), output_field=models.BigIntegerField()), models.Case(models.When(deleted_at__isnull=True, then=True), default=None), models.Case(models.When(integration='direct_paging', then=True), default=None), name='unique_direct_paging_integration_per_team'),
|
||||
),
|
||||
]
|
||||
|
|
@ -8,7 +8,7 @@ from celery import uuid as celery_uuid
|
|||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models import BigIntegerField, Case, F, Q, When
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
|
@ -215,6 +215,21 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
alert_group_labels_template: str | None = models.TextField(null=True, default=None)
|
||||
"""Stores a Jinja2 template for "advanced label templating" for alert group labels."""
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
# This constraint ensures that there's at most one active direct paging integration per team
|
||||
# This should work with SQLite, PostgreSQL and MySQL >= 8.0.13.
|
||||
# From the docs: Functional indexes are ignored with MySQL < 8.0.13 and MariaDB as neither supports them.
|
||||
# https://docs.djangoproject.com/en/4.2/ref/models/constraints/#expressions
|
||||
models.UniqueConstraint(
|
||||
F("organization"),
|
||||
Case(When(team=None, then=0), default=F("team"), output_field=BigIntegerField()),
|
||||
Case(When(deleted_at__isnull=True, then=True), default=None),
|
||||
Case(When(integration="direct_paging", then=True), default=None),
|
||||
name="unique_direct_paging_integration_per_team",
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
short_name_with_emojis = emojize(self.short_name, language="alias")
|
||||
return f"{self.pk}: {short_name_with_emojis}"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from unittest import mock
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.db import IntegrityError
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel
|
||||
|
|
@ -229,3 +230,20 @@ def test_delete_duplicate_names(make_organization, make_alert_receive_channel):
|
|||
for _ in range(2):
|
||||
make_alert_receive_channel(organization, verbal_name="duplicate")
|
||||
organization.alert_receive_channels.all().delete()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_duplicate_direct_paging_integrations(make_organization, make_team, make_alert_receive_channel):
|
||||
"""Check that it's not possible to have more than one active direct paging integration per team."""
|
||||
|
||||
organization = make_organization()
|
||||
team = make_team(organization)
|
||||
make_alert_receive_channel(organization, team=team, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING)
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
arc = AlertReceiveChannel(
|
||||
organization=organization,
|
||||
team=team,
|
||||
integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING,
|
||||
)
|
||||
super(AlertReceiveChannel, arc).save() # bypass the custom save method, so that IntegrityError is raised
|
||||
|
|
|
|||
|
|
@ -852,35 +852,6 @@ def test_update_alert_receive_channels_direct_paging(
|
|||
assert response.json()["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_alert_receive_channel_direct_paging_duplicate(
|
||||
make_organization_and_user_with_plugin_token, make_team, make_alert_receive_channel, make_user_auth_headers
|
||||
):
|
||||
"""Check that it's possible to delete direct paging integration even if there is a duplicate for the team."""
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
integration = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=None
|
||||
)
|
||||
|
||||
# Create a team, add direct paging integration to it, then delete the team.
|
||||
# There will be 2 direct paging integrations for the team "No team" as a result.
|
||||
team = make_team(organization)
|
||||
make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=team)
|
||||
team.delete()
|
||||
assert (
|
||||
organization.alert_receive_channels.filter(
|
||||
integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=None
|
||||
).count()
|
||||
== 2
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": integration.public_primary_key})
|
||||
response = client.delete(url, **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_integration(
|
||||
make_user_auth_headers,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue