diff --git a/README.md b/README.md index 62ff60b..cbe45ed 100644 --- a/README.md +++ b/README.md @@ -323,9 +323,11 @@ Key differences from the upstream plugin: - Fully authoritative responses — correct AA flag and NXDOMAIN on misses - Wildcard record support (`*` entries served correctly) - NS records returned in the additional section +- **Built-in file caching** — CoreDNS caches zone data from MySQL to local files, so queries are served from the cache if MySQL is temporarily unreachable. This also eliminates the per-query MySQL round-trip for frequently resolved names. -Use the BIND backend if you want a zero-dependency setup with no custom CoreDNS -build required. +The file cache makes Topology B significantly more resilient to MySQL hiccups: CoreDNS keeps serving from cache while directdnsonly's retry queue waits for MySQL to recover. + +Use the NSD or BIND backend if you want a zero-dependency setup with no custom CoreDNS build required. --- @@ -502,12 +504,20 @@ The built-in env var mapping targets the backend named `coredns_mysql`. For mult #### Peer sync -| Config key | Environment variable | Default | Description | -|---|---|---|---| -| `peer_sync.enabled` | `DADNS_PEER_SYNC_ENABLED` | `false` | Enable background peer-to-peer zone sync | -| `peer_sync.interval_minutes` | `DADNS_PEER_SYNC_INTERVAL_MINUTES` | `15` | How often each peer is polled | +| Config key / Environment variable | Default | Description | +|---|---|---| +| `peer_sync.enabled` / `DADNS_PEER_SYNC_ENABLED` | `false` | Enable background peer-to-peer zone sync | +| `peer_sync.interval_minutes` / `DADNS_PEER_SYNC_INTERVAL_MINUTES` | `15` | How often each peer is polled | -> The `peer_sync.peers` list (peer URLs, credentials) requires a config file — it cannot be expressed as simple env vars. +For a **single peer** (the typical two-node Topology C setup) the peer can be configured entirely via env vars — no config file required: + +| Environment variable | Default | Description | +|---|---|---| +| `DADNS_PEER_SYNC_PEER_URL` | _(unset)_ | URL of the single peer (e.g. `http://ddo-2:2222`). When set, this peer is automatically appended to the peers list. | +| `DADNS_PEER_SYNC_PEER_USERNAME` | `directdnsonly` | Basic auth username for the peer | +| `DADNS_PEER_SYNC_PEER_PASSWORD` | _(empty)_ | Basic auth password for the peer | + +> For **multiple peers**, use a config file with the `peer_sync.peers` list. A peer defined via env var is deduped — if the same URL already appears in the config file it will not be added twice. --- @@ -540,8 +550,11 @@ services: DADNS_APP_AUTH_PASSWORD: my-strong-secret DADNS_DNS_DEFAULT_BACKEND: nsd DADNS_DNS_BACKENDS_NSD_ENABLED: "true" + DADNS_PEER_SYNC_ENABLED: "true" + DADNS_PEER_SYNC_PEER_URL: http://directdnsonly-mlb:2222 + DADNS_PEER_SYNC_PEER_USERNAME: directdnsonly + DADNS_PEER_SYNC_PEER_PASSWORD: my-strong-secret volumes: - - ./config/syd:/app/config # contains peer_sync.peers list - syd-data:/app/data directdnsonly-mlb: @@ -553,8 +566,11 @@ services: DADNS_APP_AUTH_PASSWORD: my-strong-secret DADNS_DNS_DEFAULT_BACKEND: nsd DADNS_DNS_BACKENDS_NSD_ENABLED: "true" + DADNS_PEER_SYNC_ENABLED: "true" + DADNS_PEER_SYNC_PEER_URL: http://directdnsonly-syd:2222 + DADNS_PEER_SYNC_PEER_USERNAME: directdnsonly + DADNS_PEER_SYNC_PEER_PASSWORD: my-strong-secret volumes: - - ./config/mlb:/app/config # contains peer_sync.peers list - mlb-data:/app/data volumes: diff --git a/directdnsonly/app/peer_sync.py b/directdnsonly/app/peer_sync.py index 3088ac0..ad70b9b 100644 --- a/directdnsonly/app/peer_sync.py +++ b/directdnsonly/app/peer_sync.py @@ -19,6 +19,7 @@ Safety properties: with older peer data """ import datetime +import os import threading from loguru import logger import requests @@ -36,7 +37,22 @@ class PeerSyncWorker: def __init__(self, peer_sync_config: dict): self.enabled = peer_sync_config.get("enabled", False) self.interval_seconds = peer_sync_config.get("interval_minutes", 15) * 60 - self.peers = peer_sync_config.get("peers") or [] + self.peers = list(peer_sync_config.get("peers") or []) + + # Support single-peer config via env vars for env-var-only deployments. + # DADNS_PEER_SYNC_PEER_URL, DADNS_PEER_SYNC_PEER_USERNAME, DADNS_PEER_SYNC_PEER_PASSWORD + env_url = os.environ.get("DADNS_PEER_SYNC_PEER_URL", "").strip() + if env_url and not any(p.get("url") == env_url for p in self.peers): + self.peers.append( + { + "url": env_url, + "username": os.environ.get( + "DADNS_PEER_SYNC_PEER_USERNAME", "directdnsonly" + ), + "password": os.environ.get("DADNS_PEER_SYNC_PEER_PASSWORD", ""), + } + ) + logger.debug(f"[peer_sync] Added peer from env vars: {env_url}") self._stop_event = threading.Event() self._thread = None diff --git a/tests/test_peer_sync.py b/tests/test_peer_sync.py index 3a68fbb..f004670 100644 --- a/tests/test_peer_sync.py +++ b/tests/test_peer_sync.py @@ -58,6 +58,27 @@ def test_peers_stored(): assert worker.peers[0]["url"] == "http://ddo-2:2222" +def test_peer_from_env_var(monkeypatch): + """DADNS_PEER_SYNC_PEER_URL adds a peer without a config file.""" + monkeypatch.setenv("DADNS_PEER_SYNC_PEER_URL", "http://ddo-env:2222") + monkeypatch.setenv("DADNS_PEER_SYNC_PEER_USERNAME", "admin") + monkeypatch.setenv("DADNS_PEER_SYNC_PEER_PASSWORD", "secret") + worker = PeerSyncWorker({"enabled": True}) + assert len(worker.peers) == 1 + assert worker.peers[0]["url"] == "http://ddo-env:2222" + assert worker.peers[0]["username"] == "admin" + assert worker.peers[0]["password"] == "secret" + + +def test_env_peer_not_duplicated_when_also_in_config(monkeypatch): + """Env var peer is not added if it already appears in the config file peers list.""" + monkeypatch.setenv("DADNS_PEER_SYNC_PEER_URL", "http://ddo-2:2222") + worker = PeerSyncWorker(BASE_CONFIG) + # BASE_CONFIG already has http://ddo-2:2222 — must remain exactly one entry + urls = [p["url"] for p in worker.peers] + assert urls.count("http://ddo-2:2222") == 1 + + def test_start_skips_when_disabled(caplog): worker = PeerSyncWorker({"enabled": False}) worker.start()