From 1db7c29c2f7199cd2b6e78e7aac38526efea2d35 Mon Sep 17 00:00:00 2001 From: Rob Kooper Date: Sun, 9 Oct 2022 16:18:29 -0500 Subject: [PATCH] initial code --- CHANGELOG.md | 10 +++ Dockerfile | 10 +++ README.md | 77 +++++++++++++++++++++++ main.py | 129 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + traefik-certmanager.yaml | 58 ++++++++++++++++++ 6 files changed, 285 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 traefik-certmanager.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f989c51 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## 1.0.0 - 2022-10-09 + +This is the initial release. This will work with Traefik IngressRoutes and create Certificates from them. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1a32609 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:alpine + +ENV PYTHONUNBUFFERED=1 \ + ISSUER_NAME=letsencrypt \ + ISSUER_KIND=ClusterIssuer \ + CERT_CLEANUP=false + +RUN pip install kubernetes +COPY main.py / +CMD python /main.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cf5ebd --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +This will create a certificate request for IngressRoute objects for Traefik. + +# Installing Cert-Manager and Traefik + +The default values assume you have cert-manager installed, see also [cert-manager installation](https://cert-manager.io/docs/installation/helm/): + +```bash +helm install \ + cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --version v1.9.1 \ + --set installCRDs=true +``` + +As well as Traefik, see also [traefik installation](https://doc.traefik.io/traefik/getting-started/install-traefik/#use-the-helm-chart): + +``` +helm install \ + traefik traefik/traefik \ + --namespace cert-manager \ + --create-namespace \ + +``` + +## Adding ClusterIssuer to Cert-Manager + +Next you install the ClusterIssuer using `kubectl apply` + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt +spec: + acme: + email: manager@example.com + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: lets-encrypt + solvers: + - http01: + ingress: + class: "" +``` + +# Installing Traefik to Cert-Manager + +Finally you can install the traefik-certmanager. + +```bash +kubectl apply -f traefik-certmanager.yaml +``` + +This will create a deployment, service account and role that can read/watch IngressRoutes and can add/delete Certficates. When starting it will check all existing IngressRoutes and see if there is a certificate for them (only for those that have a secretName). Next it will watch the addition and/or deleting of IngressRoutes. If an IngressRoute is removed, it can (false by default) remove the certificate as well. + +This is an example of a IngressRoute that will be picked up by this deployment: + +```yaml +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: traefik-dashboard + namespace: traefik +spec: + entryPoints: + - websecure + routes: + - match: Host(`traefik.example.com`) + kind: Rule + services: + - name: api@internal + kind: TraefikService + tls: + secretName: trafik.example +``` + diff --git a/main.py b/main.py new file mode 100644 index 0000000..eb7ef78 --- /dev/null +++ b/main.py @@ -0,0 +1,129 @@ +# helm install \ +# cert-manager jetstack/cert-manager \ +# --namespace cert-manager \ +# --create-namespace \ +# --version v1.9.1 \ +# --set installCRDs=true + +from unicodedata import name +from kubernetes import client, config, watch +from kubernetes.client.rest import ApiException +import json +import re +import os + + +TRAEFIK_GROUP = "traefik.containo.us" +TRAEFIK_VERSION = "v1alpha1" +TRAEFIK_PLURAL = "ingressroutes" + +CERT_GROUP = "cert-manager.io" +CERT_VERSION = "v1" +CERT_KIND = "Certificate" +CERT_PLURAL = "certificates" +CERT_ISSUER_NAME = os.getenv("ISSUER_NAME", "letsencrypt") +CERT_ISSUER_KIND = os.getenv("ISSUER_KIND", "ClusterIssuer") +CERT_CLEANUP = os.getenv("CERT_CLEANUP", "false").lower() in ("yes", "true", "t", "1") + +def safe_get(obj, keys, default=None): + """ + Get a value from the give dict. The key is in json format, i.e. seperated by a period. + """ + v = obj + for k in keys.split("."): + if k not in v: + return default + v = v[k] + return v + + +def create_certificate(crds, namespace, secretname, routes): + """ + Create a certificate request for certmanager based on the IngressRoute + """ + try: + secret = crds.get_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, secretname) + print(f"{secretname} : certificate already exists.") + return + except ApiException as e: + pass + + for route in routes: + if route.get("kind") == "Rule" and "Host" in route.get("match"): + hostmatch = re.findall("Host\(([^\)]*)\)", route["match"]) + hosts = re.findall('`([^`]*?)`', ",".join(hostmatch)) + + print(f"{secretname} : requesting a new certificate for {', '.join(hosts)}") + body = { + "apiVersion": f"{CERT_GROUP}/{CERT_VERSION}", + "kind": CERT_KIND, + "metadata": { + "name": secretname + }, + "spec": { + "dnsNames": hosts, + "secretName": secretname, + "issuerRef": { + "name": CERT_ISSUER_NAME, + "kind": CERT_ISSUER_KIND + } + } + } + try: + crds.create_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, body) + except ApiException as e: + print("Exception when calling CustomObjectsApi->create_namespaced_custom_object: %s\n" % e) + + +def delete_certificate(crds, namespace, secretname): + """ + Delete a certificate request for certmanager based on the IngressRoute. + """ + if CERT_CLEANUP: + print(f"{secretname} : removing certificate") + try: + crds.delete_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, secretname) + except ApiException as e: + print("Exception when calling CustomObjectsApi->delete_namespaced_custom_object: %s\n" % e) + + +def main(): + """ + Watch Traefik IngressRoute CRD and create/delete certificates based on them + """ + #config.load_kube_config() + config.load_incluster_config() + crds = client.CustomObjectsApi() + resource_version = "" + + while True: + stream = watch.Watch().stream(crds.list_cluster_custom_object, + TRAEFIK_GROUP, TRAEFIK_VERSION, TRAEFIK_PLURAL, + resource_version=resource_version) + for event in stream: + t = event["type"] + obj = event["object"] + + # Configure where to resume streaming. + resource_version = safe_get(obj, "metadata.resourceVersion", resource_version) + + # get information about IngressRoute + namespace = safe_get(obj, "metadata.namespace") + secretname = safe_get(obj, "spec.tls.secretName") + routes = safe_get(obj, 'spec.routes') + + # create a Certificate if needed + if secretname: + if t == 'ADDED': + create_certificate(crds, namespace, secretname, routes) + + elif t == 'DELETED': + delete_certificate(crds, namespace, secretname) + + else: + print(t) + print(json.dumps(obj, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..807e21b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +kubernetes diff --git a/traefik-certmanager.yaml b/traefik-certmanager.yaml new file mode 100644 index 0000000..b7b6ed4 --- /dev/null +++ b/traefik-certmanager.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: traefik-certmanager + namespace: traefik +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: traefik-certmanager +rules: +- apiGroups: ["traefik.containo.us"] + resources: ["ingressroutes"] + verbs: ["watch"] +- apiGroups: ["cert-manager.io"] + resources: ["certificates"] + verbs: ["get", "create", "delete"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: traefik-certmanager +subjects: +- kind: ServiceAccount + name: traefik-certmanager + namespace: traefik +roleRef: + kind: ClusterRole + name: traefik-certmanager + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: traefik-certmanager + namespace: traefik +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: traefik-certmanager + template: + metadata: + labels: + app.kubernetes.io/name: traefik-certmanager + spec: + serviceAccount: traefik-certmanager + containers: + - name: traefik-certmanager + image: kooper/traefik-certmanager + imagePullPolicy: Always + env: + - name: ISSUER_NAME + value: letsencrypt + - name: ISSUER_KIND + value: ClusterIssuer + - name: CERT_CLEANUP + value: "false"