#!/usr/bin/env python3 import time import re import subprocess import logging import os import json from pathlib import Path logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s' ) log = logging.getLogger(__name__) TRAEFIK_API = os.environ.get('TRAEFIK_API', 'http://:8080') DNS_SERVER = os.environ.get('DNS_SERVER', '') DNS_ZONE = os.environ.get('DNS_ZONE', '') TARGET_IP = os.environ.get('TARGET_IP', '') TTL = int(os.environ.get('TTL', '300')) POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '60')) KEYTAB = os.environ.get('KEYTAB', '/run/secrets/dns-updater-keytab') KRB_PRINCIPAL = os.environ.get('KRB_PRINCIPAL', 'dns-updater@') STATE_FILE = os.environ.get('STATE_FILE', '/tmp/dns_state.json') def kinit(): result = subprocess.run( ['kinit', '-kt', KEYTAB, KRB_PRINCIPAL], capture_output=True, text=True ) if result.returncode != 0: raise RuntimeError(f"kinit failed: {result.stderr}") log.info("Kerberos ticket obtained") def get_traefik_hostnames(): import urllib.request import urllib.error try: with urllib.request.urlopen(f"{TRAEFIK_API}/api/http/routers", timeout=10) as r: routers = json.loads(r.read()) except urllib.error.URLError as e: log.error(f"Failed to reach Traefik API: {e}") return set() hostnames = set() for router in routers: if router.get('provider') != 'docker': continue rule = router.get('rule', '') for match in re.finditer(r'Host\(`([^`]+)`\)', rule): host = match.group(1) if host.endswith(DNS_ZONE): hostnames.add(host) return hostnames def nsupdate(commands): input_data = f"{commands}\n" result = subprocess.run( ['nsupdate', '-g'], input=input_data, capture_output=True, text=True ) if result.returncode != 0: raise RuntimeError(f"nsupdate failed: {result.stderr}") def add_record(hostname): log.info(f"Adding {hostname} -> {TARGET_IP}") nsupdate(f"""server {DNS_SERVER} zone {DNS_ZONE} update add {hostname}. {TTL} A {TARGET_IP} send""") def remove_record(hostname): log.info(f"Removing {hostname}") nsupdate(f"""server {DNS_SERVER} zone {DNS_ZONE} update delete {hostname}. A send""") def load_state(): try: return set(json.loads(Path(STATE_FILE).read_text())) except Exception: return set() def save_state(hostnames): Path(STATE_FILE).write_text(json.dumps(list(hostnames))) def sync(): kinit() current = get_traefik_hostnames() previous = load_state() to_add = current - previous to_remove = previous - current for h in to_add: try: add_record(h) except Exception as e: log.error(f"Failed to add {h}: {e}") current.discard(h) for h in to_remove: try: remove_record(h) except Exception as e: log.error(f"Failed to remove {h}: {e}") save_state(current) if __name__ == '__main__': log.info("dns-traefik-sync starting") while True: try: sync() except Exception as e: log.error(f"Sync failed: {e}") time.sleep(POLL_INTERVAL)