Skip to content

Commit

Permalink
Merge branch 'dev' into jorlando/splunk-migration-tool
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyorlando committed May 13, 2024
2 parents 1c249b0 + ccdf991 commit b61cadd
Show file tree
Hide file tree
Showing 37 changed files with 200 additions and 84 deletions.
4 changes: 2 additions & 2 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ steps:
- apt-get update && apt-get install -y netcat-traditional
- cd engine/
- pip install uv
- uv pip install -r requirements.txt -r requirements-dev.txt
- uv pip install --system -r requirements.txt -r requirements-dev.txt
- ./wait_for_test_mysql_start.sh && pytest
depends_on:
- rabbit_test
Expand Down Expand Up @@ -386,6 +386,6 @@ name: cloud_access_policy_token

---
kind: signature
hmac: d541ed21fc2472272c6772e246aaf1a2606db112b4e72a44bc4530831e9ca4d3
hmac: a045d72f3f3510895da049f4bf8f5ae4ac21f3ffa3d24cda047152c286df5bc2

...
32 changes: 23 additions & 9 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,27 @@ docker_build_sub(
],
)

# Build the plugin in the background
local_resource(
"build-ui",
labels=["OnCallUI"],
serve_cmd="cd grafana-plugin && yarn watch",
allow_parallel=True,
)
# On CI dependencies are installed separately so we just build prod bundle to be consumed by Grafana dev server
if is_ci:
local_resource(
"build-ui",
labels=["OnCallUI"],
dir="grafana-plugin",
cmd="yarn build",
allow_parallel=True,
)

# Locally we install dependencies and we run watch mode
if not is_ci:
local_resource(
"build-ui",
labels=["OnCallUI"],
dir="grafana-plugin",
cmd="yarn install",
serve_dir="grafana-plugin",
serve_cmd="yarn watch",
allow_parallel=True,
)

local_resource(
"e2e-tests",
Expand Down Expand Up @@ -130,7 +144,7 @@ configmap_create(
k8s_resource(
objects=["grafana-oncall-app-provisioning:configmap"],
new_name="grafana-oncall-app-provisioning-configmap",
resource_deps=["build-ui", "engine"],
resource_deps=["build-ui"],
labels=["Grafana"],
)

Expand All @@ -141,7 +155,7 @@ if not running_under_parent_tiltfile:
context="grafana-plugin",
plugin_files=["grafana-plugin/src/plugin.json"],
namespace="default",
deps=["grafana-oncall-app-provisioning-configmap", "build-ui", "engine"],
deps=["grafana-oncall-app-provisioning-configmap", "build-ui"],
extra_env={
"GF_SECURITY_ADMIN_PASSWORD": "oncall",
"GF_SECURITY_ADMIN_USER": "oncall",
Expand Down
1 change: 1 addition & 0 deletions dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Related: [How to develop integrations](/engine/config_integrations/README.md)
- [Tilt | Kubernetes for Prod, Tilt for Dev](https://tilt.dev/)
- [tilt-dev/ctlptl: Making local Kubernetes clusters fun and easy to set up](https://github.com/tilt-dev/ctlptl)
- [Kind](https://kind.sigs.k8s.io)
- [Node.js v18.x](https://nodejs.org/en/download)
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable)

### Launch the environment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,24 +472,32 @@ def _escalation_step_notify_if_num_alerts_in_time_window(
def _escalation_step_trigger_custom_webhook(self, alert_group: "AlertGroup", _reason: str) -> None:
tasks = []
webhook = self.custom_webhook
failure_reason = None
if webhook is not None:
custom_webhook_task = custom_webhook_result.signature(
(webhook.pk, alert_group.pk),
{
"escalation_policy_pk": self.id,
},
immutable=True,
)
tasks.append(custom_webhook_task)
if webhook.is_webhook_enabled:
custom_webhook_task = custom_webhook_result.signature(
(webhook.pk, alert_group.pk),
{
"escalation_policy_pk": self.id,
},
immutable=True,
)
tasks.append(custom_webhook_task)
else:
failure_reason = AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_IS_DISABLED
else:
failure_reason = AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_STEP_IS_NOT_CONFIGURED

if failure_reason:
log_record = AlertGroupLogRecord(
type=AlertGroupLogRecord.TYPE_ESCALATION_FAILED,
alert_group=alert_group,
escalation_policy=self.escalation_policy,
escalation_error_code=AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_STEP_IS_NOT_CONFIGURED,
escalation_error_code=failure_reason,
escalation_policy_step=self.step,
)
log_record.save()

self._execute_tasks(tasks)

def _escalation_step_repeat_escalation_n_times(
Expand Down
9 changes: 0 additions & 9 deletions engine/apps/alerts/models/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,17 +511,8 @@ def slack_app_link(self) -> typing.Optional[str]:

@property
def telegram_permalink(self) -> typing.Optional[str]:
"""
This property will attempt to access an attribute, `prefetched_telegram_messages`, representing a list of
prefetched telegram messages. If this attribute does not exist, it falls back to performing a query.
See `apps.public_api.serializers.incidents.IncidentSerializer.PREFETCH_RELATED` as an example.
"""
from apps.telegram.models.message import TelegramMessage

if hasattr(self, "prefetched_telegram_messages"):
return self.prefetched_telegram_messages[0].link if self.prefetched_telegram_messages else None

main_telegram_message = self.telegram_messages.filter(
chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE
).first()
Expand Down
5 changes: 4 additions & 1 deletion engine/apps/alerts/models/alert_group_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ class AlertGroupLogRecord(models.Model):
ERROR_ESCALATION_NOTIFY_IF_NUM_ALERTS_IN_WINDOW_STEP_IS_NOT_CONFIGURED,
ERROR_ESCALATION_TRIGGER_CUSTOM_WEBHOOK_ERROR,
ERROR_ESCALATION_NOTIFY_TEAM_MEMBERS_STEP_IS_NOT_CONFIGURED,
) = range(19)
ERROR_ESCALATION_TRIGGER_WEBHOOK_IS_DISABLED,
) = range(20)

type = models.IntegerField(choices=TYPE_CHOICES)

Expand Down Expand Up @@ -590,6 +591,8 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
usergroup_handle = self.escalation_policy.notify_to_group.handle
usergroup_handle_text = f" @{usergroup_handle}" if usergroup_handle else ""
result += f"failed to notify User Group{usergroup_handle_text} in Slack"
elif self.escalation_error_code == AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_IS_DISABLED:
result += 'skipped escalation step "Trigger Outgoing Webhook" because it is disabled'
return result

def get_step_specific_info(self):
Expand Down
27 changes: 27 additions & 0 deletions engine/apps/alerts/tests/test_escalation_policy_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,33 @@ def test_escalation_step_trigger_custom_webhook(
mock_webhook_escalation_step.assert_called_once_with(alert_group, reason)


@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
@pytest.mark.django_db
def test_escalation_step_trigger_disabled_custom_webhook(
mocked_execute_tasks,
escalation_step_test_setup,
make_custom_webhook,
make_escalation_policy,
):
organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup

custom_webhook = make_custom_webhook(organization=organization, is_webhook_enabled=False)

trigger_custom_webhook_step = make_escalation_policy(
escalation_chain=channel_filter.escalation_chain,
escalation_policy_step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=custom_webhook,
)
escalation_policy_snapshot = get_escalation_policy_snapshot_from_model(trigger_custom_webhook_step)
escalation_policy_snapshot.execute(alert_group, reason)
assert call([]) in mocked_execute_tasks.call_args_list

log_record = AlertGroupLogRecord.objects.get(
alert_group_id=alert_group.id, escalation_policy=trigger_custom_webhook_step
)
assert log_record.escalation_error_code == AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_IS_DISABLED


@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
@pytest.mark.django_db
def test_escalation_step_repeat_escalation_n_times(
Expand Down
35 changes: 23 additions & 12 deletions engine/apps/google/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

from apps.google import constants, utils
from apps.google.types import GoogleCalendarEvent as GoogleCalendarEventType
Expand All @@ -23,6 +24,11 @@ def __init__(self, event: GoogleCalendarEventType):
self.end_time_utc = self._end_time.astimezone(datetime.timezone.utc)


class GoogleCalendarHTTPError(Exception):
def __init__(self, http_error) -> None:
self.error = http_error


class GoogleCalendarAPIClient:
MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH = 250
"""
Expand Down Expand Up @@ -68,17 +74,22 @@ def fetch_out_of_office_events(self) -> typing.List[GoogleCalendarEvent]:
now + datetime.timedelta(days=constants.DAYS_IN_FUTURE_TO_CONSIDER_OUT_OF_OFFICE_EVENTS)
)

events_result = (
self.service.events()
.list(
calendarId=self.CALENDAR_ID,
timeMin=time_min,
timeMax=time_max,
maxResults=self.MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH,
singleEvents=True,
orderBy="startTime",
eventTypes="outOfOffice",
try:
events_result = (
self.service.events()
.list(
calendarId=self.CALENDAR_ID,
timeMin=time_min,
timeMax=time_max,
maxResults=self.MAX_NUMBER_OF_CALENDAR_EVENTS_TO_FETCH,
singleEvents=True,
orderBy="startTime",
eventTypes="outOfOffice",
)
.execute()
)
.execute()
)
except HttpError as e:
logger.error(f"GoogleCalendarAPIClient - Error fetching out of office events: {e}")
raise GoogleCalendarHTTPError(e)

return [GoogleCalendarEvent(event) for event in events_result.get("items", [])]
10 changes: 8 additions & 2 deletions engine/apps/google/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from celery.utils.log import get_task_logger

from apps.google import constants
from apps.google.client import GoogleCalendarAPIClient
from apps.google.client import GoogleCalendarAPIClient, GoogleCalendarHTTPError
from apps.google.models import GoogleOAuth2User
from apps.schedules.models import OnCallSchedule, ShiftSwapRequest
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
Expand Down Expand Up @@ -31,7 +31,13 @@ def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> N
if oncall_schedules_to_consider_for_shift_swaps:
users_schedules = users_schedules.filter(public_primary_key__in=oncall_schedules_to_consider_for_shift_swaps)

for out_of_office_event in google_api_client.fetch_out_of_office_events():
try:
out_of_office_events = google_api_client.fetch_out_of_office_events()
except GoogleCalendarHTTPError:
logger.info(f"Failed to fetch out of office events for user {user_id}")
return

for out_of_office_event in out_of_office_events:
raw_event = out_of_office_event.raw_event

event_title = raw_event["summary"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest
from django.utils import timezone
from googleapiclient.errors import HttpError

from apps.google import constants, tasks
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest
Expand Down Expand Up @@ -140,6 +141,28 @@ def _test_setup(out_of_office_events):
return _test_setup


class MockResponse:
def __init__(self, reason=None, status=200) -> None:
self.reason = reason or ""
self.status = status


@patch("apps.google.client.build")
@pytest.mark.django_db
def test_sync_out_of_office_calendar_events_for_user_httperror(mock_google_api_client_build, test_setup):
mock_response = MockResponse(reason="forbidden", status=403)
mock_google_api_client_build.return_value.events.return_value.list.return_value.execute.side_effect = HttpError(
resp=mock_response, content=b"error"
)

google_oauth2_user, schedule = test_setup([])
user = google_oauth2_user.user

tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk)

assert ShiftSwapRequest.objects.filter(beneficiary=user, schedule=schedule).count() == 0


@patch("apps.google.client.build")
@pytest.mark.django_db
def test_sync_out_of_office_calendar_events_for_user_no_ooo_events(mock_google_api_client_build, test_setup):
Expand Down
12 changes: 1 addition & 11 deletions engine/apps/public_api/serializers/incidents.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from django.db.models import Prefetch
from rest_framework import serializers

from apps.alerts.models import AlertGroup
from apps.telegram.models.message import TelegramMessage
from common.api_helpers.custom_fields import UserIdField
from common.api_helpers.mixins import EagerLoadingMixin

Expand All @@ -19,14 +17,6 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer):
resolved_by = UserIdField(read_only=True, source="resolved_by_user")

SELECT_RELATED = ["channel", "channel_filter", "slack_message", "channel__organization"]
PREFETCH_RELATED = [
"alerts",
Prefetch(
"telegram_messages",
TelegramMessage.objects.filter(chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE),
to_attr="prefetched_telegram_messages",
),
]

class Meta:
model = AlertGroup
Expand All @@ -50,7 +40,7 @@ def get_title(self, obj):
return obj.web_title_cache

def get_alerts_count(self, obj):
return len(obj.alerts.all())
return obj.alerts.count()

def get_state(self, obj):
return obj.state
Expand Down
4 changes: 4 additions & 0 deletions engine/apps/slack/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class SlackAPIMessageNotFoundError(SlackAPIError):
errors = ("message_not_found",)


class SlackAPICantUpdateMessageError(SlackAPIError):
errors = ("cant_update_message",)


class SlackAPIUserNotFoundError(SlackAPIError):
errors = ("user_not_found",)

Expand Down
2 changes: 1 addition & 1 deletion engine/apps/slack/models/slack_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def permalink(self) -> typing.Optional[str]:

@property
def deep_link(self) -> str:
return f"slack://channel?team={self.slack_team_identity.slack_id}&id={self.channel_id}&message={self.slack_id}"
return f"https://slack.com/app_redirect?channel={self.channel_id}&team={self.slack_team_identity.slack_id}&message={self.slack_id}"

def send_slack_notification(self, user, alert_group, notification_policy):
from apps.base.models import UserNotificationPolicyLogRecord
Expand Down
2 changes: 2 additions & 0 deletions engine/apps/slack/scenarios/distribute_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from apps.api.permissions import RBACPermission
from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME
from apps.slack.errors import (
SlackAPICantUpdateMessageError,
SlackAPIChannelArchivedError,
SlackAPIChannelInactiveError,
SlackAPIChannelNotFoundError,
Expand Down Expand Up @@ -947,6 +948,7 @@ def update_log_message(self, alert_group: AlertGroup) -> None:
SlackAPIChannelArchivedError,
SlackAPIChannelInactiveError,
SlackAPIInvalidAuthError,
SlackAPICantUpdateMessageError,
):
pass
else:
Expand Down

0 comments on commit b61cadd

Please sign in to comment.