From a14d707563b7e7469e6c9df4d6a520dc1f5e4d84 Mon Sep 17 00:00:00 2001 From: rgaudin Date: Tue, 15 Oct 2024 09:25:21 +0000 Subject: [PATCH 1/2] Using Annotated directly Storing Annotated inside a variable makes pyright unhappy and is less readable --- backend/src/mirrors_qa_backend/routes/auth.py | 7 ++++--- .../src/mirrors_qa_backend/routes/dependencies.py | 12 ++++++------ backend/src/mirrors_qa_backend/routes/tests.py | 7 ++++--- backend/src/mirrors_qa_backend/routes/worker.py | 14 ++++++++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/backend/src/mirrors_qa_backend/routes/auth.py b/backend/src/mirrors_qa_backend/routes/auth.py index 26ae002..8934838 100644 --- a/backend/src/mirrors_qa_backend/routes/auth.py +++ b/backend/src/mirrors_qa_backend/routes/auth.py @@ -3,14 +3,15 @@ import datetime from typing import Annotated -from fastapi import APIRouter, Header +from fastapi import APIRouter, Depends, Header +from sqlalchemy.orm import Session from mirrors_qa_backend import logger from mirrors_qa_backend.cryptography import verify_signed_message +from mirrors_qa_backend.db import gen_dbsession from mirrors_qa_backend.db.exceptions import RecordDoesNotExistError from mirrors_qa_backend.db.worker import get_worker from mirrors_qa_backend.exceptions import PEMPublicKeyLoadError -from mirrors_qa_backend.routes.dependencies import DbSession from mirrors_qa_backend.routes.http_errors import ( BadRequestError, ForbiddenError, @@ -25,7 +26,7 @@ @router.post("/authenticate") def authenticate_worker( - session: DbSession, + session: Annotated[Session, Depends(gen_dbsession)], x_sshauth_message: Annotated[ str, Header(description="message (format): worker_id:timestamp (UTC ISO)"), diff --git a/backend/src/mirrors_qa_backend/routes/dependencies.py b/backend/src/mirrors_qa_backend/routes/dependencies.py index eecf7a6..d14c96c 100644 --- a/backend/src/mirrors_qa_backend/routes/dependencies.py +++ b/backend/src/mirrors_qa_backend/routes/dependencies.py @@ -16,15 +16,12 @@ from mirrors_qa_backend.routes.http_errors import NotFoundError, UnauthorizedError from mirrors_qa_backend.settings.api import APISettings -DbSession = Annotated[Session, Depends(gen_dbsession)] - security = HTTPBearer(description="Access Token") -AuthorizationCredentials = Annotated[HTTPAuthorizationCredentials, Depends(security)] def get_current_worker( - session: DbSession, - authorization: AuthorizationCredentials, + session: Annotated[Session, Depends(gen_dbsession)], + authorization: Annotated[HTTPAuthorizationCredentials, Depends(security)], ) -> models.Worker: token = authorization.credentials try: @@ -51,7 +48,10 @@ def get_current_worker( CurrentWorker = Annotated[models.Worker, Depends(get_current_worker)] -def get_test(session: DbSession, test_id: Annotated[UUID4, Path()]) -> models.Test: +def get_test( + session: Annotated[Session, Depends(gen_dbsession)], + test_id: Annotated[UUID4, Path()], +) -> models.Test: """Fetches the test specified in the request.""" try: test = db_get_test(session, test_id) diff --git a/backend/src/mirrors_qa_backend/routes/tests.py b/backend/src/mirrors_qa_backend/routes/tests.py index fa47a64..a77bf2c 100644 --- a/backend/src/mirrors_qa_backend/routes/tests.py +++ b/backend/src/mirrors_qa_backend/routes/tests.py @@ -2,15 +2,16 @@ from fastapi import APIRouter, Depends, Query from fastapi import status as status_codes +from sqlalchemy.orm import Session from mirrors_qa_backend import schemas +from mirrors_qa_backend.db import gen_dbsession from mirrors_qa_backend.db.tests import list_tests as db_list_tests from mirrors_qa_backend.db.tests import update_test as update_test_model from mirrors_qa_backend.db.worker import update_worker_last_seen from mirrors_qa_backend.enums import SortDirectionEnum, StatusEnum, TestSortColumnEnum from mirrors_qa_backend.routes.dependencies import ( CurrentWorker, - DbSession, RetrievedTest, verify_worker_owns_test, ) @@ -29,7 +30,7 @@ }, ) def list_tests( - session: DbSession, + session: Annotated[Session, Depends(gen_dbsession)], worker_id: Annotated[str | None, Query()] = None, country_code: Annotated[str | None, Query(min_length=2, max_length=2)] = None, status: Annotated[list[StatusEnum] | None, Query()] = None, @@ -81,7 +82,7 @@ def get_test(test: RetrievedTest) -> Test: dependencies=[Depends(verify_worker_owns_test)], ) def update_test( - session: DbSession, + session: Annotated[Session, Depends(gen_dbsession)], current_worker: CurrentWorker, test: RetrievedTest, update: schemas.UpdateTestModel, diff --git a/backend/src/mirrors_qa_backend/routes/worker.py b/backend/src/mirrors_qa_backend/routes/worker.py index b46655b..423825f 100644 --- a/backend/src/mirrors_qa_backend/routes/worker.py +++ b/backend/src/mirrors_qa_backend/routes/worker.py @@ -1,12 +1,16 @@ +from typing import Annotated + import pycountry -from fastapi import APIRouter +from fastapi import APIRouter, Depends from fastapi import status as status_codes +from sqlalchemy.orm import Session +from mirrors_qa_backend.db import gen_dbsession from mirrors_qa_backend.db.country import update_countries as update_db_countries from mirrors_qa_backend.db.exceptions import RecordDoesNotExistError from mirrors_qa_backend.db.worker import get_worker as get_db_worker from mirrors_qa_backend.db.worker import update_worker as update_db_worker -from mirrors_qa_backend.routes.dependencies import CurrentWorker, DbSession +from mirrors_qa_backend.routes.dependencies import CurrentWorker from mirrors_qa_backend.routes.http_errors import ( BadRequestError, NotFoundError, @@ -27,7 +31,9 @@ } }, ) -def list_countries(session: DbSession, worker_id: str) -> WorkerCountries: +def list_countries( + session: Annotated[Session, Depends(gen_dbsession)], worker_id: str +) -> WorkerCountries: try: worker = get_db_worker(session, worker_id) except RecordDoesNotExistError as exc: @@ -48,7 +54,7 @@ def list_countries(session: DbSession, worker_id: str) -> WorkerCountries: }, ) def update_countries( - session: DbSession, + session: Annotated[Session, Depends(gen_dbsession)], worker_id: str, current_worker: CurrentWorker, data: UpdateWorkerCountries, From 63a34516eef755a0b00948f2cef2ff82c161a763 Mon Sep 17 00:00:00 2001 From: rgaudin Date: Tue, 15 Oct 2024 10:20:18 +0000 Subject: [PATCH 2/2] Fixed #51: Introducing Health Check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ this is not a typical endpoint! This is an API endpoint, expacting to return HTTP 200 with a JSON document describing the statuses of various parts of the project. --- backend/src/mirrors_qa_backend/main.py | 3 +- .../src/mirrors_qa_backend/routes/health.py | 40 +++++++++++++++++++ backend/src/mirrors_qa_backend/schemas.py | 4 ++ .../src/mirrors_qa_backend/settings/api.py | 12 +++++- 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 backend/src/mirrors_qa_backend/routes/health.py diff --git a/backend/src/mirrors_qa_backend/main.py b/backend/src/mirrors_qa_backend/main.py index b84916b..9ef4f07 100644 --- a/backend/src/mirrors_qa_backend/main.py +++ b/backend/src/mirrors_qa_backend/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from mirrors_qa_backend.db import initialize_mirrors, upgrade_db_schema -from mirrors_qa_backend.routes import auth, tests, worker +from mirrors_qa_backend.routes import auth, health, tests, worker @asynccontextmanager @@ -19,6 +19,7 @@ def create_app(*, debug: bool = True): app.include_router(router=tests.router) app.include_router(router=auth.router) app.include_router(router=worker.router) + app.include_router(router=health.router) return app diff --git a/backend/src/mirrors_qa_backend/routes/health.py b/backend/src/mirrors_qa_backend/routes/health.py new file mode 100644 index 0000000..32ee20f --- /dev/null +++ b/backend/src/mirrors_qa_backend/routes/health.py @@ -0,0 +1,40 @@ +import datetime +from typing import Annotated + +from fastapi import APIRouter, Depends +from fastapi import status as status_codes +from sqlalchemy import select +from sqlalchemy.orm import Session + +from mirrors_qa_backend.db import count_from_stmt, gen_dbsession +from mirrors_qa_backend.db.models import Test +from mirrors_qa_backend.enums import StatusEnum +from mirrors_qa_backend.schemas import HealthStatus +from mirrors_qa_backend.settings.api import APISettings + +router = APIRouter(prefix="/health-check", tags=["health-check"]) + + +@router.get( + "", + status_code=status_codes.HTTP_200_OK, + responses={ + status_codes.HTTP_200_OK: { + "description": "Status of monitored parts of mirrors-qa" + } + }, +) +def heatlh_status(session: Annotated[Session, Depends(gen_dbsession)]) -> HealthStatus: + test_received_after = datetime.datetime.now() - datetime.timedelta( + seconds=APISettings.UNHEALTHY_NO_TESTS_DURATION_SECONDS + ) + + nb_recent_tests_received = count_from_stmt( + session, + select(Test).where( + Test.status == StatusEnum.SUCCEEDED, + (Test.started_on >= test_received_after), + ), + ) + + return HealthStatus(receiving_tests=nb_recent_tests_received > 0) diff --git a/backend/src/mirrors_qa_backend/schemas.py b/backend/src/mirrors_qa_backend/schemas.py index 9dbb934..6409191 100644 --- a/backend/src/mirrors_qa_backend/schemas.py +++ b/backend/src/mirrors_qa_backend/schemas.py @@ -110,3 +110,7 @@ class JWTClaims(BaseModel): exp: datetime.datetime iat: datetime.datetime subject: str + + +class HealthStatus(BaseModel): + receiving_tests: bool diff --git a/backend/src/mirrors_qa_backend/settings/api.py b/backend/src/mirrors_qa_backend/settings/api.py index fbdb5f8..b27fb30 100644 --- a/backend/src/mirrors_qa_backend/settings/api.py +++ b/backend/src/mirrors_qa_backend/settings/api.py @@ -8,8 +8,16 @@ class APISettings(Settings): JWT_SECRET: str = getenv("JWT_SECRET", mandatory=True) # number of seconds before a message expire - MESSAGE_VALIDITY_SECONDS = parse_timespan( + MESSAGE_VALIDITY_SECONDS: float = parse_timespan( getenv("MESSAGE_VALIDITY_DURATION", default="1m") ) # number of hours before access tokens expire - TOKEN_EXPIRY_SECONDS = parse_timespan(getenv("TOKEN_EXPIRY_DURATION", default="6h")) + TOKEN_EXPIRY_SECONDS: float = parse_timespan( + getenv("TOKEN_EXPIRY_DURATION", default="6h") + ) + + # number of seconds after which to consider that not having received + # successful Test is an issue + UNHEALTHY_NO_TESTS_DURATION_SECONDS: float = parse_timespan( + getenv("UNHEALTHY_NO_TESTS_DURATION_SECONDS", default="6h") + )