Fixed config loading, added Dockerfile

This commit is contained in:
Merijntje Tak
2022-07-10 16:34:53 +02:00
parent 6e8bf94005
commit 5909c88c08
9 changed files with 128 additions and 42 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
**
!Pipfile
!main.py
!lib/

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM python:alpine AS base
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
FROM base AS python-build
RUN pip install pipenv
COPY Pipfile .
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy --python /usr/local/bin/python
FROM base AS runtime
# Copy virtual env from python-deps stage
COPY --from=python-build /.venv /.venv
ENV PATH="/.venv/bin:$PATH"
RUN mkdir /app
WORKDIR /app
COPY . /app/
ENTRYPOINT ["/app/main.py"]

View File

@@ -9,6 +9,3 @@ verify_ssl = true
docker = "*" docker = "*"
python-dotenv = "*" python-dotenv = "*"
dnspython = "*" dnspython = "*"
[requires]
python_version = "3.6"

View File

@@ -1,6 +1,6 @@
# docker-nsupdate-ddns # docker-nsupdate-ddns
This script pushes container/ip information from a Docker instance to a DNS server via the DNS update mechanism desribed in RFC 2136 (nsupdate). nsupdate is implemented by ISC Bind9. This script pushes container/ip information from local Docker instance to a DNS server via the DNS update mechanism desribed in RFC 2136 (nsupdate). nsupdate is implemented by ISC Bind9.
Every REFRESH_INTERVAL seconds, it queries all the Docker containers on the local host, finds their IP and pushes it with the name to the DNS server. Every REFRESH_INTERVAL seconds, it queries all the Docker containers on the local host, finds their IP and pushes it with the name to the DNS server.
@@ -13,22 +13,25 @@ The names of the environment variables is the same as in the config file. Enviro
### Environment variables ### Environment variables
```bash ```bash
docker run -d \ docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DOCKER_SOCKET=/var/run/docker.sock \
-e DOMAIN=int.mtak.nl \ -e DOMAIN=int.mtak.nl \
-e HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname \ -e HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname \
-e DEFAULT_NETWORK=10.100.0.192/26 \ -e DEFAULT_NETWORK=10.100.0.192/26 \
-e REFRESH_INTERVAL=5 \ -e REFRESH_INTERVAL=5 \
-e ONE_SHOT=True \ -e ONE_SHOT=True \
-e NAMESERVER=10.100.0.11 \ -e NAMESERVER=10.100.0.11 \
-e TSIG_NAME=dck1 \ -e TSIG_NAME=dck1 \
-e TSIG_KEY=SyYXDCJ4kIs3qhvI= \ -e TSIG_KEY=SyYXDCJ4kIs3qhvI= \
merijntjetak/docker-bind/ddns:latest merijntjetak/docker-nsupdate-ddns:latest
``` ```
### Config file ### Config file
```bash ```bash
cat <<EOF >configfile cat <<EOF >configfile
DOCKER_SOCKET=/var/run/docker.sock
DOMAIN=int.mtak.nl DOMAIN=int.mtak.nl
HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname
DEFAULT_NETWORK=10.100.0.192/26 DEFAULT_NETWORK=10.100.0.192/26
REFRESH_INTERVAL=5 REFRESH_INTERVAL=5
ONE_SHOT=True ONE_SHOT=True
@@ -38,15 +41,17 @@ TSIG_KEY=SyYXDCJ4kIs3qhvI=
EOF EOF
docker run -d \ docker run -d \
-v `pwd`/configfile:/configfile -v `pwd`/configfile:/configfile \
merijntjetak/docker-bind-ddns:latest /configfile -v /var/run/docker.sock:/var/run/docker.sock \
merijntjetak/docker-nsupdate-ddns:latest /configfile
``` ```
### Configuration ### Configuration
- `DOCKER_SOCKET` - Sets the location of the Docker socket
- `DOMAIN` - Sets the domain in which the records are created. Needs to match the Bind zone. - `DOMAIN` - Sets the domain in which the records are created. Needs to match the Bind zone.
- `HOSTNAME_LABEL` - Docker label to override the default record name with. Use with `docker --label=nl.mtak.docker-bind-ddns.hostname=nginx` to get `nginx.int.mtak.nl` - `HOSTNAME_LABEL` - Docker label to override the default record name with. Use with `docker --label=nl.mtak.docker-nsupdate-ddns.hostname=nginx` to get `nginx.int.mtak.nl`
- `DEFAULT_NETWORK` - Preferred network to find IP for, in case there are multiple networks - `DEFAULT_NETWORK` - Preferred network to find IP for, in case there are multiple networks
- `REFRESH_INTERVAL` - Interval between updates - `REFRESH_INTERVAL` - Interval between updates
- `ONE_SHOT` - Set to True for the script to update once and immediately quit (nice for debugging) - `ONE_SHOT` - Set to True for the script to update once and immediately quit (nice for debugging)
@@ -79,18 +84,17 @@ docker run -d \
## Design ## Design
### Requirements ### Requirements
- Eventual redunancy (Bind9 zone transfers to secondary) - [x] Eventual redundancy (Bind9 zone transfers to secondary)
- Support for multiple individual Docker servers - [x] Support for multiple individual Docker servers
- IPv6 support - [ ]IPv6 support
- Detect hostname in decreasing order of priority: - [x]Detect hostname in decreasing order of priority:
- label - label
- Container name - Container name
- Forward-only - [x]Forward-only
- Clean up DNS (DNS is stateful but the script isn't, so there might be a mismatch) - [ ]Clean up DNS (DNS is stateful but the script isn't, so there might be a mismatch)
### Todo ### Nice to have
- Make event-driven with [Ahab](https://github.com/instacart/ahab)
- Add tests - Add tests
### Alternatives ### Alternatives
@@ -98,3 +102,4 @@ docker run -d \
CoreDNS didn't fit the requirements, because zone transfers out of a CoreDNS server do not CoreDNS didn't fit the requirements, because zone transfers out of a CoreDNS server do not
include the records from the coredns-dockerdiscovery plugin. include the records from the coredns-dockerdiscovery plugin.
K3s and k8s would probably do this, but incur significant complexity over a standalone Docker instance.

View File

@@ -1,3 +1,4 @@
DOCKER_SOCKET=/var/run/docker.sock
DOMAIN=int.mtak.nl DOMAIN=int.mtak.nl
HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname
DEFAULT_NETWORK=10.100.0.192/26 DEFAULT_NETWORK=10.100.0.192/26

View File

@@ -11,8 +11,8 @@ def get_container_name(container):
""" """
x = container.attrs['Name'][1:] x = container.attrs['Name'][1:]
if config['hostname_label'] in container.attrs['Config']['Labels']: if config['HOSTNAME_LABEL'] in container.attrs['Config']['Labels']:
x = container.attrs['Config']['Labels'][config['hostname_label']] x = container.attrs['Config']['Labels'][config['HOSTNAME_LABEL']]
x = x.replace("_", "-") # Be compliant with RFC1035 x = x.replace("_", "-") # Be compliant with RFC1035
return x return x
@@ -28,11 +28,12 @@ def get_container_ip(container):
x = container.attrs['NetworkSettings']['IPAddress'] x = container.attrs['NetworkSettings']['IPAddress']
if next(iter(container.attrs['NetworkSettings']['Networks'])): if next(iter(container.attrs['NetworkSettings']['Networks'])):
network_name = next(iter(container.attrs['NetworkSettings']['Networks'])) network_name = next(
iter(container.attrs['NetworkSettings']['Networks']))
x = container.attrs['NetworkSettings']['Networks'][network_name]['IPAddress'] x = container.attrs['NetworkSettings']['Networks'][network_name]['IPAddress']
if config['default_network'] in container.attrs['NetworkSettings']['Networks']: if config['DEFAULT_NETWORK'] in container.attrs['NetworkSettings']['Networks']:
x = container.attrs['NetworkSettings']['Networks'][config['default_network']] x = container.attrs['NetworkSettings']['Networks'][config['DEFAULT_NETWORK']]
return x return x

View File

@@ -7,7 +7,8 @@ config = {}
def add_records(records): def add_records(records):
keyring = dns.tsigkeyring.from_text({config['tsig_name']: config['tsig_key']}) keyring = dns.tsigkeyring.from_text(
{config['TSIG_NAME']: config['TSIG_KEY']})
for hostname, ip in records.items(): for hostname, ip in records.items():
print("Adding record for " + hostname + "(" + ip + ")") print("Adding record for " + hostname + "(" + ip + ")")
@@ -18,18 +19,19 @@ def add_records(records):
if isinstance(address, ipaddress.IPv6Address): if isinstance(address, ipaddress.IPv6Address):
rrtype = "AAAA" rrtype = "AAAA"
update = dns.update.Update(config['domain'], keyring=keyring) update = dns.update.Update(config['DOMAIN'], keyring=keyring)
update.add(hostname, 60, rrtype, ip) update.add(hostname, 60, rrtype, ip)
dns.query.tcp(update, config['nameserver'], timeout=2) dns.query.tcp(update, config['NAMESERVER'], timeout=2)
def delete_records(records): def delete_records(records):
keyring = dns.tsigkeyring.from_text({config['tsig_name']: config['tsig_key']}) keyring = dns.tsigkeyring.from_text(
{config['TSIG_NAME']: config['TSIG_KEY']})
for hostname, ip in records.items(): for hostname, ip in records.items():
print("Deleting record for " + hostname + "(" + ip + ")") print("Deleting record for " + hostname + "(" + ip + ")")
update = dns.update.Update(config['domain'], keyring=keyring) update = dns.update.Update(config['DOMAIN'], keyring=keyring)
update.delete(hostname) update.delete(hostname)
dns.query.tcp(update, config['nameserver'], timeout=2) dns.query.tcp(update, config['NAMESERVER'], timeout=2)
def init(_config): def init(_config):

35
main.py
View File

@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import stat
import sys import sys
from dotenv import load_dotenv from dotenv import dotenv_values
import time import time
from lib import * from lib import *
@@ -12,12 +13,23 @@ ipam4_old = {}
def main(): def main():
get_config() global config
config = get_config()
if not config['one_shot']: try:
stat.S_ISSOCK(os.stat(config['DOCKER_SOCKET']).st_mode)
except Exception as e:
print(e)
print(
"Docker socket " +
config['DOCKER_SOCKET'] +
" not found, exiting...")
sys.exit(1)
if not config['ONE_SHOT']:
while True: while True:
loop() loop()
time.sleep(config['refresh_interval']) time.sleep(config['REFRESH_INTERVAL'])
loop() loop()
@@ -39,16 +51,13 @@ def loop():
def get_config(): def get_config():
config_file = sys.argv[1] if len(sys.argv) >= 2 else 'config' config_file = sys.argv[1] if len(sys.argv) >= 2 else 'config'
load_dotenv(config_file)
config['domain'] = os.environ.get("DOMAIN") x = {
config['hostname_label'] = os.environ.get("HOSTNAME_LABEL") **dotenv_values(os.path.join(os.getcwd(), config_file)),
config['default_network'] = os.environ.get("DEFAULT_NETWORK") **os.environ
config['refresh_interval'] = int(os.environ.get("REFRESH_INTERVAL")) }
config['one_shot'] = os.environ.get("ONE_SHOT").lower() in ['true', 'yes']
config['nameserver'] = os.environ.get("NAMESERVER") return x
config['tsig_name'] = os.environ.get("TSIG_NAME")
config['tsig_key'] = os.environ.get("TSIG_KEY")
def determine_additions(ipam, ipam_old): def determine_additions(ipam, ipam_old):

42
make.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
VERSION="1.0"
NAMESPACE='merijntjetak'
IMAGENAME='docker-nsupdate-ddns'
DOCKER_REGISTRY_HOST=''
DOCKER_REGISTRY_PORT=''
# Generate docker URI
if [[ "x$DOCKER_REGISTRY_HOST" != "x" && "x$DOCKER_REGISTRY_PORT" != "x" ]]; then
DOCKER_REGISTRY_AUTHORITY="${DOCKER_REGISTRY_HOST}:${DOCKER_REGISTRY_PORT}/"
else
DOCKER_REGISTRY_AUTHORITY=''
fi
if [[ "$1" == "build" ]]; then
docker build -t ${NAMESPACE}/${IMAGENAME} -t ${NAMESPACE}/${IMAGENAME}:${VERSION} .
elif [[ "$1" == "run" ]]; then
docker run -ti --rm \
-v '/var/run/docker.sock:/var/run/docker.sock' \
-v `pwd`/config:/config \
${NAMESPACE}/${IMAGENAME}:${VERSION} \
/config
elif [[ "$1" == "publish" ]]; then
docker tag ${PUBLISHER}/${IMAGENAME}:${VERSION} ${DOCKER_REGISTRY_AUTHORITY}${NAMESPACE}/${IMAGENAME}:$VERSION
docker push ${DOCKER_REGISTRY_AUTHORITY}${NAMESPACE}/${IMAGENAME}:$VERSION
else
cat <<EOF
Usage: $0 action
Actions:
build - Build docker image
run - Run docker image
publish - Publish docker image
Change the parameters in the run section to adjust for local testing variables.
EOF
fi