diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6afe73c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +** + +!Pipfile +!main.py +!lib/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bd54853 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Pipfile b/Pipfile index 4889fbc..d10c159 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,3 @@ verify_ssl = true docker = "*" python-dotenv = "*" dnspython = "*" - -[requires] -python_version = "3.6" diff --git a/README.md b/README.md index e5b33ad..8bc035a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. @@ -13,22 +13,25 @@ The names of the environment variables is the same as in the config file. Enviro ### Environment variables ```bash 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 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 REFRESH_INTERVAL=5 \ -e ONE_SHOT=True \ -e NAMESERVER=10.100.0.11 \ -e TSIG_NAME=dck1 \ -e TSIG_KEY=SyYXDCJ4kIs3qhvI= \ - merijntjetak/docker-bind/ddns:latest + merijntjetak/docker-nsupdate-ddns:latest ``` ### Config file ```bash cat <configfile +DOCKER_SOCKET=/var/run/docker.sock 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 REFRESH_INTERVAL=5 ONE_SHOT=True @@ -38,15 +41,17 @@ TSIG_KEY=SyYXDCJ4kIs3qhvI= EOF docker run -d \ - -v `pwd`/configfile:/configfile - merijntjetak/docker-bind-ddns:latest /configfile + -v `pwd`/configfile:/configfile \ + -v /var/run/docker.sock:/var/run/docker.sock \ + merijntjetak/docker-nsupdate-ddns:latest /configfile ``` ### 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. -- `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 - `REFRESH_INTERVAL` - Interval between updates - `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 ### Requirements -- Eventual redunancy (Bind9 zone transfers to secondary) -- Support for multiple individual Docker servers -- IPv6 support -- Detect hostname in decreasing order of priority: +- [x] Eventual redundancy (Bind9 zone transfers to secondary) +- [x] Support for multiple individual Docker servers +- [ ]IPv6 support +- [x]Detect hostname in decreasing order of priority: - label - Container name -- Forward-only -- Clean up DNS (DNS is stateful but the script isn't, so there might be a mismatch) +- [x]Forward-only +- [ ]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 ### Alternatives @@ -98,3 +102,4 @@ docker run -d \ CoreDNS didn't fit the requirements, because zone transfers out of a CoreDNS server do not include the records from the coredns-dockerdiscovery plugin. +K3s and k8s would probably do this, but incur significant complexity over a standalone Docker instance. diff --git a/config.sample b/config.sample index d7ae0a6..7170888 100644 --- a/config.sample +++ b/config.sample @@ -1,3 +1,4 @@ +DOCKER_SOCKET=/var/run/docker.sock DOMAIN=int.mtak.nl HOSTNAME_LABEL=nl.mtak.docker-bind-ddns.hostname DEFAULT_NETWORK=10.100.0.192/26 diff --git a/lib/container.py b/lib/container.py index ee1da8d..cee1980 100644 --- a/lib/container.py +++ b/lib/container.py @@ -11,8 +11,8 @@ def get_container_name(container): """ x = container.attrs['Name'][1:] - if config['hostname_label'] in container.attrs['Config']['Labels']: - x = container.attrs['Config']['Labels'][config['hostname_label']] + if config['HOSTNAME_LABEL'] in container.attrs['Config']['Labels']: + x = container.attrs['Config']['Labels'][config['HOSTNAME_LABEL']] x = x.replace("_", "-") # Be compliant with RFC1035 return x @@ -28,11 +28,12 @@ def get_container_ip(container): x = container.attrs['NetworkSettings']['IPAddress'] 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'] - if config['default_network'] in container.attrs['NetworkSettings']['Networks']: - x = container.attrs['NetworkSettings']['Networks'][config['default_network']] + if config['DEFAULT_NETWORK'] in container.attrs['NetworkSettings']['Networks']: + x = container.attrs['NetworkSettings']['Networks'][config['DEFAULT_NETWORK']] return x diff --git a/lib/nsupdate.py b/lib/nsupdate.py index dd5ed17..6612e50 100644 --- a/lib/nsupdate.py +++ b/lib/nsupdate.py @@ -7,7 +7,8 @@ config = {} 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(): print("Adding record for " + hostname + "(" + ip + ")") @@ -18,18 +19,19 @@ def add_records(records): if isinstance(address, ipaddress.IPv6Address): rrtype = "AAAA" - update = dns.update.Update(config['domain'], keyring=keyring) + update = dns.update.Update(config['DOMAIN'], keyring=keyring) 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): - 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(): 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) - dns.query.tcp(update, config['nameserver'], timeout=2) + dns.query.tcp(update, config['NAMESERVER'], timeout=2) def init(_config): diff --git a/main.py b/main.py index 15fc85a..c9de531 100755 --- a/main.py +++ b/main.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 import os +import stat import sys -from dotenv import load_dotenv +from dotenv import dotenv_values import time from lib import * @@ -12,12 +13,23 @@ ipam4_old = {} 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: loop() - time.sleep(config['refresh_interval']) + time.sleep(config['REFRESH_INTERVAL']) loop() @@ -39,16 +51,13 @@ def loop(): def get_config(): config_file = sys.argv[1] if len(sys.argv) >= 2 else 'config' - load_dotenv(config_file) - config['domain'] = os.environ.get("DOMAIN") - config['hostname_label'] = os.environ.get("HOSTNAME_LABEL") - config['default_network'] = os.environ.get("DEFAULT_NETWORK") - 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") - config['tsig_name'] = os.environ.get("TSIG_NAME") - config['tsig_key'] = os.environ.get("TSIG_KEY") + x = { + **dotenv_values(os.path.join(os.getcwd(), config_file)), + **os.environ + } + + return x def determine_additions(ipam, ipam_old): diff --git a/make.sh b/make.sh new file mode 100755 index 0000000..608df7d --- /dev/null +++ b/make.sh @@ -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 <