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
!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,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"]

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.
## 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 <<EOF >configfile
DOCKER_SOCKET=/var/run/docker.sock
cat <<EOF > 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/*";`

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

View File

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

View File

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

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