crontab url caller
Build and Push Docker Images / build-and-push (push) Successful in 46s

This commit is contained in:
Bram committed 2026-09-21 15:05:17 +02:00
1 parent 2dbaa0ebb0
commit 35dc35815b
5 files changed
+192

No files matched your search

+19
View File
@@ -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"]
+52
View File
@@ -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.
+119
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
croniter==6.2.4
+1
View File
@@ -0,0 +1 @@
latest