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.3.102 #3843

Merged
merged 6 commits into from
Feb 6, 2024
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/linting-and-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ jobs:

unit-test-backend-mysql-rabbitmq:
name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
rbac_enabled: ["True", "False"]
Expand Down Expand Up @@ -189,7 +189,7 @@ jobs:

unit-test-backend-postgresql-rabbitmq:
name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
rbac_enabled: ["True", "False"]
Expand Down Expand Up @@ -238,7 +238,7 @@ jobs:

unit-test-backend-sqlite-redis:
name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
runs-on: ubuntu-latest-8-cores
strategy:
matrix:
rbac_enabled: ["True", "False"]
Expand Down
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## v1.3.102 (2024-02-06)

## v1.3.101 (2024-05-01)
### Fixed

Maintenance release

## v1.3.101 (2024-02-05)

### Added

Expand Down
14 changes: 14 additions & 0 deletions docs/sources/oncall-api-reference/alertgroups.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ The above command returns JSON structured in the following way:
"created_at": "2020-05-19T12:37:01.430444Z",
"resolved_at": "2020-05-19T13:37:01.429805Z",
"acknowledged_at": null,
"acknowledged_by": null,
"resolved_by": "UCGEIXI1MR1NZ",
"title": "Memory above 90% threshold",
"permalinks": {
"slack": "https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008",
Expand All @@ -54,6 +56,18 @@ These available filter parameters should be provided as `GET` arguments:

`GET {{API_URL}}/api/v1/alert_groups/`

# Alert group details

```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1" \
--request GET \
--header "Authorization: meowmeowmeow"
```

**HTTP request**

`GET {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>`

# Acknowledge an alert group

```shell
Expand Down
13 changes: 8 additions & 5 deletions engine/apps/integrations/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from apps.alerts.models import AlertReceiveChannel
from apps.integrations.mixins import AlertChannelDefiningMixin

# https://github.com/pytest-dev/pytest-xdist/issues/432#issuecomment-528510433
INTEGRATION_TYPES = sorted(AlertReceiveChannel.INTEGRATION_TYPES)


class DatabaseBlocker(_DatabaseBlocker):
"""Customize pytest_django db blocker to raise OperationalError exception."""
Expand Down Expand Up @@ -78,7 +81,7 @@ def test_integration_form_data_too_big(settings, make_organization_and_user, mak
"integration_type",
[
arc_type
for arc_type in AlertReceiveChannel.INTEGRATION_TYPES
for arc_type in INTEGRATION_TYPES
if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"]
],
)
Expand Down Expand Up @@ -230,7 +233,7 @@ def test_integration_old_grafana_endpoint(
"integration_type",
[
arc_type
for arc_type in AlertReceiveChannel.INTEGRATION_TYPES
for arc_type in INTEGRATION_TYPES
if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"]
],
)
Expand Down Expand Up @@ -264,7 +267,7 @@ def test_integration_universal_endpoint_not_allow_files(
"integration_type",
[
arc_type
for arc_type in AlertReceiveChannel.INTEGRATION_TYPES
for arc_type in INTEGRATION_TYPES
if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"]
],
)
Expand Down Expand Up @@ -367,7 +370,7 @@ def test_integration_grafana_endpoint_without_db_has_alerts(
"integration_type",
[
arc_type
for arc_type in AlertReceiveChannel.INTEGRATION_TYPES
for arc_type in INTEGRATION_TYPES
if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"]
],
)
Expand Down Expand Up @@ -467,7 +470,7 @@ def test_integration_grafana_endpoint_without_cache_has_alerts(
"integration_type",
[
arc_type
for arc_type in AlertReceiveChannel.INTEGRATION_TYPES
for arc_type in INTEGRATION_TYPES
if arc_type not in ["amazon_sns", "grafana", "alertmanager", "grafana_alerting", "maintenance"]
],
)
Expand Down
5 changes: 5 additions & 0 deletions engine/apps/public_api/serializers/incidents.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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 @@ -14,6 +15,8 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer):
alerts_count = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
state = serializers.SerializerMethodField()
acknowledged_by = UserIdField(read_only=True, source="acknowledged_by_user")
resolved_by = UserIdField(read_only=True, source="resolved_by_user")

SELECT_RELATED = ["channel", "channel_filter", "slack_message", "channel__organization"]
PREFETCH_RELATED = [
Expand All @@ -35,7 +38,9 @@ class Meta:
"state",
"created_at",
"resolved_at",
"resolved_by",
"acknowledged_at",
"acknowledged_by",
"title",
"permalinks",
]
Expand Down
22 changes: 22 additions & 0 deletions engine/apps/public_api/tests/test_alert_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def construct_expected_response_from_alert_groups(alert_groups):
acknowledged_at = alert_group.acknowledged_at.isoformat()
acknowledged_at = acknowledged_at[:-6] + "Z"

def user_pk_or_none(alert_group, user_field):
u = getattr(alert_group, user_field)
if u is not None:
return u.public_primary_key

results.append(
{
"id": alert_group.public_primary_key,
Expand All @@ -39,6 +44,8 @@ def construct_expected_response_from_alert_groups(alert_groups):
"created_at": created_at,
"resolved_at": resolved_at,
"acknowledged_at": acknowledged_at,
"acknowledged_by": user_pk_or_none(alert_group, "acknowledged_by_user"),
"resolved_by": user_pk_or_none(alert_group, "resolved_by_user"),
"title": None,
"permalinks": {
"slack": None,
Expand Down Expand Up @@ -95,6 +102,21 @@ def alert_group_public_api_setup(
return token, alert_groups, integrations, routes


@pytest.mark.django_db
def test_get_alert_group(alert_group_public_api_setup):
token, _, _, _ = alert_group_public_api_setup
alert_groups = AlertGroup.objects.all().order_by("-started_at")
client = APIClient()
list_response = construct_expected_response_from_alert_groups(alert_groups)
expected_response = list_response["results"][0]

url = reverse("api-public:alert_groups-detail", kwargs={"pk": expected_response["id"]})
response = client.get(url, format="json", HTTP_AUTHORIZATION=token)

assert response.status_code == status.HTTP_200_OK
assert response.json() == expected_response


@pytest.mark.django_db
def test_get_alert_groups(alert_group_public_api_setup):
token, _, _, _ = alert_group_public_api_setup
Expand Down
4 changes: 3 additions & 1 deletion engine/apps/public_api/views/incidents.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ class IncidentByTeamFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
id = filters.CharFilter(field_name="public_primary_key")


class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyModelMixin, GenericViewSet):
class IncidentView(
RateLimitHeadersMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, GenericViewSet
):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)

Expand Down
6 changes: 5 additions & 1 deletion engine/apps/telegram/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from apps.alerts.models import AlertGroup
from apps.base.utils import live_settings
from apps.telegram.exceptions import AlertGroupTelegramMessageDoesNotExist
from apps.telegram.models import TelegramMessage
from apps.telegram.renderers.keyboard import TelegramKeyboardRenderer
from apps.telegram.renderers.message import TelegramMessageRenderer
Expand Down Expand Up @@ -157,7 +158,10 @@ def _get_message_and_keyboard(
).first()

if alert_group_message is None:
raise Exception("No alert group message found, probably it is not saved to database yet")
raise AlertGroupTelegramMessageDoesNotExist(
f"No alert group message found, probably it is not saved to database yet, "
f"alert group: {alert_group.id}"
)

include_title = message_type == TelegramMessage.LINK_TO_CHANNEL_MESSAGE
link = alert_group_message.link
Expand Down
2 changes: 2 additions & 0 deletions engine/apps/telegram/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class AlertGroupTelegramMessageDoesNotExist(Exception):
pass
13 changes: 9 additions & 4 deletions engine/apps/telegram/models/connectors/personal.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ def notify(self, alert_group: AlertGroup, notification_policy: UserNotificationP
telegram_channel = TelegramToOrganizationConnector.get_channel_for_alert_group(alert_group)

if telegram_channel is not None:
send_link_to_channel_message_or_fallback_to_full_alert_group.delay(
alert_group_pk=alert_group.pk,
notification_policy_pk=notification_policy.pk,
user_connector_pk=self.pk,
# Call this task with a countdown to avoid unnecessary retry when alert group telegram message hasn't been
# created yet
send_link_to_channel_message_or_fallback_to_full_alert_group.apply_async(
kwargs={
"alert_group_pk": alert_group.pk,
"notification_policy_pk": notification_policy.pk,
"user_connector_pk": self.pk,
},
countdown=3,
)
else:
self.send_full_alert_group(alert_group=alert_group, notification_policy=notification_policy)
Expand Down
3 changes: 1 addition & 2 deletions engine/common/cloud_auth_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ def request_signed_token(
headers=headers,
json={
"claims": claims,
"extra": {
"accessPolicy": {
"scopes": scopes,
"org_id": org_id,
},
},
)
Expand Down
62 changes: 25 additions & 37 deletions engine/common/cloud_auth_api/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from unittest.mock import patch

import httpretty
import pytest
from rest_framework import status

Expand All @@ -16,10 +16,10 @@ def configure_cloud_auth_api_client(settings):
settings.GRAFANA_CLOUD_AUTH_API_SYSTEM_TOKEN = GRAFANA_CLOUD_AUTH_API_SYSTEM_TOKEN


@patch("common.cloud_auth_api.client.requests")
@pytest.mark.django_db
@pytest.mark.parametrize("response_status_code", [status.HTTP_200_OK, status.HTTP_401_UNAUTHORIZED])
def test_request_signed_token(mock_requests, make_organization, response_status_code):
@httpretty.activate(verbose=True, allow_net_connect=False)
def test_request_signed_token(make_organization, response_status_code):
mock_auth_token = ",mnasdlkjlakjoqwejroiqwejr"
mock_response_text = "error message"

Expand All @@ -31,25 +31,12 @@ def test_request_signed_token(mock_requests, make_organization, response_status_
scopes = ["incident:write", "foo:bar"]
claims = {"vegetable": "carrot", "fruit": "apple"}

class MockResponse:
text = mock_response_text

def __init__(self, status_code):
self.status_code = status_code

def json(self):
return {
"data": {
"token": mock_auth_token,
},
}

mock_requests.post.return_value = MockResponse(response_status_code)

def _make_request():
return CloudAuthApiClient().request_signed_token(organization, scopes, claims)

url = f"{GRAFANA_CLOUD_AUTH_API_URL}/v1/sign"
mock_response = httpretty.Response(json.dumps({"data": {"token": mock_auth_token}}), status=response_status_code)
httpretty.register_uri(httpretty.POST, url, responses=[mock_response])

if response_status_code != status.HTTP_200_OK:
with pytest.raises(CloudAuthApiException) as excinfo:
Expand All @@ -62,25 +49,26 @@ def _make_request():
else:
assert _make_request() == mock_auth_token

mock_requests.post.assert_called_once_with(
url,
headers={
"Authorization": f"Bearer {GRAFANA_CLOUD_AUTH_API_SYSTEM_TOKEN}",
"X-Org-ID": str(org_id),
"X-Realms": json.dumps(
[
{
"type": "stack",
"identifier": str(stack_id),
},
]
),
last_request = httpretty.last_request()
assert last_request.method == "POST"
assert last_request.url == url

# assert we're sending the right body
assert json.loads(last_request.body) == {
"claims": claims,
"accessPolicy": {
"scopes": scopes,
},
json={
"claims": claims,
"extra": {
"scopes": scopes,
"org_id": str(org_id),
}

# assert we're sending the right headers
assert last_request.headers["Authorization"] == f"Bearer {GRAFANA_CLOUD_AUTH_API_SYSTEM_TOKEN}"
assert last_request.headers["X-Org-ID"] == str(org_id)
assert last_request.headers["X-Realms"] == json.dumps(
[
{
"type": "stack",
"identifier": str(stack_id),
},
},
]
)
15 changes: 15 additions & 0 deletions engine/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@
IS_RBAC_ENABLED = os.getenv("ONCALL_TESTING_RBAC_ENABLED", "True") == "True"


@pytest.fixture(autouse=True)
def isolated_cache(settings):
"""
https://github.com/pytest-dev/pytest-django/issues/527#issuecomment-1115887487
"""
cache_version = uuid.uuid4().hex

for name in settings.CACHES.keys():
settings.CACHES[name]["VERSION"] = cache_version

from django.test.signals import clear_cache_handlers

clear_cache_handlers(setting="CACHES")


@pytest.fixture(autouse=True)
def mock_slack_api_call(monkeypatch):
def mock_api_call(*args, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions engine/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ types-PyMySQL==1.0.19.7
types-python-dateutil==2.8.19.13
types-requests==2.31.0.1
httpretty==1.1.4
pytest-xdist[psutil]==3.5.0
Loading
Loading