diff --git a/README.md b/README.md index 5e37c50..d2f732c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,49 @@ -# On service start. +# Argos + +🚧 This is mainly a work in progress for now. It's not working, don't try to install it ! 🚧 + +Argos is an HTTP monitoring service. It's meant to be simple to configure and simple to use. + +Features : + +- [x] Uses `.yaml` files for configuration ; +- [x] Read the configuration file and convert it to tasks ; +- [x] Store tasks in a database ; +- [ ] Checks can be distributed on the network thanks to a job queue ; +- [x] Multiple paths per websites can be tested ; +- [ ] Handles multiple alerting backends (email, sms, gotify) ; +- [ ] Exposes an HTTP API that can be consumed by other systems ; +- [ ] Exposes a simple read-only website. + +Implemented checks : + +- [ ] Returned status code matches what you expect ; +- [ ] Returned body matches what you expect ; +- [ ] SSL certificate expires in more than X days ; + +## Development notes + +### On service start. 1. Read the job definitions file and populate the database. 2. From the job definition, create a list of tasks to execute. 3. From time to time (?) clean the db. -# On configuration changes : +### On configuration changes : - Find and tombstone the JobDefinitions that are not useful anymore. - Cascade delete the child tasks that are planned. Tombstone them as wel. -# On worker demand : +### On worker demand : - Find the tasks for which : - last_check is not defined - OR last_check + max_timedelta > datetime.now() - AND selected_by not defined. - Mark these tasks as selected by the current worker, on the current date. -# From time to time: +### From time to time: - Check for stalled tasks (datetime.now() - selected_at) > MAX_WORKER_TIME. Remove the lock. -# On the worker side +### On the worker side Hey, I'm XX, give me some work. OK, this is done, here are the results for Task: response. \ No newline at end of file diff --git a/argos/models.py b/argos/models.py index 8969bf5..3640d61 100644 --- a/argos/models.py +++ b/argos/models.py @@ -8,32 +8,37 @@ from datetime import datetime from .schemas import WebsiteCheck + class Base(DeclarativeBase): - type_annotation_map = { - List[WebsiteCheck]: JSON, - dict: JSON - } - -class Definition(Base): - __tablename__ = "definitions" - - id : Mapped[int]= mapped_column(primary_key=True) - domain : Mapped[str] = mapped_column() - path : Mapped[str] = mapped_column() - # checks : Mapped[List[WebsiteCheck]] = mapped_column() - tasks : Mapped[List["Task"]] = relationship(back_populates="definition") + type_annotation_map = {List[WebsiteCheck]: JSON, dict: JSON} class Task(Base): + """ + There is one task per check. + + It contains all information needed to run the jobs on the workers. + Workers will return information in the result table. + """ + __tablename__ = "tasks" - - id : Mapped[int] = mapped_column(primary_key=True) - # XXX enforce "ok" | "nok" | "unknown" - status: Mapped[str] = mapped_column(default="unknown") - max_delta_days: Mapped[int] = mapped_column() - response: Mapped[dict] = mapped_column(default={}) - last_check: Mapped[datetime] = mapped_column(nullable=True) + id: Mapped[int] = mapped_column(primary_key=True) + + # Info needed to run the task + url: Mapped[str] = mapped_column() + domain: Mapped[str] = mapped_column() + check: Mapped[str] = mapped_column() + expected: Mapped[str] = mapped_column() + + # Orchestration-related selected_by: Mapped[str] = mapped_column(nullable=True) selected_at: Mapped[datetime] = mapped_column(nullable=True) - definition_id : Mapped[str] = mapped_column(ForeignKey("definitions.id")) - definition : Mapped["Definition"] = relationship(back_populates="tasks") \ No newline at end of file + + +class Result(Base): + __tablename__ = "results" + id: Mapped[int] = mapped_column(primary_key=True) + + submitted_at: Mapped[datetime] = mapped_column() + success: Mapped[bool] = mapped_column() + content: Mapped[str] = mapped_column() diff --git a/argos/queries.py b/argos/queries.py index f4b8f4a..0903cc5 100644 --- a/argos/queries.py +++ b/argos/queries.py @@ -1,7 +1,10 @@ from sqlalchemy.orm import Session +from sqlalchemy import exists -from . import models, schemas +from . import schemas +from .models import Task from .logging import logger +from urllib.parse import urljoin def list_tasks(db: Session, limit: int = 100): @@ -10,21 +13,28 @@ def list_tasks(db: Session, limit: int = 100): def update_from_config(db: Session, config: schemas.Config): for website in config.websites: - domain = website.domain + domain = str(website.domain) for p in website.paths: - definition = models.Definition( - domain = str(website.domain), - path = str(p.path), - ) - db.add(definition) - + url = urljoin(domain, str(p.path)) for check in p.checks: - for check_key, check_value in check.items(): - logger.debug(f"{check_key=}, {check_value=}") - task = models.Task( - definition=definition, - status= "unknown", - max_delta_days = 1, #XXX This should be defined in the config. - ) - db.add(task) + for check_key, expected in check.items(): + # Check the db for already existing tasks. + + existing_task = db.query(exists().where( + Task.url == url + and Task.check == check_key + and Task.expected == expected + )).scalar() + + if not existing_task: + task = Task( + domain = domain, + url = url, + check = check_key, + expected = expected + ) + logger.debug(f"Adding a new task in the db: {task=}") + db.add(task) + else: + logger.debug(f"Skipping db task creation for {url=}, {check_key=}, {expected=}.") db.commit()