docker-hoster/hoster.py

192 lines
5.1 KiB
Python

import docker
import argparse
import shutil
import signal
import sys
import json
label_name = "hoster.domains"
start_pattern = "#-----------Docker-Hoster-Domains----------\n"
end_pattern = "#-----Do-not-add-hosts-after-this-line-----\n"
hosts_path = "/tmp/hosts"
hosts = {}
dockerClient = docker.DockerClient(base_url="unix:///var/run/docker.sock")
def signal_handler(sig, frame):
global hosts
hosts = {}
update_hosts_file()
sys.exit(0)
def main():
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
args = parse_args()
global hosts_path
hosts_path = args.file
global dockerClient
dockerClient = docker.DockerClient(base_url=f"unix://{args.socket}")
# load running containers
for c in dockerClient.containers.list():
hosts[c.id] = extract_container_info(c)
update_hosts_file()
print("Listening for Docker events...")
# listen for container events
for raw in dockerClient.events(decode=False):
try:
event = json.loads(raw.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
continue
if event.get("Type") != "container":
continue
action = event.get("Action")
container_id = event.get("id")
if action == "start":
c = get_container(container_id)
if c:
hosts[container_id] = extract_container_info(c)
update_hosts_file()
elif action in ("stop", "die", "destroy"):
if container_id in hosts:
hosts.pop(container_id)
update_hosts_file()
elif action == "rename":
c = get_container(container_id)
if c:
hosts[container_id] = extract_container_info(c)
update_hosts_file()
def get_container(container_id):
try:
return dockerClient.containers.get(container_id)
except docker.errors.NotFound:
return None
def extract_container_info(container):
info = container.attrs
name = info["Name"].strip("/")
hostname = info["Config"]["Hostname"]
domain = info["Config"]["Domainname"]
if domain:
hostname = f"{hostname}.{domain}"
result = []
networks = info["NetworkSettings"]["Networks"]
for net_name, net in networks.items():
ip = net.get("IPAddress")
if not ip:
continue
aliases = net.get("Aliases") or []
domains = set(aliases + [name, hostname])
result.append({
"ip": ip,
"name": name,
"domains": domains
})
# fallback IP
default_ip = info["NetworkSettings"].get("IPAddress")
if default_ip:
result.append({
"ip": default_ip,
"name": name,
"domains": set([name, hostname])
})
# add extra domains from label if present
labels = info["Config"]["Labels"]
extra_domains = []
if label_name in labels:
extra = labels[label_name]
extra_domains = [d.strip() for d in extra.split(",") if d.strip()]
for res in result:
res["domains"].update(extra_domains)
return result
def update_hosts_file():
if len(hosts) == 0:
print("Clearing hosts file...")
else:
print("Updating hosts file with:")
for id, addresses in hosts.items():
for addr in addresses:
print(f"ip: {addr['ip']} domains: {addr['domains']}")
# read existing hosts
try:
with open(hosts_path, "r") as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
# remove existing section(s) properly
clean_lines = []
i = 0
while i < len(lines):
if lines[i] == start_pattern:
# skip until after end_pattern
while i < len(lines) and lines[i] != end_pattern:
i += 1
if i < len(lines) and lines[i] == end_pattern:
i += 1 # skip the end_pattern
else:
clean_lines.append(lines[i])
i += 1
lines = clean_lines
# trim trailing empty lines
while lines and not lines[-1].strip():
lines.pop()
# add new entries
if hosts:
lines.append("\n") # single empty line before section
lines.append(start_pattern)
for id, addrs in hosts.items():
for addr in addrs:
domains_str = " ".join(sorted(addr["domains"]))
lines.append(f"{addr['ip']}\t{domains_str}\n")
lines.append(end_pattern)
aux = hosts_path + ".aux"
with open(aux, "w") as f:
f.writelines(lines)
shutil.move(aux, hosts_path)
def parse_args():
parser = argparse.ArgumentParser(description='Synchronize running docker container IPs with host /etc/hosts file.')
parser.add_argument('socket', type=str, nargs="?", default="/tmp/docker.sock", help='The docker socket to listen for docker events.')
parser.add_argument('file', type=str, nargs="?", default="/tmp/hosts", help='The /etc/hosts file to sync the containers with.')
return parser.parse_args()
if __name__ == "__main__":
main()