Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.10.4 #5146

Merged
merged 3 commits into from
Oct 9, 2024
Merged

v1.10.4 #5146

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions engine/apps/alerts/models/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length

if typing.TYPE_CHECKING:
from django.db.models.manager import RelatedManager

from apps.alerts.models import AlertGroup, AlertReceiveChannel, ChannelFilter

logger = logging.getLogger(__name__)
Expand All @@ -49,7 +47,6 @@ def generate_public_primary_key_for_alert():

class Alert(models.Model):
group: typing.Optional["AlertGroup"]
resolved_alert_groups: "RelatedManager['AlertGroup']"

public_primary_key = models.CharField(
max_length=20,
Expand Down Expand Up @@ -163,11 +160,6 @@ def create(
if not group.resolved and mark_as_resolved:
group.resolve_by_source()

# Store exact alert which resolved group.
if group.resolved_by == AlertGroup.SOURCE and group.resolved_by_alert is None:
group.resolved_by_alert = alert
group.save(update_fields=["resolved_by_alert"])

if group_created:
# all code below related to maintenance mode
maintenance_uuid = None
Expand Down
31 changes: 18 additions & 13 deletions engine/apps/alerts/models/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import urllib
from collections import namedtuple
from functools import partial
from urllib.parse import urljoin

from celery import uuid as celery_uuid
from django.conf import settings
Expand All @@ -13,6 +12,7 @@
from django.db.models import JSONField, Q, QuerySet
from django.utils import timezone
from django.utils.functional import cached_property
from django_deprecate_fields import deprecate_field

from apps.alerts.constants import ActionSource, AlertGroupState
from apps.alerts.escalation_snapshot import EscalationSnapshotMixin
Expand All @@ -27,10 +27,10 @@
send_alert_group_signal_for_delete,
unsilence_task,
)
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.metrics_exporter.tasks import update_metrics_for_alert_group
from apps.slack.slack_formatter import SlackFormatter
from apps.user_management.models import User
from common.constants.plugin_ids import PluginID
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
from common.utils import clean_markup, str_or_backup

Expand Down Expand Up @@ -201,7 +201,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
personal_log_records: "RelatedManager['UserNotificationPolicyLogRecord']"
resolution_notes: "RelatedManager['ResolutionNote']"
resolution_note_slack_messages: "RelatedManager['ResolutionNoteSlackMessage']"
resolved_by_alert: typing.Optional["Alert"]
resolved_by_user: typing.Optional["User"]
root_alert_group: typing.Optional["AlertGroup"]
silenced_by_user: typing.Optional["User"]
Expand Down Expand Up @@ -289,12 +288,16 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
related_name="resolved_alert_groups",
)

resolved_by_alert = models.ForeignKey(
"alerts.Alert",
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="resolved_alert_groups",
# NOTE: see https://raintank-corp.slack.com/archives/C07RGREUH4Z/p1728494111646319
# This field should eventually be dropped as it's no longer being set/read anywhere
resolved_by_alert = deprecate_field(
models.ForeignKey(
"alerts.Alert",
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="resolved_alert_groups",
)
)

resolved_at = models.DateTimeField(blank=True, null=True)
Expand Down Expand Up @@ -543,17 +546,19 @@ def permalinks(self) -> Permalinks:

@property
def web_link(self) -> str:
return urljoin(self.channel.organization.web_link, f"alert-groups/{self.public_primary_key}")
return UIURLBuilder(self.channel.organization).alert_group_detail(self.public_primary_key)

@property
def declare_incident_link(self) -> str:
"""Generate a link for AlertGroup to declare Grafana Incident by click"""
incident_link = urljoin(self.channel.organization.grafana_url, f"a/{PluginID.INCIDENT}/incidents/declare/")
"""
Generate a link for AlertGroup to declare Grafana Incident by click
"""
caption = urllib.parse.quote_plus("OnCall Alert Group")
title = urllib.parse.quote_plus(self.web_title_cache) if self.web_title_cache else DEFAULT_BACKUP_TITLE
title = title[:2000] # set max title length to avoid exceptions with too long declare incident link
link = urllib.parse.quote_plus(self.web_link)
return urljoin(incident_link, f"?caption={caption}&url={link}&title={title}")

return UIURLBuilder(self.channel.organization).declare_incident(f"?caption={caption}&url={link}&title={title}")

@property
def happened_while_maintenance(self):
Expand Down
10 changes: 5 additions & 5 deletions engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import typing
from functools import cached_property
from urllib.parse import urljoin

import emoji
from celery import uuid as celery_uuid
Expand All @@ -21,6 +20,7 @@
from apps.alerts.tasks import disable_maintenance, disconnect_integration_from_alerting_contact_points
from apps.base.messaging import get_messaging_backend_from_id
from apps.base.utils import live_settings
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.integrations.legacy_prefix import remove_legacy_prefix
from apps.integrations.metadata import heartbeat
from apps.integrations.tasks import create_alert, create_alertmanager_alerts
Expand Down Expand Up @@ -422,8 +422,8 @@ def emojized_verbal_name(self):

@property
def new_incidents_web_link(self):
return urljoin(
self.organization.web_link, f"?page=incidents&integration={self.public_primary_key}&status=0&p=1"
return UIURLBuilder(self.organization).alert_groups(
f"?integration={self.public_primary_key}&status={AlertGroup.NEW}",
)

@property
Expand Down Expand Up @@ -531,8 +531,8 @@ def created_name(self):
return f"{self.get_integration_display()} {self.smile_code}"

@property
def web_link(self):
return urljoin(self.organization.web_link, f"integrations/{self.public_primary_key}")
def web_link(self) -> str:
return UIURLBuilder(self.organization).integration_detail(self.public_primary_key)

@property
def integration_url(self) -> str | None:
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def get_cloud_connection_status(self, obj: User) -> CloudSyncStatus | None:
connector = self.context.get("connector", None)
identities = self.context.get("cloud_identities", {})
identity = identities.get(obj.email, None)
status, _ = cloud_user_identity_status(connector, identity)
status, _ = cloud_user_identity_status(obj.organization, connector, identity)
return status
return None

Expand Down
13 changes: 8 additions & 5 deletions engine/apps/api/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@

from apps.auth_token.constants import SLACK_AUTH_TOKEN_NAME
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND
from common.constants.plugin_ids import PluginID
from common.constants.slack_auth import SLACK_OAUTH_ACCESS_RESPONSE

GRAFANA_URL = "http://example.com"


@pytest.mark.django_db
@pytest.mark.parametrize(
"backend_name,expected_url",
(
("slack-login", "/a/grafana-oncall-app/users/me"),
(SLACK_INSTALLATION_BACKEND, "/a/grafana-oncall-app/chat-ops"),
("slack-login", f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users/me"),
(SLACK_INSTALLATION_BACKEND, f"{GRAFANA_URL}/a/{PluginID.ONCALL}/chat-ops"),
),
)
def test_complete_slack_auth_redirect_ok(
Expand All @@ -28,7 +31,7 @@ def test_complete_slack_auth_redirect_ok(
backend_name,
expected_url,
):
organization = make_organization()
organization = make_organization(grafana_url=GRAFANA_URL)
admin = make_user_for_organization(organization)
_, slack_token = make_slack_token_for_user(admin)

Expand Down Expand Up @@ -181,7 +184,7 @@ def test_google_complete_auth_redirect_ok(
make_user_for_organization,
make_google_oauth2_token_for_user,
):
organization = make_organization()
organization = make_organization(grafana_url=GRAFANA_URL)
admin = make_user_for_organization(organization)
_, google_oauth2_token = make_google_oauth2_token_for_user(admin)

Expand All @@ -194,7 +197,7 @@ def test_google_complete_auth_redirect_ok(
response = client.get(url)

assert response.status_code == status.HTTP_302_FOUND
assert response.url == "/a/grafana-oncall-app/users/me"
assert response.url == f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users/me"


@pytest.mark.django_db
Expand Down
21 changes: 11 additions & 10 deletions engine/apps/api/views/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
from urllib.parse import urljoin

from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
Expand All @@ -19,6 +18,7 @@
get_installation_link_from_chatops_proxy,
get_slack_oauth_response_from_chatops_proxy,
)
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.slack.installation import install_slack_integration
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND, LoginSlackOAuth2V2

Expand Down Expand Up @@ -73,13 +73,6 @@ def overridden_login_social_auth(request: Request, backend: str) -> Response:
@psa("social:complete")
def overridden_complete_social_auth(request: Request, backend: str, *args, **kwargs) -> Response:
"""Authentication complete view"""
if isinstance(request.backend, (LoginSlackOAuth2V2, GoogleOAuth2)):
# if this was a user login/linking account, redirect to profile
redirect_to = "/a/grafana-oncall-app/users/me"
else:
# InstallSlackOAuth2V2 backend
redirect_to = "/a/grafana-oncall-app/chat-ops"

kwargs.update(
user=request.user,
redirect_name=REDIRECT_FIELD_NAME,
Expand All @@ -99,8 +92,16 @@ def overridden_complete_social_auth(request: Request, backend: str, *args, **kwa
return_to = request.backend.strategy.session.get(REDIRECT_FIELD_NAME)

if return_to is None:
# We build the frontend url using org url since multiple stacks could be connected to one backend.
return_to = urljoin(request.user.organization.grafana_url, redirect_to)
url_builder = UIURLBuilder(request.user.organization)

# if this was a user login/linking account, redirect to profile (ie. users/me)
# otherwise it pertains to the InstallSlackOAuth2V2 backend, and we should redirect to the chat-ops page
return_to = (
url_builder.user_profile()
if isinstance(request.backend, (LoginSlackOAuth2V2, GoogleOAuth2))
else url_builder.chatops()
)

return HttpResponseRedirect(return_to)


Expand Down
7 changes: 6 additions & 1 deletion engine/apps/chatops_proxy/register_oncall_tenant.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# register_oncall_tenant moved to separate file from engine/apps/chatops_proxy/utils.py to avoid circular imports.
import typing

from django.conf import settings

from apps.chatops_proxy.client import APP_TYPE_ONCALL, ChatopsProxyAPIClient

if typing.TYPE_CHECKING:
from apps.user_management.models import Organization


def register_oncall_tenant(org):
def register_oncall_tenant(org: "Organization") -> None:
"""
register_oncall_tenant registers oncall organization as a tenant in chatops-proxy.
"""
Expand Down
14 changes: 9 additions & 5 deletions engine/apps/chatops_proxy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"""
import logging
import typing
from urllib.parse import urljoin

from django.conf import settings

from apps.grafana_plugin.ui_url_builder import UIURLBuilder

from .client import APP_TYPE_ONCALL, PROVIDER_TYPE_SLACK, ChatopsProxyAPIClient, ChatopsProxyAPIException
from .register_oncall_tenant import register_oncall_tenant
from .tasks import (
Expand All @@ -16,10 +17,13 @@
unregister_oncall_tenant_async,
)

if typing.TYPE_CHECKING:
from apps.user_management.models import Organization, User

logger = logging.getLogger(__name__)


def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
def get_installation_link_from_chatops_proxy(user: "User") -> typing.Optional[str]:
"""
get_installation_link_from_chatops_proxy fetches slack installation link from chatops proxy.
If there is no existing slack installation - if returns link, If slack already installed, it returns None.
Expand All @@ -30,7 +34,7 @@ def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
link, _ = client.get_slack_oauth_link(
org.stack_id,
user.user_id,
urljoin(org.web_link, "settings?tab=ChatOps&chatOpsTab=Slack"),
UIURLBuilder(org).settings("?tab=ChatOps&chatOpsTab=Slack"),
APP_TYPE_ONCALL,
)
return link
Expand All @@ -44,13 +48,13 @@ def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
raise api_exc


def get_slack_oauth_response_from_chatops_proxy(stack_id) -> dict:
def get_slack_oauth_response_from_chatops_proxy(stack_id: int) -> dict:
client = ChatopsProxyAPIClient(settings.ONCALL_GATEWAY_URL, settings.ONCALL_GATEWAY_API_TOKEN)
slack_installation, _ = client.get_oauth_installation(stack_id, PROVIDER_TYPE_SLACK)
return slack_installation.oauth_response


def register_oncall_tenant_with_async_fallback(org):
def register_oncall_tenant_with_async_fallback(org: "Organization") -> None:
"""
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.
Expand Down
Loading
Loading