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>
This commit is contained in:
Snigdhajyoti Ghosh
2024-06-25 00:10:41 +05:30
committed by GitHub
parent f601634857
commit 2c9b686062
13 changed files with 170 additions and 198 deletions

View File

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

View File

@@ -1,6 +1,7 @@
name: docker-build-and-push name: docker-build-and-push
on: on:
workflow_dispatch:
push: push:
branches: branches:
- 'main' - 'main'
@@ -10,17 +11,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v6
with: with:
push: true push: true
tags: merijntjetak/docker-nsupdate-ddns:latest tags: ${{ secrets.DOCKERHUB_USERNAME }}/docker-nsupdate-ddns:latest
platforms: linux/amd64,linux/arm64

View File

@@ -1,25 +1,17 @@
FROM python:alpine AS base FROM python:3.12-alpine AS base
ENV LANG C.UTF-8 ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8 ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1 ENV PYTHONFAULTHANDLER 1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED 1
FROM base AS python-build WORKDIR /build
RUN pip install pipenv
COPY Pipfile . COPY . .
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy --python /usr/local/bin/python RUN pip install --no-cache-dir .
FROM base AS runtime WORKDIR /
RUN rm -rf /build
# Copy virtual env from python-deps stage ENTRYPOINT ["docker-nsupdate-ddns"]
COPY --from=python-build /.venv /.venv
ENV PATH="/.venv/bin:$PATH"
RUN mkdir /app
WORKDIR /app
COPY . /app/
ENTRYPOINT ["/app/main.py"]

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,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. 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. 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 ### Environment variables
```bash ```bash
docker run -d \ docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \ -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 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 NAMESERVER=10.100.0.11 \
-e TSIG_NAME=dck1 \ -e TSIG_NAME=dck1 \
-e TSIG_KEY=SyYXDCJ4kIs3qhvI= \ -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 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
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 NAMESERVER=10.100.0.11
TSIG_NAME=dck1 TSIG_NAME=dck1
TSIG_KEY=SyYXDCJ4kIs3qhvI= 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 EOF
docker run -d \ 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 ### Bind9 integration
1. Generate a key 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/*";` `include "/etc/bind/keys/*";`

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 import docker
config = {} config = {}
LOG = logging.getLogger(__name__)
def get_container_name(container): def get_container_name(container):
""" """
@@ -46,6 +50,7 @@ def generate_container_list():
for container in container_list: for container in container_list:
if config['IGNORE_LABEL'] in container.attrs['Config']['Labels']: if config['IGNORE_LABEL'] in container.attrs['Config']['Labels']:
LOG.debug(f"Ignoring container {container.attrs['Name']} as ignore label present")
continue continue
container_name = get_container_name(container) container_name = get_container_name(container)

View File

@@ -1,19 +1,20 @@
import logging
import dns.update import dns.update
import dns.query import dns.query
import dns.tsigkeyring import dns.tsigkeyring
import ipaddress import ipaddress
from datetime import datetime
config = {} config = {}
LOG = logging.getLogger(__name__)
def add_records(records): def add_records(records):
keyring = dns.tsigkeyring.from_text( keyring = dns.tsigkeyring.from_text({config['TSIG_NAME']: config['TSIG_KEY']})
{config['TSIG_NAME']: config['TSIG_KEY']})
for hostname, ip in records.items(): for hostname, ip in records.items():
print(datetime.now().isoformat(), end=" ") LOG.info(f"Adding record for {hostname}({ip})")
print("Adding record for " + hostname + "(" + ip + ")")
rrtype = "A" rrtype = "A"
address = ipaddress.ip_address(ip) address = ipaddress.ip_address(ip)
@@ -23,17 +24,15 @@ def add_records(records):
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, int(config['DNS_RECORD_TTL']), 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( keyring = dns.tsigkeyring.from_text({config['TSIG_NAME']: config['TSIG_KEY']})
{config['TSIG_NAME']: config['TSIG_KEY']})
for hostname, ip in records.items(): for hostname, ip in records.items():
print(datetime.now().isoformat(), end=" ") LOG.info(f"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)

View File

@@ -1,30 +1,23 @@
#!/usr/bin/env python3
import os import os
import stat import stat
import sys import sys
from dotenv import dotenv_values from dotenv import dotenv_values
import time import time
from lib import * from docker_nsupdate_ddns.lib import *
import logging
config = {} config = {}
ipam4_old = {} ipam4_old = {}
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s [%(levelname)s] %(message)s')
LOG = logging.getLogger(__name__)
def main(): def main():
global config global config
config = get_config() config = get_config()
check_required_vars(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 eval(config['ONE_SHOT']): if not eval(config['ONE_SHOT']):
while True: while True:
@@ -32,6 +25,44 @@ def main():
time.sleep(int(config['REFRESH_INTERVAL'])) time.sleep(int(config['REFRESH_INTERVAL']))
loop() 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(): def loop():
@@ -50,27 +81,14 @@ 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.env'
x = { x = {
**dotenv_values(os.path.join(os.path.dirname(__file__), 'default.config.env')),
**dotenv_values(os.path.join(os.getcwd(), config_file)), **dotenv_values(os.path.join(os.getcwd(), config_file)),
**os.environ **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 return x

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"

View File

@@ -1,10 +1,11 @@
DOCKER_SOCKET=/var/run/docker.sock
DOMAIN=int.mtak.nl 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 NAMESERVER=10.100.0.11
TSIG_NAME=dck1 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