Run generic reconciliation in container

This commit is contained in:
Lyncoln
2026-09-20 17:34:01 -04:00
parent d6ad917b57
commit d4ceb7e1df
4 changed files with 143 additions and 22 deletions
+7 -2
View File
@@ -2,13 +2,18 @@ FROM docker.io/library/python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app
PYTHONPATH=/app \
DNS_SYNC_STATE_DB=/var/lib/dns-sync/state.sqlite3 \
DNS_SYNC_PLUGIN_PATH=/plugins
RUN pip install --no-cache-dir requests PyYAML
WORKDIR /app
COPY dns_sync /app/dns_sync
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh
RUN mkdir -p /var/lib/dns-sync /plugins \
&& chmod 0755 /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
+66 -10
View File
@@ -26,31 +26,87 @@ Plugins are convention-loaded from environment variables. No registry edit is ne
- `PROXY=npm` loads `dns_sync.plugins.sources.npm.Plugin`
- `DNS_PROVIDER=verizon` loads `dns_sync.plugins.providers.verizon.Plugin`
## Current plugins
## Plugins
Sources: `npm`, `file`
The generic reconciliation engine uses adapter plugins.
Providers: `verizon`
Built-in adapters:
- `npm` - Nginx Proxy Manager proxy hosts and certificates
External adapters are loaded from `DNS_SYNC_PLUGIN_PATH` and must also be
explicitly enabled through `ENABLED_PLUGINS`.
Example:
```text
ENABLED_PLUGINS=npm,verizon
DNS_SYNC_PLUGIN_PATH=/plugins
```
The Verizon adapter lives under `external_plugins/verizon` and is deliberately
not copied into the dns-sync container image. The external plugin directory is
mounted read-only at runtime.
## Container runtime
The container periodically executes the generic reconciliation engine:
```text
python -m dns_sync reconcile
```
The default container mode is read-only.
```text
SYNC_INTERVAL=1
DNS_SYNC_APPLY=false
DNS_SYNC_STATE_DB=/var/lib/dns-sync/state.sqlite3
DNS_SYNC_PLUGIN_PATH=/plugins
ENABLED_PLUGINS=npm,verizon
```
`SYNC_INTERVAL` is expressed in minutes. The interval begins after each
reconciliation completes, so runs do not overlap.
To permit planned changes to be applied and verified:
```text
DNS_SYNC_APPLY=true
```
An optional mutation budget can be specified:
```text
DNS_SYNC_MAX_CHANGES=10
```
When `DNS_SYNC_APPLY=false`, reconciliation still performs discovery,
normalization, desired-state reconciliation, planning, and state persistence,
but no endpoint mutations are executed.
The SQLite database should be stored on persistent storage.
External plugins should be mounted read-only at `/plugins`.
## Main environment
```text
PROXY=npm
PROXY_IP=192.168.1.101
NPM_ENDPOINTS=192.168.1.3,192.168.1.4,192.168.1.5
PROXY_API_PORT=81
PROXY_API_SCHEME=http
DNS_PROVIDER=verizon
DNS_TARGET_IP=192.168.1.101
VERIZON_ENDPOINTS=192.168.1.1
DOMAIN_MATCHING=false
DOMAIN=example.com
SYNC_INTERVAL=1
NPM_IDENTITY=...
NPM_SECRET=...
ROUTER_PASSWORD=...
```
`DNS_TARGET_IP` optionally overrides the default DNS destination, which is `PROXY_IP`.
## Domain filtering
Filtering is performed by the central engine after source discovery and before
+14 -4
View File
@@ -1,5 +1,5 @@
[Unit]
Description=Reverse Proxy DNS Sync
Description=DNS Sync Reconciliation Service
Wants=network-online.target
After=network-online.target
@@ -7,20 +7,30 @@ After=network-online.target
Image=localhost/dns-sync:latest
ContainerName=dns-sync
Environment=PROXY=npm
Environment=PROXY_IP=192.168.1.101
Environment=ENABLED_PLUGINS=npm,verizon
Environment=DNS_SYNC_PLUGIN_PATH=/plugins
Environment=DNS_SYNC_STATE_DB=/var/lib/dns-sync/state.sqlite3
Environment=NPM_ENDPOINTS=192.168.1.3,192.168.1.4,192.168.1.5
Environment=DNS_TARGET_IP=192.168.1.101
Environment=PROXY_API_PORT=81
Environment=PROXY_API_SCHEME=http
Environment=DNS_PROVIDER=verizon
Environment=VERIZON_ENDPOINTS=192.168.1.1
Environment=DOMAIN_MATCHING=false
Environment=DOMAIN=loxthelion.com
Environment=SYNC_INTERVAL=1
Environment=DNS_SYNC_APPLY=false
Environment=NPM_IDENTITY=davidfolkesmd@gmail.com
Secret=dns-sync-npm-secret,type=env,target=NPM_SECRET
Secret=dns-sync-router-password,type=env,target=ROUTER_PASSWORD
Volume=dns-sync-state:/var/lib/dns-sync:Z
Volume=./external_plugins:/plugins:ro,Z
[Service]
Restart=on-failure
RestartSec=30
+56 -6
View File
@@ -2,17 +2,44 @@
set -u
INTERVAL_MINUTES="${SYNC_INTERVAL:-1}"
APPLY_MODE="${DNS_SYNC_APPLY:-false}"
STATE_DB="${DNS_SYNC_STATE_DB:-/var/lib/dns-sync/state.sqlite3}"
MAX_CHANGES="${DNS_SYNC_MAX_CHANGES:-}"
case "$INTERVAL_MINUTES" in
''|*[!0-9]*)
echo "ERROR: SYNC_INTERVAL must be an integer number of minutes" >&2
exit 1
;;
esac
if [ "$INTERVAL_MINUTES" -lt 1 ]; then
echo "ERROR: SYNC_INTERVAL must be at least 1 minute" >&2
exit 1
fi
case "$(printf '%s' "$APPLY_MODE" | tr '[:upper:]' '[:lower:]')" in
1|true|yes|on)
APPLY_MODE=true
;;
0|false|no|off)
APPLY_MODE=false
;;
*)
echo "ERROR: DNS_SYNC_APPLY must be true or false" >&2
exit 1
;;
esac
if [ -n "$MAX_CHANGES" ]; then
case "$MAX_CHANGES" in
*[!0-9]*)
echo "ERROR: DNS_SYNC_MAX_CHANGES must be a non-negative integer" >&2
exit 1
;;
esac
fi
INTERVAL_SECONDS=$((INTERVAL_MINUTES * 60))
sleep_pid=""
@@ -21,22 +48,45 @@ shutdown() {
[ -z "$sleep_pid" ] || kill "$sleep_pid" 2>/dev/null || true
exit 0
}
trap shutdown INT TERM
run_reconcile() {
if [ "$APPLY_MODE" = true ]; then
if [ -n "$MAX_CHANGES" ]; then
python -m dns_sync reconcile \
--state-db "$STATE_DB" \
--apply \
--max-changes "$MAX_CHANGES"
else
python -m dns_sync reconcile \
--state-db "$STATE_DB" \
--apply
fi
else
python -m dns_sync reconcile \
--state-db "$STATE_DB"
fi
}
echo "DNS sync service started"
echo "Sync interval: ${INTERVAL_MINUTES} minute(s)"
echo "Source plugin: ${PROXY:-npm}"
echo "DNS provider: ${DNS_PROVIDER:-verizon}"
echo "Reconciliation mode: $([ "$APPLY_MODE" = true ] && echo APPLY || echo READ-ONLY)"
echo "State database: ${STATE_DB}"
echo "Enabled plugins: ${ENABLED_PLUGINS:-npm}"
echo "External plugin path: ${DNS_SYNC_PLUGIN_PATH:-/plugins}"
while :; do
echo
echo "=== DNS sync: $(date -Iseconds) ==="
if python -m dns_sync sync; then
echo "Sync run completed successfully"
echo "=== DNS reconciliation: $(date -Iseconds) ==="
if run_reconcile; then
echo "Reconciliation run completed successfully"
else
rc=$?
echo "ERROR: Sync run failed with exit code ${rc}" >&2
echo "ERROR: Reconciliation run failed with exit code ${rc}" >&2
fi
sleep "$INTERVAL_SECONDS" &
sleep_pid=$!
wait "$sleep_pid" || true