diff --git a/argos/agent.py b/argos/agent.py index 75c62c4..e1dc0d4 100644 --- a/argos/agent.py +++ b/argos/agent.py @@ -44,7 +44,7 @@ class ArgosAgent: } self._http_client = httpx.AsyncClient(headers=headers) async with self._http_client: - while True: + while "forever": retry_now = await self._get_and_complete_tasks() if not retry_now: logger.error(f"Waiting {self.wait_time} seconds before next retry") diff --git a/argos/checks/base.py b/argos/checks/base.py index 6676ec6..8781294 100644 --- a/argos/checks/base.py +++ b/argos/checks/base.py @@ -105,7 +105,7 @@ class BaseCheck: - All ERRORS should be reported as CRITICAL. This behaviour can be changed in each check, by defining the `finalize` method. - XXX This can also be tweaked by the config. + XXX Allow this to be tweaked by the config. """ if result.status in (Status.SUCCESS, Status.ERROR): return result.status, Severity.OK diff --git a/argos/checks/checks.py b/argos/checks/checks.py index 39ce03a..ada20e9 100644 --- a/argos/checks/checks.py +++ b/argos/checks/checks.py @@ -60,13 +60,21 @@ class SSLCertificateExpiration(BaseCheck): return self.response(status=Status.ON_CHECK, expires_in=expires_in) @classmethod - async def finalize(cls, config, result, expires_in): - thresholds = config.ssl.thresholds - thresholds.sort() - for days, severity in thresholds: - if expires_in < days: - return Status.FAILURE, severity - return Status.SUCCESS, Severity.OK + async def finalize(cls, config, result, **context): + if result.status != Status.ON_CHECK: + return result.status, Severity.WARNING + elif "expires_in" in context: + thresholds = config.ssl.thresholds + thresholds.sort() + for days, severity in thresholds: + if context["expires_in"] < days: + return Status.FAILURE, severity + return Status.SUCCESS, Severity.OK + else: + raise ValueError( + "The SSLCertificateExpiration check didn't provide an 'expires_in' " + "context variable." + ) @classmethod def get_description(cls, config): diff --git a/argos/server/routes/api.py b/argos/server/routes/api.py index e0408a0..1410c00 100644 --- a/argos/server/routes/api.py +++ b/argos/server/routes/api.py @@ -44,7 +44,6 @@ async def create_results( for agent_result in results: result = await queries.create_result(db, agent_result, agent_id) # XXX Maybe offload this to a queue. - # XXX Use a schema for the on-check value. # XXX Get all the tasks at once, to limit the queries on the db task = await queries.get_task(db, agent_result.task_id) if not task: diff --git a/tests/conftest.py b/tests/conftest.py index 5e3a6a4..84e1a44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import os import pytest from fastapi import FastAPI +from fastapi.testclient import TestClient from sqlalchemy.orm import Session os.environ["ARGOS_APP_ENV"] = "test" @@ -27,6 +28,14 @@ def app() -> FastAPI: models.Base.metadata.drop_all(bind=app.state.engine) +@pytest.fixture +def authorized_client(app): + with TestClient(app) as client: + token = app.state.config.service.secrets[0] + client.headers = {"Authorization": f"Bearer {token}"} + yield client + + def _create_app() -> FastAPI: from argos.server.main import ( # local import for testing purpose get_application, diff --git a/tests/test_api.py b/tests/test_api.py index 09512f8..53ca7a7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,7 @@ +import pytest from fastapi.testclient import TestClient -from argos.schemas import AgentResult +from argos.schemas import AgentResult, SerializableException from argos.server import models @@ -10,10 +11,8 @@ def test_read_tasks_requires_auth(app): assert response.status_code == 403 -def test_tasks_retrieval_and_results(app): - with TestClient(app) as client: - token = app.state.config.service.secrets[0] - client.headers = {"Authorization": f"Bearer {token}"} +def test_tasks_retrieval_and_results(authorized_client, app): + with authorized_client as client: response = client.get("/api/tasks") assert response.status_code == 200 @@ -35,3 +34,49 @@ def test_tasks_retrieval_and_results(app): # The list of tasks should be empty now response = client.get("/api/tasks") assert len(response.json()) == 0 + + +def test_agents_can_report_errors(authorized_client): + with authorized_client as client: + exc = Exception("This is an error") + serialized_exc = SerializableException.from_exception(exc) + agent_result = AgentResult(task_id=1, status="error", context=serialized_exc) + + response = client.post( + "/api/results", + json=[ + agent_result.model_dump(), + ], + ) + assert response.status_code == 201 + + +@pytest.fixture +def ssl_task(db): + task = models.Task( + url="https://exemple.com/", + domain="https://exemple.com/", + check="ssl-certificate-expiration", + expected="on-check", + frequency=1, + ) + db.add(task) + db.commit() + return task + + +def test_specialized_checks_can_report_errors(authorized_client, ssl_task): + with authorized_client as client: + exc = Exception("This is an error") + serialized_exc = SerializableException.from_exception(exc) + agent_result = AgentResult( + task_id=ssl_task.id, status="error", context=serialized_exc + ) + + response = client.post( + "/api/results", + json=[ + agent_result.model_dump(), + ], + ) + assert response.status_code == 201