Skip to content

Commit

Permalink
feat: Backend support for Organisation-level integrations (#4400)
Browse files Browse the repository at this point in the history
  • Loading branch information
khvn26 authored Aug 29, 2024
1 parent df6ce44 commit 3e6b96f
Show file tree
Hide file tree
Showing 15 changed files with 509 additions and 57 deletions.
23 changes: 22 additions & 1 deletion api/audit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from audit.related_object_type import RelatedObjectType
from projects.models import Project

if typing.TYPE_CHECKING:
from organisations.models import Organisation

RELATED_OBJECT_TYPES = ((tag.name, tag.value) for tag in RelatedObjectType)


Expand Down Expand Up @@ -68,6 +71,21 @@ class Meta:
verbose_name_plural = "Audit Logs"
ordering = ("-created_date",)

@property
def organisation(self) -> "Organisation | None":
# TODO properly implement organisation relation
# maybe the relation list should not be _that_ exhaustive...
for relation in (
"project",
"environment",
"author",
"master_api_key",
"history_record",
):
if hasattr(related_instance := getattr(self, relation), "organisation"):
return related_instance.organisation
return None

@property
def environment_document_updated(self) -> bool:
if self.related_object_type == RelatedObjectType.CHANGE_REQUEST.name:
Expand Down Expand Up @@ -126,7 +144,10 @@ def add_created_date(self) -> None:
when="environment_document_updated",
is_now=True,
)
def process_environment_update(self):
def process_environment_update(self) -> None:
if not self.project:
return

from environments.models import Environment
from environments.tasks import process_environment_update

Expand Down
16 changes: 10 additions & 6 deletions api/audit/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from audit.models import AuditLog, RelatedObjectType
from audit.serializers import AuditLogListSerializer
from integrations.common.models import IntegrationsModel
from integrations.datadog.datadog import DataDogWrapper
from integrations.dynatrace.dynatrace import DynatraceWrapper
from integrations.grafana.grafana import GrafanaWrapper
Expand Down Expand Up @@ -41,12 +42,15 @@ def call_webhooks(sender, instance, **kwargs):
)


def _get_integration_config(instance, integration_name):
if hasattr(instance.project, integration_name):
return getattr(instance.project, integration_name)
elif hasattr(instance.environment, integration_name):
return getattr(instance.environment, integration_name)

def _get_integration_config(
instance: AuditLog, integration_name: str
) -> IntegrationsModel | None:
if hasattr(project := instance.project, integration_name):
return getattr(project, integration_name)
if hasattr(environment := instance.environment, integration_name):
return getattr(environment, integration_name)
if hasattr(organisation := instance.organisation, integration_name):
return getattr(organisation, integration_name)
return None


Expand Down
4 changes: 2 additions & 2 deletions api/environments/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)
from integrations.amplitude.views import AmplitudeConfigurationViewSet
from integrations.dynatrace.views import DynatraceConfigurationViewSet
from integrations.grafana.views import GrafanaConfigurationViewSet
from integrations.grafana.views import GrafanaProjectConfigurationViewSet
from integrations.heap.views import HeapConfigurationViewSet
from integrations.mixpanel.views import MixpanelConfigurationViewSet
from integrations.rudderstack.views import RudderstackConfigurationViewSet
Expand Down Expand Up @@ -83,7 +83,7 @@
)
environments_router.register(
r"integrations/grafana",
GrafanaConfigurationViewSet,
GrafanaProjectConfigurationViewSet,
basename="integrations-grafana",
)
environments_router.register(
Expand Down
4 changes: 4 additions & 0 deletions api/integrations/common/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ class BaseEnvironmentIntegrationModelSerializer(_BaseIntegrationModelSerializer)

class BaseProjectIntegrationModelSerializer(_BaseIntegrationModelSerializer):
one_to_one_field_name = "project_id"


class BaseOrganisationIntegrationModelSerializer(_BaseIntegrationModelSerializer):
one_to_one_field_name = "organisation_id"
29 changes: 29 additions & 0 deletions api/integrations/common/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from environments.models import Environment
from environments.permissions.constants import VIEW_ENVIRONMENT
from environments.permissions.permissions import NestedEnvironmentPermissions
from organisations.permissions.permissions import (
NestedOrganisationEntityPermission,
)
from projects.permissions import VIEW_PROJECT, NestedProjectPermissions


Expand Down Expand Up @@ -74,3 +77,29 @@ def perform_create(self, serializer: BaseSerializer) -> None:

def perform_update(self, serializer: BaseSerializer) -> None:
serializer.save(project_id=self.kwargs["project_pk"])


class OrganisationIntegrationBaseViewSet(viewsets.ModelViewSet):
serializer_class = None
pagination_class = None
model_class = None

permission_classes = [IsAuthenticated, NestedOrganisationEntityPermission]

def get_queryset(self) -> QuerySet:
if getattr(self, "swagger_fake_view", False):
return self.model_class.objects.none()

return self.model_class.objects.filter(
organisation_id=self.kwargs["organisation_pk"]
)

def perform_create(self, serializer: BaseSerializer) -> None:
if self.get_queryset().exists():
raise ValidationError(
"This integration already exists for this organisation."
)
serializer.save(organisation_id=self.kwargs["organisation_pk"])

def perform_update(self, serializer: BaseSerializer) -> None:
serializer.save(organisation_id=self.kwargs["organisation_pk"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Generated by Django 3.2.25 on 2024-07-27 14:21

from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):
dependencies = [
("organisations", "0055_alter_percent_usage"),
("projects", "0024_add_project_edge_v2_migration_read_capacity_budget"),
("grafana", "0001_initial"),
]

operations = [
migrations.RenameModel(
old_name="GrafanaConfiguration",
new_name="GrafanaProjectConfiguration",
),
migrations.CreateModel(
name="GrafanaOrganisationConfiguration",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"deleted_at",
models.DateTimeField(
blank=True,
db_index=True,
default=None,
editable=False,
null=True,
),
),
(
"uuid",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
("base_url", models.URLField(null=True)),
("api_key", models.CharField(max_length=100)),
(
"organisation",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="grafana_config",
to="organisations.organisation",
),
),
],
options={
"abstract": False,
},
),
]
68 changes: 37 additions & 31 deletions api/integrations/grafana/models.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
"""
Example `integration_data` entry:
```
"grafana": {
"perEnvironment": false,
"image": "/static/images/integrations/grafana.svg",
"docs": "https://docs.flagsmith.com/integrations/apm/grafana",
"fields": [
{
"key": "base_url",
"label": "Base URL",
"default": "https://grafana.com"
},
{
"key": "api_key",
"label": "Service account token",
"hidden": true
}
],
"tags": [
"logging"
],
"title": "Grafana",
"description": "Receive Flagsmith annotations to your Grafana instance on feature flag and segment changes."
},
```
"""

import logging

from django.db import models

from integrations.common.models import IntegrationsModel
from organisations.models import Organisation
from projects.models import Project

logger = logging.getLogger(__name__)


class GrafanaConfiguration(IntegrationsModel):
"""
Example `integration_data` entry:
```
"grafana": {
"perEnvironment": false,
"image": "/static/images/integrations/grafana.svg",
"docs": "https://docs.flagsmith.com/integrations/apm/grafana",
"fields": [
{
"key": "base_url",
"label": "Base URL",
"default": "https://grafana.com"
},
{
"key": "api_key",
"label": "Service account token",
"hidden": true
}
],
"tags": [
"logging"
],
"title": "Grafana",
"description": "Receive Flagsmith annotations to your Grafana instance on feature flag and segment changes."
},
```
"""

base_url = models.URLField(blank=False, null=True)
class GrafanaProjectConfiguration(IntegrationsModel):
project = models.OneToOneField(
Project, on_delete=models.CASCADE, related_name="grafana_config"
)


class GrafanaOrganisationConfiguration(IntegrationsModel):
organisation = models.OneToOneField(
Organisation, on_delete=models.CASCADE, related_name="grafana_config"
)
19 changes: 16 additions & 3 deletions api/integrations/grafana/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
from integrations.common.serializers import (
BaseOrganisationIntegrationModelSerializer,
BaseProjectIntegrationModelSerializer,
)
from integrations.grafana.models import (
GrafanaOrganisationConfiguration,
GrafanaProjectConfiguration,
)


from .models import GrafanaConfiguration
class GrafanaProjectConfigurationSerializer(
BaseProjectIntegrationModelSerializer,
):
class Meta:
model = GrafanaProjectConfiguration
fields = ("id", "base_url", "api_key")


class GrafanaConfigurationSerializer(BaseProjectIntegrationModelSerializer):
class GrafanaOrganisationConfigurationSerializer(
BaseOrganisationIntegrationModelSerializer,
):
class Meta:
model = GrafanaConfiguration
model = GrafanaOrganisationConfiguration
fields = ("id", "base_url", "api_key")
27 changes: 21 additions & 6 deletions api/integrations/grafana/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
from integrations.common.views import ProjectIntegrationBaseViewSet
from integrations.grafana.models import GrafanaConfiguration
from integrations.grafana.serializers import GrafanaConfigurationSerializer
from integrations.common.views import (
OrganisationIntegrationBaseViewSet,
ProjectIntegrationBaseViewSet,
)
from integrations.grafana.models import (
GrafanaOrganisationConfiguration,
GrafanaProjectConfiguration,
)
from integrations.grafana.serializers import (
GrafanaOrganisationConfigurationSerializer,
GrafanaProjectConfigurationSerializer,
)


class GrafanaConfigurationViewSet(ProjectIntegrationBaseViewSet):
serializer_class = GrafanaConfigurationSerializer
class GrafanaProjectConfigurationViewSet(ProjectIntegrationBaseViewSet):
serializer_class = GrafanaProjectConfigurationSerializer
pagination_class = None # set here to ensure documentation is correct
model_class = GrafanaConfiguration
model_class = GrafanaProjectConfiguration


class GrafanaOrganisationConfigurationViewSet(OrganisationIntegrationBaseViewSet):
serializer_class = GrafanaOrganisationConfigurationSerializer
pagination_class = None # set here to ensure documentation is correct
model_class = GrafanaOrganisationConfiguration
7 changes: 7 additions & 0 deletions api/organisations/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
fetch_repo_contributors,
fetch_repositories,
)
from integrations.grafana.views import GrafanaOrganisationConfigurationViewSet
from metadata.views import MetaDataModelFieldViewSet
from organisations.views import (
OrganisationAPIUsageNotificationView,
Expand Down Expand Up @@ -77,6 +78,12 @@
"audit", OrganisationAuditLogViewSet, basename="audit-log"
)

organisations_router.register(
r"integrations/grafana",
GrafanaOrganisationConfigurationViewSet,
basename="integrations-grafana",
)

organisations_router.register(
r"integrations/github",
GithubConfigurationViewSet,
Expand Down
4 changes: 2 additions & 2 deletions api/projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from features.multivariate.views import MultivariateFeatureOptionViewSet
from features.views import FeatureViewSet
from integrations.datadog.views import DataDogConfigurationViewSet
from integrations.grafana.views import GrafanaConfigurationViewSet
from integrations.grafana.views import GrafanaProjectConfigurationViewSet
from integrations.launch_darkly.views import LaunchDarklyImportRequestViewSet
from integrations.new_relic.views import NewRelicConfigurationViewSet
from projects.tags.views import TagViewSet
Expand Down Expand Up @@ -59,7 +59,7 @@
)
projects_router.register(
r"integrations/grafana",
GrafanaConfigurationViewSet,
GrafanaProjectConfigurationViewSet,
basename="integrations-grafana",
)
projects_router.register(
Expand Down
Loading

0 comments on commit 3e6b96f

Please sign in to comment.