Skip to content

Commit

Permalink
Rewrite LabelsAPIClient (#3422)
Browse files Browse the repository at this point in the history
Rewrite LabelAPIClient to be able to return error messages from Label
Repo API.
Main features:
1. Raises LabelRepoAPIException when response code is 400 or 500 level.
2. Always return response as a second argument to further inspect it, if
necessary.
  • Loading branch information
Konstantinov-Innokentii authored Nov 29, 2023
1 parent 96b88ed commit 8c82dac
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 43 deletions.
19 changes: 12 additions & 7 deletions engine/apps/api/tests/test_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@
from apps.api.permissions import LegacyAccessControlRole


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


@patch(
"apps.labels.client.LabelsAPIClient.get_keys",
return_value=([{"name": "team", "id": "keyid123"}], {"status_code": status.HTTP_200_OK}),
return_value=([{"name": "team", "id": "keyid123"}], MockResponse(status_code=200)),
)
@pytest.mark.django_db
def test_labels_get_keys(
Expand All @@ -35,7 +40,7 @@ def test_labels_get_keys(
"apps.labels.client.LabelsAPIClient.get_values",
return_value=(
{"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]},
{"status_code": status.HTTP_200_OK},
MockResponse(status_code=200),
),
)
@pytest.mark.django_db
Expand All @@ -59,7 +64,7 @@ def test_get_update_key_get(
"apps.labels.client.LabelsAPIClient.rename_key",
return_value=(
{"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]},
{"status_code": status.HTTP_200_OK},
MockResponse(status_code=200),
),
)
@pytest.mark.django_db
Expand All @@ -84,7 +89,7 @@ def test_get_update_key_put(
"apps.labels.client.LabelsAPIClient.add_value",
return_value=(
{"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]},
{"status_code": status.HTTP_200_OK},
MockResponse(status_code=200),
),
)
@pytest.mark.django_db
Expand All @@ -109,7 +114,7 @@ def test_add_value(
"apps.labels.client.LabelsAPIClient.rename_value",
return_value=(
{"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]},
{"status_code": status.HTTP_200_OK},
MockResponse(status_code=200),
),
)
@pytest.mark.django_db
Expand All @@ -134,7 +139,7 @@ def test_rename_value(
"apps.labels.client.LabelsAPIClient.get_value",
return_value=(
{"id": "valueid123", "name": "yolo"},
{"status_code": status.HTTP_200_OK},
MockResponse(status_code=200),
),
)
@pytest.mark.django_db
Expand All @@ -158,7 +163,7 @@ def test_get_value(
"apps.labels.client.LabelsAPIClient.create_label",
return_value=(
{"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]},
{"status_code": status.HTTP_201_CREATED},
MockResponse(status_code=201),
),
)
@pytest.mark.django_db
Expand Down
47 changes: 28 additions & 19 deletions engine/apps/api/views/labels.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

import requests
from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
Expand All @@ -14,7 +15,7 @@
LabelValueSerializer,
)
from apps.auth_token.auth import PluginAuthentication
from apps.labels.client import LabelsAPIClient
from apps.labels.client import LabelsAPIClient, LabelsRepoAPIException
from apps.labels.tasks import update_instances_labels_cache, update_labels_cache
from apps.labels.utils import is_labels_feature_enabled
from common.api_helpers.exceptions import BadRequest
Expand Down Expand Up @@ -50,26 +51,24 @@ class LabelsViewSet(LabelsFeatureFlagViewSet):
def get_keys(self, request):
"""List of labels keys"""
organization = self.request.auth.organization
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).get_keys()
return Response(result, status=response_info["status_code"])
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).get_keys()
return Response(result, status=response.status_code)

@extend_schema(responses=LabelKeyValuesSerializer)
def get_key(self, request, key_id):
"""Key with the list of values"""
organization = self.request.auth.organization
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).get_values(key_id)
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).get_values(key_id)
self._update_labels_cache(result)
return Response(result, status=response_info["status_code"])
return Response(result, status=response.status_code)

@extend_schema(responses=LabelValueSerializer)
def get_value(self, request, key_id, value_id):
"""Value name"""
organization = self.request.auth.organization
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).get_value(
key_id, value_id
)
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).get_value(key_id, value_id)
self._update_labels_cache(result)
return Response(result, status=response_info["status_code"])
return Response(result, status=response.status_code)

@extend_schema(request=LabelReprSerializer, responses=LabelKeyValuesSerializer)
def rename_key(self, request, key_id):
Expand All @@ -78,11 +77,11 @@ def rename_key(self, request, key_id):
label_data = self.request.data
if not label_data:
raise BadRequest(detail="name is required")
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).rename_key(
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).rename_key(
key_id, label_data
)
self._update_labels_cache(result)
return Response(result, status=response_info["status_code"])
return Response(result, status=response.status_code)

@extend_schema(
request=inline_serializer(
Expand All @@ -98,10 +97,8 @@ def create_label(self, request):
label_data = self.request.data
if not label_data:
raise BadRequest(detail="key data (name, values) is required")
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).create_label(
label_data
)
return Response(result, status=response_info["status_code"])
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).create_label(label_data)
return Response(result, status=response.status_code)

@extend_schema(request=LabelReprSerializer, responses=LabelKeyValuesSerializer)
def add_value(self, request, key_id):
Expand All @@ -110,10 +107,11 @@ def add_value(self, request, key_id):
label_data = self.request.data
if not label_data:
raise BadRequest(detail="name is required")
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).add_value(
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).add_value(
key_id, label_data
)
return Response(result, status=response_info["status_code"])
status = response.status_code
return Response(result, status=status)

@extend_schema(request=LabelReprSerializer, responses=LabelKeyValuesSerializer)
def rename_value(self, request, key_id, value_id):
Expand All @@ -122,11 +120,12 @@ def rename_value(self, request, key_id, value_id):
label_data = self.request.data
if not label_data:
raise BadRequest(detail="name is required")
result, response_info = LabelsAPIClient(organization.grafana_url, organization.api_token).rename_value(
result, response = LabelsAPIClient(organization.grafana_url, organization.api_token).rename_value(
key_id, value_id, label_data
)
status = response.status_code
self._update_labels_cache(result)
return Response(result, status=response_info["status_code"])
return Response(result, status=status)

def _update_labels_cache(self, label_data):
if not label_data:
Expand All @@ -135,6 +134,16 @@ def _update_labels_cache(self, label_data):
if serializer.is_valid():
update_labels_cache.apply_async((label_data,))

def handle_exception(self, exc):
if isinstance(exc, LabelsRepoAPIException):
logging.error(f'msg="LabelsViewSet: LabelRepo error: {exc}"')
return Response({"message": exc.msg}, status=exc.status)
elif isinstance(exc, requests.RequestException):
logging.error(f'msg="LabelsViewSet: error while requesting LabelRepo: {exc}"')
return Response({"message": "Something went wrong"}, status=500)
else:
return super().handle_exception(exc)


class AlertGroupLabelsViewSet(LabelsFeatureFlagViewSet):
"""
Expand Down
109 changes: 92 additions & 17 deletions engine/apps/labels/client.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,117 @@
import typing
from urllib.parse import urljoin

from apps.grafana_plugin.helpers.client import APIClient
import requests
from django.conf import settings

if typing.TYPE_CHECKING:
from apps.labels.utils import LabelKeyData, LabelsKeysData, LabelUpdateParam


class LabelsAPIClient(APIClient):
class LabelsRepoAPIException(Exception):
"""A generic 400 or 500 level exception from the Label Repo API"""

def __init__(self, status, url, msg="", method="GET"):
self.url = url
self.status = status
self.method = method

# Error-message returned by label repo.
# If status is 400 level it will contain user-visible error message.
self.msg = msg

def __str__(self):
return f"LabelsRepoAPIException: status={self.status} url={self.url} method={self.method}"


TIMEOUT = 5


class LabelsAPIClient:
LABELS_API_URL = "/api/plugins/grafana-labels-app/resources/v1/labels/"

def __init__(self, api_url: str, api_token: str) -> None:
super().__init__(api_url, api_token)
self.api_token = api_token
self.api_url = urljoin(api_url, self.LABELS_API_URL)

def create_label(self, label_data: "LabelUpdateParam") -> typing.Tuple[typing.Optional["LabelKeyData"], dict]:
return self.api_post("", label_data)
def create_label(
self, label_data: "LabelUpdateParam"
) -> typing.Tuple[typing.Optional["LabelKeyData"], requests.models.Response]:
url = self.api_url
response = requests.post(url, json=label_data, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def get_keys(self) -> typing.Tuple[typing.Optional["LabelsKeysData"], requests.models.Response]:
url = urljoin(self.api_url, "keys")

response = requests.get(url, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def get_keys(self) -> typing.Tuple[typing.Optional["LabelsKeysData"], dict]:
return self.api_get("keys")
def get_values(self, key_id: str) -> typing.Tuple[typing.Optional["LabelKeyData"], requests.models.Response]:
url = urljoin(self.api_url, f"id/{key_id}")

def get_values(self, key_id: str) -> typing.Tuple[typing.Optional["LabelKeyData"], dict]:
return self.api_get(f"id/{key_id}")
response = requests.get(url, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def get_value(self, key_id: str, value_id: str) -> typing.Tuple[typing.Optional["LabelKeyData"], dict]:
return self.api_get(f"id/{key_id}/values/{value_id}")
def get_value(
self, key_id: str, value_id: str
) -> typing.Tuple[typing.Optional["LabelKeyData"], requests.models.Response]:
url = urljoin(self.api_url, f"id/{key_id}/values/{value_id}")

response = requests.get(url, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def add_value(
self, key_id: str, label_data: "LabelUpdateParam"
) -> typing.Tuple[typing.Optional["LabelKeyData"], dict]:
return self.api_post(f"id/{key_id}/values", label_data)
) -> typing.Tuple[typing.Optional["LabelKeyData"], requests.models.Response]:
url = urljoin(self.api_url, f"id/{key_id}/values")

response = requests.post(url, json=label_data, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def rename_key(
self, key_id: str, label_data: "LabelUpdateParam"
) -> typing.Tuple[typing.Optional["LabelKeyData"], dict]:
return self.api_put(f"id/{key_id}", label_data)
) -> typing.Tuple[typing.Optional["LabelKeyData"], requests.models.Response]:
url = urljoin(self.api_url, f"id/{key_id}")

response = requests.put(url, json=label_data, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def rename_value(
self, key_id: str, value_id: str, label_data: "LabelUpdateParam"
) -> typing.Tuple[typing.Optional["LabelKeyData"], dict]:
return self.api_put(f"id/{key_id}/values/{value_id}", label_data)
) -> typing.Tuple[typing.Optional["LabelKeyData"], requests.models.Response]:
url = urljoin(self.api_url, f"id/{key_id}/values/{value_id}")

response = requests.put(url, json=label_data, timeout=TIMEOUT, headers=self._request_headers)
self._check_response(response)
return response.json(), response

def _check_response(self, response: requests.models.Response):
"""
Wraps an exceptional response to LabelsRepoAPIException
"""
message = None

if 400 <= response.status_code < 500:
error_data = response.json()
message = error_data.get("message", None)
elif 500 <= response.status_code < 600:
message = response.reason

if message:
raise LabelsRepoAPIException(
status=response.status_code,
url=response.request.url,
msg=message,
method=response.request.method,
)

@property
def _request_headers(self):
return {"User-Agent": settings.GRAFANA_COM_USER_AGENT, "Authorization": f"Bearer {self.api_token}"}

0 comments on commit 8c82dac

Please sign in to comment.