Run generic reconciliation in container
This commit is contained in:
+7
-2
@@ -2,13 +2,18 @@ FROM docker.io/library/python:3.13-slim
|
|||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=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
|
RUN pip install --no-cache-dir requests PyYAML
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY dns_sync /app/dns_sync
|
COPY dns_sync /app/dns_sync
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
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"]
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|||||||
@@ -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`
|
- `PROXY=npm` loads `dns_sync.plugins.sources.npm.Plugin`
|
||||||
- `DNS_PROVIDER=verizon` loads `dns_sync.plugins.providers.verizon.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
|
## Main environment
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PROXY=npm
|
NPM_ENDPOINTS=192.168.1.3,192.168.1.4,192.168.1.5
|
||||||
PROXY_IP=192.168.1.101
|
|
||||||
PROXY_API_PORT=81
|
PROXY_API_PORT=81
|
||||||
PROXY_API_SCHEME=http
|
PROXY_API_SCHEME=http
|
||||||
DNS_PROVIDER=verizon
|
DNS_TARGET_IP=192.168.1.101
|
||||||
|
|
||||||
|
VERIZON_ENDPOINTS=192.168.1.1
|
||||||
|
|
||||||
DOMAIN_MATCHING=false
|
DOMAIN_MATCHING=false
|
||||||
DOMAIN=example.com
|
DOMAIN=example.com
|
||||||
SYNC_INTERVAL=1
|
|
||||||
NPM_IDENTITY=...
|
NPM_IDENTITY=...
|
||||||
NPM_SECRET=...
|
NPM_SECRET=...
|
||||||
ROUTER_PASSWORD=...
|
ROUTER_PASSWORD=...
|
||||||
```
|
```
|
||||||
|
|
||||||
`DNS_TARGET_IP` optionally overrides the default DNS destination, which is `PROXY_IP`.
|
|
||||||
|
|
||||||
|
|
||||||
## Domain filtering
|
## Domain filtering
|
||||||
|
|
||||||
Filtering is performed by the central engine after source discovery and before
|
Filtering is performed by the central engine after source discovery and before
|
||||||
|
|||||||
+14
-4
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Reverse Proxy DNS Sync
|
Description=DNS Sync Reconciliation Service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
@@ -7,20 +7,30 @@ After=network-online.target
|
|||||||
Image=localhost/dns-sync:latest
|
Image=localhost/dns-sync:latest
|
||||||
ContainerName=dns-sync
|
ContainerName=dns-sync
|
||||||
|
|
||||||
Environment=PROXY=npm
|
Environment=ENABLED_PLUGINS=npm,verizon
|
||||||
Environment=PROXY_IP=192.168.1.101
|
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_PORT=81
|
||||||
Environment=PROXY_API_SCHEME=http
|
Environment=PROXY_API_SCHEME=http
|
||||||
Environment=DNS_PROVIDER=verizon
|
|
||||||
|
Environment=VERIZON_ENDPOINTS=192.168.1.1
|
||||||
Environment=DOMAIN_MATCHING=false
|
Environment=DOMAIN_MATCHING=false
|
||||||
Environment=DOMAIN=loxthelion.com
|
Environment=DOMAIN=loxthelion.com
|
||||||
|
|
||||||
Environment=SYNC_INTERVAL=1
|
Environment=SYNC_INTERVAL=1
|
||||||
|
Environment=DNS_SYNC_APPLY=false
|
||||||
|
|
||||||
Environment=NPM_IDENTITY=davidfolkesmd@gmail.com
|
Environment=NPM_IDENTITY=davidfolkesmd@gmail.com
|
||||||
|
|
||||||
Secret=dns-sync-npm-secret,type=env,target=NPM_SECRET
|
Secret=dns-sync-npm-secret,type=env,target=NPM_SECRET
|
||||||
Secret=dns-sync-router-password,type=env,target=ROUTER_PASSWORD
|
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]
|
[Service]
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
|
|||||||
+56
-6
@@ -2,17 +2,44 @@
|
|||||||
set -u
|
set -u
|
||||||
|
|
||||||
INTERVAL_MINUTES="${SYNC_INTERVAL:-1}"
|
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
|
case "$INTERVAL_MINUTES" in
|
||||||
''|*[!0-9]*)
|
''|*[!0-9]*)
|
||||||
echo "ERROR: SYNC_INTERVAL must be an integer number of minutes" >&2
|
echo "ERROR: SYNC_INTERVAL must be an integer number of minutes" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$INTERVAL_MINUTES" -lt 1 ]; then
|
if [ "$INTERVAL_MINUTES" -lt 1 ]; then
|
||||||
echo "ERROR: SYNC_INTERVAL must be at least 1 minute" >&2
|
echo "ERROR: SYNC_INTERVAL must be at least 1 minute" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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))
|
INTERVAL_SECONDS=$((INTERVAL_MINUTES * 60))
|
||||||
sleep_pid=""
|
sleep_pid=""
|
||||||
|
|
||||||
@@ -21,22 +48,45 @@ shutdown() {
|
|||||||
[ -z "$sleep_pid" ] || kill "$sleep_pid" 2>/dev/null || true
|
[ -z "$sleep_pid" ] || kill "$sleep_pid" 2>/dev/null || true
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
trap shutdown INT TERM
|
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 "DNS sync service started"
|
||||||
echo "Sync interval: ${INTERVAL_MINUTES} minute(s)"
|
echo "Sync interval: ${INTERVAL_MINUTES} minute(s)"
|
||||||
echo "Source plugin: ${PROXY:-npm}"
|
echo "Reconciliation mode: $([ "$APPLY_MODE" = true ] && echo APPLY || echo READ-ONLY)"
|
||||||
echo "DNS provider: ${DNS_PROVIDER:-verizon}"
|
echo "State database: ${STATE_DB}"
|
||||||
|
echo "Enabled plugins: ${ENABLED_PLUGINS:-npm}"
|
||||||
|
echo "External plugin path: ${DNS_SYNC_PLUGIN_PATH:-/plugins}"
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
echo
|
echo
|
||||||
echo "=== DNS sync: $(date -Iseconds) ==="
|
echo "=== DNS reconciliation: $(date -Iseconds) ==="
|
||||||
if python -m dns_sync sync; then
|
|
||||||
echo "Sync run completed successfully"
|
if run_reconcile; then
|
||||||
|
echo "Reconciliation run completed successfully"
|
||||||
else
|
else
|
||||||
rc=$?
|
rc=$?
|
||||||
echo "ERROR: Sync run failed with exit code ${rc}" >&2
|
echo "ERROR: Reconciliation run failed with exit code ${rc}" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep "$INTERVAL_SECONDS" &
|
sleep "$INTERVAL_SECONDS" &
|
||||||
sleep_pid=$!
|
sleep_pid=$!
|
||||||
wait "$sleep_pid" || true
|
wait "$sleep_pid" || true
|
||||||
|
|||||||
Reference in New Issue
Block a user