Skip to content

Commit

Permalink
Merge branch 'dev' into jinja-filter-base64decode
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyorlando authored Nov 5, 2023
2 parents 77aec96 + 14244e9 commit 96ea973
Show file tree
Hide file tree
Showing 55 changed files with 1,038 additions and 633 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Add `b64decode` Jinja2 template helper filter by @jorgeav ([#3242](https://github.com/grafana/oncall/pull/3242))

## v1.3.53 (2023-11-03)

### Fixed

- Fix db migration for mobile app @Ferril ([#3260](https://github.com/grafana/oncall/pull/3260))
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ x-environment: &oncall-environment
PROMETHEUS_EXPORTER_SECRET: ${PROMETHEUS_EXPORTER_SECRET:-}
REDIS_URI: redis://redis:6379/0
DJANGO_SETTINGS_MODULE: settings.hobby
CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery"
CELERY_WORKER_QUEUE: "default,critical,long,slack,telegram,webhook,retry,celery,grafana"
CELERY_WORKER_CONCURRENCY: "1"
CELERY_WORKER_MAX_TASKS_PER_CHILD: "100"
CELERY_WORKER_SHUTDOWN_INTERVAL: "65m"
Expand Down
25 changes: 0 additions & 25 deletions engine/apps/alerts/paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,28 +197,3 @@ def unpage_user(alert_group: AlertGroup, user: User, from_user: User) -> None:
def user_is_oncall(user: User) -> bool:
schedules_with_oncall_users = get_oncall_users_for_multiple_schedules(OnCallSchedule.objects.related_to_user(user))
return user.pk in {user.pk for _, users in schedules_with_oncall_users.items() for user in users}


def integration_is_notifiable(integration: AlertReceiveChannel) -> bool:
"""
Returns true if:
- the integration has more than one channel filter associated with it
- the default channel filter has at least one notification method specified or an escalation chain associated with it
"""
if integration.channel_filters.count() > 1:
return True

default_channel_filter = integration.default_channel_filter
if not default_channel_filter:
return False

organization = integration.organization
notify_via_slack = organization.slack_is_configured and default_channel_filter.notify_in_slack
notify_via_telegram = organization.telegram_is_configured and default_channel_filter.notify_in_telegram

notify_via_chatops = notify_via_slack or notify_via_telegram
custom_messaging_backend_configured = default_channel_filter.notification_backends is not None

return (
default_channel_filter.escalation_chain is not None or notify_via_chatops or custom_messaging_backend_configured
)
65 changes: 0 additions & 65 deletions engine/apps/alerts/tests/test_paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
DirectPagingUserTeamValidationError,
_construct_title,
direct_paging,
integration_is_notifiable,
unpage_user,
user_is_oncall,
)
Expand Down Expand Up @@ -292,67 +291,3 @@ def _title(middle_portion: str) -> str:
assert _construct_title(from_user, team, multiple_users) == _title(
f"{team.name}, {user1.username}, {user2.username} and {user3.username}"
)


@pytest.mark.django_db
def test_integration_is_notifiable(
make_organization,
make_alert_receive_channel,
make_channel_filter,
make_escalation_chain,
make_slack_team_identity,
make_telegram_channel,
):
organization = make_organization()

# integration has no default channel filter
arc = make_alert_receive_channel(organization)
make_channel_filter(arc, is_default=False)
assert integration_is_notifiable(arc) is False

# integration has more than one channel filter
arc = make_alert_receive_channel(organization)
make_channel_filter(arc, is_default=False)
make_channel_filter(arc, is_default=False)
assert integration_is_notifiable(arc) is True

# integration's default channel filter is setup to notify via slack but Slack is not configured for the org
arc = make_alert_receive_channel(organization)
make_channel_filter(arc, is_default=True, notify_in_slack=True)
assert integration_is_notifiable(arc) is False

# integration's default channel filter is setup to notify via slack and Slack is configured for the org
arc = make_alert_receive_channel(organization)
slack_team_identity = make_slack_team_identity()
organization.slack_team_identity = slack_team_identity
organization.save()

make_channel_filter(arc, is_default=True, notify_in_slack=True)
assert integration_is_notifiable(arc) is True

# integration's default channel filter is setup to notify via telegram but Telegram is not configured for the org
arc = make_alert_receive_channel(organization)
make_channel_filter(arc, is_default=True, notify_in_slack=False, notify_in_telegram=True)
assert integration_is_notifiable(arc) is False

# integration's default channel filter is setup to notify via telegram and Telegram is configured for the org
arc = make_alert_receive_channel(organization)
make_channel_filter(arc, is_default=True, notify_in_slack=False, notify_in_telegram=True)
make_telegram_channel(organization)
assert integration_is_notifiable(arc) is True

# integration's default channel filter is contactable via a custom messaging backend
arc = make_alert_receive_channel(organization)
make_channel_filter(
arc,
is_default=True,
notify_in_slack=False,
notification_backends={"MSTEAMS": {"channel": "test", "enabled": True}},
)
assert integration_is_notifiable(arc) is True

# integration's default channel filter has an escalation chain attached to it
arc = make_alert_receive_channel(organization)
escalation_chain = make_escalation_chain(organization)
make_channel_filter(arc, is_default=True, notify_in_slack=False, escalation_chain=escalation_chain)
assert integration_is_notifiable(arc) is True
11 changes: 8 additions & 3 deletions engine/apps/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,19 @@ class Meta:
]


class UserLongSerializer(UserSerializer):
class UserIsCurrentlyOnCallSerializer(UserShortSerializer, EagerLoadingMixin):
context: UserSerializerContext

teams = FastTeamSerializer(read_only=True, many=True)
is_currently_oncall = serializers.SerializerMethodField()

class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + [
SELECT_RELATED = ["organization"]
PREFETCH_RELATED = ["teams"]

class Meta(UserShortSerializer.Meta):
fields = UserShortSerializer.Meta.fields + [
"name",
"timezone",
"teams",
"is_currently_oncall",
]
Expand Down
34 changes: 33 additions & 1 deletion engine/apps/api/tests/test_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ def get_payload_from_team(team, long=False):
return payload


@pytest.mark.django_db
def test_get_team(make_organization_and_user_with_plugin_token, make_team, make_user_auth_headers):
organization, user, token = make_organization_and_user_with_plugin_token()
team = make_team(organization)

client = APIClient()

# team exists
url = reverse("api-internal:team-detail", kwargs={"pk": team.public_primary_key})
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json() == get_payload_from_team(team)

# 404 scenario
url = reverse("api-internal:team-detail", kwargs={"pk": "asdfasdflkjlkajsdf"})
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_404_NOT_FOUND


@pytest.mark.django_db
def test_list_teams(
make_organization,
Expand Down Expand Up @@ -100,7 +119,20 @@ def test_list_teams_only_include_notifiable_teams(
client = APIClient()
url = reverse("api-internal:team-list")

with patch("apps.api.views.team.integration_is_notifiable", side_effect=lambda obj: obj.id == arc1.id):
def mock_get_notifiable_direct_paging_integrations():
class MockRelatedManager:
def filter(self, *args, **kwargs):
return self

def values_list(self, *args, **kwargs):
return [arc1.team.pk]

return MockRelatedManager()

with patch(
"apps.user_management.models.Organization.get_notifiable_direct_paging_integrations",
side_effect=mock_get_notifiable_direct_paging_integrations,
):
response = client.get(
f"{url}?only_include_notifiable_teams=true&include_no_team=false",
format="json",
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/api/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -1935,7 +1935,7 @@ def test_users_is_currently_oncall_attribute_works_properly(
schedule.refresh_ical_final_schedule()

client = APIClient()
url = f"{reverse('api-internal:user-list')}?short=false"
url = f"{reverse('api-internal:user-list')}?is_currently_oncall=all"
response = client.get(url, format="json", **make_user_auth_headers(user1, token))

oncall_statuses = {
Expand Down
22 changes: 12 additions & 10 deletions engine/apps/api/views/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from apps.alerts.paging import integration_is_notifiable
from apps.api.permissions import RBACPermission
from apps.api.serializers.team import TeamLongSerializer, TeamSerializer
from apps.auth_token.auth import PluginAuthentication
Expand All @@ -14,7 +13,13 @@
from common.api_helpers.mixins import PublicPrimaryKeyMixin


class TeamViewSet(PublicPrimaryKeyMixin, mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
class TeamViewSet(
PublicPrimaryKeyMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet,
):
authentication_classes = (
MobileAppAuthTokenAuthentication,
PluginAuthentication,
Expand Down Expand Up @@ -61,14 +66,11 @@ def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())

if self.request.query_params.get("only_include_notifiable_teams", "false") == "true":
# filters down to only teams that have a direct paging integration that is "notifiable"
orgs_direct_paging_integrations = self.request.user.organization.get_direct_paging_integrations()
notifiable_direct_paging_integrations = [
i for i in orgs_direct_paging_integrations if integration_is_notifiable(i)
]
team_ids = [i.team.pk for i in notifiable_direct_paging_integrations if i.team is not None]

queryset = queryset.filter(pk__in=team_ids)
queryset = queryset.filter(
pk__in=self.request.user.organization.get_notifiable_direct_paging_integrations()
.filter(team__isnull=False)
.values_list("team__pk", flat=True)
)

queryset = queryset.order_by("name")

Expand Down
19 changes: 6 additions & 13 deletions engine/apps/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
CurrentUserSerializer,
FilterUserSerializer,
UserHiddenFieldsSerializer,
UserLongSerializer,
UserIsCurrentlyOnCallSerializer,
UserSerializer,
)
from apps.api.throttlers import (
Expand Down Expand Up @@ -238,20 +238,14 @@ def _get_is_currently_oncall_query_param(self) -> str:
return self.request.query_params.get("is_currently_oncall", "").lower()

def _is_currently_oncall_request(self) -> bool:
return self._get_is_currently_oncall_query_param() in ["true", "false"]

def _is_long_request(self) -> bool:
return self.request.query_params.get("short", "true").lower() == "false"

def _is_currently_oncall_or_long_request(self) -> bool:
return self._is_currently_oncall_request() or self._is_long_request()
return self._get_is_currently_oncall_query_param() in ["true", "false", "all"]

def get_serializer_context(self):
context = super().get_serializer_context()
context.update(
{
"schedules_with_oncall_users": self.schedules_with_oncall_users
if self._is_currently_oncall_or_long_request()
if self._is_currently_oncall_request()
else {}
}
)
Expand All @@ -268,8 +262,8 @@ def get_serializer_class(self):

if is_list_request and is_filters_request:
return self.get_filter_serializer_class()
elif is_list_request and self._is_currently_oncall_or_long_request():
return UserLongSerializer
elif is_list_request and self._is_currently_oncall_request():
return UserIsCurrentlyOnCallSerializer

is_users_own_data = kwargs.get("pk") is not None and kwargs.get("pk") == user.public_primary_key
has_admin_permission = user_is_authorized(user, [RBACPermission.Permissions.USER_SETTINGS_ADMIN])
Expand All @@ -296,8 +290,7 @@ def list(self, request, *args, **kwargs) -> Response:
def _get_oncall_user_ids():
return {user.pk for _, users in self.schedules_with_oncall_users.items() for user in users}

is_currently_oncall_query_param = self._get_is_currently_oncall_query_param()
if is_currently_oncall_query_param == "true":
if (is_currently_oncall_query_param := self._get_is_currently_oncall_query_param()) == "true":
# client explicitly wants to filter out users that are on-call
queryset = queryset.filter(pk__in=_get_oncall_user_ids())
elif is_currently_oncall_query_param == "false":
Expand Down
10 changes: 8 additions & 2 deletions engine/apps/heartbeat/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@ def _get_timeout_expression() -> ExpressionWrapper:
# * is enabled,
# * is not already expired,
# * last check in was before the timeout period start
expired_heartbeats = enabled_heartbeats.select_for_update().filter(
last_heartbeat_time__lte=F("period_start"), previous_alerted_state_was_life=True
expired_heartbeats = (
enabled_heartbeats.select_for_update()
.filter(
last_heartbeat_time__lte=F("period_start"),
previous_alerted_state_was_life=True,
alert_receive_channel__organization__deleted_at__isnull=True,
)
.select_related("alert_receive_channel")
)
# Schedule alert creation for each expired heartbeat after transaction commit
for heartbeat in expired_heartbeats:
Expand Down
15 changes: 13 additions & 2 deletions engine/apps/heartbeat/tests/test_integration_heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ def test_check_heartbeats(
assert mock_create_alert_apply_async.call_count == 0

# Prepare heartbeat
team, _ = make_organization_and_user()
organization, _ = make_organization_and_user()
timeout = 60
last_heartbeat_time = timezone.now()
alert_receive_channel = make_alert_receive_channel(team, integration=integration)
alert_receive_channel = make_alert_receive_channel(organization, integration=integration)
integration_heartbeat = make_integration_heartbeat(
alert_receive_channel, timeout, last_heartbeat_time=last_heartbeat_time, previous_alerted_state_was_life=True
)
Expand Down Expand Up @@ -78,3 +78,14 @@ def test_check_heartbeats(
result = check_heartbeats()
assert result == "Found 0 expired and 0 restored heartbeats"
assert mock_create_alert_apply_async.call_count == 0

# Hearbeat expires, but organization was deleted, don't send an alert
integration_heartbeat.refresh_from_db()
integration_heartbeat.last_heartbeat_time = timezone.now() - timezone.timedelta(seconds=timeout * 10)
integration_heartbeat.save()
organization.delete()
with patch.object(create_alert, "apply_async") as mock_create_alert_apply_async:
with django_capture_on_commit_callbacks(execute=True):
result = check_heartbeats()
assert result == "Found 0 expired and 0 restored heartbeats"
assert mock_create_alert_apply_async.call_count == 0
Loading

0 comments on commit 96ea973

Please sign in to comment.