diff --git a/.devcontainer/ansible/roles/setup-container/defaults/main.yml b/.devcontainer/ansible/roles/setup-container/defaults/main.yml index 8dc4717..112af62 100644 --- a/.devcontainer/ansible/roles/setup-container/defaults/main.yml +++ b/.devcontainer/ansible/roles/setup-container/defaults/main.yml @@ -1,2 +1,2 @@ -dotfiles_url: "{{ lookup('env', 'DOTFILES_URL') }}" +dotfiles_url: "{{ lookup('env', 'DOTFILES_URL', default='https://github.com/ilude/dotfiles.git') }}" dotfiles_path: "{{ lookup('env', 'HOME') + '/.dotfiles' }}" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7f3aeb2..b23cbc8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -48,7 +48,6 @@ "peakchen90.open-html-in-browser", "ms-python.autopep8", "codezombiech.gitignore", - "ms-vscode.makefile-tools", "yy0931.save-as-root", "ms-python.isort", "ZainChen.json", diff --git a/Dockerfile b/Dockerfile index 062a15a..e2e6f2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,8 @@ ENV PYTHONPATH="${PYTHONPATH}:${PYTHON_DEPS_PATH}" ENV PYTHONUNBUFFERED=TRUE ENV LANGUAGE=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 ENV LANG=en_US.UTF-8 -ENV LC_ALL=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive ENV DEBCONF_NONINTERACTIVE_SEEN=true @@ -45,6 +45,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ sudo \ tzdata \ zsh && \ + # locales + echo "$(LANGUAGE)" > /etc/locale.gen && \ + locale-gen en_US.UTF-8 && \ # cleanup apt-get autoremove -fy && \ apt-get clean && \ @@ -191,8 +194,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* + RUN echo ${USER} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USER} && \ - chmod 0440 /etc/sudoers.d/${USER} + chmod 0440 /etc/sudoers.d/${USER} && \ + chown ${USER} /var/run/docker.sock USER ${USER} diff --git a/Makefile b/Makefile index 9575296..ed49b8f 100644 --- a/Makefile +++ b/Makefile @@ -30,5 +30,5 @@ bash-image: build-image docker run -it --rm -p 9830:9830 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/traefikturkey/onboard:latest bash ansible: - -LC_ALL=C.UTF-8 ansible-playbook --inventory 127.0.0.1 --connection=local .devcontainer/ansible/requirements.yml - -LC_ALL=C.UTF-8 ansible-playbook --inventory 127.0.0.1 --connection=local .devcontainer/ansible/setup-container.yml \ No newline at end of file + -ansible-playbook --inventory 127.0.0.1 --connection=local .devcontainer/ansible/requirements.yml + -ansible-playbook --inventory 127.0.0.1 --connection=local .devcontainer/ansible/setup-container.yml \ No newline at end of file diff --git a/app/app.py b/app/app.py index c907e0c..261ce76 100644 --- a/app/app.py +++ b/app/app.py @@ -1,70 +1,38 @@ -import docker import time import signal import sys -import re +from docker_monitor import DockerMonitor -# Create a Docker client instance -client = docker.from_env() +monitor = None -# Define a function to extract FQDNs from Traefik labels +def signal_handler(received_signal: signal.Signals, frame): + print(f"\nReceived signal: {signal.Signals(received_signal).name}, stopping event listener...") + if monitor: + monitor.stop() + sys.exit(0) -def extract_fqdns_from_labels(labels): - traefik_host_labels = {k: v for k, v in labels.items() if k.startswith('traefik.http.routers.')} - fqdns = [] - for label_key, label_value in traefik_host_labels.items(): - match = re.search(r'rule=Host\(`(.+?)`\)', label_value) - if match: - fqdn = match.group(1) - fqdns.append(fqdn) - return fqdns - -# Define a callback function to handle events - - -def event_callback(event): - if event['Action'] == 'start': - container_name = event['Actor']['Attributes']['name'] - container = client.containers.get(container_name) - labels = container.labels - fqdns = extract_fqdns_from_labels(labels) - - if fqdns: - print(f"Container {container_name} started with Traefik FQDNs: {', '.join(fqdns)}") - else: - print(f"Container {container_name} started without Traefik host labels.") - - elif event['Action'] == 'stop': - print(f"Container {event['Actor']['Attributes']['name']} stopped.") - -# Define a signal handler function +def main(): + # Set up signal handlers + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) -def signal_handler(signal, frame): - print("Received signal, stopping event listener...") - sys.exit(0) + # Create a DockerMonitor instance + global monitor + monitor = DockerMonitor() + # Start the event listener in a separate thread + event_thread = monitor.start() -# Set up signal handlers -signal.signal(signal.SIGTERM, signal_handler) -signal.signal(signal.SIGINT, signal_handler) + try: + # Keep the main thread alive + while True: + pass + except KeyboardInterrupt: + print("Keyboard interrupt received, stopping event listener...") + monitor.stop() -# Get running containers with Traefik labels on startup -print("Fetching running containers with Traefik labels...") -running_containers = client.containers.list(filters={'status': 'running'}) -for container in running_containers: - labels = container.labels - fqdns = extract_fqdns_from_labels(labels) - if fqdns: - print(f"Container {container.name} is running with Traefik FQDNs: {', '.join(fqdns)}") -# Start listening for events -print("Listening for Docker events...") -try: - for event in client.events(decode=True): - event_callback(event) - # Flush the output buffer to ensure the message is printed immediately - time.sleep(0.1) -except KeyboardInterrupt: - print("Keyboard interrupt received, stopping event listener...") +if __name__ == "__main__": + main() diff --git a/app/corefile_generateor.py b/app/corefile_generateor.py new file mode 100644 index 0000000..a982cd0 --- /dev/null +++ b/app/corefile_generateor.py @@ -0,0 +1,45 @@ +from enum import Enum +from typing import Set +from jinja2 import Environment, FileSystemLoader + + +class docker: + class ActionType(Enum): + START = 1 + STOP = 2 + + class Event: + def __init__(self, fqdn_name: str, action_type): + self._fqdn_name = fqdn_name + self._action_type = action_type + + @property + def fqdn_name(self) -> str: + return self._fqdn_name + + @property + def action_type(self): + return self._action_type + + +class CorefileGenerator: + def __init__(self, template_path: str): + self.template_path = template_path + self.active_fqdns: Set[str] = set() + self.env = Environment(loader=FileSystemLoader('/path/to/templates')) + + def process_event(self, event: docker.Event): + fqdn_name = event.fqdn_name + action_type = event.action_type + + if action_type == docker.ActionType.START: + self.active_fqdns.add(fqdn_name) + elif action_type == docker.ActionType.STOP: + self.active_fqdns.discard(fqdn_name) + + def generate_corefile(self, output_path: str): + template = self.env.get_template('Corefile.template') + corefile_content = template.render(active_fqdns=self.active_fqdns) + + with open(output_path, 'w') as corefile: + corefile.write(corefile_content) diff --git a/app/docker_monitor.py b/app/docker_monitor.py new file mode 100644 index 0000000..6fe3d53 --- /dev/null +++ b/app/docker_monitor.py @@ -0,0 +1,77 @@ +import os +import docker +import re +import threading +from typing import Set + + +class DockerMonitor: + is_manager: bool = False + host_ip: str + active_names: Set[str] = set() + _stop_event = threading.Event() + + def __init__(self): + self.client = docker.from_env() + self.host_ip = os.environ.get('HOSTIP', '127.0.0.1') + self.get_running_containers_with_traefik_labels() + + def update_core_dns(self, host_ip, fqdns): + print("Updating CoreDNS with new FQDNs...") + + def extract_fqdns_from_labels(self, container, action): + labels = container.labels + traefik_host_labels = {k: v for k, v in labels.items() if k.startswith('traefik.http.routers.')} + onetime_enabled = any(label.startswith('onetime.enabled=false') for label in labels) + if not onetime_enabled: + return False + + changed = False + for _, label_value in traefik_host_labels.items(): + match = re.search(r'rule=Host\(`(.+?)`\)', label_value) + if match and match.group(1).strip() not in self.active_names: + fqdn = match.group(1).strip() + if action == 'start' and fqdn not in self.active_names: + self.active_names.add(fqdn) + changed = True + if action == 'stop' and fqdn in self.active_names: + self.active_names.discard(fqdn) + changed = True + return changed + + def event_callback(self, event): + container_name = event['Actor']['Attributes']['name'] + container = self.client.containers.get(container_name) + changed = self.extract_fqdns_from_labels(container, event['Action']) + if changed: + self.update_core_dns(self.host_ip, self.active_names) + + def get_running_containers_with_traefik_labels(self): + for container in self.client.containers.list(filters={'status': 'running'}): + if 'coredns' in container.image.tags: + self.is_manager = True + next + self.extract_fqdns_from_labels(container, 'start') + self.update_core_dns(self.host_ip, self.active_names) + + def start(self): + print("Listening for Docker events...") + event_thread = threading.Thread(target=self.event_loop) + event_thread.start() + return event_thread + + def stop(self): + self._stop_event.set() + + def event_loop(self): + while not self._stop_event.is_set(): + try: + events = self.client.events(decode=True) + for event in events: + if self._stop_event.is_set(): + break + self.event_callback(event) + events.close() + except KeyboardInterrupt: + self.stop() + break diff --git a/app/reloader.py b/app/reloader.py new file mode 100644 index 0000000..be4943d --- /dev/null +++ b/app/reloader.py @@ -0,0 +1,42 @@ +import os +import sys +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + + +class ReloadHandler(FileSystemEventHandler): + def on_any_event(self, event): + if event.is_directory: + return + elif event.event_type == 'created': + print(f"Created file: {event.src_path}") + elif event.event_type == 'modified': + print(f"Modified file: {event.src_path}") + self.reload() + + def reload(self): + python = sys.executable + args = sys.argv[:] + print(f"Reloading {' '.join(args)}") + args.insert(0, python) + os.execvp(python, args) + + +def start_observer(paths): + observer = Observer() + event_handler = ReloadHandler() + for path in paths: + observer.schedule(event_handler, path, recursive=True) + observer.start() + return observer + + +if __name__ == "__main__": + paths = ['.'] + observer = start_observer(paths) + try: + while True: + pass + except KeyboardInterrupt: + observer.stop() + observer.join() diff --git a/app/templates/Corefile.template b/app/templates/Corefile.template new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 18d5c69..2d8ad8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ docker python-dotenv pyyaml +watchdog \ No newline at end of file