from easymapping import HaproxyConfigGenerator from functions import Functions, Consts import yaml import sys import os import json import base64 import docker import socket from kubernetes import client, config from kubernetes.client.rest import ApiException class ContainerEnv: @staticmethod def read(): env_vars = { "customerrors": True if os.getenv("HAPROXY_CUSTOMERRORS") == "true" else False, "ssl_mode": os.getenv("EASYHAPROXY_SSL_MODE").lower() if os.getenv("EASYHAPROXY_SSL_MODE") else 'default' } if os.getenv("HAPROXY_PASSWORD"): env_vars["stats"] = { "username": os.getenv("HAPROXY_USERNAME") if os.getenv("HAPROXY_USERNAME") else "admin", "password": os.getenv("HAPROXY_PASSWORD"), "port": os.getenv("HAPROXY_STATS_PORT") if os.getenv("HAPROXY_STATS_PORT") else "1936", } env_vars["lookup_label"] = os.getenv("EASYHAPROXY_LABEL_PREFIX") if os.getenv("EASYHAPROXY_LABEL_PREFIX") else "easyhaproxy" if (os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")): env_vars["letsencrypt"] = { "email": os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL"), "server": os.getenv("EASYHAPROXY_LETSENCRYPT_SERVER", "false").lower() in ["true", "1", "yes"] } env_vars["logLevel"] = { "easyhaproxy": os.getenv("EASYHAPROXY_LOG_LEVEL") if os.getenv("EASYHAPROXY_LOG_LEVEL") else Functions.DEBUG, "haproxy": os.getenv("HAPROXY_LOG_LEVEL") if os.getenv("HAPROXY_LOG_LEVEL") else Functions.INFO, "certbot": os.getenv("CERTBOT_LOG_LEVEL") if os.getenv("CERTBOT_LOG_LEVEL") else Functions.DEBUG, } return env_vars class ProcessorInterface: static_file = Consts.easyhaproxy_config def __init__(self, filename = None): self.filename = filename self.refresh() @staticmethod def factory(mode): if mode == "static": return Static(ProcessorInterface.static_file) elif mode == "docker": return Docker() elif mode == "swarm": return Swarm() elif mode == "kubernetes": return Kubernetes() else: Functions.log("EASYHAPROXY", Functions.FATAL, "Expected mode to be 'static', 'docker', 'swarm' or 'kubernetes'. I got '%s'" % (mode)) return None def refresh(self): self.letsencrypt_hosts = None self.parsed_object = None self.cfg = None self.hosts = None self.inspect_network() self.parse() def inspect_network(self): #Abstract pass def parse(self): self.cfg = HaproxyConfigGenerator(ContainerEnv.read()) def get_letsencrypt_hosts(self): return self.letsencrypt_hosts def get_hosts(self): return self.hosts def get_parsed_object(self): return self.parsed_object def get_certs(self, key = None): if key is None: return self.cfg.certs else: return None if key not in self.cfg.certs else self.cfg.certs[key] def get_haproxy_conf(self): conf = self.cfg.generate(self.parsed_object) self.letsencrypt_hosts = self.cfg.letsencrypt_hosts self.hosts = self.cfg.serving_hosts return conf def save_config(self, filename): Functions.save(filename, self.get_haproxy_conf()) def save_certs(self, path): for cert in self.get_certs(): Functions.save("{0}/{1}".format(path, cert), self.get_certs(cert)) class Static(ProcessorInterface): def inspect_network(self): self.parsed_object = {} self.static_content = None def get_parsed_object(self): return self.static_content["easymapping"] if "easymapping" in self.static_content else [] def get_hosts(self): hosts = [] for object in self.get_parsed_object(): if "hosts" not in object: continue for host in object["hosts"].keys(): hosts.append("%s:%s" % (host, object["port"])) return hosts def parse(self): self.static_content = yaml.load(Functions.load(self.filename), Loader=yaml.FullLoader) self.cfg = HaproxyConfigGenerator(self.static_content) class Docker(ProcessorInterface): def __init__(self, filename = None): self.client = docker.from_env() super().__init__() def inspect_network(self): try: ha_proxy_network_name = next(iter(self.client.containers.get(socket.gethostname()).attrs["NetworkSettings"]["Networks"])) except: # HAProxy is not running in a container, get first container network if len(self.client.containers.list()) == 0: return ha_proxy_network_name = next(iter(self.client.containers.get(self.client.containers.list()[0].name).attrs["NetworkSettings"]["Networks"])) ha_proxy_network = self.client.networks.get(ha_proxy_network_name) self.parsed_object = {} for container in self.client.containers.list(): # Issue 32 - Docker container cannot connect to containers in different network. if ha_proxy_network_name not in container.attrs["NetworkSettings"]["Networks"].keys(): ha_proxy_network.connect(container.name) container = self.client.containers.get(container.name) # refresh object ip_address = container.attrs["NetworkSettings"]["Networks"][ha_proxy_network_name]["IPAddress"] self.parsed_object[ip_address] = container.labels class Swarm(ProcessorInterface): def __init__(self, filename = None): self.client = docker.from_env() super().__init__() def inspect_network(self): ha_proxy_service_name = self.client.containers.get(socket.gethostname()).name.split('.')[0] for endpoint in self.client.services.get(ha_proxy_service_name).attrs['Endpoint']["VirtualIPs"]: ha_proxy_network_id = endpoint["NetworkID"] if self.client.networks.get(ha_proxy_network_id).name != 'ingress': break self.parsed_object = {} for service in self.client.services.list(): ip_address = None network_list = [] for endpoint in service.attrs["Endpoint"]["VirtualIPs"]: if ha_proxy_network_id == endpoint["NetworkID"]: ip_address = endpoint["Addr"].split("/")[0] break network_list.append(endpoint["NetworkID"]) if ip_address is None: network_list.append(ha_proxy_network_id) service.update(networks = network_list) continue # skip to the next service to give time to update the network self.parsed_object[ip_address] = service.attrs["Spec"]["Labels"] class Kubernetes(ProcessorInterface): def __init__(self, filename = None): config.load_incluster_config() config.verify_ssl = False self.api_instance = client.CoreV1Api() self.v1 = client.NetworkingV1Api() self.cert_cache = {} super().__init__() def _check_annotation(self, annotations, key, default = None): if key not in annotations: return default return annotations[key] def inspect_network(self): ret = self.v1.list_ingress_for_all_namespaces(watch=False) self.parsed_object = {} for ingress in ret.items: if 'kubernetes.io/ingress.class' not in ingress.metadata.annotations: continue if ingress.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": continue ssl_hosts = [] letsencrypt = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.letsencrypt") redirect_ssl = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect_ssl") redirect = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.redirect") mode = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.mode") listen_port = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.listen_port") if listen_port is None: listen_port = 80 data = {} data["creation_timestamp"] = ingress.metadata.creation_timestamp.strftime("%x %X") data["resource_version"] = ingress.metadata.resource_version data["namespace"] = ingress.metadata.namespace ingress_name = ingress.metadata.namespace if ingress.spec.tls is not None: for tls in ingress.spec.tls: try: secret = self.api_instance.read_namespaced_secret(tls.secret_name, ingress.metadata.namespace) if "tls.crt" not in secret.data or "tls.key" not in secret.data: continue if tls.secret_name not in self.cert_cache or self.cert_cache[tls.secret_name] != secret.data: self.cert_cache[tls.secret_name] = secret.data Functions.save( "{0}/{1}.pem".format(Consts.certs_haproxy, tls.secret_name), base64.b64decode(secret.data["tls.crt"]).decode('ascii') + "\n" + base64.b64decode(secret.data["tls.key"]).decode('ascii') ) ssl_hosts.extend(tls.hosts) except Exception as e: Functions.log("EASYHAPROXY", Functions.WARN, "Ingress %s - Get secret failed: '%s'" % (ingress_name, e)) Functions.log("EASYHAPROXY", Functions.TRACE, "Ingress %s - SSL Hosts found '%s'" % (ingress_name, ssl_hosts)) for rule in ingress.spec.rules: rule_data = {} port_number = rule.http.paths[0].backend.service.port.number definition = "easyhaproxy.%s_%s" % (rule.host.replace(".", "-"), port_number) rule_data["%s.host" % (definition)] = rule.host rule_data["%s.port" % (definition)] = listen_port rule_data["%s.localport" % (definition)] = port_number if rule.host in ssl_hosts: rule_data["%s.clone_to_ssl" % (definition)] = 'true' if redirect_ssl is not None: rule_data["%s.redirect_ssl" % (definition)] = redirect_ssl if letsencrypt is not None: rule_data["%s.letsencrypt" % (definition)] = letsencrypt if redirect is not None: rule_data["%s.redirect" % (definition)] = redirect if mode is not None: rule_data["%s.mode" % (definition)] = mode rule_data["%s.balance" % (definition)] = self._check_annotation(ingress.metadata.annotations, "easyhaproxy.balance", "roundrobin") service_name = rule.http.paths[0].backend.service.name try: api_response = self.api_instance.read_namespaced_service(service_name, ingress.metadata.namespace) cluster_ip = api_response.spec.cluster_ip except ApiException as e: cluster_ip = None Functions.log("EASYHAPROXY", Functions.WARN, "Ingress %s - Service %s - Failed: '%s'" % (ingress_name, service_name, e)) if cluster_ip is not None: if cluster_ip not in self.parsed_object.keys(): self.parsed_object[cluster_ip] = data self.parsed_object[cluster_ip].update(rule_data)