Skip to content
Snippets Groups Projects
Commit c5fb35c0 authored by Niels Alexander Buegel's avatar Niels Alexander Buegel
Browse files

Improved the default routes

parent 3f29bf89
No related branches found
No related tags found
No related merge requests found
Showing
with 184 additions and 44 deletions
......@@ -8,4 +8,4 @@ RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./ctarestapi/ /app/ctarestapi
CMD ["python3", "-m", "ctarestapi.server"]
CMD ["python3", "-m", "ctarestapi.server_fastapi"]
from functools import lru_cache
from ctarestapi.internal.catalogue.catalogue import Catalogue
from ctarestapi.internal.catalogue import Catalogue
import os
......
from .catalogue import CatalogueStatus
from .catalogue import Catalogue
from sqlalchemy import create_engine
from ast import Str
import logging
from sqlalchemy import create_engine, Engine, text
from sqlalchemy.exc import SQLAlchemyError
from ctarestapi.internal.catalogue.drive_queries import DriveQueries
from pydantic import BaseModel
from typing import Literal
class CatalogueStatus(BaseModel):
status: Literal["ok", "degraded", "unavailable"]
schemaVersion: str
backend: str
host: str
database: str
class Catalogue:
drives: DriveQueries
_engine: Engine
def __init__(self, connection_string: str):
engine = create_engine(connection_string)
self.drives = DriveQueries(engine)
self._engine = create_engine(connection_string)
def get_status(self) -> CatalogueStatus:
try:
with self._engine.connect() as conn:
query = text(
"""
SELECT
CTA_CATALOGUE.SCHEMA_VERSION_MAJOR,
CTA_CATALOGUE.SCHEMA_VERSION_MINOR
FROM
CTA_CATALOGUE
LIMIT 1
"""
)
row = conn.execute(query).mappings().first()
schema_version = f"{row['schema_version_major']}.{row['schema_version_minor']}"
return CatalogueStatus(
status="ok",
schemaVersion=schema_version,
backend=self._engine.url.get_backend_name(),
host=self._engine.url.host or "unknown",
database=self._engine.url.database or "unknown",
)
except SQLAlchemyError as error:
logging.warning(f"SQL Error: {error}")
return CatalogueStatus(
status="unavailable",
schemaVersion="unknown",
backend=self._engine.url.get_backend_name(),
host=self._engine.url.host or "unknown",
database=self._engine.url.database or "unknown",
)
def is_reachable(self) -> bool:
try:
with self._engine.connect() as conn:
conn.execute(text("SELECT 1"))
return True
except SQLAlchemyError:
return False
from .jwt_middleware import JWTMiddleware
from fastapi import APIRouter
router = APIRouter(prefix="", tags=[], dependencies=[])
@router.get("/")
async def root():
return {"message": "Welcome to the CTA REST API"}
@router.get("/status")
def status_check():
return {"status": "ok"}
from fastapi import APIRouter, Response, Depends
from ctarestapi.version import __version__
from ctarestapi.dependencies import Catalogue, get_catalogue
from ctarestapi.internal.catalogue import CatalogueStatus
router = APIRouter(prefix="", tags=[], dependencies=[])
@router.get("/")
async def root():
return {"message": "Welcome to the CTA REST API"}
@router.get("/health")
def status_check():
return Response(status_code=200)
@router.get("/status")
def get_status(catalogue: Catalogue = Depends(get_catalogue)):
catalogue_status: CatalogueStatus = catalogue.get_status()
api_status = "ok"
components_status_list = [catalogue_status.status]
if "unavailable" in components_status_list:
api_status = "degraded"
if all(v == "unavailable" for v in components_status_list):
api_status = "unavailable"
return {
"status": api_status,
"version": __version__,
"components": {"catalogue": catalogue_status.model_dump()},
}
......@@ -2,9 +2,9 @@ import os
import sys
import logging
from fastapi import FastAPI
from ctarestapi.routers import drives
from ctarestapi.routers import home
from ctarestapi.middleware.jwt_middleware import JWTMiddleware
from ctarestapi.routers.v0 import drives as drives_v0
from ctarestapi.routers.v0 import root as root_v0
from ctarestapi.middleware import JWTMiddleware
# Set the logging level from the env variable LOGLEVEL
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper())
......@@ -36,11 +36,14 @@ def create_app() -> FastAPI:
allowed_algorithms=allowed_algorithms,
jwks_endpoint=jwks_endpoint,
jwks_cache_expiry=jwks_cache_expiry,
unauthenticated_routes={"/status"},
unauthenticated_routes={"/v0/health"},
)
app.include_router(home.router)
app.include_router(drives.router)
app.include_router(
root_v0.router,
prefix="/v0",
)
app.include_router(drives_v0.router, prefix="/v0")
return app
......
__version__ = "0.1.0"
......@@ -3,5 +3,7 @@ PyJWT>=2.10.1
SQLAlchemy>=2.0.40
uvicorn>=0.34.0
cryptography>=44.0.2
psycopg2-binary>=2.9.10
httpx>=0.28.1
pytest>=8.3.5
black>=25.1.0
......@@ -2,3 +2,5 @@ fastapi>=0.115.12
PyJWT>=2.10.1
SQLAlchemy>=2.0.40
uvicorn>=0.34.0
cryptography>=44.0.2
psycopg2-binary>=2.9.10
......@@ -4,7 +4,7 @@ import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock, Mock
from ctarestapi.server import create_app
from ctarestapi.server_fastapi import create_app
from ctarestapi.dependencies import get_catalogue
from fastapi.testclient import TestClient
from jwt.utils import base64url_encode
......
import pytest
def test_status_can_be_done_without_authentication(client_with_auth):
response = client_with_auth.get("/status")
assert response.status_code == 200
......@@ -4,23 +4,25 @@ import pytest
def test_get_drives(client):
client.mock_catalogue.drives.get_all_drives.return_value = []
response = client.get("/drives/")
response = client.get("/v0/drives/")
assert response.status_code == 200
client.mock_catalogue.drives.get_all_drives.assert_called_once_with(limit=100, offset=0)
def test_get_drives_with_pagination(client):
client.mock_catalogue.drives.get_all_drives.return_value = []
limit=3
offset=2
response = client.get(f"/drives/?limit={limit}&offset={offset}")
limit = 3
offset = 2
response = client.get(f"/v0/drives/?limit={limit}&offset={offset}")
assert response.status_code == 200
client.mock_catalogue.drives.get_all_drives.assert_called_once_with(limit=limit, offset=offset)
def test_get_drive_found(client):
client.mock_catalogue.drives.get_drive.return_value = {"drive_name": "test"}
response = client.get("/drives/test")
response = client.get("/v0/drives/test")
assert response.status_code == 200
client.mock_catalogue.drives.get_drive.assert_called_once_with(drive_name="test")
......@@ -28,7 +30,7 @@ def test_get_drive_found(client):
def test_get_drive_not_found(client):
client.mock_catalogue.drives.get_drive.return_value = None
response = client.get("/drives/test")
response = client.get("/v0/drives/test")
assert response.status_code == 404
assert response.json()["detail"] == "Drive not found"
......@@ -36,7 +38,7 @@ def test_get_drive_not_found(client):
def test_update_drive_state_up(client):
client.mock_catalogue.drives.set_drive_up.return_value = True
response = client.put("/drives/test/state", json={"desired_state": "up", "reason": "fix"})
response = client.put("/v0/drives/test/state", json={"desired_state": "up", "reason": "fix"})
assert response.status_code == 200
client.mock_catalogue.drives.set_drive_up.assert_called_once_with("test", "fix")
......@@ -45,7 +47,7 @@ def test_update_drive_state_down_with_reason(client):
client.mock_catalogue.drives.set_drive_down.return_value = True
response = client.put(
"/drives/test/state?force=true",
"/v0/drives/test/state?force=true",
json={"desired_state": "down", "reason": "maintenance"},
)
assert response.status_code == 200
......@@ -53,24 +55,24 @@ def test_update_drive_state_down_with_reason(client):
def test_update_drive_state_down_missing_reason(client):
response = client.put("/drives/test/state", json={"desired_state": "down"})
response = client.put("/v0/drives/test/state", json={"desired_state": "down"})
assert response.status_code == 422
def test_update_drive_comment(client):
client.mock_catalogue.drives.update_drive_comment.return_value = True
response = client.put("/drives/test/comment", json={"comment": "new comment"})
response = client.put("/v0/drives/test/comment", json={"comment": "new comment"})
assert response.status_code == 200
client.mock_catalogue.drives.update_drive_comment.assert_called_once_with("test", "new comment")
def test_update_drive_comment_too_short(client):
response = client.put("/drives/test/comment", json={"comment": ""})
response = client.put("/v0/drives/test/comment", json={"comment": ""})
assert response.status_code == 422
def test_delete_drive(client):
response = client.delete("/drives/test")
response = client.delete("/v0/drives/test")
assert response.status_code == 501
assert response.json()["detail"] == "Not implemented yet."
import pytest
from ctarestapi.internal.catalogue import CatalogueStatus
def test_health_can_be_done_without_authentication(client_with_auth):
response = client_with_auth.get("/v0/health")
assert response.status_code == 200
def test_status_ok(client):
client.mock_catalogue.get_status.return_value = CatalogueStatus(
status="ok", schemaVersion="1.2.3", backend="postgresql", host="cta-db", database="cta"
)
response = client.get("/v0/status")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["components"]["catalogue"]["status"] == "ok"
assert data["components"]["catalogue"]["schemaVersion"] == "1.2.3"
client.mock_catalogue.get_status.assert_called_once()
def test_status_degraded(client):
client.mock_catalogue.get_status.return_value = CatalogueStatus(
status="unavailable", schemaVersion="unknown", backend="postgresql", host="cta-db", database="cta"
)
response = client.get("/v0/status")
assert response.status_code == 200
data = response.json()
assert data["status"] == "unavailable"
assert data["components"]["catalogue"]["status"] == "unavailable"
assert data["components"]["catalogue"]["schemaVersion"] == "unknown"
client.mock_catalogue.get_status.assert_called_once()
......@@ -31,7 +31,7 @@ spec:
stdin: true
readinessProbe:
httpGet:
path: /status
path: /v0/health
port: 80
initialDelaySeconds: 5
periodSeconds: 15
......
#!/bin/bash
set -e
response=$(curl -X POST http://auth-keycloak:8080/realms/master/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=admin-cli" \
-d "username=admin" \
-d "password=admin" \
-s)
access_token=$(echo ${response} | jq -r .access_token)
curl -s http://cta-rest-api/v0/health
curl -s -H "Authorization: Bearer ${access_token}" http://cta-rest-api/v0/status | jq
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment