diff --git a/examples/kubernetes/service.yml b/examples/kubernetes/service.yml index d98399f..2ac821a 100644 --- a/examples/kubernetes/service.yml +++ b/examples/kubernetes/service.yml @@ -5,7 +5,7 @@ metadata: annotations: kubernetes.io/ingress.class: easyhaproxy-ingress name: container-example - namespace: parking + namespace: default spec: rules: - host: example.org @@ -32,7 +32,7 @@ apiVersion: v1 kind: Service metadata: name: container-example - namespace: parking + namespace: default spec: ports: - name: http @@ -46,7 +46,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: container-example - namespace: parking + namespace: default spec: replicas: 1 revisionHistoryLimit: 10 diff --git a/examples/kubernetes/service_tls.yml b/examples/kubernetes/service_tls.yml new file mode 100644 index 0000000..e351fc2 --- /dev/null +++ b/examples/kubernetes/service_tls.yml @@ -0,0 +1,86 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: easyhaproxy-ingress + name: tls-example + namespace: default +spec: + tls: + - hosts: + - host2.local + secretName: host2-tls + rules: + - host: host2.local + http: + paths: + - backend: + service: + name: tls-example + port: + number: 8080 + pathType: ImplementationSpecific + +--- +apiVersion: v1 +kind: Secret +metadata: + name: host2-tls + namespace: default +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxVENDQXBHZ0F3SUJBZ0lVSWQ1Yjl0OXVxSDc4ZzAyRXpiV0Y2RktWdzNnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pERUxNQWtHQTFVRUJoTUNRbEl4RnpBVkJnTlZCQWdNRGxKcGJ5QmtaU0JLWVc1bGFYSnZNUmN3RlFZRApWUVFIREE1U2FXOGdaR1VnU21GdVpXbHliekVOTUFzR0ExVUVDZ3dFUVVOTlJURVVNQklHQTFVRUF3d0xhRzl6CmRESXViRzlqWVd3d0hoY05Nakl3T0RFMU1EUXlOekExV2hjTk1qTXdPREUxTURReU56QTFXakJrTVFzd0NRWUQKVlFRR0V3SkNVakVYTUJVR0ExVUVDQXdPVW1sdklHUmxJRXBoYm1WcGNtOHhGekFWQmdOVkJBY01EbEpwYnlCawpaU0JLWVc1bGFYSnZNUTB3Q3dZRFZRUUtEQVJCUTAxRk1SUXdFZ1lEVlFRRERBdG9iM04wTWk1c2IyTmhiRENDCkFTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTVNLdnJPYWhhdkNYbnZTRjUxMzFocG8KNms2NUM1N2pnUlE4NEZhRGo1TWJKT1ZsWVFWRnRNRzBYT2s3YStoaDV2MWZlNHdIMFI3STZGRG8wVjlzUytzcwprbzVic0VsYzF4WWxnNUhidUtxODl2UlNLZzZFRGx6dHgzQktiaTkxMlBtdDV2RkdOSjE2emN3NzdEVXJRSVhvCjRJL2I0YTNwbUJpV2o0M05vVElybVNXSHRzR3d3T2ozaUR2U3dlcWRZWEpJcjNocEhINXU2cG9oakRvUXZxRHoKSzZNdThwNm1oQ1VLTnM3S0ZKbk5Jbk5HMjVvUVQ2TzBuNE9HdG1nUmpMV29wZEVuT2hNa0tzZklvSTFYdGxYQgpMQkR2N2h1SUNrM3Q1eXd0ZkNReU8wOWtYN2xGSWdkNXJuNytNandINVdOZXFiUUp4dWFxam9YUW5OWlVnVXNDCkF3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZOaE1CRzhxNmEraUsybkVDd1ZUbjZCOUVYWk9NQjhHQTFVZEl3UVkKTUJhQUZOaE1CRzhxNmEraUsybkVDd1ZUbjZCOUVYWk9NQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSQpodmNOQVFFTEJRQURnZ0VCQUptdWR2eDgrcDVpSVVzVDhmbS9mYlZNMERBNnFXQUxEWVVKblRuM2o2THE0dnBmClBGQytxMUxtdVdmQlFNeXFLckhyUDNlNDkzRWN0WG9pU0taTzZpTjVkVkpJdXIwMk9qR3VpQUVjc1l1WTFuTG4KczlwaWlJK1VFd3hINnV4MU5hSFVuenNXYXVvQnZSaHpqWHZPNlNBVlNaSllhOWRZNW1pelhrbER5RE51RzVVMApsWHY5ZWdNR0JzeTBkRzZlRlhrVTVDUGR4V1U1NDB5STJzQ3RTQWo3eitXUlVENWs3Z0o3dFZvWTMvL2pIUVpHCjVTVFRtbTV0OWtwSVpUV2twdHlKb3M5b1pKRllNSVhxVzJGYzZ0eUxacFJwMzFSNzh0RHM2RVRJa1RvRGMwUlIKano2NnRoNkhJK1psZ0lCUWh3MDkraFlBaEJEZTkrRG1kL1N6UVpjPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktrd2dnU2xBZ0VBQW9JQkFRREVpcjZ6bW9XcndsNTcKMGhlZGQ5WWFhT3BPdVF1ZTQ0RVVQT0JXZzQrVEd5VGxaV0VGUmJUQnRGenBPMnZvWWViOVgzdU1COUVleU9oUQo2TkZmYkV2ckxKS09XN0JKWE5jV0pZT1IyN2lxdlBiMFVpb09oQTVjN2Nkd1NtNHZkZGo1cmVieFJqU2RlczNNCk8rdzFLMENGNk9DUDIrR3Q2WmdZbG8rTnphRXlLNWtsaDdiQnNNRG85NGc3MHNIcW5XRnlTSzk0YVJ4K2J1cWEKSVl3NkVMNmc4eXVqTHZLZXBvUWxDamJPeWhTWnpTSnpSdHVhRUUranRKK0RoclpvRVl5MXFLWFJKem9USkNySAp5S0NOVjdaVndTd1E3KzRiaUFwTjdlY3NMWHdrTWp0UFpGKzVSU0lIZWE1Ky9qSThCK1ZqWHFtMENjYm1xbzZGCjBKeldWSUZMQWdNQkFBRUNnZ0VCQUtjZWl0VlJPUVE1ZS9teFJSOUNmSzFzTkgvSDNOZTMvMVBrQjZYSXJGYWIKcUIzZXZFYXRaT3VvbjdBNk5LRWVUamwzN1NlK3BkU1ZaT1VYY3FDL0J6YnJhWnJlMytFaHJrcElqNzJBcFYrWQoyaXdaaVdWYWFKUWdJNHVaM21OQXc4UmFXSnNqNVMxYTlJOExET2lRNUlaNDVDbXZBQkRQSmVNU2N2SlN2UlJZCmU1TjBMNnN0cVM3WitJb3lHVktVZnAxaU5PMFl5eXdPVWlTa0lSWGdzY3VSWFpHWXBpR1BvbXNKK0pzMWVqelcKanlTdGxaSkVyNEwxMjg1ckdQcm1IcWpUd0ZkK2hHODBXYzQxNzl4TCtXUkU2SEJFVVpTaXk5NWZlNmtjUEhYWApCZ2lWWXRjRkttaUJpMmRUYnhsNGU5NHV0MjM5aTBIdGxKMVpKdExoK0JFQ2dZRUE4YTI5OEsyelhrSG9zeGhOCnRSckg3WGZNUFRrSEREZDNyeE0yMUxUK2ZJWHFpbkdVcDlMWWFEY2JqdVRQczhlMzN1S01kN1I2UTQweDg5eVcKSVhOa2EvVkwwUFhVZVY2N2FDVkxMcWdYRExHdWRsdUppbkgwWHZtSTBDbUJlY0ZTTXFJRm1RbGdxRVJvR0dzMwpVTWFjMHA4NzZUNFhrR1FRSmRmNjJiRnBFNFVDZ1lFQTBEQkgwUERsT3B3WGNjRGdYck1mYXlyOEhBaEkrRzVSCnlXUS8vOWlpcnRVODNjaHdJV3draDUzZUxMTXpMZ2RxSm5QaVd5YVVXNUJxem1ZdUQyM25oeGNRN1BOZElxT08KSDFzRTZ6cUxOc2h2NDZ0NVFLbGgxUTRxamQ3VXF0Z3JTclk2M1JYSkNNV1R3bk5NZURMdGo4Z2FLYmprckczUgpCTTJpbGR0NlVvOENnWUJYN05EVWxpMVNsb0gxWGxzdkQwNDdTOEZIYU03eWw5OTRGM0owVW1EZnBzemNqMVA0CjlwRjY0TW1xNC8zWXQwbGkwbU11VGIvSmdiM3hyWWdGSlhrY2VjS2FoRVZIM3JvcHVwK3Vtc0xBQUlpclVNUXEKVlNrRndKMFF0bmovZGVEVXdQTnVhT1g4Y2Q2NU81Q0ZWNnpJUjl4QkVERDhmQnNQMlpMT3ptZWZEUUtCZ1FERgptMjR2VnRoZC8xY0pkQ2dEKzBWeE5ZWERIZUlWWExGbzFTMGlMWUNOTG4zdGpabFJRQktVWHpaSmUzYXkwL3JmCnNOTkQ3YVNZSE1Za1R6eWRESmJjMVBvTnp4bXlEVWlUWHBPV3F5VUV4TS9mYkIxVlVQRTVoNDdBeHFkWjJvR04KRXRkZ2pwTVpMbUNJQzJTa0dzTCszTkpvazhVS0hkcHVFcm1tUUlNazVRS0JnUUNnRVdjWXRMWEMzWURZTUZkSQpVZ2NUZWJGcVNzM21MWWd1YjF4ZWtXM0lYUjJ5b200VjVmUVRMaUY3WWZuMmRwRFc0SWNNVTBVSkZZVnNVbGhLCmFHdGV0NFZtNU5uOCtNZ2hvdDV5QWpxTzl5QVVhdWI3d2dpZktJZTk5dFFLZDh1WnlDdkowaGh2bUREU2Z4NG0KQi9URWlGQU85OXlGNDlpU3hFVlNBUzZwcVE9PQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t +type: kubernetes.io/tls + +--- +apiVersion: v1 +kind: Service +metadata: + name: tls-example + namespace: default +spec: + ports: + - name: http + port: 8080 + selector: + app: tls-example + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tls-example + namespace: default +spec: + replicas: 1 + revisionHistoryLimit: 10 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: tls-example + template: + metadata: + labels: + app: tls-example + spec: + containers: + - name: tls-example + image: byjg/static-httpserver + ports: + - containerPort: 8080 + resources: + limits: + cpu: '0.05' + memory: '20Mi' + requests: + cpu: '0.05' + memory: '20Mi' + env: + - name: TITLE + value: "My Host Example" diff --git a/src/functions/__init__.py b/src/functions/__init__.py index 450758c..1be98bd 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -95,6 +95,12 @@ class Functions: Functions.log(source, Functions.ERROR, "%s" % (e)) +class Consts: + easyhaproxy_config = "/etc/haproxy/easyconfig.yml" + haproxy_config = "/etc/haproxy/haproxy.cfg" + certs_letsencrypt = "/certs/letsencrypt" + certs_haproxy = "/certs/haproxy" + class DaemonizeHAProxy: def __init__(self): self.process = None diff --git a/src/main.py b/src/main.py index 5d2d75f..ee3447d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,24 +1,19 @@ -from functions import Functions, DaemonizeHAProxy, Certbot +from functions import Functions, DaemonizeHAProxy, Certbot, Consts from processor import ProcessorInterface import os import time from deepdiff import DeepDiff -easyhaproxy_config = "/etc/haproxy/easyconfig.yml" -haproxy_config = "/etc/haproxy/haproxy.cfg" -certs_letsencrypt = "/certs/letsencrypt" -certs_haproxy = "/certs/haproxy" - def start(): processor_obj = ProcessorInterface.factory(os.getenv("EASYHAPROXY_DISCOVER")) if processor_obj is None: exit(1) - os.makedirs(certs_letsencrypt, exist_ok=True) - os.makedirs(certs_haproxy, exist_ok=True) + os.makedirs(Consts.certs_letsencrypt, exist_ok=True) + os.makedirs(Consts.certs_haproxy, exist_ok=True) - processor_obj.save_config(haproxy_config) - processor_obj.save_certs(certs_haproxy) + processor_obj.save_config(Consts.haproxy_config) + processor_obj.save_certs(Consts.certs_haproxy) letsencrypt_certs_found = processor_obj.get_letsencrypt_hosts() Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config @@ -26,7 +21,7 @@ def start(): haproxy = DaemonizeHAProxy() haproxy.haproxy("start") - certbot = Certbot(certs_letsencrypt, os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")) + certbot = Certbot(Consts.certs_letsencrypt, os.getenv("EASYHAPROXY_LETSENCRYPT_EMAIL")) while True: time.sleep(10) @@ -38,8 +33,8 @@ def start(): processor_obj.refresh() if DeepDiff(old_parsed, processor_obj.get_parsed_object()) != {} or not haproxy.is_alive(): Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'New configuration found. Reloading...') - processor_obj.save_config(haproxy_config) - processor_obj.save_certs(certs_haproxy) + processor_obj.save_config(Consts.haproxy_config) + processor_obj.save_certs(Consts.certs_haproxy) letsencrypt_certs_found = processor_obj.get_letsencrypt_hosts() Functions.log(Functions.EASYHAPROXY_LOG, Functions.DEBUG, 'Found hosts: %s' % ", ".join(processor_obj.get_hosts())) # Needs to after save_config old_haproxy = haproxy diff --git a/src/processor/__init__.py b/src/processor/__init__.py index 212a316..74aff20 100644 --- a/src/processor/__init__.py +++ b/src/processor/__init__.py @@ -1,9 +1,10 @@ from easymapping import HaproxyConfigGenerator -from functions import Functions +from functions import Functions, Consts import yaml import sys import os import json +import base64 import docker from kubernetes import client, config from kubernetes.client.rest import ApiException @@ -131,8 +132,10 @@ class Swarm(ProcessorInterface): 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): @@ -145,36 +148,58 @@ class Kubernetes(ProcessorInterface): ret = self.v1.list_ingress_for_all_namespaces(watch=False) self.parsed_object = {} - for i in ret.items: - if i.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": + for ingress in ret.items: + if ingress.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": continue - letsencrypt = self._check_annotation(i.metadata.annotations, "easyhaproxy.letsencrypt") - redirect_ssl = self._check_annotation(i.metadata.annotations, "easyhaproxy.redirect_ssl") - redirect = self._check_annotation(i.metadata.annotations, "easyhaproxy.redirect") + 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") data = {} - #ingress_name = i.metadata.name - data["creation_timestamp"] = i.metadata.creation_timestamp.strftime("%x %X") - data["resource_version"] = i.metadata.resource_version - data["namespace"] = i.metadata.namespace - for rule in i.spec.rules: + data["creation_timestamp"] = ingress.metadata.creation_timestamp.strftime("%x %X") + data["resource_version"] = ingress.metadata.resource_version + data["namespace"] = 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: + pass + + for rule in ingress.spec.rules: rule_data = {} port_number = rule.http.paths[0].backend.service.port.number - definition = rule.host.replace(".", "-") - rule_data["easyhaproxy.%s_%s.host" % (definition, port_number)] = rule.host - rule_data["easyhaproxy.%s_%s.port" % (definition, port_number)] = "80" - rule_data["easyhaproxy.%s_%s.localport" % (definition, port_number)] = port_number + definition = "easyhaproxy.%s_%s" % (rule.host.replace(".", "-"), port_number) + rule_data["%s.host" % (definition)] = rule.host + rule_data["%s.port" % (definition)] = "80" + rule_data["%s.localport" % (definition)] = port_number + if rule.host in ssl_hosts: + rule_data["%s.ssl" % (definition)] = 'true' if redirect_ssl is not None: - rule_data["easyhaproxy.%s_%s.redirect_ssl" % (definition, port_number)] = 'true' + rule_data["%s.redirect_ssl" % (definition)] = 'true' if letsencrypt is not None: - rule_data["easyhaproxy.%s_%s.letsencrypt" % (definition, port_number)] = 'true' + rule_data["%s.letsencrypt" % (definition)] = 'true' if redirect is not None: - rule_data["easyhaproxy.%s_%s.redirect" % (definition, port_number)] = redirect + rule_data["%s.redirect" % (definition)] = redirect service_name = rule.http.paths[0].backend.service.name try: - api_response = self.api_instance.read_namespaced_service(service_name, i.metadata.namespace) + 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