Compare commits

..

10 Commits

Author SHA1 Message Date
Snigdhajyoti Ghosh
2c9b686062 Enhancements: Upgraded Actions, Improved Logging, Restructured Project (#1)
* update python project use setup tools, and upgrade github action version

* add manual run trigger

* add check not found check

* remove TODO as not possible

* Update doc and add default value of non required config

---------

Co-authored-by: Snigdhajyoti Ghosh <snigdhasjg@users.noreply.github.com>
2024-06-24 20:40:41 +02:00
Merijntje Tak
f601634857 Format README.md 2022-07-11 11:54:51 +02:00
Merijntje Tak
f037b8f859 Added LICENSE.md 2022-07-10 18:36:19 +02:00
Merijntje Tak
2c11e9db30 Added timestamps for logging 2022-07-10 18:22:17 +02:00
Merijntje Tak
953cd9ced3 Fixed config value listing 2022-07-10 18:12:56 +02:00
Merijntje Tak
1c924a45fb Added ignore label 2022-07-10 18:07:19 +02:00
Merijntje Tak
276f77b7c2 Unbuffered output 2022-07-10 17:57:26 +02:00
Merijntje Tak
84bcfca7af Added unbuffered output for docker logging 2022-07-10 17:54:01 +02:00
Merijntje Tak
4d24766145 Added typing for config values 2022-07-10 17:45:17 +02:00
Merijntje Tak
0337571499 Added config dump 2022-07-10 17:35:02 +02:00
16 changed files with 263 additions and 243 deletions

View File

@@ -1,5 +1,6 @@
**
!Pipfile
!main.py
!lib/
!docker_nsupdate_ddns
!pyproject.toml
!README.md
!LICENSE.md

View File

@@ -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

View File

@@ -1,24 +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
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"]

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Merijntje Tak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
Pipfile
View File

@@ -1,11 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
docker = "*"
python-dotenv = "*"
dnspython = "*"

89
Pipfile.lock generated
View File

@@ -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": {}
}

View File

@@ -4,40 +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 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 <<EOF >configfile
DOCKER_SOCKET=/var/run/docker.sock
cat <<EOF > configfile
DOMAIN=int.mtak.nl
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 \
@@ -47,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/*";`
@@ -86,12 +93,12 @@ docker run -d \
- [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:
- [ ] IPv6 support
- [x] Detect hostname in decreasing order of priority:
- label
- Container name
- [x]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)
### Nice to have

View File

@@ -1,9 +0,0 @@
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
REFRESH_INTERVAL=10
ONE_SHOT=True
NAMESERVER=10.100.0.11
TSIG_NAME=dck1
TSIG_KEY=SyYhvI=

View File

@@ -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

View File

@@ -1,7 +1,11 @@
import logging
import docker
config = {}
LOG = logging.getLogger(__name__)
def get_container_name(container):
"""
@@ -45,6 +49,10 @@ def generate_container_list():
ipam4 = {}
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)
container_ip = get_container_ip(container)
if container_ip:

View File

@@ -1,3 +1,5 @@
import logging
import dns.update
import dns.query
import dns.tsigkeyring
@@ -5,12 +7,14 @@ import ipaddress
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("Adding record for " + hostname + "(" + ip + ")")
LOG.info(f"Adding record for {hostname}({ip})")
rrtype = "A"
address = ipaddress.ip_address(ip)
@@ -20,15 +24,16 @@ 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("Deleting record for " + hostname + "(" + ip + ")")
LOG.info(f"Deleting record for {hostname}({ip})")
update = dns.update.Update(config['DOMAIN'], keyring=keyring)
update.delete(hostname)
dns.query.tcp(update, config['NAMESERVER'], timeout=2)

105
docker_nsupdate_ddns/main.py Executable file
View File

@@ -0,0 +1,105 @@
import os
import stat
import sys
from dotenv import dotenv_values
import time
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()
check_required_vars(config)
if not eval(config['ONE_SHOT']):
while True:
loop()
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():
container.init(config)
ipam4 = container.generate_container_list()
global ipam4_old
additions4 = determine_additions(ipam4, ipam4_old)
deletions4 = determine_deletions(ipam4, ipam4_old)
nsupdate.init(config)
nsupdate.delete_records(deletions4)
nsupdate.add_records(additions4)
ipam4_old = ipam4
def get_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
}
return x
def determine_additions(ipam, ipam_old):
return {k: v for k, v in ipam.items() if k not in ipam_old}
def determine_deletions(ipam, ipam_old):
return {k: v for k, v in ipam_old.items() if k not in ipam}
pass
if __name__ == "__main__":
main()

73
main.py
View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
import os
import stat
import sys
from dotenv import dotenv_values
import time
from lib import *
config = {}
ipam4_old = {}
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)
if not config['ONE_SHOT']:
while True:
loop()
time.sleep(config['REFRESH_INTERVAL'])
loop()
def loop():
container.init(config)
ipam4 = container.generate_container_list()
global ipam4_old
additions4 = determine_additions(ipam4, ipam4_old)
deletions4 = determine_deletions(ipam4, ipam4_old)
nsupdate.init(config)
nsupdate.delete_records(deletions4)
nsupdate.add_records(additions4)
ipam4_old = ipam4
def get_config():
config_file = sys.argv[1] if len(sys.argv) >= 2 else 'config'
x = {
**dotenv_values(os.path.join(os.getcwd(), config_file)),
**os.environ
}
return x
def determine_additions(ipam, ipam_old):
return {k: v for k, v in ipam.items() if k not in ipam_old}
def determine_deletions(ipam, ipam_old):
return {k: v for k, v in ipam_old.items() if k not in ipam}
pass
if __name__ == "__main__":
main()

39
pyproject.toml Normal file
View File

@@ -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"

11
sample.config.env Normal file
View File

@@ -0,0 +1,11 @@
DOMAIN=int.mtak.nl
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