From bc6272744b1ddee4518d4a27dd3b7ef8d921be16 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 21 Jun 2024 14:40:34 +0800 Subject: [PATCH] Post stack slug to chatops proxy (#4559) # What this PR does It's part of work on https://github.com/grafana/oncall-gateway/issues/247. I added stack_slug to call to the chatops-proxy tenant/register API. On a side note I figured out that we didn't cleanup slack integration in chatops-proxy, once it's uninstalled on OnCall side, so it's [fixed](https://github.com/grafana/oncall/pull/4559/files#diff-1784f1d0d65fa477f4562e73aa23fe1c757b171f36e03f12600bdb021f121307R577) as well. Changes are validated locally. --- engine/apps/chatops_proxy/client.py | 19 ++++++++++++++- engine/apps/chatops_proxy/tasks.py | 4 +++- engine/apps/chatops_proxy/utils.py | 23 ++++++++++++++++++- engine/apps/slack/views.py | 12 ++++++++++ .../user_management/models/organization.py | 4 +++- 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/engine/apps/chatops_proxy/client.py b/engine/apps/chatops_proxy/client.py index baa382d5..1a0b63bc 100644 --- a/engine/apps/chatops_proxy/client.py +++ b/engine/apps/chatops_proxy/client.py @@ -65,7 +65,7 @@ class ChatopsProxyAPIClient: # OnCall Tenant def register_tenant( - self, service_tenant_id: str, cluster_slug: str, service_type: str, stack_id: int + self, service_tenant_id: str, cluster_slug: str, service_type: str, stack_id: int, stack_slug: str ) -> tuple[Tenant, requests.models.Response]: url = f"{self.api_base_url}/tenants/register" d = { @@ -74,6 +74,7 @@ class ChatopsProxyAPIClient: "cluster_slug": cluster_slug, "service_type": service_type, "stack_id": stack_id, + "stack_slug": stack_slug, } } response = requests.post(url=url, json=d, headers=self._headers) @@ -170,6 +171,22 @@ class ChatopsProxyAPIClient: self._check_response(response) return OAuthInstallation(**response.json()["oauth_installation"]), response + def delete_oauth_installation( + self, + stack_id: int, + provider_type: str, + grafana_user_id: int, + ) -> tuple[bool, requests.models.Response]: + url = f"{self.api_base_url}/oauth_installations/uninstall" + d = { + "stack_id": stack_id, + "provider_type": provider_type, + "grafana_user_id": grafana_user_id, + } + response = requests.post(url=url, json=d, headers=self._headers) + self._check_response(response) + return response.json()["removed"], response + def _check_response(self, response: requests.models.Response): """ Wraps an exceptional response to ChatopsProxyAPIException diff --git a/engine/apps/chatops_proxy/tasks.py b/engine/apps/chatops_proxy/tasks.py index bbe3dcd0..2793dca5 100644 --- a/engine/apps/chatops_proxy/tasks.py +++ b/engine/apps/chatops_proxy/tasks.py @@ -18,14 +18,16 @@ def register_oncall_tenant_async(**kwargs): cluster_slug = kwargs.get("cluster_slug") service_type = kwargs.get("service_type") stack_id = kwargs.get("stack_id") + stack_slug = kwargs.get("stack_slug") client = ChatopsProxyAPIClient(settings.ONCALL_GATEWAY_URL, settings.ONCALL_GATEWAY_API_TOKEN) try: - client.register_tenant(service_tenant_id, cluster_slug, service_type, stack_id) + client.register_tenant(service_tenant_id, cluster_slug, service_type, stack_id, stack_slug) except ChatopsProxyAPIException as api_exc: task_logger.error( f'msg="Failed to register OnCall tenant: {api_exc.msg}" service_tenant_id={service_tenant_id} cluster_slug={cluster_slug}' ) + # TODO: remove this check once new upsert tenant api is released if api_exc.status == 409: # 409 Indicates that it's impossible to register tenant, because tenant already registered. # Not retrying in this case, because manual conflict-resolution needed. diff --git a/engine/apps/chatops_proxy/utils.py b/engine/apps/chatops_proxy/utils.py index 60d70391..6be8ea7d 100644 --- a/engine/apps/chatops_proxy/utils.py +++ b/engine/apps/chatops_proxy/utils.py @@ -48,7 +48,7 @@ def get_slack_oauth_response_from_chatops_proxy(stack_id) -> dict: return slack_installation.oauth_response -def register_oncall_tenant(service_tenant_id: str, cluster_slug: str, stack_id: int): +def register_oncall_tenant(service_tenant_id: str, cluster_slug: str, stack_id: int, stack_slug: str): """ register_oncall_tenant tries to register oncall tenant synchronously and fall back to task in case of any exceptions to make sure that tenant is registered. @@ -61,6 +61,7 @@ def register_oncall_tenant(service_tenant_id: str, cluster_slug: str, stack_id: cluster_slug, SERVICE_TYPE_ONCALL, stack_id, + stack_slug, ) except Exception as e: logger.error( @@ -141,3 +142,23 @@ def unlink_slack_team(service_tenant_id: str, slack_team_id: str): "service_type": SERVICE_TYPE_ONCALL, } ) + + +def uninstall_slack(stack_id: int, grafana_user_id: int) -> bool: + """ + uninstall_slack uninstalls slack integration from chatops-proxy and returns bool indicating if it was removed. + If such installation does not exist - returns True as well.s + """ + client = ChatopsProxyAPIClient(settings.ONCALL_GATEWAY_URL, settings.ONCALL_GATEWAY_API_TOKEN) + try: + removed, response = client.delete_oauth_installation(stack_id, PROVIDER_TYPE_SLACK, grafana_user_id) + except ChatopsProxyAPIException as api_exc: + if api_exc.status == 404: + return True + logger.exception( + "uninstall_slack: error trying to install slack from chatops-proxy: " "error=%s", + api_exc, + ) + return False + + return removed is True diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index e921c609..9578bbc4 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -15,6 +15,7 @@ from rest_framework.views import APIView from apps.api.permissions import RBACPermission from apps.auth_token.auth import PluginAuthentication from apps.base.utils import live_settings +from apps.chatops_proxy.utils import uninstall_slack as uninstall_slack_from_chatops_proxy from apps.slack.client import SlackClient from apps.slack.errors import SlackAPIError from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING @@ -573,8 +574,19 @@ class ResetSlackView(APIView): "Grafana OnCall is temporary unable to connect your slack account or install OnCall to your slack workspace", status=400, ) + if settings.UNIFIED_SLACK_APP_ENABLED: + # If unified slack app is enabled - uninstall slack integration from chatops-proxy first and on success - + # uninstall it from OnCall. + removed = uninstall_slack_from_chatops_proxy(request.user.organization.stack_id, request.user.user_id) + else: + # just a placeholder value to continute uninstallation until UNIFIED_SLACK_APP_ENABLED is not enabled + removed = True + if not removed: + return Response({"error": "Failed to uninstall slack integration"}, status=500) + try: uninstall_slack_integration(request.user.organization, request.user) except SlackInstallationExc as e: return Response({"error": e.error_message}, status=400) + return Response(status=200) diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index e82b8ace..506951e2 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -61,7 +61,9 @@ class OrganizationQuerySet(models.QuerySet): def create(self, **kwargs): instance = super().create(**kwargs) if settings.FEATURE_MULTIREGION_ENABLED: - register_oncall_tenant(str(instance.uuid), settings.ONCALL_BACKEND_REGION, instance.stack_id) + register_oncall_tenant( + str(instance.uuid), settings.ONCALL_BACKEND_REGION, instance.stack_id, instance.stack_slug + ) return instance def delete(self):