mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
feat: Add support for NAMESPACES environment variable + Make it so that Ingress specific variables are applied to all declared services in Kubernetes
This commit adds support for the `NAMESPACES` environment variable in the Controller files. The `NAMESPACES` variable is used to filter instances and services based on the specified namespaces. Only instances and services in the specified namespaces will be considered.
This commit is contained in:
parent
b10d8acf6c
commit
a03fb245db
6 changed files with 116 additions and 80 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from os import getenv
|
||||
from time import sleep
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
|
@ -43,18 +42,13 @@ class Config:
|
|||
self._settings.update(plugin["settings"])
|
||||
|
||||
def __get_full_env(self) -> dict:
|
||||
env_instances = {"SERVER_NAME": ""}
|
||||
for instance in self.__instances:
|
||||
for variable, value in instance["env"].items():
|
||||
env_instances[variable] = value
|
||||
|
||||
config = {"SERVER_NAME": "", "MULTISITE": "yes"}
|
||||
for service in self.__services:
|
||||
server_name = service["SERVER_NAME"].split(" ")[0]
|
||||
if not server_name:
|
||||
continue
|
||||
for variable, value in chain(env_instances.items(), service.items()):
|
||||
if variable.startswith("CUSTOM_CONF") or not variable.isupper():
|
||||
for variable, value in service.items():
|
||||
if variable == "NAMESPACE" or variable.startswith("CUSTOM_CONF") or not variable.isupper():
|
||||
continue
|
||||
if not self._db.is_setting(variable, multisite=True):
|
||||
if variable in service:
|
||||
|
|
@ -155,12 +149,7 @@ class Config:
|
|||
exploded = file.split("/")
|
||||
site = exploded[0]
|
||||
name = exploded[1]
|
||||
custom_configs.append(
|
||||
{
|
||||
"value": data,
|
||||
"exploded": [site, config_type, name.replace(".conf", "")],
|
||||
}
|
||||
)
|
||||
custom_configs.append({"value": data, "exploded": [site, config_type, name.replace(".conf", "")]})
|
||||
|
||||
# update instances in database
|
||||
if "instances" in changes:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ class Controller(Config):
|
|||
self._services = []
|
||||
self._configs = {config_type: {} for config_type in self._supported_config_types}
|
||||
self._logger = setup_logger(f"{self._type}-controller", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
|
||||
self._namespaces = None
|
||||
namespaces = getenv("NAMESPACES")
|
||||
if namespaces:
|
||||
self._namespaces = namespaces.strip().split(" ")
|
||||
self._logger.info(
|
||||
"Only instances and services in the " + ", ".join(f"{namespace!r}" for namespace in self._namespaces) + " namespace(s) will be considered."
|
||||
)
|
||||
|
||||
def wait(self, wait_time: int) -> list:
|
||||
all_ready = False
|
||||
|
|
|
|||
|
|
@ -17,10 +17,32 @@ class DockerController(Controller):
|
|||
self.__custom_confs_rx = re_compile(r"^bunkerweb.CUSTOM_CONF_(SERVER_STREAM|SERVER_HTTP|MODSEC_CRS|MODSEC|CRS_PLUGINS_BEFORE|CRS_PLUGINS_AFTER)_(.+)$")
|
||||
|
||||
def _get_controller_instances(self) -> List[Container]:
|
||||
return self.__client.containers.list(filters={"label": "bunkerweb.INSTANCE"})
|
||||
containers: List[Container] = self.__client.containers.list(filters={"label": "bunkerweb.INSTANCE"})
|
||||
if not self._namespaces:
|
||||
return containers
|
||||
return [
|
||||
container
|
||||
for container in containers
|
||||
if any(
|
||||
({label: "" for label in container.labels} if isinstance(container.labels, list) else container.labels).get("bunkerweb.NAMESPACE", "")
|
||||
== namespace
|
||||
for namespace in self._namespaces
|
||||
)
|
||||
]
|
||||
|
||||
def _get_controller_services(self) -> List[Container]:
|
||||
return self.__client.containers.list(filters={"label": "bunkerweb.SERVER_NAME"})
|
||||
containers: List[Container] = self.__client.containers.list(filters={"label": "bunkerweb.SERVER_NAME"})
|
||||
if not self._namespaces:
|
||||
return containers
|
||||
return [
|
||||
container
|
||||
for container in containers
|
||||
if any(
|
||||
({label: "" for label in container.labels} if isinstance(container.labels, list) else container.labels).get("bunkerweb.NAMESPACE", "")
|
||||
== namespace
|
||||
for namespace in self._namespaces
|
||||
)
|
||||
]
|
||||
|
||||
def _to_instances(self, controller_instance) -> List[dict]:
|
||||
instance = {}
|
||||
|
|
@ -50,6 +72,9 @@ class DockerController(Controller):
|
|||
if isinstance(labels, list):
|
||||
labels = {label: "" for label in labels}
|
||||
|
||||
if self._namespaces and not any(labels.get("bunkerweb.NAMESPACE", "") == namespace for namespace in self._namespaces):
|
||||
continue
|
||||
|
||||
# extract server_name
|
||||
server_name = labels.get("bunkerweb.SERVER_NAME", "").split(" ")[0]
|
||||
|
||||
|
|
@ -79,6 +104,7 @@ class DockerController(Controller):
|
|||
"Actor" in event
|
||||
and "Attributes" in event["Actor"]
|
||||
and ("bunkerweb.INSTANCE" in event["Actor"]["Attributes"] or "bunkerweb.SERVER_NAME" in event["Actor"]["Attributes"])
|
||||
and (not self._namespaces or any(event["Actor"]["Attributes"].get("bunkerweb.NAMESPACE", "") == namespace for namespace in self._namespaces))
|
||||
)
|
||||
|
||||
def process_events(self):
|
||||
|
|
|
|||
|
|
@ -20,17 +20,29 @@ class IngressController(Controller):
|
|||
self.__networkingv1 = client.NetworkingV1Api()
|
||||
|
||||
def _get_controller_instances(self) -> list:
|
||||
return [
|
||||
pod
|
||||
for pod in self.__corev1.list_pod_for_all_namespaces(watch=False).items
|
||||
if (pod.metadata.annotations and "bunkerweb.io/INSTANCE" in pod.metadata.annotations)
|
||||
]
|
||||
instances = []
|
||||
pods = self.__corev1.list_pod_for_all_namespaces(watch=False).items
|
||||
for pod in pods:
|
||||
if (
|
||||
pod.metadata.annotations
|
||||
and "bunkerweb.io/INSTANCE" in pod.metadata.annotations
|
||||
and (pod.metadata.namespace in self._namespaces if self._namespaces else True)
|
||||
):
|
||||
instances.append(pod)
|
||||
return instances
|
||||
|
||||
def _get_controller_services(self) -> list:
|
||||
services = []
|
||||
ingresses = self.__networkingv1.list_ingress_for_all_namespaces(watch=False).items
|
||||
for ingress in ingresses:
|
||||
if ingress.metadata.namespace in self._namespaces if self._namespaces else True:
|
||||
services.append(ingress)
|
||||
return services
|
||||
|
||||
def _to_instances(self, controller_instance) -> List[dict]:
|
||||
instance = {
|
||||
"name": controller_instance.metadata.name,
|
||||
"hostname": controller_instance.metadata.name,
|
||||
"env": self._get_scheduler_env(),
|
||||
}
|
||||
health = False
|
||||
if controller_instance.status.conditions:
|
||||
|
|
@ -52,18 +64,12 @@ class IngressController(Controller):
|
|||
instance["env"][env.name] = env.value or ""
|
||||
for controller_service in self._get_controller_services():
|
||||
if controller_service.metadata.annotations:
|
||||
for (
|
||||
annotation,
|
||||
value,
|
||||
) in controller_service.metadata.annotations.items():
|
||||
for annotation, value in controller_service.metadata.annotations.items():
|
||||
if not annotation.startswith("bunkerweb.io/"):
|
||||
continue
|
||||
instance["env"][annotation.replace("bunkerweb.io/", "", 1)] = value
|
||||
return [instance]
|
||||
|
||||
def _get_controller_services(self) -> list:
|
||||
return self.__networkingv1.list_ingress_for_all_namespaces(watch=False).items
|
||||
|
||||
def _to_services(self, controller_service) -> List[dict]:
|
||||
if not controller_service.spec or not controller_service.spec.rules:
|
||||
return []
|
||||
|
|
@ -72,9 +78,7 @@ class IngressController(Controller):
|
|||
# parse rules
|
||||
for rule in controller_service.spec.rules:
|
||||
if not rule.host:
|
||||
self._logger.warning(
|
||||
"Ignoring unsupported ingress rule without host.",
|
||||
)
|
||||
self._logger.warning("Ignoring unsupported ingress rule without host.")
|
||||
continue
|
||||
service = {}
|
||||
service["SERVER_NAME"] = rule.host
|
||||
|
|
@ -84,35 +88,26 @@ class IngressController(Controller):
|
|||
location = 1
|
||||
for path in rule.http.paths:
|
||||
if not path.path:
|
||||
self._logger.warning(
|
||||
"Ignoring unsupported ingress rule without path.",
|
||||
)
|
||||
self._logger.warning("Ignoring unsupported ingress rule without path.")
|
||||
continue
|
||||
elif not path.backend.service:
|
||||
self._logger.warning(
|
||||
"Ignoring unsupported ingress rule without backend service.",
|
||||
)
|
||||
self._logger.warning("Ignoring unsupported ingress rule without backend service.")
|
||||
continue
|
||||
elif not path.backend.service.port:
|
||||
self._logger.warning(
|
||||
"Ignoring unsupported ingress rule without backend service port.",
|
||||
)
|
||||
self._logger.warning("Ignoring unsupported ingress rule without backend service port.")
|
||||
continue
|
||||
elif not path.backend.service.port.number:
|
||||
self._logger.warning(
|
||||
"Ignoring unsupported ingress rule without backend service port number.",
|
||||
)
|
||||
self._logger.warning("Ignoring unsupported ingress rule without backend service port number.")
|
||||
continue
|
||||
|
||||
service_list = self.__corev1.list_service_for_all_namespaces(
|
||||
service_list = self.__corev1.list_namespaced_service(
|
||||
namespace,
|
||||
watch=False,
|
||||
field_selector=f"metadata.name={path.backend.service.name},metadata.namespace={namespace}",
|
||||
field_selector=f"metadata.name={path.backend.service.name}",
|
||||
).items
|
||||
|
||||
if not service_list:
|
||||
self._logger.warning(
|
||||
f"Ignoring ingress rule with service {path.backend.service.name} : service not found.",
|
||||
)
|
||||
self._logger.warning(f"Ignoring ingress rule with service {path.backend.service.name} : service not found.")
|
||||
continue
|
||||
|
||||
reverse_proxy_host = f"http://{path.backend.service.name}.{namespace}.svc.cluster.local:{path.backend.service.port.number}"
|
||||
|
|
@ -129,17 +124,14 @@ class IngressController(Controller):
|
|||
# parse annotations
|
||||
if controller_service.metadata.annotations:
|
||||
for service in services:
|
||||
for (
|
||||
annotation,
|
||||
value,
|
||||
) in controller_service.metadata.annotations.items():
|
||||
for annotation, value in controller_service.metadata.annotations.items():
|
||||
if not annotation.startswith("bunkerweb.io/"):
|
||||
continue
|
||||
|
||||
variable = annotation.replace("bunkerweb.io/", "", 1)
|
||||
server_name = service["SERVER_NAME"].strip().split(" ")[0]
|
||||
if not variable.startswith(f"{server_name}_"):
|
||||
continue
|
||||
variable = f"{server_name}_{variable}" # ? Ingress specific global variables are applied to all services
|
||||
service[variable.replace(f"{server_name}_", "", 1)] = value
|
||||
|
||||
# parse tls
|
||||
|
|
@ -149,25 +141,21 @@ class IngressController(Controller):
|
|||
for host in tls.hosts:
|
||||
for service in services:
|
||||
if host in service["SERVER_NAME"].split(" "):
|
||||
secrets_tls = self.__corev1.list_secret_for_all_namespaces(
|
||||
secrets_tls = self.__corev1.list_namespaced_secret(
|
||||
namespace,
|
||||
watch=False,
|
||||
field_selector=f"metadata.name={tls.secret_name},metadata.namespace={namespace}",
|
||||
field_selector=f"metadata.name={tls.secret_name}",
|
||||
).items
|
||||
if len(secrets_tls) == 0:
|
||||
self._logger.warning(
|
||||
f"Ignoring tls setting for {host} : secret {tls.secret_name} not found.",
|
||||
)
|
||||
if not secrets_tls:
|
||||
self._logger.warning(f"Ignoring tls setting for {host} : secret {tls.secret_name} not found.")
|
||||
break
|
||||
|
||||
secret_tls = secrets_tls[0]
|
||||
if not secret_tls.data:
|
||||
self._logger.warning(
|
||||
f"Ignoring tls setting for {host} : secret {tls.secret_name} contains no data.",
|
||||
)
|
||||
self._logger.warning(f"Ignoring tls setting for {host} : secret {tls.secret_name} contains no data.")
|
||||
break
|
||||
if "tls.crt" not in secret_tls.data or "tls.key" not in secret_tls.data:
|
||||
self._logger.warning(
|
||||
f"Ignoring tls setting for {host} : secret {tls.secret_name} is missing tls data.",
|
||||
)
|
||||
elif "tls.crt" not in secret_tls.data or "tls.key" not in secret_tls.data:
|
||||
self._logger.warning(f"Ignoring tls setting for {host} : secret {tls.secret_name} is missing tls data.")
|
||||
break
|
||||
service["USE_CUSTOM_SSL"] = "yes"
|
||||
service["CUSTOM_SSL_CERT_DATA"] = secret_tls.data["tls.crt"]
|
||||
|
|
@ -177,28 +165,30 @@ class IngressController(Controller):
|
|||
def get_configs(self) -> dict:
|
||||
configs = {config_type: {} for config_type in self._supported_config_types}
|
||||
for configmap in self.__corev1.list_config_map_for_all_namespaces(watch=False).items:
|
||||
if not configmap.metadata.annotations or "bunkerweb.io/CONFIG_TYPE" not in configmap.metadata.annotations:
|
||||
if (
|
||||
not configmap.metadata.annotations
|
||||
or "bunkerweb.io/CONFIG_TYPE" not in configmap.metadata.annotations
|
||||
or not (configmap.metadata.namespace in self._namespaces if self._namespaces else True)
|
||||
):
|
||||
continue
|
||||
|
||||
config_type = configmap.metadata.annotations["bunkerweb.io/CONFIG_TYPE"]
|
||||
if config_type not in self._supported_config_types:
|
||||
self._logger.warning(
|
||||
f"Ignoring unsupported CONFIG_TYPE {config_type} for ConfigMap {configmap.metadata.name}",
|
||||
)
|
||||
self._logger.warning(f"Ignoring unsupported CONFIG_TYPE {config_type} for ConfigMap {configmap.metadata.name}")
|
||||
continue
|
||||
elif not configmap.data:
|
||||
self._logger.warning(
|
||||
f"Ignoring blank ConfigMap {configmap.metadata.name}",
|
||||
)
|
||||
self._logger.warning(f"Ignoring blank ConfigMap {configmap.metadata.name}")
|
||||
continue
|
||||
|
||||
config_site = ""
|
||||
if "bunkerweb.io/CONFIG_SITE" in configmap.metadata.annotations:
|
||||
if not self._is_service_present(configmap.metadata.annotations["bunkerweb.io/CONFIG_SITE"]):
|
||||
self._logger.warning(
|
||||
f"Ignoring config {configmap.metadata.name} because {configmap.metadata.annotations['bunkerweb.io/CONFIG_SITE']} doesn't exist",
|
||||
f"Ignoring config {configmap.metadata.name} because {configmap.metadata.annotations['bunkerweb.io/CONFIG_SITE']} doesn't exist"
|
||||
)
|
||||
continue
|
||||
config_site = f"{configmap.metadata.annotations['bunkerweb.io/CONFIG_SITE']}/"
|
||||
|
||||
for config_name, config_data in configmap.data.items():
|
||||
configs[config_type][f"{config_site}{config_name}"] = config_data
|
||||
return configs
|
||||
|
|
@ -206,6 +196,10 @@ class IngressController(Controller):
|
|||
def __process_event(self, event):
|
||||
obj = event["object"]
|
||||
metadata = obj.metadata if obj else None
|
||||
|
||||
if metadata and self._namespaces and metadata.namespace not in self._namespaces:
|
||||
return False
|
||||
|
||||
annotations = metadata.annotations if metadata else None
|
||||
data = getattr(obj, "data", None) if obj else None
|
||||
if not obj:
|
||||
|
|
|
|||
|
|
@ -23,11 +23,25 @@ class SwarmController(Controller):
|
|||
|
||||
def _get_controller_instances(self) -> List[Service]:
|
||||
self.__swarm_instances = []
|
||||
return self.__client.services.list(filters={"label": "bunkerweb.INSTANCE"})
|
||||
services = self.__client.services.list(filters={"label": "bunkerweb.INSTANCE"})
|
||||
if not self._namespaces:
|
||||
return services
|
||||
return [
|
||||
service
|
||||
for service in services
|
||||
if any(service.attrs["Spec"]["Labels"].get("bunkerweb.NAMESPACE", "") == namespace for namespace in self._namespaces)
|
||||
]
|
||||
|
||||
def _get_controller_services(self) -> List[Service]:
|
||||
self.__swarm_services = []
|
||||
return self.__client.services.list(filters={"label": "bunkerweb.SERVER_NAME"})
|
||||
services = self.__client.services.list(filters={"label": "bunkerweb.SERVER_NAME"})
|
||||
if not self._namespaces:
|
||||
return services
|
||||
return [
|
||||
service
|
||||
for service in services
|
||||
if any(service.attrs["Spec"]["Labels"].get("bunkerweb.NAMESPACE", "") == namespace for namespace in self._namespaces)
|
||||
]
|
||||
|
||||
def _to_instances(self, controller_instance) -> List[dict]:
|
||||
self.__swarm_instances.append(controller_instance.id)
|
||||
|
|
@ -46,7 +60,7 @@ class SwarmController(Controller):
|
|||
"name": task["ID"],
|
||||
"hostname": f"{controller_instance.name}.{task['NodeID']}.{task['ID']}",
|
||||
"health": task["Status"]["State"] == "running",
|
||||
"env": self._get_scheduler_env(),
|
||||
"env": instance_env,
|
||||
}
|
||||
)
|
||||
return instances
|
||||
|
|
@ -106,14 +120,19 @@ class SwarmController(Controller):
|
|||
return True
|
||||
try:
|
||||
labels = self.__client.services.get(event["Actor"]["ID"]).attrs["Spec"]["Labels"]
|
||||
return "bunkerweb.INSTANCE" in labels or "bunkerweb.SERVER_NAME" in labels
|
||||
return ("bunkerweb.INSTANCE" in labels or "bunkerweb.SERVER_NAME" in labels) and (
|
||||
not self._namespaces or any(labels.get("bunkerweb.NAMESPACE", "") == namespace for namespace in self._namespaces)
|
||||
)
|
||||
except:
|
||||
return False
|
||||
if event["Type"] == "config":
|
||||
if event["Actor"]["ID"] in self.__swarm_configs:
|
||||
return True
|
||||
try:
|
||||
return "bunkerweb.CONFIG_TYPE" in self.__client.configs.get(event["Actor"]["ID"]).attrs["Spec"]["Labels"]
|
||||
labels = self.__client.services.get(event["Actor"]["ID"]).attrs["Spec"]["Labels"]
|
||||
return "bunkerweb.CONFIG_TYPE" in labels and (
|
||||
not self._namespaces or any(labels.get("bunkerweb.NAMESPACE", "") == namespace for namespace in self._namespaces)
|
||||
)
|
||||
except:
|
||||
return False
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ class Configurator:
|
|||
"PWD",
|
||||
"SHLVL",
|
||||
"SERVER_SOFTWARE",
|
||||
"NAMESPACE",
|
||||
)
|
||||
):
|
||||
self.__logger.warning(f"Ignoring variable {variable} : {err}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue