mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 18:02:41 +02:00
✨ — The HTTP method used by checks is now configurable
This commit is contained in:
parent
d3766a79c6
commit
8ac2519398
10 changed files with 118 additions and 37 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue