From 35dc35815b28e6d29d556ab711e3bd78562f3727 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 21 Sep 2026 15:05:17 +0200 Subject: [PATCH] crontab url caller --- Dockers/cron-url-caller/Dockerfile | 19 ++++ Dockers/cron-url-caller/README.md | 52 ++++++++++ Dockers/cron-url-caller/entrypoint.py | 119 +++++++++++++++++++++++ Dockers/cron-url-caller/requirements.txt | 1 + Dockers/cron-url-caller/version | 1 + 5 files changed, 192 insertions(+) create mode 100644 Dockers/cron-url-caller/Dockerfile create mode 100644 Dockers/cron-url-caller/README.md create mode 100644 Dockers/cron-url-caller/entrypoint.py create mode 100644 Dockers/cron-url-caller/requirements.txt create mode 100644 Dockers/cron-url-caller/version diff --git a/Dockers/cron-url-caller/Dockerfile b/Dockers/cron-url-caller/Dockerfile new file mode 100644 index 0000000..c4d634a --- /dev/null +++ b/Dockers/cron-url-caller/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3-alpine + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app + +COPY requirements.txt . +RUN python -m pip install --no-cache-dir -r requirements.txt + +COPY entrypoint.py /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.py + +ENV TZ=Europe/Brussels +ENV URL="" +ENV CRON="" +ENV TIMEOUT=30 +ENV ON_STARTUP=true + +ENTRYPOINT ["/usr/local/bin/entrypoint.py"] diff --git a/Dockers/cron-url-caller/README.md b/Dockers/cron-url-caller/README.md new file mode 100644 index 0000000..88cd6c7 --- /dev/null +++ b/Dockers/cron-url-caller/README.md @@ -0,0 +1,52 @@ +# cron-url-caller + +Minimale Docker image die op een gegeven **CRON**-schema een **GET**-request doet naar een **URL**. + +## Environment variables + +### Required + +| Variable | Description | +|----------|-------------| +| `URL` | URL die aangeroepen wordt | +| `CRON` | 5-veld cron-expressie (bijv. `0 3 * * *`) of macro (`@hourly`, `@daily`, ...) | + +### Optional + +| Variable | Default | Description | +|----------|---------|-------------| +| `TZ` | `Europe/Brussels` | Tijdzone | +| `TIMEOUT` | `30` | Request-timeout in seconden | +| `ON_STARTUP` | `true` | Direct een request doen bij opstart (`true`/`false`) | + +## Voorbeeld + +```bash +docker run -d \ + -e URL=https://example.com/health-check \ + -e 'CRON=*/5 * * * *' \ + -e TZ=Europe/Brussels \ + --name cron-url-caller \ + bramkel/cron-url-caller:latest +``` + +## Compose + +```yaml + cron-url-caller: + image: bramkel/cron-url-caller:latest + container_name: cron-url-caller + restart: unless-stopped + environment: + - TZ=Europe/Brussels + - URL=https://example.com/health-check + - 'CRON=*/5 * * * *' + - TIMEOUT=30 + - ON_STARTUP=true +``` + +## Gedrag + +1. Bij opstart wordt (indien `ON_STARTUP=true`) direct een GET-request gedaan. +2. Vervolgens wordt gewacht tot het volgende CRON-tijdstip en opnieuw een GET-request gedaan. +3. HTTP-status en eventuele fouten worden gelogd. diff --git a/Dockers/cron-url-caller/entrypoint.py b/Dockers/cron-url-caller/entrypoint.py new file mode 100644 index 0000000..25a8029 --- /dev/null +++ b/Dockers/cron-url-caller/entrypoint.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Call a URL on a cron schedule.""" + +from __future__ import annotations + +import os +import sys +import time +from datetime import datetime +from typing import Any +from urllib.error import URLError +from urllib.request import Request, urlopen +from zoneinfo import ZoneInfo + +from croniter import croniter + +CRON_MACROS = { + "@yearly": "0 0 1 1 *", + "@annually": "0 0 1 1 *", + "@monthly": "0 0 1 * *", + "@weekly": "0 0 * * 0", + "@daily": "0 0 * * *", + "@midnight": "0 0 * * *", + "@hourly": "0 * * * *", +} + + +def log(msg: str) -> None: + print(f"[{datetime.now().astimezone().isoformat(timespec='seconds')}] {msg}", file=sys.stderr) + + +def zone() -> ZoneInfo: + name = os.environ.get("TZ") or "UTC" + try: + return ZoneInfo(name) + except Exception as exc: + raise SystemExit(f"Invalid TZ '{name}': {exc}") from exc + + +def resolve_cron_expr() -> str: + cron = os.environ.get("CRON", "").strip() + if not cron: + raise SystemExit("CRON must be set (e.g. '0 3 * * *')") + + expr = CRON_MACROS.get(cron.lower(), cron) + if not croniter.is_valid(expr): + raise SystemExit(f"Invalid CRON '{expr}' (expected a 5-field cron expression)") + return expr + + +def resolve_url() -> str: + url = os.environ.get("URL", "").strip() + if not url: + raise SystemExit("URL must be set") + return url + + +def timeout_seconds() -> int: + raw = os.environ.get("TIMEOUT", "30").strip() + try: + value = int(raw) + except ValueError as exc: + raise SystemExit(f"Invalid TIMEOUT '{raw}' (expected integer)") from exc + if value <= 0: + raise SystemExit("Invalid TIMEOUT (expected a positive integer)") + return value + + +def run_on_startup() -> bool: + return os.environ.get("ON_STARTUP", "true").lower() in ("1", "true", "yes", "on") + + +def next_run(expr: str, after: datetime) -> datetime: + return croniter(expr, after).get_next(datetime) + + +def do_request(url: str, timeout: int) -> None: + req = Request(url, method="GET") + try: + with urlopen(req, timeout=timeout) as response: + log(f"GET {url} -> HTTP {response.status}") + except URLError as exc: + reason = exc.reason if hasattr(exc, "reason") else str(exc) + log(f"GET {url} failed: {reason}") + + +def sleep_until(target: datetime, tz: ZoneInfo) -> None: + while True: + remaining = (target - datetime.now(tz)).total_seconds() + if remaining <= 0: + return + time.sleep(min(remaining, 60.0)) + + +def main() -> None: + url = resolve_url() + expr = resolve_cron_expr() + tz = zone() + timeout = timeout_seconds() + on_startup = run_on_startup() + + log(f"Starting cron-url-caller (TZ={tz.key}, CRON='{expr}', URL='{url}', TIMEOUT={timeout}s)") + + if on_startup: + do_request(url, timeout) + + now = datetime.now(tz) + nxt = next_run(expr, now) + log(f"Next scheduled call at {nxt.isoformat(timespec='seconds')}") + + while True: + sleep_until(nxt, tz) + do_request(url, timeout) + nxt = next_run(expr, datetime.now(tz)) + log(f"Next scheduled call at {nxt.isoformat(timespec='seconds')}") + + +if __name__ == "__main__": + main() diff --git a/Dockers/cron-url-caller/requirements.txt b/Dockers/cron-url-caller/requirements.txt new file mode 100644 index 0000000..60fcbaf --- /dev/null +++ b/Dockers/cron-url-caller/requirements.txt @@ -0,0 +1 @@ +croniter==6.2.4 diff --git a/Dockers/cron-url-caller/version b/Dockers/cron-url-caller/version new file mode 100644 index 0000000..b9bc2fd --- /dev/null +++ b/Dockers/cron-url-caller/version @@ -0,0 +1 @@ +latest \ No newline at end of file