— The HTTP method used by checks is now configurable

This commit is contained in:
Luc Didry 2024-11-26 15:59:19 +01:00
parent d3766a79c6
commit 8ac2519398
No known key found for this signature in database
GPG key ID: EA868E12D0257E3C
10 changed files with 118 additions and 37 deletions

View file

@ -4,6 +4,8 @@
- 💄 — Show only not-OK domains by default in domains list, to reduce the load on browser - 💄 — Show only not-OK domains by default in domains list, to reduce the load on browser
- ♿️ — Fix not-OK domains display if javascript is disabled - ♿️ — Fix not-OK domains display if javascript is disabled
- ✨ — Retry check right after a httpx.ReadError
- ✨ — The HTTP method used by checks is now configurable
## 0.5.0 ## 0.5.0

View file

@ -24,16 +24,15 @@ class HTTPStatus(BaseCheck):
expected_cls = ExpectedIntValue expected_cls = ExpectedIntValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
return self.response( return self.response(
@ -50,16 +49,15 @@ class HTTPStatusIn(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
return self.response( return self.response(
@ -79,10 +77,14 @@ class HTTPToHTTPS(BaseCheck):
task = self.task task = self.task
url = URL(task.url).copy_with(scheme="http") url = URL(task.url).copy_with(scheme="http")
try: try:
response = await self.http_client.request(method="get", url=url, timeout=60) response = await self.http_client.request(
method=task.method, url=url, timeout=60
)
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request(method="get", url=url, timeout=60) response = await self.http_client.request(
method=task.method, url=url, timeout=60
)
expected_dict = json.loads(self.expected) expected_dict = json.loads(self.expected)
expected = range(300, 400) expected = range(300, 400)
@ -108,16 +110,15 @@ class HTTPHeadersContain(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
status = True status = True
@ -140,16 +141,15 @@ class HTTPHeadersHave(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
status = True status = True
@ -176,16 +176,15 @@ class HTTPHeadersLike(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
status = True status = True
@ -213,12 +212,12 @@ class HTTPBodyContains(BaseCheck):
async def run(self) -> dict: async def run(self) -> dict:
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=self.task.url, timeout=60 method=self.task.method, url=self.task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=self.task.url, timeout=60 method=self.task.method, url=self.task.url, timeout=60
) )
return self.response(status=self.expected in response.text) return self.response(status=self.expected in response.text)
@ -232,12 +231,12 @@ class HTTPBodyLike(BaseCheck):
async def run(self) -> dict: async def run(self) -> dict:
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=self.task.url, timeout=60 method=self.task.method, url=self.task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=self.task.url, timeout=60 method=self.task.method, url=self.task.url, timeout=60
) )
if re.search(rf"{self.expected}", response.text): if re.search(rf"{self.expected}", response.text):
return self.response(status=True) return self.response(status=True)
@ -253,16 +252,15 @@ class HTTPJsonContains(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
obj = response.json() obj = response.json()
@ -289,16 +287,15 @@ class HTTPJsonHas(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
obj = response.json() obj = response.json()
@ -329,16 +326,15 @@ class HTTPJsonLike(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
obj = response.json() obj = response.json()
@ -368,16 +364,15 @@ class HTTPJsonIs(BaseCheck):
expected_cls = ExpectedStringValue expected_cls = ExpectedStringValue
async def run(self) -> dict: async def run(self) -> dict:
# XXX Get the method from the task
task = self.task task = self.task
try: try:
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.request( response = await self.http_client.request(
method="get", url=task.url, timeout=60 method=task.method, url=task.url, timeout=60
) )
obj = response.json() obj = response.json()
@ -400,10 +395,14 @@ class SSLCertificateExpiration(BaseCheck):
async def run(self): async def run(self):
"""Returns the number of days in which the certificate will expire.""" """Returns the number of days in which the certificate will expire."""
try: try:
response = await self.http_client.get(self.task.url, timeout=60) response = await self.http_client.request(
method=self.task.method, url=self.task.url, timeout=60
)
except ReadError: except ReadError:
sleep(1) sleep(1)
response = await self.http_client.get(self.task.url, timeout=60) response = await self.http_client.request(
method=self.task.method, url=self.task.url, timeout=60
)
network_stream = response.extensions["network_stream"] network_stream = response.extensions["network_stream"]
ssl_obj = network_stream.get_extra_info("ssl_object") ssl_obj = network_stream.get_extra_info("ssl_object")

View file

@ -93,6 +93,11 @@ websites:
- domain: "https://mypads.example.org" - domain: "https://mypads.example.org"
paths: paths:
- path: "/mypads/" - path: "/mypads/"
# Specify the method of the HTTP request
# Valid values are "GET", "HEAD", "POST", "OPTIONS",
# "CONNECT", "TRACE", "PUT", "PATCH" and "DELETE"
# default is "GET" if omitted
method: "GET"
checks: checks:
# Check that the returned HTTP status is 200 # Check that the returned HTTP status is 200
- status-is: 200 - status-is: 200

View file

@ -22,7 +22,7 @@ from pydantic.networks import UrlConstraints
from pydantic_core import Url from pydantic_core import Url
from typing_extensions import Annotated from typing_extensions import Annotated
from argos.schemas.utils import string_to_duration from argos.schemas.utils import string_to_duration, Method
Severity = Literal["warning", "error", "critical", "unknown"] Severity = Literal["warning", "error", "critical", "unknown"]
Environment = Literal["dev", "test", "production"] Environment = Literal["dev", "test", "production"]
@ -104,6 +104,7 @@ def parse_checks(value):
class WebsitePath(BaseModel): class WebsitePath(BaseModel):
path: str path: str
method: Method = "GET"
checks: List[ checks: List[
Annotated[ Annotated[
Tuple[str, str], Tuple[str, str],

View file

@ -8,6 +8,8 @@ from typing import Literal
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from argos.schemas.utils import Method
# XXX Refactor using SQLModel to avoid duplication of model data # XXX Refactor using SQLModel to avoid duplication of model data
@ -18,6 +20,7 @@ class Task(BaseModel):
url: str url: str
domain: str domain: str
check: str check: str
method: Method
expected: str expected: str
selected_at: datetime | None selected_at: datetime | None
selected_by: str | None selected_by: str | None

View file

@ -1,6 +1,11 @@
from typing import Literal from typing import Literal
Method = Literal[
"GET", "HEAD", "POST", "OPTIONS", "CONNECT", "TRACE", "PUT", "PATCH", "DELETE"
]
def string_to_duration( def string_to_duration(
value: str, target: Literal["days", "hours", "minutes"] value: str, target: Literal["days", "hours", "minutes"]
) -> int | float: ) -> int | float:

View file

@ -0,0 +1,45 @@
"""Specify check method
Revision ID: dcf73fa19fce
Revises: c780864dc407
Create Date: 2024-11-26 14:40:27.510587
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "dcf73fa19fce"
down_revision: Union[str, None] = "c780864dc407"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
with op.batch_alter_table("tasks", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"method",
sa.Enum(
"GET",
"HEAD",
"POST",
"OPTIONS",
"CONNECT",
"TRACE",
"PUT",
"PATCH",
"DELETE",
name="method",
),
nullable=False,
)
)
def downgrade() -> None:
with op.batch_alter_table("tasks", schema=None) as batch_op:
batch_op.drop_column("method")

View file

@ -12,6 +12,7 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from argos.checks import BaseCheck, get_registered_check from argos.checks import BaseCheck, get_registered_check
from argos.schemas import WebsiteCheck from argos.schemas import WebsiteCheck
from argos.schemas.utils import Method
class Base(DeclarativeBase): class Base(DeclarativeBase):
@ -35,6 +36,21 @@ class Task(Base):
check: Mapped[str] = mapped_column() check: Mapped[str] = mapped_column()
expected: Mapped[str] = mapped_column() expected: Mapped[str] = mapped_column()
frequency: Mapped[int] = mapped_column() frequency: Mapped[int] = mapped_column()
method: Mapped[Method] = mapped_column(
Enum(
"GET",
"HEAD",
"POST",
"OPTIONS",
"CONNECT",
"TRACE",
"PUT",
"PATCH",
"DELETE",
name="method",
),
insert_default="GET",
)
# Orchestration-related # Orchestration-related
selected_by: Mapped[str] = mapped_column(nullable=True) selected_by: Mapped[str] = mapped_column(nullable=True)

View file

@ -146,6 +146,7 @@ async def update_from_config(db: Session, config: schemas.Config):
db.query(Task) db.query(Task)
.filter( .filter(
Task.url == url, Task.url == url,
Task.method == p.method,
Task.check == check_key, Task.check == check_key,
Task.expected == expected, Task.expected == expected,
) )
@ -159,8 +160,10 @@ async def update_from_config(db: Session, config: schemas.Config):
existing_task.frequency = frequency existing_task.frequency = frequency
logger.debug( logger.debug(
"Skipping db task creation for url=%s, " "Skipping db task creation for url=%s, "
"check_key=%s, expected=%s, frequency=%s.", "method=%s, check_key=%s, expected=%s, "
"frequency=%s.",
url, url,
p.method,
check_key, check_key,
expected, expected,
frequency, frequency,
@ -173,6 +176,7 @@ async def update_from_config(db: Session, config: schemas.Config):
task = Task( task = Task(
domain=domain, domain=domain,
url=url, url=url,
method=p.method,
check=check_key, check=check_key,
expected=expected, expected=expected,
frequency=frequency, frequency=frequency,

View file

@ -35,6 +35,7 @@ def ssl_task(now):
id=1, id=1,
url="https://example.org", url="https://example.org",
domain="https://example.org", domain="https://example.org",
method="GET",
check="ssl-certificate-expiration", check="ssl-certificate-expiration",
expected="on-check", expected="on-check",
selected_at=now, selected_at=now,