From 673d2e9595973799a298f63dd71f583f82be409e Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 11 Oct 2024 14:57:59 -0400 Subject: [PATCH] feat: persist `is_grafana_irm_enabled` from backend plugin sync data (#5171) # What this PR does Will start persisting the `organization.is_grafana_irm_enabled` flag from the backend plugin's sync data that is sent to the oncall backend. The implications of this are that when `is_grafana_irm_enabled` is set to True, we will: - start using `grafana-irm-app` prefixed RBAC permissions (RBAC permissions for `grafana-irm-app`, as well as `grafana-oncall-app`, are already being synced to the OnCall backend since https://github.com/grafana/irm/pull/200 was merged/deployed) - start building UI URLs w/ `grafana-irm-app` instead of `grafana-oncall-app` ## Which issue(s) this PR closes Closes https://github.com/grafana/irm/issues/242 ## 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. --- engine/apps/grafana_plugin/helpers/client.py | 10 ++- .../grafana_plugin/serializers/sync_data.py | 1 + engine/apps/grafana_plugin/sync_data.py | 1 + engine/apps/grafana_plugin/tasks/sync_v2.py | 2 +- .../apps/grafana_plugin/tests/test_sync_v2.py | 75 ++++++++++++++++++- .../user_management/models/organization.py | 8 ++ engine/apps/user_management/sync.py | 9 +++ .../tests/test_organization.py | 14 ++++ .../apps/user_management/tests/test_sync.py | 1 + 9 files changed, 117 insertions(+), 4 deletions(-) diff --git a/engine/apps/grafana_plugin/helpers/client.py b/engine/apps/grafana_plugin/helpers/client.py index f1abc7a0..2beafa8b 100644 --- a/engine/apps/grafana_plugin/helpers/client.py +++ b/engine/apps/grafana_plugin/helpers/client.py @@ -11,6 +11,9 @@ from rest_framework import status from apps.api.permissions import GrafanaAPIPermission, GrafanaAPIPermissions from common.constants.plugin_ids import PluginID +if typing.TYPE_CHECKING: + from apps.user_management.models import Organization + logger = logging.getLogger(__name__) @@ -309,6 +312,9 @@ class GrafanaAPIClient(APIClient): def get_grafana_labels_plugin_settings(self) -> APIClientResponse["GrafanaAPIClient.Types.PluginSettings"]: return self.get_grafana_plugin_settings(PluginID.LABELS) + def get_grafana_irm_plugin_settings(self) -> APIClientResponse["GrafanaAPIClient.Types.PluginSettings"]: + return self.get_grafana_plugin_settings(PluginID.IRM) + def get_service_account(self, login: str) -> APIClientResponse["GrafanaAPIClient.Types.ServiceAccountResponse"]: return self.api_get(f"api/serviceaccounts/search?query={login}") @@ -328,8 +334,8 @@ class GrafanaAPIClient(APIClient): def get_service_account_token_permissions(self) -> APIClientResponse[typing.Dict[str, typing.List[str]]]: return self.api_get("api/access-control/user/permissions") - def sync(self) -> APIClientResponse: - return self.api_post("api/plugins/grafana-oncall-app/resources/plugin/sync") + def sync(self, organization: "Organization") -> APIClientResponse: + return self.api_post(f"api/plugins/{organization.active_ui_plugin_id}/resources/plugin/sync") @staticmethod def validate_grafana_token_format(grafana_token: str) -> bool: diff --git a/engine/apps/grafana_plugin/serializers/sync_data.py b/engine/apps/grafana_plugin/serializers/sync_data.py index a0d1dd30..79902529 100644 --- a/engine/apps/grafana_plugin/serializers/sync_data.py +++ b/engine/apps/grafana_plugin/serializers/sync_data.py @@ -71,6 +71,7 @@ class SyncOnCallSettingsSerializer(serializers.Serializer): incident_enabled = serializers.BooleanField() incident_backend_url = serializers.CharField(allow_blank=True) labels_enabled = serializers.BooleanField() + irm_enabled = serializers.BooleanField(default=False) def create(self, validated_data): return SyncSettings(**validated_data) diff --git a/engine/apps/grafana_plugin/sync_data.py b/engine/apps/grafana_plugin/sync_data.py index b4a86857..a5f39e3f 100644 --- a/engine/apps/grafana_plugin/sync_data.py +++ b/engine/apps/grafana_plugin/sync_data.py @@ -40,6 +40,7 @@ class SyncSettings: incident_enabled: bool incident_backend_url: str labels_enabled: bool + irm_enabled: bool @dataclass diff --git a/engine/apps/grafana_plugin/tasks/sync_v2.py b/engine/apps/grafana_plugin/tasks/sync_v2.py index 1a479aa7..1a88eded 100644 --- a/engine/apps/grafana_plugin/tasks/sync_v2.py +++ b/engine/apps/grafana_plugin/tasks/sync_v2.py @@ -49,7 +49,7 @@ def sync_organizations_v2(org_ids=None): organization_qs = Organization.objects.filter(id__in=org_ids) for org in organization_qs: client = GrafanaAPIClient(api_url=org.grafana_url, api_token=org.api_token) - _, status = client.sync() + _, status = client.sync(org) if status["status_code"] != 200: logger.error( f"Failed to request sync org_id={org.pk} stack_slug={org.stack_slug} status_code={status['status_code']} url={status['url']} message={status['message']}" diff --git a/engine/apps/grafana_plugin/tests/test_sync_v2.py b/engine/apps/grafana_plugin/tests/test_sync_v2.py index 704ff9a3..8f1a9271 100644 --- a/engine/apps/grafana_plugin/tests/test_sync_v2.py +++ b/engine/apps/grafana_plugin/tests/test_sync_v2.py @@ -12,7 +12,8 @@ from rest_framework.test import APIClient from apps.api.permissions import LegacyAccessControlRole from apps.grafana_plugin.serializers.sync_data import SyncTeamSerializer from apps.grafana_plugin.sync_data import SyncData, SyncSettings, SyncUser -from apps.grafana_plugin.tasks.sync_v2 import start_sync_organizations_v2 +from apps.grafana_plugin.tasks.sync_v2 import start_sync_organizations_v2, sync_organizations_v2 +from common.constants.plugin_ids import PluginID @pytest.mark.django_db @@ -121,6 +122,7 @@ def test_sync_v2_content_encoding( incident_enabled=False, incident_backend_url="", labels_enabled=False, + irm_enabled=False, ), ) @@ -140,6 +142,57 @@ def test_sync_v2_content_encoding( mock_sync.assert_called() +@pytest.mark.parametrize( + "irm_enabled,expected", + [ + (True, True), + (False, False), + ], +) +@pytest.mark.django_db +def test_sync_v2_irm_enabled( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + settings, + irm_enabled, + expected, +): + settings.LICENSE = settings.CLOUD_LICENSE_NAME + organization, _, token = make_organization_and_user_with_plugin_token() + + assert organization.is_grafana_irm_enabled is False + + client = APIClient() + headers = make_user_auth_headers(None, token, organization=organization) + url = reverse("grafana-plugin:sync-v2") + + data = SyncData( + users=[], + teams=[], + team_members={}, + settings=SyncSettings( + stack_id=organization.stack_id, + org_id=organization.org_id, + license=settings.CLOUD_LICENSE_NAME, + oncall_api_url="http://localhost", + oncall_token="", + grafana_url="http://localhost", + grafana_token="fake_token", + rbac_enabled=False, + incident_enabled=False, + incident_backend_url="", + labels_enabled=False, + irm_enabled=irm_enabled, + ), + ) + + response = client.post(url, format="json", data=asdict(data), **headers) + assert response.status_code == status.HTTP_200_OK + + organization.refresh_from_db() + assert organization.is_grafana_irm_enabled == expected + + @pytest.mark.parametrize( "test_team, validation_pass", [ @@ -190,3 +243,23 @@ def test_sync_batch_tasks(make_organization, settings): assert check_call(actual_call, expected_call) assert mock_sync.call_count == len(expected_calls) + + +@patch( + "apps.grafana_plugin.tasks.sync_v2.GrafanaAPIClient.api_post", + return_value=(None, {"status_code": status.HTTP_200_OK}), +) +@pytest.mark.parametrize( + "is_grafana_irm_enabled,expected", + [ + (True, PluginID.IRM), + (False, PluginID.ONCALL), + ], +) +@pytest.mark.django_db +def test_sync_organizations_v2_calls_right_backend_plugin_sync_endpoint( + mocked_grafana_api_client_api_post, make_organization, is_grafana_irm_enabled, expected +): + org = make_organization(is_grafana_irm_enabled=is_grafana_irm_enabled) + sync_organizations_v2(org_ids=[org.pk]) + mocked_grafana_api_client_api_post.assert_called_once_with(f"api/plugins/{expected}/resources/plugin/sync") diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index a6068d2e..a6dcc622 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -18,6 +18,7 @@ from apps.chatops_proxy.utils import ( from apps.grafana_plugin.ui_url_builder import UIURLBuilder from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy from apps.user_management.types import AlertGroupTableColumn +from common.constants.plugin_ids import PluginID from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -352,6 +353,13 @@ class Organization(MaintainableObject): """ return UIURLBuilder(self).home(f"?oncall-uuid={self.uuid}") + @property + def active_ui_plugin_id(self) -> str: + """ + If `is_grafana_irm_enabled` is True, this will be IRM, otherwise OnCall + """ + return PluginID.IRM if self.is_grafana_irm_enabled else PluginID.ONCALL + @classmethod def __str__(self): return f"{self.pk}: {self.org_title}" diff --git a/engine/apps/user_management/sync.py b/engine/apps/user_management/sync.py index 71848658..d94edd37 100644 --- a/engine/apps/user_management/sync.py +++ b/engine/apps/user_management/sync.py @@ -69,6 +69,12 @@ def _sync_organization(organization: Organization) -> None: if grafana_labels_plugin_settings is not None: is_grafana_labels_enabled = grafana_labels_plugin_settings["enabled"] + # get IRM plugin settings + is_grafana_irm_enabled = False + grafana_irm_plugin_settings, _ = grafana_api_client.get_grafana_labels_plugin_settings() + if grafana_irm_plugin_settings is not None: + is_grafana_irm_enabled = grafana_irm_plugin_settings["enabled"] + oncall_api_url = settings.BASE_URL if settings.LICENSE == CLOUD_LICENSE_NAME: oncall_api_url = settings.GRAFANA_CLOUD_ONCALL_API_URL @@ -85,6 +91,7 @@ def _sync_organization(organization: Organization) -> None: incident_enabled=is_grafana_incident_enabled, incident_backend_url=grafana_incident_backend_url, labels_enabled=is_grafana_labels_enabled, + irm_enabled=is_grafana_irm_enabled, ) _sync_organization_data(organization, sync_settings) if organization.api_token_status == Organization.API_TOKEN_STATUS_OK: @@ -288,6 +295,7 @@ def _sync_organization_data(organization: Organization, sync_settings: SyncSetti organization.is_rbac_permissions_enabled = sync_settings.rbac_enabled logger.info(f"RBAC status org={organization.pk} rbac_enabled={organization.is_rbac_permissions_enabled}") + organization.is_grafana_irm_enabled = sync_settings.irm_enabled organization.is_grafana_labels_enabled = sync_settings.labels_enabled organization.is_grafana_incident_enabled = sync_settings.incident_enabled organization.grafana_incident_backend_url = sync_settings.incident_backend_url @@ -321,6 +329,7 @@ def _sync_organization_data(organization: Organization, sync_settings: SyncSetti "is_rbac_permissions_enabled", "is_grafana_incident_enabled", "is_grafana_labels_enabled", + "is_grafana_irm_enabled", "grafana_incident_backend_url", ] ) diff --git a/engine/apps/user_management/tests/test_organization.py b/engine/apps/user_management/tests/test_organization.py index ca5ff342..1f4607e5 100644 --- a/engine/apps/user_management/tests/test_organization.py +++ b/engine/apps/user_management/tests/test_organization.py @@ -9,6 +9,7 @@ from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRe from apps.schedules.models import OnCallScheduleICal, OnCallScheduleWeb from apps.telegram.models import TelegramMessage from apps.user_management.models import Organization +from common.constants.plugin_ids import PluginID @pytest.mark.django_db @@ -276,3 +277,16 @@ def test_get_notifiable_direct_paging_integrations( make_channel_filter(arc, is_default=False) notifiable_direct_paging_integrations = _assert(org, arc) assert notifiable_direct_paging_integrations.count() == 1 + + +@pytest.mark.parametrize( + "is_grafana_irm_enabled,expected", + [ + (True, PluginID.IRM), + (False, PluginID.ONCALL), + ], +) +@pytest.mark.django_db +def test_active_ui_plugin_id(make_organization, is_grafana_irm_enabled, expected): + org = make_organization(is_grafana_irm_enabled=is_grafana_irm_enabled) + assert org.active_ui_plugin_id == expected diff --git a/engine/apps/user_management/tests/test_sync.py b/engine/apps/user_management/tests/test_sync.py index 0f605dd2..d1c63bbd 100644 --- a/engine/apps/user_management/tests/test_sync.py +++ b/engine/apps/user_management/tests/test_sync.py @@ -587,6 +587,7 @@ def test_apply_sync_data_none_values(make_organization): grafana_token=organization.api_token, oncall_token=organization.gcom_token, grafana_url=organization.grafana_url, + irm_enabled=False, ), )