This commit is contained in:
1 parent
2dbaa0ebb0
commit
35dc35815b
5 files changed
+192
No files matched your search
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
croniter==6.2.4
|
||||
@@ -0,0 +1 @@
|
||||
latest
|
||||
Reference in new issue
Block a user