Backend support for "connected" integrations (#4030)
# What this PR does Adds a Django model and internal API for connected integrations. Based on https://github.com/grafana/oncall/pull/3983 ## Which issue(s) this PR closes Related to https://github.com/grafana/oncall-private/issues/2540 ## 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] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
This commit is contained in:
parent
f6b6bb053c
commit
cf1fac8997
8 changed files with 368 additions and 1 deletions
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-07 13:45
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0045_escalationpolicy_notify_to_team_members_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AlertReceiveChannelConnection',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('backsync', models.BooleanField(default=False)),
|
||||
('connected_alert_receive_channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='source_alert_receive_channels', to='alerts.alertreceivechannel')),
|
||||
('source_alert_receive_channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='connected_alert_receive_channels', to='alerts.alertreceivechannel')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['source_alert_receive_channel', 'connected_alert_receive_channel'],
|
||||
'unique_together': {('source_alert_receive_channel', 'connected_alert_receive_channel')},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -4,6 +4,7 @@ from .alert_group_counter import AlertGroupCounter # noqa: F401
|
|||
from .alert_group_log_record import AlertGroupLogRecord, listen_for_alertgrouplogrecord # noqa: F401
|
||||
from .alert_manager_models import AlertForAlertManager, AlertGroupForAlertManager # noqa: F401
|
||||
from .alert_receive_channel import AlertReceiveChannel, listen_for_alertreceivechannel_model_save # noqa: F401
|
||||
from .alert_receive_channel_connection import AlertReceiveChannelConnection # noqa: F401
|
||||
from .channel_filter import ChannelFilter # noqa: F401
|
||||
from .custom_button import CustomButton # noqa: F401
|
||||
from .escalation_chain import EscalationChain # noqa: F401
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class AlertReceiveChannelConnection(models.Model):
|
||||
"""
|
||||
This model represents a connection between two integrations (e.g. when an Alertmanager integration is connected to a
|
||||
ServiceNow integration).
|
||||
"""
|
||||
|
||||
source_alert_receive_channel = models.ForeignKey(
|
||||
"AlertReceiveChannel", on_delete=models.CASCADE, related_name="connected_alert_receive_channels"
|
||||
)
|
||||
connected_alert_receive_channel = models.ForeignKey(
|
||||
"AlertReceiveChannel", on_delete=models.CASCADE, related_name="source_alert_receive_channels"
|
||||
)
|
||||
backsync = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ["source_alert_receive_channel", "connected_alert_receive_channel"]
|
||||
unique_together = ("source_alert_receive_channel", "connected_alert_receive_channel")
|
||||
|
|
@ -5,6 +5,7 @@ from apps.alerts.models import (
|
|||
AlertGroup,
|
||||
AlertGroupLogRecord,
|
||||
AlertReceiveChannel,
|
||||
AlertReceiveChannelConnection,
|
||||
ChannelFilter,
|
||||
CustomButton,
|
||||
EscalationChain,
|
||||
|
|
@ -24,6 +25,11 @@ class AlertReceiveChannelFactory(factory.DjangoModelFactory):
|
|||
model = AlertReceiveChannel
|
||||
|
||||
|
||||
class AlertReceiveChannelConnectionFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = AlertReceiveChannelConnection
|
||||
|
||||
|
||||
class ChannelFilterFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = ChannelFilter
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel, AlertReceiveChannelConnection
|
||||
from apps.api.serializers.alert_receive_channel import FastAlertReceiveChannelSerializer
|
||||
|
||||
|
||||
class AlertReceiveChannelSourceChannelSerializer(serializers.ModelSerializer):
|
||||
alert_receive_channel = FastAlertReceiveChannelSerializer(source="source_alert_receive_channel", read_only=True)
|
||||
backsync = serializers.BooleanField()
|
||||
|
||||
class Meta:
|
||||
model = AlertReceiveChannelConnection
|
||||
fields = ["alert_receive_channel", "backsync"]
|
||||
|
||||
|
||||
class AlertReceiveChannelConnectedChannelSerializer(serializers.ModelSerializer):
|
||||
alert_receive_channel = FastAlertReceiveChannelSerializer(source="connected_alert_receive_channel", read_only=True)
|
||||
backsync = serializers.BooleanField()
|
||||
|
||||
class Meta:
|
||||
model = AlertReceiveChannelConnection
|
||||
fields = ["alert_receive_channel", "backsync"]
|
||||
|
||||
|
||||
class AlertReceiveChannelConnectionSerializer(serializers.ModelSerializer):
|
||||
source_alert_receive_channels = AlertReceiveChannelSourceChannelSerializer(read_only=True, many=True)
|
||||
connected_alert_receive_channels = AlertReceiveChannelConnectedChannelSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = AlertReceiveChannel
|
||||
fields = ["source_alert_receive_channels", "connected_alert_receive_channels"]
|
||||
|
||||
|
||||
class AlertReceiveChannelNewConnectionSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
backsync = serializers.BooleanField()
|
||||
|
|
@ -1850,3 +1850,185 @@ def test_alert_receive_channel_webhooks_delete(
|
|||
webhook.refresh_from_db()
|
||||
assert webhook.deleted_at is not None
|
||||
assert alert_receive_channel.webhooks.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_connected_alert_receive_channels_get(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_receive_channel_connection,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
source_alert_receive_channel = make_alert_receive_channel(organization)
|
||||
connected_alert_receive_channel = make_alert_receive_channel(organization)
|
||||
make_alert_receive_channel_connection(source_alert_receive_channel, connected_alert_receive_channel)
|
||||
|
||||
# get integrations connected to source integration
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-connected-alert-receive-channels-get",
|
||||
kwargs={"pk": source_alert_receive_channel.public_primary_key},
|
||||
)
|
||||
response = client.get(url, **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"source_alert_receive_channels": [],
|
||||
"connected_alert_receive_channels": [
|
||||
{
|
||||
"alert_receive_channel": {
|
||||
"id": connected_alert_receive_channel.public_primary_key,
|
||||
"integration": connected_alert_receive_channel.integration,
|
||||
"verbal_name": connected_alert_receive_channel.verbal_name,
|
||||
"deleted": False,
|
||||
},
|
||||
"backsync": False,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# get source integrations for particular integration
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-connected-alert-receive-channels-get",
|
||||
kwargs={"pk": connected_alert_receive_channel.public_primary_key},
|
||||
)
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"source_alert_receive_channels": [
|
||||
{
|
||||
"alert_receive_channel": {
|
||||
"id": source_alert_receive_channel.public_primary_key,
|
||||
"integration": source_alert_receive_channel.integration,
|
||||
"verbal_name": source_alert_receive_channel.verbal_name,
|
||||
"deleted": False,
|
||||
},
|
||||
"backsync": False,
|
||||
},
|
||||
],
|
||||
"connected_alert_receive_channels": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_connected_alert_receive_channels_post(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
source_alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_receive_channel_to_connect_1 = make_alert_receive_channel(organization)
|
||||
alert_receive_channel_to_connect_2 = make_alert_receive_channel(organization)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-connected-alert-receive-channels-get",
|
||||
kwargs={"pk": source_alert_receive_channel.public_primary_key},
|
||||
)
|
||||
response = client.post(
|
||||
url,
|
||||
data=[
|
||||
{"id": alert_receive_channel_to_connect_1.public_primary_key, "backsync": False},
|
||||
{"id": alert_receive_channel_to_connect_2.public_primary_key, "backsync": True},
|
||||
],
|
||||
format="json",
|
||||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == {
|
||||
"source_alert_receive_channels": [],
|
||||
"connected_alert_receive_channels": [
|
||||
{
|
||||
"alert_receive_channel": {
|
||||
"id": alert_receive_channel_to_connect_1.public_primary_key,
|
||||
"integration": alert_receive_channel_to_connect_1.integration,
|
||||
"verbal_name": alert_receive_channel_to_connect_1.verbal_name,
|
||||
"deleted": False,
|
||||
},
|
||||
"backsync": False,
|
||||
},
|
||||
{
|
||||
"alert_receive_channel": {
|
||||
"id": alert_receive_channel_to_connect_2.public_primary_key,
|
||||
"integration": alert_receive_channel_to_connect_2.integration,
|
||||
"verbal_name": alert_receive_channel_to_connect_2.verbal_name,
|
||||
"deleted": False,
|
||||
},
|
||||
"backsync": True,
|
||||
},
|
||||
],
|
||||
}
|
||||
assert source_alert_receive_channel.connected_alert_receive_channels.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_connected_alert_receive_channels_put(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_receive_channel_connection,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
source_alert_receive_channel = make_alert_receive_channel(organization)
|
||||
connected_alert_receive_channel = make_alert_receive_channel(organization)
|
||||
connection = make_alert_receive_channel_connection(source_alert_receive_channel, connected_alert_receive_channel)
|
||||
|
||||
# update backsync for connected integration
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-connected-alert-receive-channels-put",
|
||||
kwargs={
|
||||
"pk": source_alert_receive_channel.public_primary_key,
|
||||
"connected_alert_receive_channel_id": connected_alert_receive_channel.public_primary_key,
|
||||
},
|
||||
)
|
||||
response = client.put(url, data={"backsync": True}, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"alert_receive_channel": {
|
||||
"id": connected_alert_receive_channel.public_primary_key,
|
||||
"integration": connected_alert_receive_channel.integration,
|
||||
"verbal_name": connected_alert_receive_channel.verbal_name,
|
||||
"deleted": False,
|
||||
},
|
||||
"backsync": True,
|
||||
}
|
||||
|
||||
connection.refresh_from_db()
|
||||
assert connection.backsync is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_connected_alert_receive_channels_delete(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_receive_channel_connection,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
|
||||
source_alert_receive_channel = make_alert_receive_channel(organization)
|
||||
connected_alert_receive_channel_1 = make_alert_receive_channel(organization)
|
||||
connected_alert_receive_channel_2 = make_alert_receive_channel(organization)
|
||||
|
||||
make_alert_receive_channel_connection(source_alert_receive_channel, connected_alert_receive_channel_1)
|
||||
make_alert_receive_channel_connection(source_alert_receive_channel, connected_alert_receive_channel_2)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-connected-alert-receive-channels-put",
|
||||
kwargs={
|
||||
"pk": source_alert_receive_channel.public_primary_key,
|
||||
"connected_alert_receive_channel_id": connected_alert_receive_channel_1.public_primary_key,
|
||||
},
|
||||
)
|
||||
response = client.delete(url, **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert source_alert_receive_channel.connected_alert_receive_channels.count() == 1
|
||||
assert (
|
||||
source_alert_receive_channel.connected_alert_receive_channels.first().connected_alert_receive_channel
|
||||
== connected_alert_receive_channel_2
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager
|
||||
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
|
||||
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel, AlertReceiveChannelConnection
|
||||
from apps.alerts.models.maintainable_object import MaintainableObject
|
||||
from apps.api.label_filtering import parse_label_query
|
||||
from apps.api.permissions import RBACPermission
|
||||
|
|
@ -24,6 +24,11 @@ from apps.api.serializers.alert_receive_channel import (
|
|||
AlertReceiveChannelUpdateSerializer,
|
||||
FilterAlertReceiveChannelSerializer,
|
||||
)
|
||||
from apps.api.serializers.alert_receive_channel_connection import (
|
||||
AlertReceiveChannelConnectedChannelSerializer,
|
||||
AlertReceiveChannelConnectionSerializer,
|
||||
AlertReceiveChannelNewConnectionSerializer,
|
||||
)
|
||||
from apps.api.serializers.webhook import WebhookSerializer
|
||||
from apps.api.throttlers import DemoAlertThrottler
|
||||
from apps.api.views.labels import schedule_update_label_cache
|
||||
|
|
@ -155,6 +160,10 @@ class AlertReceiveChannelView(
|
|||
"webhooks_post": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"webhooks_put": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"webhooks_delete": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"connected_alert_receive_channels_get": [RBACPermission.Permissions.INTEGRATIONS_READ],
|
||||
"connected_alert_receive_channels_post": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"connected_alert_receive_channels_put": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"connected_alert_receive_channels_delete": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
|
@ -676,3 +685,74 @@ class AlertReceiveChannelView(
|
|||
raise NotFound
|
||||
webhook.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@extend_schema(request=None, responses=AlertReceiveChannelConnectionSerializer)
|
||||
@action(detail=True, methods=["get"], url_path="connected_alert_receive_channels")
|
||||
def connected_alert_receive_channels_get(self, request, pk):
|
||||
instance = self.get_object()
|
||||
return Response(AlertReceiveChannelConnectionSerializer(instance).data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
request=AlertReceiveChannelNewConnectionSerializer(many=True), responses=AlertReceiveChannelConnectionSerializer
|
||||
)
|
||||
@connected_alert_receive_channels_get.mapping.post
|
||||
def connected_alert_receive_channels_post(self, request, pk):
|
||||
instance = self.get_object()
|
||||
serializer = AlertReceiveChannelNewConnectionSerializer(data=request.data, many=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
backsync_map = {connection["id"]: connection["backsync"] for connection in serializer.validated_data}
|
||||
|
||||
# bulk create connections
|
||||
AlertReceiveChannelConnection.objects.bulk_create(
|
||||
[
|
||||
AlertReceiveChannelConnection(
|
||||
source_alert_receive_channel=instance,
|
||||
connected_alert_receive_channel=alert_receive_channel,
|
||||
backsync=backsync_map[alert_receive_channel.public_primary_key],
|
||||
)
|
||||
for alert_receive_channel in instance.organization.alert_receive_channels.filter(
|
||||
public_primary_key__in=backsync_map.keys()
|
||||
)
|
||||
],
|
||||
ignore_conflicts=True,
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
return Response(AlertReceiveChannelConnectionSerializer(instance).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@extend_schema(
|
||||
request=AlertReceiveChannelConnectedChannelSerializer,
|
||||
responses=AlertReceiveChannelConnectedChannelSerializer,
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["put"],
|
||||
url_path=r"connected_alert_receive_channels/(?P<connected_alert_receive_channel_id>\w+)",
|
||||
)
|
||||
def connected_alert_receive_channels_put(self, request, pk, connected_alert_receive_channel_id):
|
||||
instance = self.get_object()
|
||||
try:
|
||||
connection = instance.connected_alert_receive_channels.get(
|
||||
connected_alert_receive_channel_id__public_primary_key=connected_alert_receive_channel_id
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
serializer = AlertReceiveChannelConnectedChannelSerializer(connection, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(request=None, responses=None)
|
||||
@connected_alert_receive_channels_put.mapping.delete
|
||||
def connected_alert_receive_channels_delete(self, request, pk, connected_alert_receive_channel_id):
|
||||
instance = self.get_object()
|
||||
try:
|
||||
connection = instance.connected_alert_receive_channels.get(
|
||||
connected_alert_receive_channel_id__public_primary_key=connected_alert_receive_channel_id
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
connection.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from apps.alerts.tests.factories import (
|
|||
AlertFactory,
|
||||
AlertGroupFactory,
|
||||
AlertGroupLogRecordFactory,
|
||||
AlertReceiveChannelConnectionFactory,
|
||||
AlertReceiveChannelFactory,
|
||||
ChannelFilterFactory,
|
||||
CustomActionFactory,
|
||||
|
|
@ -103,6 +104,7 @@ register(TeamFactory)
|
|||
|
||||
|
||||
register(AlertReceiveChannelFactory)
|
||||
register(AlertReceiveChannelConnectionFactory)
|
||||
register(ChannelFilterFactory)
|
||||
register(EscalationPolicyFactory)
|
||||
register(OnCallScheduleICalFactory)
|
||||
|
|
@ -481,6 +483,19 @@ def make_alert_receive_channel():
|
|||
return _make_alert_receive_channel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_alert_receive_channel_connection():
|
||||
def _make_alert_receive_channel_connection(source_alert_receive_channel, connected_alert_receive_channel, **kwargs):
|
||||
alert_receive_channel_connection = AlertReceiveChannelConnectionFactory(
|
||||
source_alert_receive_channel=source_alert_receive_channel,
|
||||
connected_alert_receive_channel=connected_alert_receive_channel,
|
||||
**kwargs,
|
||||
)
|
||||
return alert_receive_channel_connection
|
||||
|
||||
return _make_alert_receive_channel_connection
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_alert_receive_channel_with_post_save_signal():
|
||||
def _make_alert_receive_channel(organization, **kwargs):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue