From 4365497b277e7fff52efbdb2d5422d82b9029f0c Mon Sep 17 00:00:00 2001 From: bart-maykin Date: Fri, 10 May 2024 17:40:09 +0200 Subject: [PATCH] :sparkles: [#1617] added feature to see if connection is alive in service admin list page --- tests/test_models.py | 46 +++++++++++++++++++ zgw_consumers/admin.py | 15 +++++- .../migrations/0020_service_timeout.py | 1 - .../0021_service_api_health_check_endpoint.py | 23 ++++++++++ zgw_consumers/models/services.py | 29 ++++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 zgw_consumers/migrations/0021_service_api_health_check_endpoint.py diff --git a/tests/test_models.py b/tests/test_models.py index 9bf671a..bd20f3c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,9 +1,13 @@ from django.core.exceptions import ValidationError import pytest +import requests_mock +from ape_pie import APIClient +from zgw_consumers.client import ServiceConfigAdapter from zgw_consumers.constants import APITypes, AuthTypes from zgw_consumers.models import Service +from zgw_consumers.test.factories import ServiceFactory def test_model_validation_with_oas_fields_enabled_none_provided(settings): @@ -76,3 +80,45 @@ def test_model_validation_with_oas_fields_disabled_both_provided(settings): service.clean() except ValidationError: pytest.fail("OAS fields should be ignored") + + +@pytest.mark.django_db +def test_health_check_indication_service_model_badly_configured(settings): + service = ServiceFactory.create( + api_root="https://example.com/", + api_health_check_endpoint="foo", + auth_type=AuthTypes.zgw, + client_id="my-client-id", + secret="my-secret", + ) + adapter = ServiceConfigAdapter(service) + client = APIClient.configure_from(adapter) + + with requests_mock.Mocker() as m, client: + m.get( + "https://example.com/foo", + status_code=404, + ) + service.refresh_from_db() + assert service.get_health_check_indication == False + + +@pytest.mark.django_db +def test_health_check_indication_service_model_correctly_configured(settings): + service = ServiceFactory.create( + api_root="https://example.com/", + api_health_check_endpoint="foo", + auth_type=AuthTypes.zgw, + client_id="my-client-id", + secret="my-secret", + ) + adapter = ServiceConfigAdapter(service) + client = APIClient.configure_from(adapter) + + with requests_mock.Mocker() as m, client: + m.get( + "https://example.com/foo", + status_code=200, + ) + service.refresh_from_db() + assert service.get_health_check_indication == True diff --git a/zgw_consumers/admin.py b/zgw_consumers/admin.py index f94f0c3..93e3827 100644 --- a/zgw_consumers/admin.py +++ b/zgw_consumers/admin.py @@ -13,9 +13,22 @@ @admin.register(Service) class ServiceAdmin(admin.ModelAdmin): - list_display = ("label", "api_type", "api_root", "nlx", "auth_type") + list_display = ( + "label", + "api_type", + "api_root", + "nlx", + "auth_type", + ) list_filter = ("api_type", "auth_type") search_fields = ("label", "api_root", "nlx", "uuid") + readonly_fields = [ + "get_health_check_indication", + ] + + @admin.display(description="Health Check", boolean=True) + def get_health_check_indication(self, obj): + return obj.get_health_check_indication def get_fields(self, request: HttpRequest, obj: models.Model | None = None): fields = super().get_fields(request, obj=obj) diff --git a/zgw_consumers/migrations/0020_service_timeout.py b/zgw_consumers/migrations/0020_service_timeout.py index 34e23ab..5d24526 100644 --- a/zgw_consumers/migrations/0020_service_timeout.py +++ b/zgw_consumers/migrations/0020_service_timeout.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("zgw_consumers", "0019_alter_service_uuid"), ] diff --git a/zgw_consumers/migrations/0021_service_api_health_check_endpoint.py b/zgw_consumers/migrations/0021_service_api_health_check_endpoint.py new file mode 100644 index 0000000..3b9126d --- /dev/null +++ b/zgw_consumers/migrations/0021_service_api_health_check_endpoint.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2024-05-10 08:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("zgw_consumers", "0020_service_timeout"), + ] + + operations = [ + migrations.AddField( + model_name="service", + name="api_health_check_endpoint", + field=models.CharField( + help_text="An optional API endpoint which will be used to check if the API is configured correctly and is currently up or down. This field is only used for in the admin's 'health check' field.", + max_length=255, + blank=True, + verbose_name="health check endpoint", + ), + ), + ] diff --git a/zgw_consumers/models/services.py b/zgw_consumers/models/services.py index 7b7e2dd..89ef162 100644 --- a/zgw_consumers/models/services.py +++ b/zgw_consumers/models/services.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import socket import uuid from typing import TYPE_CHECKING @@ -11,6 +12,7 @@ from django.utils.translation import gettext_lazy as _ from privates.fields import PrivateMediaFileField +from requests.exceptions import ConnectionError, RequestException from simple_certmanager.models import Certificate from solo.models import SingletonModel from typing_extensions import Self, deprecated @@ -20,6 +22,8 @@ from ..constants import APITypes, AuthTypes, NLXDirectories from .abstract import RestAPIService +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from ..legacy.client import ZGWClient @@ -28,6 +32,15 @@ class Service(RestAPIService): uuid = models.UUIDField(_("UUID"), default=uuid.uuid4) api_type = models.CharField(_("type"), max_length=20, choices=APITypes.choices) api_root = models.CharField(_("api root url"), max_length=255, unique=True) + api_health_check_endpoint = models.CharField( + _("health check endpoint"), + help_text=_( + "An optional API endpoint which will be used to check if the API is configured correctly and " + "is currently up or down. This field is only used for in the admin's 'health check' field." + ), + max_length=255, + blank=True, + ) # credentials for the API client_id = models.CharField(max_length=255, blank=True) @@ -113,6 +126,22 @@ def clean(self): {"header_key": _("If header_value is set, header_key must also be set")} ) + @property + def get_health_check_indication(self) -> bool: + from zgw_consumers.client import build_client + + try: + client = build_client(self) + if ( + client.get(self.api_health_check_endpoint or self.api_root).status_code + == 200 + ): + return True + except (ConnectionError, RequestException) as e: + logger.exception(self, exc_info=e) + + return False + @deprecated( "The `build_client` method is deprecated and will be removed in the next major release. " "Instead, use the new `ape_pie.APIClient` or `zgw_consumers.nlx.NLXClient`.",