diff --git a/backend/README.md b/backend/README.md index cd463fd6..a0b465e4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -91,6 +91,17 @@ For debugging we have two env variables ## Open API generate You can update the OpenAPI schema by running the `generate-schema.sh` script + +## Discord Webhook Integration + +The webdashboard backend can send notifications to discord via a webhook. In order to enable that, export an environment variable with the URL to the discord webhook called `DISCORD_WEBHOOK_URL`, which should be in the structure of: + +`export DISCORD_WEBHOOK_URL https://discord.com/api/webhooks//` + +For an introduction on discord webhooks, visit https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks. + +For more detailed developer resources, visit https://discord.com/developers/docs/resources/webhook. + ## IDE Specific: You are free to use whichever tool you would like, but here are tips for specific IDEs 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..9ee26cec 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].get("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/docker-compose.yml b/docker-compose.yml index c085fb19..03a95b55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,7 @@ services: - DB_DEFAULT_USER=${DB_DEFAULT_USER:-kernelci} - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY} - DEBUG=False + - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} dashboard: build: ./dashboard