feat: peer sync configurable via env vars + document CoreDNS file cache 🔗

- PeerSyncWorker reads DADNS_PEER_SYNC_PEER_URL / _USERNAME / _PASSWORD env
  vars to populate a single peer without a config file; deduped against any
  config-file peers so the URL never appears twice
- 2 new tests (119 total, all passing)
- README: peer sync single-peer env var table; Topology C compose example
  updated to use env vars only (no config file needed for two-node setup)
- README: document cybercinch/coredns_mysql_extend built-in file caching —
  serves from cache during MySQL outages, eliminates per-query round-trips
This commit is contained in:
2026-02-20 06:41:46 +13:00
parent fbb6220728
commit d98f08a408
3 changed files with 63 additions and 10 deletions

View File

@@ -323,9 +323,11 @@ Key differences from the upstream plugin:
- Fully authoritative responses — correct AA flag and NXDOMAIN on misses - Fully authoritative responses — correct AA flag and NXDOMAIN on misses
- Wildcard record support (`*` entries served correctly) - Wildcard record support (`*` entries served correctly)
- NS records returned in the additional section - 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 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.
build required.
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 #### Peer sync
| Config key | Environment variable | Default | Description | | Config key / Environment variable | Default | Description |
|---|---|---|---| |---|---|---|
| `peer_sync.enabled` | `DADNS_PEER_SYNC_ENABLED` | `false` | Enable background peer-to-peer zone sync | | `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 | | `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_APP_AUTH_PASSWORD: my-strong-secret
DADNS_DNS_DEFAULT_BACKEND: nsd DADNS_DNS_DEFAULT_BACKEND: nsd
DADNS_DNS_BACKENDS_NSD_ENABLED: "true" 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: volumes:
- ./config/syd:/app/config # contains peer_sync.peers list
- syd-data:/app/data - syd-data:/app/data
directdnsonly-mlb: directdnsonly-mlb:
@@ -553,8 +566,11 @@ services:
DADNS_APP_AUTH_PASSWORD: my-strong-secret DADNS_APP_AUTH_PASSWORD: my-strong-secret
DADNS_DNS_DEFAULT_BACKEND: nsd DADNS_DNS_DEFAULT_BACKEND: nsd
DADNS_DNS_BACKENDS_NSD_ENABLED: "true" 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: volumes:
- ./config/mlb:/app/config # contains peer_sync.peers list
- mlb-data:/app/data - mlb-data:/app/data
volumes: volumes:

View File

@@ -19,6 +19,7 @@ Safety properties:
with older peer data with older peer data
""" """
import datetime import datetime
import os
import threading import threading
from loguru import logger from loguru import logger
import requests import requests
@@ -36,7 +37,22 @@ class PeerSyncWorker:
def __init__(self, peer_sync_config: dict): def __init__(self, peer_sync_config: dict):
self.enabled = peer_sync_config.get("enabled", False) self.enabled = peer_sync_config.get("enabled", False)
self.interval_seconds = peer_sync_config.get("interval_minutes", 15) * 60 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._stop_event = threading.Event()
self._thread = None self._thread = None

View File

@@ -58,6 +58,27 @@ def test_peers_stored():
assert worker.peers[0]["url"] == "http://ddo-2:2222" 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): def test_start_skips_when_disabled(caplog):
worker = PeerSyncWorker({"enabled": False}) worker = PeerSyncWorker({"enabled": False})
worker.start() worker.start()