Skip to content

Commit b5f4fc7

Browse files
authored
[Feature] Config hot-reloading (#5)
1 parent ea21783 commit b5f4fc7

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Clients use the standard [Technitium API](https://github.com/TechnitiumSoftware/
1818
- Global read-only tokens that allow full read access across all zones without write permissions
1919
- Tiered endpoint classification where only record and zone-list endpoints are proxied; zone management and admin endpoints are blocked
2020
- Zone list filtering where `/api/zones/list` responses only show zones the token is allowed to access
21+
- Hot reload of configuration on file change (no restart required)
2122
- Structured audit logging via structlog
2223
- Multi-arch Docker images (linux/amd64, linux/arm64)
2324
- Standalone binary builds via PyInstaller
@@ -178,6 +179,7 @@ bin/start.sh
178179
| `HOST` | `0.0.0.0` | Host/IP to bind |
179180
| `PORT` | `31399` | Port to bind |
180181
| `LOG_LEVEL` | `info` | Log level (`debug`, `info`, `warning`, `error`) |
182+
| `RELOAD_INTERVAL` | `5` | Seconds between config file change checks (0 to disable) |
181183

182184
---
183185

proxy/main.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import json
45
from contextlib import asynccontextmanager
6+
from pathlib import Path
57
from typing import Any, AsyncIterator
68

79
import os
@@ -18,16 +20,40 @@
1820
from proxy.policy import Tier, classify_endpoint, evaluate_policy, extract_operation, is_read_only_endpoint, is_record_endpoint, resolve_zone
1921

2022
audit_log = structlog.get_logger("proxy.audit")
23+
_reload_log = structlog.get_logger("proxy.reload")
2124

2225
_config = load_config()
2326
setup_logging(os.environ.get("LOG_LEVEL", "info"))
2427

28+
_CONFIG_PATH = Path(os.environ.get("CONFIG_PATH", "config.yml"))
29+
_RELOAD_INTERVAL = int(os.environ.get("RELOAD_INTERVAL", "5"))
30+
31+
32+
async def _watch_config(app: FastAPI) -> None:
33+
"""Poll config file for changes and hot-reload on modification."""
34+
last_mtime: float = _CONFIG_PATH.stat().st_mtime if _CONFIG_PATH.exists() else 0
35+
while True:
36+
await asyncio.sleep(_RELOAD_INTERVAL)
37+
try:
38+
current_mtime = _CONFIG_PATH.stat().st_mtime
39+
if current_mtime <= last_mtime:
40+
continue
41+
last_mtime = current_mtime
42+
new_config = load_config()
43+
app.state.config = new_config
44+
_reload_log.info("config_reloaded", config_path=str(_CONFIG_PATH))
45+
except Exception as exc:
46+
_reload_log.error("config_reload_failed", error=str(exc), config_path=str(_CONFIG_PATH))
47+
2548

2649
@asynccontextmanager
2750
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
2851
app.state.config = _config
2952
app.state.http_client = httpx.AsyncClient(verify=_config.technitium.verify_ssl)
53+
watcher = asyncio.create_task(_watch_config(app)) if _RELOAD_INTERVAL > 0 else None
3054
yield
55+
if watcher is not None:
56+
watcher.cancel()
3157
await app.state.http_client.aclose()
3258

3359

0 commit comments

Comments
 (0)