bunkerweb/src/autoconf/DockerController.py
2023-10-03 13:05:26 +02:00

123 lines
5.2 KiB
Python

#!/usr/bin/python3
from typing import Any, Dict, List
from docker import DockerClient
from re import compile as re_compile
from traceback import format_exc
from docker.models.containers import Container
from Controller import Controller
class DockerController(Controller):
def __init__(self, docker_host):
super().__init__("docker")
self.__client = DockerClient(base_url=docker_host)
self.__custom_confs_rx = re_compile(r"^bunkerweb.CUSTOM_CONF_(SERVER_HTTP|MODSEC_CRS|MODSEC)_(.+)$")
def _get_controller_instances(self) -> List[Container]:
return self.__client.containers.list(filters={"label": "bunkerweb.INSTANCE"})
def _get_controller_services(self) -> List[Container]:
return self.__client.containers.list(filters={"label": "bunkerweb.SERVER_NAME"})
def _to_instances(self, controller_instance) -> List[dict]:
instance = {}
instance["name"] = controller_instance.name
instance["hostname"] = controller_instance.name
instance["health"] = controller_instance.status == "running" and controller_instance.attrs["State"]["Health"]["Status"] == "healthy"
instance["env"] = {}
for env in controller_instance.attrs["Config"]["Env"]:
variable = env.split("=")[0]
value = env.replace(f"{variable}=", "", 1)
if self._is_setting(variable):
instance["env"][variable] = value
return [instance]
def _to_services(self, controller_service) -> List[dict]:
service = {}
for variable, value in controller_service.labels.items():
if not variable.startswith("bunkerweb."):
continue
real_variable = variable.replace("bunkerweb.", "", 1)
if not self._is_setting_context(real_variable, "multisite"):
continue
service[real_variable] = value
return [service]
def _get_static_services(self) -> List[dict]:
services = []
variables = {}
for instance in self.__client.containers.list(filters={"label": "bunkerweb.INSTANCE"}):
if not instance.attrs or not instance.attrs.get("Config", {}).get("Env"):
continue
for env in instance.attrs["Config"]["Env"]:
variable = env.split("=")[0]
value = env.replace(f"{variable}=", "", 1)
variables[variable] = value
if "SERVER_NAME" in variables and variables["SERVER_NAME"].strip():
for server_name in variables["SERVER_NAME"].strip().split(" "):
service = {"SERVER_NAME": server_name}
for variable, value in variables.items():
prefix = variable.split("_")[0]
real_variable = variable.replace(f"{prefix}_", "", 1)
if prefix == server_name and self._is_setting_context(real_variable, "multisite"):
service[real_variable] = value
services.append(service)
return services
def get_configs(self) -> Dict[str, Dict[str, Any]]:
configs = {config_type: {} for config_type in self._supported_config_types}
# get site configs from labels
for container in self.__client.containers.list(filters={"label": "bunkerweb.SERVER_NAME"}):
labels = container.labels # type: ignore (labels is inside a container)
if isinstance(labels, list):
labels = {label: "" for label in labels}
# extract server_name
server_name = labels.get("bunkerweb.SERVER_NAME", "").split(" ")[0]
# extract configs
if not server_name:
continue
for variable, value in labels.items():
if not variable.startswith("bunkerweb."):
continue
result = self.__custom_confs_rx.search(variable)
if result is None:
continue
configs[result.group(1).lower().replace("_", "-")][f"{server_name}/{result.group(2)}"] = value
return configs
def apply_config(self) -> bool:
return self.apply(
self._instances,
self._services,
configs=self._configs,
first=not self._loaded,
)
def process_events(self):
self._set_autoconf_load_db()
for _ in self.__client.events(decode=True, filters={"type": "container"}):
try:
self._update_settings()
self._instances = self.get_instances()
self._services = self.get_services()
self._configs = self.get_configs()
if not self.update_needed(self._instances, self._services, configs=self._configs):
continue
self._logger.info("Caught Docker event, deploying new configuration ...")
if not self.apply_config():
self._logger.error("Error while deploying new configuration")
else:
self._logger.info(
"Successfully deployed new configuration 🚀",
)
self._set_autoconf_load_db()
except:
self._logger.error(f"Exception while processing events :\n{format_exc()}")