From 26f2b859775546f85923da7d5262034df851f56a Mon Sep 17 00:00:00 2001 From: quietsy Date: Tue, 30 Jun 2026 21:51:07 +0300 Subject: [PATCH 01/14] Add support for remote hosts --- root/app/swag-ondemand.py | 95 ++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index e99b0aa6..b1a52816 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -10,6 +10,7 @@ CONTAINER_QUERY_SLEEP = float(os.environ.get("SWAG_ONDEMAND_CONTAINER_QUERY_SLEEP", "5.0")) LOG_READER_SLEEP = float(os.environ.get("SWAG_ONDEMAND_LOG_READER_SLEEP", "1.0")) STOP_THRESHOLD = int(os.environ.get("SWAG_ONDEMAND_STOP_THRESHOLD", "600")) +REMOTE_HOSTS_PREFIX = "SWAG_ONDEMAND_REMOTE" last_accessed_urls = set() last_accessed_urls_lock = threading.Lock() @@ -19,42 +20,70 @@ def __init__(self): super().__init__() self.daemon = True self.ondemand_containers = {} - self.init_docker() + self.init_docker_hosts() + self.docker_hosts = [] - def init_docker(self): + def init_docker_hosts(self): try: - docker_host = os.environ.get("DOCKER_HOST", None) - if docker_host: - if not docker_host.startswith("tcp://"): - docker_host = f"tcp://{docker_host}:2375" - self.docker_client = docker.DockerClient(base_url=docker_host) + docker_host = {} + docker_host_url = os.environ.get("DOCKER_HOST", None) + if docker_host_url: + if not docker_host_url.startswith("tcp://"): + docker_host_url = f"tcp://{docker_host_url}:2375" + docker_host["docker_client"] = docker.DockerClient(base_url=docker_host_url) else: - self.docker_client = docker.from_env() - except Exception as e: - logging.exception(e) - os._exit(1) + docker_host["docker_client"] = docker.from_env() + self.docker_hosts.append(docker_host) + except Exception: + pass + + try: + remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } + for i in range(1, 20): + remote_host = {} + if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: + break + docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] + if not docker_host_url.startswith("tcp://"): + docker_host_url = f"tcp://{docker_host_url}:2375" + remote_host["docker_host"] = docker_host_url + remote_host["docker_client"] = docker.DockerClient(base_url=docker_host_url) + if f"{REMOTE_HOSTS_PREFIX}{i}_MAC" in remote_hosts_env_vars: + remote_host["mac"] = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}_MAC"] + if f"{REMOTE_HOSTS_PREFIX}{i}_IP" in remote_hosts_env_vars: + remote_host["ip"] = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}_IP"] + if f"{REMOTE_HOSTS_PREFIX}{i}_URLS" in remote_hosts_env_vars: + remote_host["urls"] = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}_URLS"] + self.docker_hosts.append(remote_host) + except Exception: + pass + + if not self.docker_hosts: + logging.error("Failed to connect to any docker host") def process_containers(self): - containers = self.docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) - container_names = {container.name for container in containers} + for docker_host in self.docker_hosts: + docker_client = docker_host["docker_client"] + containers = docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) + container_names = {container.name for container in containers} - for container_name in list(self.ondemand_containers.keys()): - if container_name in container_names: - continue - self.ondemand_containers.pop(container_name) - logging.info(f"Stopped monitoring {container_name}") - - for container in containers: - default_url = container.labels.get("swag_url", f"{container.name}.").rstrip("*") - container_urls = container.labels.get("swag_ondemand_urls", f"https://{default_url},http://{default_url}") - if container.name not in self.ondemand_containers.keys(): - last_accessed = datetime.now() - logging.info(f"Started monitoring {container.name} for urls: {container_urls}") - else: - last_accessed = self.ondemand_containers[container.name]["last_accessed"] - if container_urls != self.ondemand_containers[container.name]["urls"]: - logging.info(f"Updated urls for {container.name} to: {container_urls}") - self.ondemand_containers[container.name] = { "status": container.status, "urls": container_urls, "last_accessed": last_accessed } + for container_name in list(self.ondemand_containers.keys()): + if container_name in container_names: + continue + self.ondemand_containers.pop(container_name) + logging.info(f"Stopped monitoring {container_name}") + + for container in containers: + default_url = container.labels.get("swag_url", f"{container.name}.").rstrip("*") + container_urls = container.labels.get("swag_ondemand_urls", f"https://{default_url},http://{default_url}") + if container.name not in self.ondemand_containers.keys(): + last_accessed = datetime.now() + logging.info(f"Started monitoring {container.name} for urls: {container_urls}") + else: + last_accessed = self.ondemand_containers[container.name]["last_accessed"] + if container_urls != self.ondemand_containers[container.name]["urls"]: + logging.info(f"Updated urls for {container.name} to: {container_urls}") + self.ondemand_containers[container.name] = { "docker_client": docker_client, "status": container.status, "urls": container_urls, "last_accessed": last_accessed } def stop_containers(self): for container_name in self.ondemand_containers.keys(): @@ -63,7 +92,8 @@ def stop_containers(self): inactive_seconds = (datetime.now() - self.ondemand_containers[container_name]["last_accessed"]).total_seconds() if inactive_seconds < STOP_THRESHOLD: continue - self.docker_client.containers.get(container_name).stop() + docker_client = self.ondemand_containers[container_name]["docker_client"] + docker_client.containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") def start_containers(self): @@ -80,7 +110,8 @@ def start_containers(self): accessed = True if not accessed or self.ondemand_containers[container_name]["status"] == "running": continue - self.docker_client.containers.get(container_name).start() + docker_client = self.ondemand_containers[container_name]["docker_client"] + docker_client.containers.get(container_name).start() logging.info(f"Started {container_name}") self.ondemand_containers[container_name]["status"] = "running" From adf4441220eba75367684fed19e1b34d3ef3e1b0 Mon Sep 17 00:00:00 2001 From: quietsy Date: Tue, 30 Jun 2026 22:00:34 +0300 Subject: [PATCH 02/14] Fix typo --- root/app/swag-ondemand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index b1a52816..153e7ad5 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -20,8 +20,8 @@ def __init__(self): super().__init__() self.daemon = True self.ondemand_containers = {} - self.init_docker_hosts() self.docker_hosts = [] + self.init_docker_hosts() def init_docker_hosts(self): try: From 1ddc0fbe75818a3927e96179c78d9e3f34ffa096 Mon Sep 17 00:00:00 2001 From: quietsy Date: Tue, 30 Jun 2026 22:34:49 +0300 Subject: [PATCH 03/14] Fix bug --- root/app/swag-ondemand.py | 62 +++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 153e7ad5..41d2e039 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -19,13 +19,13 @@ class ContainerThread(threading.Thread): def __init__(self): super().__init__() self.daemon = True - self.ondemand_containers = {} self.docker_hosts = [] self.init_docker_hosts() def init_docker_hosts(self): try: docker_host = {} + docker_host["ondemand_containers"] = {} docker_host_url = os.environ.get("DOCKER_HOST", None) if docker_host_url: if not docker_host_url.startswith("tcp://"): @@ -41,6 +41,7 @@ def init_docker_hosts(self): remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } for i in range(1, 20): remote_host = {} + remote_host["ondemand_containers"] = {} if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: break docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] @@ -63,57 +64,62 @@ def init_docker_hosts(self): def process_containers(self): for docker_host in self.docker_hosts: + ondemand_containers = docker_host["ondemand_containers"] docker_client = docker_host["docker_client"] containers = docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) container_names = {container.name for container in containers} - for container_name in list(self.ondemand_containers.keys()): + for container_name in list(ondemand_containers.keys()): if container_name in container_names: continue - self.ondemand_containers.pop(container_name) + ondemand_containers.pop(container_name) logging.info(f"Stopped monitoring {container_name}") for container in containers: default_url = container.labels.get("swag_url", f"{container.name}.").rstrip("*") container_urls = container.labels.get("swag_ondemand_urls", f"https://{default_url},http://{default_url}") - if container.name not in self.ondemand_containers.keys(): + if container.name not in ondemand_containers.keys(): last_accessed = datetime.now() logging.info(f"Started monitoring {container.name} for urls: {container_urls}") else: - last_accessed = self.ondemand_containers[container.name]["last_accessed"] - if container_urls != self.ondemand_containers[container.name]["urls"]: + last_accessed = ondemand_containers[container.name]["last_accessed"] + if container_urls != ondemand_containers[container.name]["urls"]: logging.info(f"Updated urls for {container.name} to: {container_urls}") - self.ondemand_containers[container.name] = { "docker_client": docker_client, "status": container.status, "urls": container_urls, "last_accessed": last_accessed } + ondemand_containers[container.name] = { "docker_client": docker_client, "status": container.status, "urls": container_urls, "last_accessed": last_accessed } def stop_containers(self): - for container_name in self.ondemand_containers.keys(): - if self.ondemand_containers[container_name]["status"] != "running": - continue - inactive_seconds = (datetime.now() - self.ondemand_containers[container_name]["last_accessed"]).total_seconds() - if inactive_seconds < STOP_THRESHOLD: - continue - docker_client = self.ondemand_containers[container_name]["docker_client"] - docker_client.containers.get(container_name).stop() - logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") + for docker_host in self.docker_hosts: + ondemand_containers = docker_host["ondemand_containers"] + for container_name in ondemand_containers.keys(): + if ondemand_containers[container_name]["status"] != "running": + continue + inactive_seconds = (datetime.now() - ondemand_containers[container_name]["last_accessed"]).total_seconds() + if inactive_seconds < STOP_THRESHOLD: + continue + docker_client = ondemand_containers[container_name]["docker_client"] + docker_client.containers.get(container_name).stop() + logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") def start_containers(self): with last_accessed_urls_lock: last_accessed_urls_combined = ",".join(last_accessed_urls) last_accessed_urls.clear() - for container_name in self.ondemand_containers.keys(): - accessed = False - for ondemand_url in self.ondemand_containers[container_name]["urls"].split(","): - if ondemand_url not in last_accessed_urls_combined: + for docker_host in self.docker_hosts: + ondemand_containers = docker_host["ondemand_containers"] + for container_name in ondemand_containers.keys(): + accessed = False + for ondemand_url in ondemand_containers[container_name]["urls"].split(","): + if ondemand_url not in last_accessed_urls_combined: + continue + ondemand_containers[container_name]["last_accessed"] = datetime.now() + accessed = True + if not accessed or ondemand_containers[container_name]["status"] == "running": continue - self.ondemand_containers[container_name]["last_accessed"] = datetime.now() - accessed = True - if not accessed or self.ondemand_containers[container_name]["status"] == "running": - continue - docker_client = self.ondemand_containers[container_name]["docker_client"] - docker_client.containers.get(container_name).start() - logging.info(f"Started {container_name}") - self.ondemand_containers[container_name]["status"] = "running" + docker_client = ondemand_containers[container_name]["docker_client"] + docker_client.containers.get(container_name).start() + logging.info(f"Started {container_name}") + ondemand_containers[container_name]["status"] = "running" def run(self): while True: From 7888c0ffc3e8abfc969f0f441d71c614ad63fb2b Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 08:53:33 +0300 Subject: [PATCH 04/14] Fix exceptions in the log --- root/app/swag-ondemand.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 41d2e039..1a783d98 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -61,11 +61,20 @@ def init_docker_hosts(self): if not self.docker_hosts: logging.error("Failed to connect to any docker host") - + + def is_docker_connected(self, client: docker.DockerClient) -> bool: + try: + return client.ping() + except Exception: + return False + def process_containers(self): for docker_host in self.docker_hosts: - ondemand_containers = docker_host["ondemand_containers"] docker_client = docker_host["docker_client"] + if not self.is_docker_connected(docker_client): + continue + + ondemand_containers = docker_host["ondemand_containers"] containers = docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) container_names = {container.name for container in containers} @@ -97,6 +106,8 @@ def stop_containers(self): if inactive_seconds < STOP_THRESHOLD: continue docker_client = ondemand_containers[container_name]["docker_client"] + if not self.is_docker_connected(docker_client): + continue docker_client.containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") @@ -117,6 +128,8 @@ def start_containers(self): if not accessed or ondemand_containers[container_name]["status"] == "running": continue docker_client = ondemand_containers[container_name]["docker_client"] + if not self.is_docker_connected(docker_client): + continue docker_client.containers.get(container_name).start() logging.info(f"Started {container_name}") ondemand_containers[container_name]["status"] = "running" From b94d5badb1b5d77fb464a3f1935eec925afbddc7 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 09:25:33 +0300 Subject: [PATCH 05/14] Add readme and cleanup --- README.md | 1 + root/app/swag-ondemand.py | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 882dd155..60f9b7bd 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This mod gives SWAG the ability to start containers on-demand when accessed thro - `SWAG_ONDEMAND_STOP_THRESHOLD` - duration of inactivity in seconds before stopping on-demand containers, defaults to `600` (10 minutes). - `SWAG_ONDEMAND_CONTAINER_QUERY_SLEEP` - sleep time in seconds between querying containers, defaults to `5.0`. - `SWAG_ONDEMAND_LOG_READER_SLEEP` - sleep time in seconds between log reads, defaults to `1.0`. + - `SWAG_ONDEMAND_REMOTE1` - the remote API of other hosts, such as: tcp://otherhost:2375. can add up to 20. ### Loading Page: diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 1a783d98..09fac1a6 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -49,12 +49,6 @@ def init_docker_hosts(self): docker_host_url = f"tcp://{docker_host_url}:2375" remote_host["docker_host"] = docker_host_url remote_host["docker_client"] = docker.DockerClient(base_url=docker_host_url) - if f"{REMOTE_HOSTS_PREFIX}{i}_MAC" in remote_hosts_env_vars: - remote_host["mac"] = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}_MAC"] - if f"{REMOTE_HOSTS_PREFIX}{i}_IP" in remote_hosts_env_vars: - remote_host["ip"] = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}_IP"] - if f"{REMOTE_HOSTS_PREFIX}{i}_URLS" in remote_hosts_env_vars: - remote_host["urls"] = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}_URLS"] self.docker_hosts.append(remote_host) except Exception: pass From 854ac6c5a638250788153a2b77c34c58c2d0ae6f Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 15:36:07 +0300 Subject: [PATCH 06/14] Refactor --- root/app/swag-ondemand.py | 70 +++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 09fac1a6..d81846fa 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -2,8 +2,10 @@ import docker import logging import os +import requests import threading import time +from typing import Optional ACCESS_LOG_FILE = "/config/log/nginx/access.log" LOG_FILE = "/config/log/ondemand/ondemand.log" @@ -15,6 +17,25 @@ last_accessed_urls = set() last_accessed_urls_lock = threading.Lock() +def get_docker_client(docker_host_url: str, from_env: bool = False) -> Optional[docker.DockerClient]: + try: + if docker_host_url: + if not docker_host_url.startswith("tcp://"): + docker_host_url = f"tcp://{docker_host_url}:2375" + return docker.DockerClient(base_url=docker_host_url) + elif from_env: + return docker.from_env() + else: + return None + except (docker.errors.DockerException, requests.exceptions.ConnectionError): + return None + +def is_docker_connected(client: docker.DockerClient) -> bool: + try: + return client.ping() + except (docker.errors.DockerException, requests.exceptions.ConnectionError): + return False + class ContainerThread(threading.Thread): def __init__(self): super().__init__() @@ -23,45 +44,28 @@ def __init__(self): self.init_docker_hosts() def init_docker_hosts(self): - try: - docker_host = {} - docker_host["ondemand_containers"] = {} - docker_host_url = os.environ.get("DOCKER_HOST", None) - if docker_host_url: - if not docker_host_url.startswith("tcp://"): - docker_host_url = f"tcp://{docker_host_url}:2375" - docker_host["docker_client"] = docker.DockerClient(base_url=docker_host_url) - else: - docker_host["docker_client"] = docker.from_env() + docker_host = {} + docker_host["ondemand_containers"] = {} + docker_host_url = os.environ.get("DOCKER_HOST", None) + docker_host["docker_client"] = get_docker_client(docker_host_url, True) + if docker_host["docker_client"]: self.docker_hosts.append(docker_host) - except Exception: - pass - try: - remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } - for i in range(1, 20): - remote_host = {} - remote_host["ondemand_containers"] = {} - if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: - break - docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] - if not docker_host_url.startswith("tcp://"): - docker_host_url = f"tcp://{docker_host_url}:2375" - remote_host["docker_host"] = docker_host_url - remote_host["docker_client"] = docker.DockerClient(base_url=docker_host_url) - self.docker_hosts.append(remote_host) - except Exception: - pass + remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } + for i in range(1, 20): + remote_host = {} + remote_host["ondemand_containers"] = {} + if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: + break + docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] + remote_host["docker_client"] = get_docker_client(docker_host_url) + if not remote_host["docker_client"]: + continue + self.docker_hosts.append(remote_host) if not self.docker_hosts: logging.error("Failed to connect to any docker host") - def is_docker_connected(self, client: docker.DockerClient) -> bool: - try: - return client.ping() - except Exception: - return False - def process_containers(self): for docker_host in self.docker_hosts: docker_client = docker_host["docker_client"] From 9bf6e655196166f5c8833186ebf81bda39b4ad4e Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 15:41:52 +0300 Subject: [PATCH 07/14] Fix typo --- root/app/swag-ondemand.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index d81846fa..589064a8 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -69,7 +69,7 @@ def init_docker_hosts(self): def process_containers(self): for docker_host in self.docker_hosts: docker_client = docker_host["docker_client"] - if not self.is_docker_connected(docker_client): + if not is_docker_connected(docker_client): continue ondemand_containers = docker_host["ondemand_containers"] @@ -104,7 +104,7 @@ def stop_containers(self): if inactive_seconds < STOP_THRESHOLD: continue docker_client = ondemand_containers[container_name]["docker_client"] - if not self.is_docker_connected(docker_client): + if not is_docker_connected(docker_client): continue docker_client.containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") @@ -126,7 +126,7 @@ def start_containers(self): if not accessed or ondemand_containers[container_name]["status"] == "running": continue docker_client = ondemand_containers[container_name]["docker_client"] - if not self.is_docker_connected(docker_client): + if not is_docker_connected(docker_client): continue docker_client.containers.get(container_name).start() logging.info(f"Started {container_name}") From 9d3c090c6015f6c0646087300bd43a00f1418184 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 16:15:46 +0300 Subject: [PATCH 08/14] Add logging --- README.md | 2 +- root/app/swag-ondemand.py | 35 +++++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 60f9b7bd..b1d1f7e5 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This mod gives SWAG the ability to start containers on-demand when accessed thro - `SWAG_ONDEMAND_STOP_THRESHOLD` - duration of inactivity in seconds before stopping on-demand containers, defaults to `600` (10 minutes). - `SWAG_ONDEMAND_CONTAINER_QUERY_SLEEP` - sleep time in seconds between querying containers, defaults to `5.0`. - `SWAG_ONDEMAND_LOG_READER_SLEEP` - sleep time in seconds between log reads, defaults to `1.0`. - - `SWAG_ONDEMAND_REMOTE1` - the remote API of other hosts, such as: tcp://otherhost:2375. can add up to 20. + - `SWAG_ONDEMAND_REMOTE1...20` - the remote API of other hosts for ondemand to manage, such as: tcp://otherhost:2375. can add up to 20. ### Loading Page: diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 589064a8..07d6b08d 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -17,18 +17,19 @@ last_accessed_urls = set() last_accessed_urls_lock = threading.Lock() -def get_docker_client(docker_host_url: str, from_env: bool = False) -> Optional[docker.DockerClient]: + +def get_docker_client(docker_host_url: str, from_env: bool = False) -> tuple[Optional[docker.DockerClient], str]: try: if docker_host_url: if not docker_host_url.startswith("tcp://"): docker_host_url = f"tcp://{docker_host_url}:2375" - return docker.DockerClient(base_url=docker_host_url) + return docker.DockerClient(base_url=docker_host_url), docker_host_url elif from_env: - return docker.from_env() + return docker.from_env(), "unix://var/run/docker.sock" else: - return None + return None, "" except (docker.errors.DockerException, requests.exceptions.ConnectionError): - return None + return None, "" def is_docker_connected(client: docker.DockerClient) -> bool: try: @@ -44,21 +45,21 @@ def __init__(self): self.init_docker_hosts() def init_docker_hosts(self): - docker_host = {} + docker_host = { "is_connected": False } docker_host["ondemand_containers"] = {} docker_host_url = os.environ.get("DOCKER_HOST", None) - docker_host["docker_client"] = get_docker_client(docker_host_url, True) + docker_host["docker_client"], docker_host["docker_host_url"] = get_docker_client(docker_host_url, True) if docker_host["docker_client"]: self.docker_hosts.append(docker_host) remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } for i in range(1, 20): - remote_host = {} + remote_host = { "is_connected": False } remote_host["ondemand_containers"] = {} if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: break docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] - remote_host["docker_client"] = get_docker_client(docker_host_url) + remote_host["docker_client"], remote_host["docker_host_url"] = get_docker_client(docker_host_url) if not remote_host["docker_client"]: continue self.docker_hosts.append(remote_host) @@ -69,9 +70,15 @@ def init_docker_hosts(self): def process_containers(self): for docker_host in self.docker_hosts: docker_client = docker_host["docker_client"] + docker_host["is_connected"] = True if not is_docker_connected(docker_client): + if docker_host["is_connected"]: + docker_host_url = docker_host["docker_host_url"] + logging.warning(f"Lost connection to {docker_host_url}") + docker_host["is_connected"] = False continue + docker_host["is_connected"] = True ondemand_containers = docker_host["ondemand_containers"] containers = docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) container_names = {container.name for container in containers} @@ -92,7 +99,7 @@ def process_containers(self): last_accessed = ondemand_containers[container.name]["last_accessed"] if container_urls != ondemand_containers[container.name]["urls"]: logging.info(f"Updated urls for {container.name} to: {container_urls}") - ondemand_containers[container.name] = { "docker_client": docker_client, "status": container.status, "urls": container_urls, "last_accessed": last_accessed } + ondemand_containers[container.name] = { "status": container.status, "urls": container_urls, "last_accessed": last_accessed } def stop_containers(self): for docker_host in self.docker_hosts: @@ -103,8 +110,10 @@ def stop_containers(self): inactive_seconds = (datetime.now() - ondemand_containers[container_name]["last_accessed"]).total_seconds() if inactive_seconds < STOP_THRESHOLD: continue - docker_client = ondemand_containers[container_name]["docker_client"] + docker_client = docker_host["docker_client"] if not is_docker_connected(docker_client): + docker_host_url = docker_host["docker_host_url"] + logging.warning(f"Failed to stop {container_name}, docker host {docker_host_url} is unavailable") continue docker_client.containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") @@ -125,8 +134,10 @@ def start_containers(self): accessed = True if not accessed or ondemand_containers[container_name]["status"] == "running": continue - docker_client = ondemand_containers[container_name]["docker_client"] + docker_client = docker_host["docker_client"] if not is_docker_connected(docker_client): + docker_host_url = docker_host["docker_host_url"] + logging.warning(f"Failed to start {container_name}, docker host {docker_host_url} is unavailable") continue docker_client.containers.get(container_name).start() logging.info(f"Started {container_name}") From c829951fcae2c98e6c9dfe8f9454321e5397a7d9 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 16:24:54 +0300 Subject: [PATCH 09/14] Refactoring --- root/app/swag-ondemand.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 07d6b08d..3a1a782c 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -69,18 +69,19 @@ def init_docker_hosts(self): def process_containers(self): for docker_host in self.docker_hosts: - docker_client = docker_host["docker_client"] - docker_host["is_connected"] = True - if not is_docker_connected(docker_client): + if not is_docker_connected(docker_host["docker_client"]): if docker_host["is_connected"]: docker_host_url = docker_host["docker_host_url"] logging.warning(f"Lost connection to {docker_host_url}") docker_host["is_connected"] = False continue - docker_host["is_connected"] = True + if not docker_host["is_connected"]: + logging.info(f"Connection to {docker_host_url} has been restored") + docker_host["is_connected"] = True + ondemand_containers = docker_host["ondemand_containers"] - containers = docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) + containers = docker_host["docker_client"].containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) container_names = {container.name for container in containers} for container_name in list(ondemand_containers.keys()): @@ -110,12 +111,11 @@ def stop_containers(self): inactive_seconds = (datetime.now() - ondemand_containers[container_name]["last_accessed"]).total_seconds() if inactive_seconds < STOP_THRESHOLD: continue - docker_client = docker_host["docker_client"] - if not is_docker_connected(docker_client): + if not is_docker_connected(docker_host["docker_client"]): docker_host_url = docker_host["docker_host_url"] logging.warning(f"Failed to stop {container_name}, docker host {docker_host_url} is unavailable") continue - docker_client.containers.get(container_name).stop() + docker_host["docker_client"].containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") def start_containers(self): @@ -134,12 +134,11 @@ def start_containers(self): accessed = True if not accessed or ondemand_containers[container_name]["status"] == "running": continue - docker_client = docker_host["docker_client"] - if not is_docker_connected(docker_client): + if not is_docker_connected(docker_host["docker_client"]): docker_host_url = docker_host["docker_host_url"] logging.warning(f"Failed to start {container_name}, docker host {docker_host_url} is unavailable") continue - docker_client.containers.get(container_name).start() + docker_host["docker_client"].containers.get(container_name).start() logging.info(f"Started {container_name}") ondemand_containers[container_name]["status"] = "running" From 35b91d5dde94a15a30d1d3f76ab28166a088cf29 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 16:28:57 +0300 Subject: [PATCH 10/14] Fix typo --- root/app/swag-ondemand.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/root/app/swag-ondemand.py b/root/app/swag-ondemand.py index 3a1a782c..e2d67156 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/swag-ondemand.py @@ -71,13 +71,12 @@ def process_containers(self): for docker_host in self.docker_hosts: if not is_docker_connected(docker_host["docker_client"]): if docker_host["is_connected"]: - docker_host_url = docker_host["docker_host_url"] - logging.warning(f"Lost connection to {docker_host_url}") + logging.warning(f"Lost connection to {docker_host['docker_host_url']}") docker_host["is_connected"] = False continue if not docker_host["is_connected"]: - logging.info(f"Connection to {docker_host_url} has been restored") + logging.info(f"Connection to {docker_host['docker_host_url']} has been restored") docker_host["is_connected"] = True ondemand_containers = docker_host["ondemand_containers"] @@ -112,8 +111,7 @@ def stop_containers(self): if inactive_seconds < STOP_THRESHOLD: continue if not is_docker_connected(docker_host["docker_client"]): - docker_host_url = docker_host["docker_host_url"] - logging.warning(f"Failed to stop {container_name}, docker host {docker_host_url} is unavailable") + logging.warning(f"Failed to stop {container_name}, docker host {docker_host['docker_host_url']} is unavailable") continue docker_host["docker_client"].containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") @@ -135,8 +133,7 @@ def start_containers(self): if not accessed or ondemand_containers[container_name]["status"] == "running": continue if not is_docker_connected(docker_host["docker_client"]): - docker_host_url = docker_host["docker_host_url"] - logging.warning(f"Failed to start {container_name}, docker host {docker_host_url} is unavailable") + logging.warning(f"Failed to start {container_name}, docker host {docker_host['docker_host_url']} is unavailable") continue docker_host["docker_client"].containers.get(container_name).start() logging.info(f"Started {container_name}") From 9338b62ac678095a8daefadbdb960bc5e7246582 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 17:18:50 +0300 Subject: [PATCH 11/14] Refactor --- .../container_thread.py} | 99 ++----------------- root/app/ondemand/helper.py | 24 +++++ root/app/ondemand/log_reader_thread.py | 51 ++++++++++ root/app/ondemand/main.py | 24 +++++ root/app/ondemand/shared_state.py | 4 + .../s6-rc.d/svc-mod-swag-ondemand/run | 2 +- 6 files changed, 113 insertions(+), 91 deletions(-) rename root/app/{swag-ondemand.py => ondemand/container_thread.py} (64%) create mode 100644 root/app/ondemand/helper.py create mode 100644 root/app/ondemand/log_reader_thread.py create mode 100644 root/app/ondemand/main.py create mode 100644 root/app/ondemand/shared_state.py diff --git a/root/app/swag-ondemand.py b/root/app/ondemand/container_thread.py similarity index 64% rename from root/app/swag-ondemand.py rename to root/app/ondemand/container_thread.py index e2d67156..670faa1f 100644 --- a/root/app/swag-ondemand.py +++ b/root/app/ondemand/container_thread.py @@ -1,45 +1,20 @@ +import helper +from shared_state import last_accessed_urls, last_accessed_urls_lock + from datetime import datetime -import docker import logging import os -import requests import threading import time -from typing import Optional -ACCESS_LOG_FILE = "/config/log/nginx/access.log" -LOG_FILE = "/config/log/ondemand/ondemand.log" CONTAINER_QUERY_SLEEP = float(os.environ.get("SWAG_ONDEMAND_CONTAINER_QUERY_SLEEP", "5.0")) -LOG_READER_SLEEP = float(os.environ.get("SWAG_ONDEMAND_LOG_READER_SLEEP", "1.0")) STOP_THRESHOLD = int(os.environ.get("SWAG_ONDEMAND_STOP_THRESHOLD", "600")) REMOTE_HOSTS_PREFIX = "SWAG_ONDEMAND_REMOTE" -last_accessed_urls = set() -last_accessed_urls_lock = threading.Lock() - -def get_docker_client(docker_host_url: str, from_env: bool = False) -> tuple[Optional[docker.DockerClient], str]: - try: - if docker_host_url: - if not docker_host_url.startswith("tcp://"): - docker_host_url = f"tcp://{docker_host_url}:2375" - return docker.DockerClient(base_url=docker_host_url), docker_host_url - elif from_env: - return docker.from_env(), "unix://var/run/docker.sock" - else: - return None, "" - except (docker.errors.DockerException, requests.exceptions.ConnectionError): - return None, "" - -def is_docker_connected(client: docker.DockerClient) -> bool: - try: - return client.ping() - except (docker.errors.DockerException, requests.exceptions.ConnectionError): - return False - class ContainerThread(threading.Thread): def __init__(self): - super().__init__() + super().__init__(name="ContainerThread") self.daemon = True self.docker_hosts = [] self.init_docker_hosts() @@ -48,7 +23,7 @@ def init_docker_hosts(self): docker_host = { "is_connected": False } docker_host["ondemand_containers"] = {} docker_host_url = os.environ.get("DOCKER_HOST", None) - docker_host["docker_client"], docker_host["docker_host_url"] = get_docker_client(docker_host_url, True) + docker_host["docker_client"], docker_host["docker_host_url"] = helper.get_docker_client(docker_host_url, True) if docker_host["docker_client"]: self.docker_hosts.append(docker_host) @@ -59,7 +34,7 @@ def init_docker_hosts(self): if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: break docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] - remote_host["docker_client"], remote_host["docker_host_url"] = get_docker_client(docker_host_url) + remote_host["docker_client"], remote_host["docker_host_url"] = helper.get_docker_client(docker_host_url) if not remote_host["docker_client"]: continue self.docker_hosts.append(remote_host) @@ -69,7 +44,7 @@ def init_docker_hosts(self): def process_containers(self): for docker_host in self.docker_hosts: - if not is_docker_connected(docker_host["docker_client"]): + if not helper.is_docker_connected(docker_host["docker_client"]): if docker_host["is_connected"]: logging.warning(f"Lost connection to {docker_host['docker_host_url']}") docker_host["is_connected"] = False @@ -110,7 +85,7 @@ def stop_containers(self): inactive_seconds = (datetime.now() - ondemand_containers[container_name]["last_accessed"]).total_seconds() if inactive_seconds < STOP_THRESHOLD: continue - if not is_docker_connected(docker_host["docker_client"]): + if not helper.is_docker_connected(docker_host["docker_client"]): logging.warning(f"Failed to stop {container_name}, docker host {docker_host['docker_host_url']} is unavailable") continue docker_host["docker_client"].containers.get(container_name).stop() @@ -132,7 +107,7 @@ def start_containers(self): accessed = True if not accessed or ondemand_containers[container_name]["status"] == "running": continue - if not is_docker_connected(docker_host["docker_client"]): + if not helper.is_docker_connected(docker_host["docker_client"]): logging.warning(f"Failed to start {container_name}, docker host {docker_host['docker_host_url']} is unavailable") continue docker_host["docker_client"].containers.get(container_name).start() @@ -148,59 +123,3 @@ def run(self): time.sleep(CONTAINER_QUERY_SLEEP) except Exception as e: logging.exception(e) - -class LogReaderThread(threading.Thread): - def __init__(self): - super().__init__() - self.daemon = True - - def tail(self, f): - f.seek(0,2) - inode = os.fstat(f.fileno()).st_ino - - while True: - line = f.readline() - if not line: - time.sleep(LOG_READER_SLEEP) - if os.stat(ACCESS_LOG_FILE).st_ino != inode: - f.close() - f = open(ACCESS_LOG_FILE, 'r') - inode = os.fstat(f.fileno()).st_ino - continue - yield line - - def run(self): - while True: - try: - if not os.path.exists(ACCESS_LOG_FILE): - time.sleep(1) - continue - - logfile = open(ACCESS_LOG_FILE, "r") - for line in self.tail(logfile): - if '" 302 ' in line: - continue - for part in line.split(): - if not part.startswith("http"): - continue - with last_accessed_urls_lock: - last_accessed_urls.add(part) - break - except Exception as e: - logging.exception(e) - time.sleep(1) - -if __name__ == "__main__": - os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) - logging.basicConfig(filename=LOG_FILE, - filemode='a', - format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO) - logging.info("Starting swag-ondemand...") - - ContainerThread().start() - LogReaderThread().start() - - while True: - time.sleep(1) diff --git a/root/app/ondemand/helper.py b/root/app/ondemand/helper.py new file mode 100644 index 00000000..4af9d35c --- /dev/null +++ b/root/app/ondemand/helper.py @@ -0,0 +1,24 @@ +import docker +import requests +from typing import Optional + + +def get_docker_client(docker_host_url: str, from_env: bool = False) -> tuple[Optional[docker.DockerClient], str]: + try: + if docker_host_url: + if not docker_host_url.startswith("tcp://"): + docker_host_url = f"tcp://{docker_host_url}:2375" + return docker.DockerClient(base_url=docker_host_url), docker_host_url + elif from_env: + return docker.from_env(), "unix://var/run/docker.sock" + else: + return None, "" + except (docker.errors.DockerException, requests.exceptions.ConnectionError): + return None, "" + +def is_docker_connected(client: docker.DockerClient) -> bool: + try: + return client.ping() + except (docker.errors.DockerException, requests.exceptions.ConnectionError): + return False + \ No newline at end of file diff --git a/root/app/ondemand/log_reader_thread.py b/root/app/ondemand/log_reader_thread.py new file mode 100644 index 00000000..c624ca37 --- /dev/null +++ b/root/app/ondemand/log_reader_thread.py @@ -0,0 +1,51 @@ +from shared_state import last_accessed_urls, last_accessed_urls_lock + +import logging +import os +import threading +import time + +ACCESS_LOG_FILE = "/config/log/nginx/access.log" +LOG_READER_SLEEP = float(os.environ.get("SWAG_ONDEMAND_LOG_READER_SLEEP", "1.0")) + + +class LogReaderThread(threading.Thread): + def __init__(self): + super().__init__(name="LogReaderThread") + self.daemon = True + + def tail(self, f): + f.seek(0,2) + inode = os.fstat(f.fileno()).st_ino + + while True: + line = f.readline() + if not line: + time.sleep(LOG_READER_SLEEP) + if os.stat(ACCESS_LOG_FILE).st_ino != inode: + f.close() + f = open(ACCESS_LOG_FILE, 'r') + inode = os.fstat(f.fileno()).st_ino + continue + yield line + + def run(self): + while True: + try: + if not os.path.exists(ACCESS_LOG_FILE): + time.sleep(1) + continue + + logfile = open(ACCESS_LOG_FILE, "r") + for line in self.tail(logfile): + if '" 302 ' in line: + continue + for part in line.split(): + if not part.startswith("http"): + continue + with last_accessed_urls_lock: + last_accessed_urls.add(part) + break + except Exception as e: + logging.exception(e) + time.sleep(1) diff --git a/root/app/ondemand/main.py b/root/app/ondemand/main.py new file mode 100644 index 00000000..a0278f53 --- /dev/null +++ b/root/app/ondemand/main.py @@ -0,0 +1,24 @@ +from container_thread import ContainerThread +from log_reader_thread import LogReaderThread + +import logging +import os +import time + +LOG_FILE = "/config/log/ondemand/ondemand.log" + + +if __name__ == "__main__": + os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) + logging.basicConfig(filename=LOG_FILE, + filemode='a', + format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + level=logging.INFO) + logging.info("Starting swag-ondemand...") + + ContainerThread().start() + LogReaderThread().start() + + while True: + time.sleep(1) diff --git a/root/app/ondemand/shared_state.py b/root/app/ondemand/shared_state.py new file mode 100644 index 00000000..d6bc12af --- /dev/null +++ b/root/app/ondemand/shared_state.py @@ -0,0 +1,4 @@ +import threading + +last_accessed_urls = set() +last_accessed_urls_lock = threading.Lock() diff --git a/root/etc/s6-overlay/s6-rc.d/svc-mod-swag-ondemand/run b/root/etc/s6-overlay/s6-rc.d/svc-mod-swag-ondemand/run index abb7df10..c93e2bf3 100755 --- a/root/etc/s6-overlay/s6-rc.d/svc-mod-swag-ondemand/run +++ b/root/etc/s6-overlay/s6-rc.d/svc-mod-swag-ondemand/run @@ -1,6 +1,6 @@ #!/usr/bin/with-contenv bash -result=$(exec s6-setuidgid abc python3 /app/swag-ondemand.py) +result=$(exec s6-setuidgid abc python3 /app/ondemand/main.py) if [ $? -ne 0 ]; then echo "***** Error: swag-ondemand failed to run *****" echo "${result}" From f82a477877a2f6a9c1a70d9d6456a605a180d4cd Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 17:38:25 +0300 Subject: [PATCH 12/14] Refactor data classes --- root/app/ondemand/container_thread.py | 105 ++++++++++++++------------ root/app/ondemand/data_classes.py | 16 ++++ 2 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 root/app/ondemand/data_classes.py diff --git a/root/app/ondemand/container_thread.py b/root/app/ondemand/container_thread.py index 670faa1f..57c579b4 100644 --- a/root/app/ondemand/container_thread.py +++ b/root/app/ondemand/container_thread.py @@ -1,3 +1,4 @@ +from data_classes import DockerHost, OnDemandContainer import helper from shared_state import last_accessed_urls, last_accessed_urls_lock @@ -16,79 +17,83 @@ class ContainerThread(threading.Thread): def __init__(self): super().__init__(name="ContainerThread") self.daemon = True - self.docker_hosts = [] + self.docker_hosts: list[DockerHost] = [] self.init_docker_hosts() def init_docker_hosts(self): - docker_host = { "is_connected": False } - docker_host["ondemand_containers"] = {} docker_host_url = os.environ.get("DOCKER_HOST", None) - docker_host["docker_client"], docker_host["docker_host_url"] = helper.get_docker_client(docker_host_url, True) - if docker_host["docker_client"]: - self.docker_hosts.append(docker_host) + client, url = helper.get_docker_client(docker_host_url, True) + if client: + self.docker_hosts.append(DockerHost(docker_client=client, docker_host_url=url)) remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } for i in range(1, 20): - remote_host = { "is_connected": False } - remote_host["ondemand_containers"] = {} if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: break + docker_host_url = remote_hosts_env_vars[f"{REMOTE_HOSTS_PREFIX}{i}"] - remote_host["docker_client"], remote_host["docker_host_url"] = helper.get_docker_client(docker_host_url) - if not remote_host["docker_client"]: - continue - self.docker_hosts.append(remote_host) + client, url = helper.get_docker_client(docker_host_url) + + if client: + self.docker_hosts.append(DockerHost(docker_client=client, docker_host_url=url)) if not self.docker_hosts: logging.error("Failed to connect to any docker host") def process_containers(self): for docker_host in self.docker_hosts: - if not helper.is_docker_connected(docker_host["docker_client"]): - if docker_host["is_connected"]: - logging.warning(f"Lost connection to {docker_host['docker_host_url']}") - docker_host["is_connected"] = False + if not helper.is_docker_connected(docker_host.docker_client): + if docker_host.is_connected: + logging.warning(f"Lost connection to {docker_host.docker_host_url}") + docker_host.is_connected = False continue - if not docker_host["is_connected"]: - logging.info(f"Connection to {docker_host['docker_host_url']} has been restored") - docker_host["is_connected"] = True + if not docker_host.is_connected: + logging.info(f"Connection to {docker_host.docker_host_url} has been restored") + docker_host.is_connected = True - ondemand_containers = docker_host["ondemand_containers"] - containers = docker_host["docker_client"].containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) + containers = docker_host.docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) container_names = {container.name for container in containers} - for container_name in list(ondemand_containers.keys()): - if container_name in container_names: - continue - ondemand_containers.pop(container_name) - logging.info(f"Stopped monitoring {container_name}") + for container_name in list(docker_host.ondemand_containers.keys()): + if container_name not in container_names: + docker_host.ondemand_containers.pop(container_name) + logging.info(f"Stopped monitoring {container_name}") for container in containers: default_url = container.labels.get("swag_url", f"{container.name}.").rstrip("*") container_urls = container.labels.get("swag_ondemand_urls", f"https://{default_url},http://{default_url}") - if container.name not in ondemand_containers.keys(): + + if container.name not in docker_host.ondemand_containers: last_accessed = datetime.now() logging.info(f"Started monitoring {container.name} for urls: {container_urls}") else: - last_accessed = ondemand_containers[container.name]["last_accessed"] - if container_urls != ondemand_containers[container.name]["urls"]: + existing_container = docker_host.ondemand_containers[container.name] + last_accessed = existing_container.last_accessed + if container_urls != existing_container.urls: logging.info(f"Updated urls for {container.name} to: {container_urls}") - ondemand_containers[container.name] = { "status": container.status, "urls": container_urls, "last_accessed": last_accessed } + + docker_host.ondemand_containers[container.name] = OnDemandContainer( + status=container.status, + urls=container_urls, + last_accessed=last_accessed + ) def stop_containers(self): for docker_host in self.docker_hosts: - ondemand_containers = docker_host["ondemand_containers"] - for container_name in ondemand_containers.keys(): - if ondemand_containers[container_name]["status"] != "running": + for container_name, container in docker_host.ondemand_containers.items(): + if container.status != "running": continue - inactive_seconds = (datetime.now() - ondemand_containers[container_name]["last_accessed"]).total_seconds() + + inactive_seconds = (datetime.now() - container.last_accessed).total_seconds() if inactive_seconds < STOP_THRESHOLD: continue - if not helper.is_docker_connected(docker_host["docker_client"]): - logging.warning(f"Failed to stop {container_name}, docker host {docker_host['docker_host_url']} is unavailable") + + if not helper.is_docker_connected(docker_host.docker_client): + logging.warning(f"Failed to stop {container_name}, docker host {docker_host.docker_host_url} is unavailable") continue - docker_host["docker_client"].containers.get(container_name).stop() + + docker_host.docker_client.containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") def start_containers(self): @@ -97,22 +102,24 @@ def start_containers(self): last_accessed_urls.clear() for docker_host in self.docker_hosts: - ondemand_containers = docker_host["ondemand_containers"] - for container_name in ondemand_containers.keys(): + for container_name, container in docker_host.ondemand_containers.items(): accessed = False - for ondemand_url in ondemand_containers[container_name]["urls"].split(","): - if ondemand_url not in last_accessed_urls_combined: - continue - ondemand_containers[container_name]["last_accessed"] = datetime.now() - accessed = True - if not accessed or ondemand_containers[container_name]["status"] == "running": + for ondemand_url in container.urls.split(","): + if ondemand_url in last_accessed_urls_combined: + container.last_accessed = datetime.now() + accessed = True + break + + if not accessed or container.status == "running": continue - if not helper.is_docker_connected(docker_host["docker_client"]): - logging.warning(f"Failed to start {container_name}, docker host {docker_host['docker_host_url']} is unavailable") + + if not helper.is_docker_connected(docker_host.docker_client): + logging.warning(f"Failed to start {container_name}, docker host {docker_host.docker_host_url} is unavailable") continue - docker_host["docker_client"].containers.get(container_name).start() + + docker_host.docker_client.containers.get(container_name).start() logging.info(f"Started {container_name}") - ondemand_containers[container_name]["status"] = "running" + container.status = "running" def run(self): while True: diff --git a/root/app/ondemand/data_classes.py b/root/app/ondemand/data_classes.py new file mode 100644 index 00000000..9f478bd2 --- /dev/null +++ b/root/app/ondemand/data_classes.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass, field +from datetime import datetime +import docker + +@dataclass +class OnDemandContainer: + status: str + urls: str + last_accessed: datetime + +@dataclass +class DockerHost: + docker_client: docker.DockerClient + docker_host_url: str + is_connected: bool = False + ondemand_containers: dict = field(default_factory=dict) From d187bded8525e6dabc52439541f6aabec9211641 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 18:22:16 +0300 Subject: [PATCH 13/14] Rename vars --- root/app/ondemand/container_thread.py | 24 ++++++++++++------------ root/app/ondemand/data_classes.py | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/root/app/ondemand/container_thread.py b/root/app/ondemand/container_thread.py index 57c579b4..fc444389 100644 --- a/root/app/ondemand/container_thread.py +++ b/root/app/ondemand/container_thread.py @@ -24,7 +24,7 @@ def init_docker_hosts(self): docker_host_url = os.environ.get("DOCKER_HOST", None) client, url = helper.get_docker_client(docker_host_url, True) if client: - self.docker_hosts.append(DockerHost(docker_client=client, docker_host_url=url)) + self.docker_hosts.append(DockerHost(client=client, url=url)) remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } for i in range(1, 20): @@ -35,24 +35,24 @@ def init_docker_hosts(self): client, url = helper.get_docker_client(docker_host_url) if client: - self.docker_hosts.append(DockerHost(docker_client=client, docker_host_url=url)) + self.docker_hosts.append(DockerHost(client=client, url=url)) if not self.docker_hosts: logging.error("Failed to connect to any docker host") def process_containers(self): for docker_host in self.docker_hosts: - if not helper.is_docker_connected(docker_host.docker_client): + if not helper.is_docker_connected(docker_host.client): if docker_host.is_connected: - logging.warning(f"Lost connection to {docker_host.docker_host_url}") + logging.warning(f"Lost connection to {docker_host.url}") docker_host.is_connected = False continue if not docker_host.is_connected: - logging.info(f"Connection to {docker_host.docker_host_url} has been restored") + logging.info(f"Connection to {docker_host.url} has been restored") docker_host.is_connected = True - containers = docker_host.docker_client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) + containers = docker_host.client.containers.list(all=True, filters={ "label": ["swag_ondemand=enable"] }) container_names = {container.name for container in containers} for container_name in list(docker_host.ondemand_containers.keys()): @@ -89,11 +89,11 @@ def stop_containers(self): if inactive_seconds < STOP_THRESHOLD: continue - if not helper.is_docker_connected(docker_host.docker_client): - logging.warning(f"Failed to stop {container_name}, docker host {docker_host.docker_host_url} is unavailable") + if not helper.is_docker_connected(docker_host.client): + logging.warning(f"Failed to stop {container_name}, docker host {docker_host.url} is unavailable") continue - docker_host.docker_client.containers.get(container_name).stop() + docker_host.client.containers.get(container_name).stop() logging.info(f"Stopped {container_name} after {STOP_THRESHOLD}s of inactivity") def start_containers(self): @@ -113,11 +113,11 @@ def start_containers(self): if not accessed or container.status == "running": continue - if not helper.is_docker_connected(docker_host.docker_client): - logging.warning(f"Failed to start {container_name}, docker host {docker_host.docker_host_url} is unavailable") + if not helper.is_docker_connected(docker_host.client): + logging.warning(f"Failed to start {container_name}, docker host {docker_host.url} is unavailable") continue - docker_host.docker_client.containers.get(container_name).start() + docker_host.client.containers.get(container_name).start() logging.info(f"Started {container_name}") container.status = "running" diff --git a/root/app/ondemand/data_classes.py b/root/app/ondemand/data_classes.py index 9f478bd2..ccc4384b 100644 --- a/root/app/ondemand/data_classes.py +++ b/root/app/ondemand/data_classes.py @@ -10,7 +10,7 @@ class OnDemandContainer: @dataclass class DockerHost: - docker_client: docker.DockerClient - docker_host_url: str + client: docker.DockerClient + url: str is_connected: bool = False - ondemand_containers: dict = field(default_factory=dict) + ondemand_containers: dict[str, OnDemandContainer] = field(default_factory=dict) From 040cca4ca036d93cee922db99c97ca67da61cc81 Mon Sep 17 00:00:00 2001 From: quietsy Date: Wed, 1 Jul 2026 18:31:25 +0300 Subject: [PATCH 14/14] Fix logic bug --- root/app/ondemand/container_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/root/app/ondemand/container_thread.py b/root/app/ondemand/container_thread.py index fc444389..7bdd10bf 100644 --- a/root/app/ondemand/container_thread.py +++ b/root/app/ondemand/container_thread.py @@ -27,7 +27,7 @@ def init_docker_hosts(self): self.docker_hosts.append(DockerHost(client=client, url=url)) remote_hosts_env_vars = { key: value for key, value in os.environ.items() if key.startswith(REMOTE_HOSTS_PREFIX) } - for i in range(1, 20): + for i in range(1, 21): if f"{REMOTE_HOSTS_PREFIX}{i}" not in remote_hosts_env_vars: break