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.
This commit is contained in:
Joey Orlando 2024-10-11 14:57:59 -04:00 committed by GitHub
parent 22ab249b3b
commit 673d2e9595
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 117 additions and 4 deletions

View file

@ -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:

View file

@ -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)

View file

@ -40,6 +40,7 @@ class SyncSettings:
incident_enabled: bool
incident_backend_url: str
labels_enabled: bool
irm_enabled: bool
@dataclass

View file

@ -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']}"

View file

@ -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")

View file

@ -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}"

View file

@ -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",
]
)

View file

@ -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

View file

@ -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,
),
)