diff --git a/.dockerignore b/.dockerignore index 6afe73c..444cbcc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ ** -!Pipfile -!main.py -!lib/ +!docker_nsupdate_ddns +!pyproject.toml +!README.md +!LICENSE.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8454d0e..77bb50e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,7 @@ name: docker-build-and-push on: + workflow_dispatch: push: branches: - 'main' @@ -10,17 +11,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: push: true - tags: merijntjetak/docker-nsupdate-ddns:latest + tags: ${{ secrets.DOCKERHUB_USERNAME }}/docker-nsupdate-ddns:latest + platforms: linux/amd64,linux/arm64 diff --git a/Dockerfile b/Dockerfile index 33afcd9..7db2131 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,17 @@ -FROM python:alpine AS base +FROM python:3.12-alpine AS base ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 -ENV PYTHONUNBUFFERED=1 +ENV PYTHONUNBUFFERED 1 -FROM base AS python-build -RUN pip install pipenv +WORKDIR /build -COPY Pipfile . -RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy --python /usr/local/bin/python +COPY . . +RUN pip install --no-cache-dir . -FROM base AS runtime +WORKDIR / +RUN rm -rf /build -# 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"] +ENTRYPOINT ["docker-nsupdate-ddns"] diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d10c159..0000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] - -[packages] -docker = "*" -python-dotenv = "*" -dnspython = "*" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index e3c49e7..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,89 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "2a9a9c218218349a7c550a97bb3bc3b6113227211c48e027755bdc8baf5822a1" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.6" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "version": "==2022.6.15" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3'", - "version": "==2.0.12" - }, - "dnspython": { - "hashes": [ - "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", - "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" - ], - "index": "pypi", - "version": "==2.2.1" - }, - "docker": { - "hashes": [ - "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0", - "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb" - ], - "index": "pypi", - "version": "==5.0.3" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "python-dotenv": { - "hashes": [ - "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", - "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" - ], - "index": "pypi", - "version": "==0.20.0" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "version": "==2.27.1" - }, - "urllib3": { - "hashes": [ - "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", - "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" - ], - "version": "==1.26.10" - }, - "websocket-client": { - "hashes": [ - "sha256:074e2ed575e7c822fc0940d31c3ac9bb2b1142c303eafcf3e304e6ce035522e8", - "sha256:6278a75065395418283f887de7c3beafb3aa68dada5cacbe4b214e8d26da499b" - ], - "version": "==1.3.1" - } - }, - "develop": {} -} diff --git a/README.md b/README.md index 5430611..d873050 100644 --- a/README.md +++ b/README.md @@ -4,42 +4,59 @@ This script pushes container/ip information from local Docker instance to a DNS 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. -## Running +## Configuration +The script takes environment variables or a config file. A sample config file is provided in [`sample.config.env`](sample.config.env). -The script takes environment variables or a config file. A sample config file is provided in `config.sample`. The file name of the config file can be passed as argument, but defaults to `config`. +The file name of the config file can be passed as argument, but defaults to `/config.env`. The names of the environment variables is the same as in the config file. Environment variables have precendence over the config file. +| Config | Required | Default Value | Description | +|------------------|----------|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DOMAIN | Yes | | Sets the domain in which the records are created. Needs to match the Bind zone. | +| NAMESERVER | Yes | | Nameserver to push updates to. | +| TSIG_NAME | Yes | | TSIG key name for secure updates. | +| TSIG_KEY | Yes | | TSIG key value for secure updates. | +| DOCKER_SOCKET | No | /var/run/docker.sock | Sets the location of the Docker socket. | +| HOSTNAME_LABEL | No | nl.mtak.docker-nsupdate-ddns.hostname | 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` _If the label value present on the container, use it as hostname otherwise the container name._ | +| IGNORE_LABEL | No | nl.mtak.docker-nsupdate-ddns.ignore | Container label to exclude containers from DNS updates. | +| DNS_RECORD_TTL | No | 60 | Time to Live (TTL) for DNS records (seconds). | +| DEFAULT_NETWORK | No | | Preferred network name to find IP for, in case there are multiple networks. | +| REFRESH_INTERVAL | No | 60 | Interval between checks for container changes (seconds). | +| ONE_SHOT | No | False | Run once and exit instead of continuously monitoring. | + ### 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 IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore \ - -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= \ + -e DOCKER_SOCKET=/var/run/docker.sock \ + -e HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname \ + -e IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore \ + -e DNS_RECORD_TTL=60 \ + -e DEFAULT_NETWORK=10.100.0.192/26 \ + -e REFRESH_INTERVAL=60 \ + -e ONE_SHOT=true \ merijntjetak/docker-nsupdate-ddns:latest ``` ### Config file ```bash -cat <configfile -DOCKER_SOCKET=/var/run/docker.sock +cat < configfile DOMAIN=int.mtak.nl -IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore -HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname -DEFAULT_NETWORK=10.100.0.192/26 -REFRESH_INTERVAL=5 -ONE_SHOT=True NAMESERVER=10.100.0.11 TSIG_NAME=dck1 TSIG_KEY=SyYXDCJ4kIs3qhvI= +DOCKER_SOCKET=/var/run/docker.sock +HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname +IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore +DNS_RECORD_TTL=60 +DEFAULT_NETWORK=10.100.0.192/26 +REFRESH_INTERVAL=60 +ONE_SHOT=False EOF docker run -d \ @@ -49,25 +66,13 @@ docker run -d \ ``` -### 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-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) -- `NAMESERVER` - Nameserver to push updates to -- `TSIG_NAME` - Name of the TSIG key -- `TSIG_KEY` - TSIG-KEY - ### Bind9 integration 1. Generate a key - `tsig-keygen clientname >/etc/bind/keys/clientname.key` + `tsig-keygen clientname > /etc/bind/keys/clientname.key` -2. Include keys in your Bind9 configuration +2. Include keys in your Bind9 `named.config` configuration file `include "/etc/bind/keys/*";` diff --git a/docker_nsupdate_ddns/default.config.env b/docker_nsupdate_ddns/default.config.env new file mode 100644 index 0000000..9ecf261 --- /dev/null +++ b/docker_nsupdate_ddns/default.config.env @@ -0,0 +1,7 @@ +DOCKER_SOCKET=/var/run/docker.sock +HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname +IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore +DNS_RECORD_TTL=60 +REFRESH_INTERVAL=60 +ONE_SHOT=False +DEFAULT_NETWORK=None diff --git a/lib/__init__.py b/docker_nsupdate_ddns/lib/__init__.py similarity index 100% rename from lib/__init__.py rename to docker_nsupdate_ddns/lib/__init__.py diff --git a/lib/container.py b/docker_nsupdate_ddns/lib/container.py similarity index 92% rename from lib/container.py rename to docker_nsupdate_ddns/lib/container.py index c53bd14..8199bc7 100644 --- a/lib/container.py +++ b/docker_nsupdate_ddns/lib/container.py @@ -1,7 +1,11 @@ +import logging + import docker config = {} +LOG = logging.getLogger(__name__) + def get_container_name(container): """ @@ -46,6 +50,7 @@ def generate_container_list(): for container in container_list: if config['IGNORE_LABEL'] in container.attrs['Config']['Labels']: + LOG.debug(f"Ignoring container {container.attrs['Name']} as ignore label present") continue container_name = get_container_name(container) diff --git a/lib/nsupdate.py b/docker_nsupdate_ddns/lib/nsupdate.py similarity index 62% rename from lib/nsupdate.py rename to docker_nsupdate_ddns/lib/nsupdate.py index 34c75f4..c11fb1e 100644 --- a/lib/nsupdate.py +++ b/docker_nsupdate_ddns/lib/nsupdate.py @@ -1,19 +1,20 @@ +import logging + import dns.update import dns.query import dns.tsigkeyring import ipaddress -from datetime import datetime config = {} +LOG = logging.getLogger(__name__) + 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(datetime.now().isoformat(), end=" ") - print("Adding record for " + hostname + "(" + ip + ")") + LOG.info(f"Adding record for {hostname}({ip})") rrtype = "A" address = ipaddress.ip_address(ip) @@ -23,17 +24,15 @@ def add_records(records): rrtype = "AAAA" update = dns.update.Update(config['DOMAIN'], keyring=keyring) - update.add(hostname, 60, rrtype, ip) + update.add(hostname, int(config['DNS_RECORD_TTL']), rrtype, ip) 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(datetime.now().isoformat(), end=" ") - print("Deleting record for " + hostname + "(" + ip + ")") + LOG.info(f"Deleting record for {hostname}({ip})") update = dns.update.Update(config['DOMAIN'], keyring=keyring) update.delete(hostname) diff --git a/main.py b/docker_nsupdate_ddns/main.py similarity index 50% rename from main.py rename to docker_nsupdate_ddns/main.py index 0afb018..df17545 100755 --- a/main.py +++ b/docker_nsupdate_ddns/main.py @@ -1,30 +1,23 @@ -#!/usr/bin/env python3 - import os import stat import sys from dotenv import dotenv_values import time -from lib import * +from docker_nsupdate_ddns.lib import * +import logging config = {} ipam4_old = {} +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s [%(levelname)s] %(message)s') +LOG = logging.getLogger(__name__) + def main(): global config config = get_config() - - 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) + check_required_vars(config) if not eval(config['ONE_SHOT']): while True: @@ -32,6 +25,44 @@ def main(): time.sleep(int(config['REFRESH_INTERVAL'])) loop() + LOG.info("Ending the process as ONE_SHOT is True") + + +def check_required_vars(_config): + # Check for all required config + required_vars = [ + 'DOMAIN', + 'NAMESERVER', + 'TSIG_NAME', + 'DOCKER_SOCKET', + 'HOSTNAME_LABEL', + 'IGNORE_LABEL', + 'DNS_RECORD_TTL', + 'DEFAULT_NETWORK', + 'REFRESH_INTERVAL', + 'ONE_SHOT' + ] + missing_vars = [] + for item in required_vars: + if item in _config: + LOG.info(f"Detected config value: {item}={_config[item]}") + else: + missing_vars.append(item) + if 'TSIG_KEY' not in _config: + # Don't log it as it's a secret + missing_vars.append('TSIG_KEY') + if len(missing_vars) > 1: + LOG.error(f"Missing required config: {', '.join(missing_vars)}") + exit(1) + + # Check if docker socket is correct + try: + if not stat.S_ISSOCK(os.stat(_config['DOCKER_SOCKET']).st_mode): + LOG.error(f"{_config['DOCKER_SOCKET']} not a docker socket file, exiting...") + exit(1) + except Exception as e: + LOG.error(f"Docker socket {_config['DOCKER_SOCKET']} not found.", e) + raise e def loop(): @@ -50,27 +81,14 @@ def loop(): 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.env' x = { + **dotenv_values(os.path.join(os.path.dirname(__file__), 'default.config.env')), **dotenv_values(os.path.join(os.getcwd(), config_file)), **os.environ } - print("Detected config values:") - for item in [ - 'DOCKER_SOCKET', - 'DOMAIN', - 'IGNORE_LABEL', - 'HOSTNAME_LABEL', - 'DEFAULT_NETWORK', - 'REFRESH_INTERVAL', - 'ONE_SHOT', - 'NAMESERVER', - 'TSIG_NAME']: - print(item, end=": ") - print(x[item]) - return x diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..92e95df --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "docker-nsupdate-ddns" +version = "0.1" +description = "Docker DNS update in Bind9 with nsupdate based on docker container hostname or docker label" +readme = "README.md" +requires-python = ">=3" +keywords = [ + "nsupdate", + "docker", + "ddns", + "dns", + "bind9" +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python" +] +dependencies = [ + "docker", + "python-dotenv", + "dnspython" +] +license = {file = "LICENSE.md"} + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["docker_nsupdate_ddns*"] + +[tool.setuptools.package-data] +docker_nsupdate_ddns = ["default.config.env"] + +[project.urls] +homepage = "https://github.com/mtak/docker-nsupdate-ddns" +repository = "https://github.com/mtak/docker-nsupdate-ddns" + +[project.scripts] +docker-nsupdate-ddns = "docker_nsupdate_ddns.main:main" diff --git a/config.sample b/sample.config.env similarity index 78% rename from config.sample rename to sample.config.env index 2150fc4..88934d8 100644 --- a/config.sample +++ b/sample.config.env @@ -1,10 +1,11 @@ -DOCKER_SOCKET=/var/run/docker.sock DOMAIN=int.mtak.nl -IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore -HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname -DEFAULT_NETWORK=10.100.0.192/26 -REFRESH_INTERVAL=10 -ONE_SHOT=False NAMESERVER=10.100.0.11 TSIG_NAME=dck1 -TSIG_KEY=SyYhvI= +TSIG_KEY=SyYXDCJ4kIs3qhvI= +DOCKER_SOCKET=/var/run/docker.sock +HOSTNAME_LABEL=nl.mtak.docker-nsupdate-ddns.hostname +IGNORE_LABEL=nl.mtak.docker-nsupdate-ddns.ignore +DNS_RECORD_TTL=60 +DEFAULT_NETWORK=10.100.0.192/26 +REFRESH_INTERVAL=60 +ONE_SHOT=False