diff --git a/backend/kernelCI_app/helpers/discordWebhook.py b/backend/kernelCI_app/helpers/discordWebhook.py new file mode 100644 index 00000000..abde8270 --- /dev/null +++ b/backend/kernelCI_app/helpers/discordWebhook.py @@ -0,0 +1,65 @@ +from typing import Any, Optional, TypedDict + +import requests +import os + +from kernelCI_app.helpers.logger import log_message + +# For more information on discord webhook structure, visit +# https://discord.com/developers/docs/resources/webhook#execute-webhook + +AVATAR_URL = "https://avatars.githubusercontent.com/u/11725450?s=200&v=4" +WEBHOOK_NAME = "KernelCI Dashboard Notifications" + + +class DiscordImage(TypedDict): + url: str + width: Optional[int] + height: Optional[int] + + +class DiscordEmbed(TypedDict): + title: str + description: Optional[str] + url: Optional[str] + image: Optional[DiscordImage] + + +def send_discord_notification( + *, + content: Optional[str] = None, + embeds: Optional[list[DiscordEmbed]] = None, + avatar_url: Optional[str] = AVATAR_URL, + webhook_name: Optional[str] = WEBHOOK_NAME, +) -> None: + url = os.getenv("DISCORD_WEBHOOK_URL") + if not url: + log_message("DISCORD_WEBHOOK_URL environment variable is not set.") + return + + if not content and not embeds: + log_message( + "Either content or embeds must be set in order to send notifications." + ) + return + + if embeds is not None and len(embeds) > 10: + log_message("The embed list can contain at most 10 elements.") + return + + data: dict[str, Any] = { + "avatar_url": avatar_url, + "username": webhook_name, + } + if content is not None: + data["content"] = content + if embeds is not None: + data["embeds"] = embeds + + try: + result = requests.post(url=url, json=data) + result.raise_for_status() + except requests.HTTPError as e: + log_message(e) + + return diff --git a/backend/kernelCI_app/helpers/logger.py b/backend/kernelCI_app/helpers/logger.py index c2e12692..6409c261 100644 --- a/backend/kernelCI_app/helpers/logger.py +++ b/backend/kernelCI_app/helpers/logger.py @@ -1,4 +1,23 @@ +from django.http import HttpRequest +from datetime import datetime + + # For logging that we care about, we create a function so we can easily use # a more sophisticated logging library later. def log_message(message: str) -> None: print(message) + + +def create_endpoint_notification(*, message: str, request: HttpRequest) -> str: + return ( + message + + "\n\nEndpoint:\n" + + request.build_absolute_uri() + + ( + ("\nBody:\n```json\n" + request.body.decode("utf-8") + "```") + if request.body + else "" + ) + + "\nAccessed in: " + + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ) diff --git a/backend/kernelCI_app/views/buildTestsView.py b/backend/kernelCI_app/views/buildTestsView.py index 339446b7..f4e8981b 100644 --- a/backend/kernelCI_app/views/buildTestsView.py +++ b/backend/kernelCI_app/views/buildTestsView.py @@ -1,4 +1,6 @@ from http import HTTPStatus +from kernelCI_app.helpers.discordWebhook import send_discord_notification +from kernelCI_app.helpers.logger import create_endpoint_notification from kernelCI_app.typeModels.buildDetails import BuildTestsResponse from kernelCI_app.models import Tests from drf_spectacular.utils import extend_schema @@ -18,6 +20,7 @@ def get(self, request, build_id: str) -> Response: "start_time", "environment_compatible", "environment_misc", + "build__valid", ) if not result: @@ -26,6 +29,13 @@ def get(self, request, build_id: str) -> Response: status=HTTPStatus.OK, ) + if result[0]["build__valid"] is False: + notification = create_endpoint_notification( + message="Found tests for a failed build.", + request=request, + ) + send_discord_notification(content=notification) + try: valid_response = BuildTestsResponse(result) except ValidationError as e: diff --git a/backend/kernelCI_app/views/treeDetailsView.py b/backend/kernelCI_app/views/treeDetailsView.py index 3ffe966e..ead173c5 100644 --- a/backend/kernelCI_app/views/treeDetailsView.py +++ b/backend/kernelCI_app/views/treeDetailsView.py @@ -3,12 +3,14 @@ from rest_framework.views import APIView from rest_framework.response import Response from kernelCI_app.helpers.commonDetails import PossibleTabs +from kernelCI_app.helpers.discordWebhook import send_discord_notification from kernelCI_app.helpers.filters import ( FilterParams, ) from drf_spectacular.utils import extend_schema from pydantic import ValidationError from kernelCI_app.helpers.errorHandling import create_api_error_response +from kernelCI_app.helpers.logger import create_endpoint_notification from kernelCI_app.helpers.treeDetails import ( call_based_on_compatible_and_misc_platform, decide_if_is_boot_filtered_out, @@ -194,6 +196,11 @@ def get(self, request, commit_hash: str | None): self.filters = FilterParams(request) if len(rows) == 0: + notification = create_endpoint_notification( + message="Found tree with no builds, boots or tests.", + request=request, + ) + send_discord_notification(content=notification) return create_api_error_response( error_message="Tree not found", status_code=HTTPStatus.OK )