version 1.1.0

This commit is contained in:
Rob Kooper 2023-11-21 23:33:00 -06:00
parent 24e5e587d1
commit ccb1f91ed4
6 changed files with 89 additions and 33 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
.git
venv

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
venv

View file

@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## 1.1.0 - 2023-11-21
### Changed
- now monitors `traefik.io` as well as `traefik.containo.us`
### Added
- catch signals to exit quicker
- will add secretName to ingressRoute if missing (PATCH_SECRETNAME=true)
## 1.0.0 - 2022-10-09 ## 1.0.0 - 2022-10-09
This is the initial release. This will work with Traefik IngressRoutes and create Certificates from them. This is the initial release. This will work with Traefik IngressRoutes and create Certificates from them.

View file

@ -3,7 +3,8 @@ FROM python:alpine
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
ISSUER_NAME=letsencrypt \ ISSUER_NAME=letsencrypt \
ISSUER_KIND=ClusterIssuer \ ISSUER_KIND=ClusterIssuer \
CERT_CLEANUP=false CERT_CLEANUP=false \
PATCH_SECRETNAME=true
RUN pip install kubernetes RUN pip install kubernetes
COPY main.py / COPY main.py /

98
main.py
View file

@ -1,22 +1,16 @@
# helm install \ import json
# cert-manager jetstack/cert-manager \ import logging
# --namespace cert-manager \ import os
# --create-namespace \ import re
# --version v1.9.1 \ import signal
# --set installCRDs=true import sys
import threading
from unicodedata import name from unicodedata import name
from kubernetes import client, config, watch from kubernetes import client, config, watch
from kubernetes.client.rest import ApiException 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_GROUP = "cert-manager.io"
CERT_VERSION = "v1" CERT_VERSION = "v1"
CERT_KIND = "Certificate" CERT_KIND = "Certificate"
@ -24,6 +18,8 @@ CERT_PLURAL = "certificates"
CERT_ISSUER_NAME = os.getenv("ISSUER_NAME", "letsencrypt") CERT_ISSUER_NAME = os.getenv("ISSUER_NAME", "letsencrypt")
CERT_ISSUER_KIND = os.getenv("ISSUER_KIND", "ClusterIssuer") CERT_ISSUER_KIND = os.getenv("ISSUER_KIND", "ClusterIssuer")
CERT_CLEANUP = os.getenv("CERT_CLEANUP", "false").lower() in ("yes", "true", "t", "1") CERT_CLEANUP = os.getenv("CERT_CLEANUP", "false").lower() in ("yes", "true", "t", "1")
PATCH_SECRETNAME = os.getenv("PATCH_SECRETNAME", "false").lower() in ("yes", "true", "t", "1")
def safe_get(obj, keys, default=None): def safe_get(obj, keys, default=None):
""" """
@ -43,17 +39,17 @@ def create_certificate(crds, namespace, secretname, routes):
""" """
try: try:
secret = crds.get_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, secretname) secret = crds.get_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, secretname)
print(f"{secretname} : certificate already exists.") logging.info(f"{secretname} : certificate request already exists.")
return return
except ApiException as e: except ApiException as e:
pass pass
for route in routes: for route in routes:
if route.get("kind") == "Rule" and "Host" in route.get("match"): if route.get("kind") == "Rule" and "Host" in route.get("match"):
hostmatch = re.findall("Host\(([^\)]*)\)", route["match"]) hostmatch = re.findall(r"Host\(([^\)]*)\)", route["match"])
hosts = re.findall('`([^`]*?)`', ",".join(hostmatch)) hosts = re.findall(r'`([^`]*?)`', ",".join(hostmatch))
print(f"{secretname} : requesting a new certificate for {', '.join(hosts)}") logging.info(f"{secretname} : requesting a new certificate for {', '.join(hosts)}")
body = { body = {
"apiVersion": f"{CERT_GROUP}/{CERT_VERSION}", "apiVersion": f"{CERT_GROUP}/{CERT_VERSION}",
"kind": CERT_KIND, "kind": CERT_KIND,
@ -72,7 +68,7 @@ def create_certificate(crds, namespace, secretname, routes):
try: try:
crds.create_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, body) crds.create_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, body)
except ApiException as e: except ApiException as e:
print("Exception when calling CustomObjectsApi->create_namespaced_custom_object: %s\n" % e) logging.exception("Exception when calling CustomObjectsApi->create_namespaced_custom_object:", e)
def delete_certificate(crds, namespace, secretname): def delete_certificate(crds, namespace, secretname):
@ -80,14 +76,14 @@ def delete_certificate(crds, namespace, secretname):
Delete a certificate request for certmanager based on the IngressRoute. Delete a certificate request for certmanager based on the IngressRoute.
""" """
if CERT_CLEANUP: if CERT_CLEANUP:
print(f"{secretname} : removing certificate") logging.info(f"{secretname} : removing certificate")
try: try:
crds.delete_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, secretname) crds.delete_namespaced_custom_object(CERT_GROUP, CERT_VERSION, namespace, CERT_PLURAL, secretname)
except ApiException as e: except ApiException as e:
print("Exception when calling CustomObjectsApi->delete_namespaced_custom_object: %s\n" % e) logging.exception("Exception when calling CustomObjectsApi->delete_namespaced_custom_object:", e)
def main(): def watch_crd(group, version, plural):
""" """
Watch Traefik IngressRoute CRD and create/delete certificates based on them Watch Traefik IngressRoute CRD and create/delete certificates based on them
""" """
@ -96,9 +92,11 @@ def main():
crds = client.CustomObjectsApi() crds = client.CustomObjectsApi()
resource_version = "" resource_version = ""
logging.info(f"Watching {group}/{version}/{plural}")
while True: while True:
stream = watch.Watch().stream(crds.list_cluster_custom_object, stream = watch.Watch().stream(crds.list_cluster_custom_object,
TRAEFIK_GROUP, TRAEFIK_VERSION, TRAEFIK_PLURAL, group=group, version=version, plural=plural,
resource_version=resource_version) resource_version=resource_version)
for event in stream: for event in stream:
t = event["type"] t = event["type"]
@ -109,21 +107,61 @@ def main():
# get information about IngressRoute # get information about IngressRoute
namespace = safe_get(obj, "metadata.namespace") namespace = safe_get(obj, "metadata.namespace")
name = safe_get(obj, "metadata.name")
secretname = safe_get(obj, "spec.tls.secretName") secretname = safe_get(obj, "spec.tls.secretName")
routes = safe_get(obj, 'spec.routes') routes = safe_get(obj, 'spec.routes')
# create a Certificate if needed # create or delete certificate based on event type
if secretname:
if t == 'ADDED': if t == 'ADDED':
# if no secretName is set, add one to the IngressRoute
if not secretname and PATCH_SECRETNAME:
logging.info(f"{namespace}/{name} : no secretName found in IngressRoute, patch to add one")
patch = { "spec": { "tls": { "secretName": name }}}
crds.patch_namespaced_custom_object(group, version, namespace, plural, name, patch)
secretname = name
if secretname:
create_certificate(crds, namespace, secretname, routes) create_certificate(crds, namespace, secretname, routes)
elif t == 'DELETED':
delete_certificate(crds, namespace, secretname)
else: else:
print(t) logging.info(f"{namespace}/{name} : no secretName found in IngressRoute, skipping adding")
print(json.dumps(obj, indent=2)) elif t == 'DELETED':
if secretname:
delete_certificate(crds, namespace, secretname)
else:
logging.info(f"{namespace}/{name} : no secretName found in IngressRoute, skipping delete")
elif t == 'MODIFIED':
if secretname:
create_certificate(crds, namespace, secretname, routes)
else:
logging.info(f"{namespace}/{name} : no secretName found in IngressRoute, skipping modify")
else:
logging.info(f"{namespace}/{name} : unknown event type: {t}")
logging.debug(json.dumps(obj, indent=2))
def exit_gracefully(signum, frame):
logging.info(f"Shutting down gracefully on {signum}")
sys.exit(0)
def main():
signal.signal(signal.SIGINT, exit_gracefully)
signal.signal(signal.SIGTERM, exit_gracefully)
# deprecated traefik CRD
th1 = threading.Thread(target=watch_crd, args=("traefik.containo.us", "v1alpha1", "ingressroutes"), daemon=True)
th1.start()
# new traefik CRD
th2 = threading.Thread(target=watch_crd, args=("traefik.io", "v1alpha1", "ingressroutes"), daemon=True)
th2.start()
# wait for threads to finish
while th1.is_alive() and th2.is_alive():
th1.join(0.1)
th2.join(0.1)
logging.info(f"One of the threads exited {th1.is_alive()}, {th2.is_alive()}")
if __name__ == '__main__': if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
main() main()

View file

@ -11,7 +11,10 @@ metadata:
rules: rules:
- apiGroups: ["traefik.containo.us"] - apiGroups: ["traefik.containo.us"]
resources: ["ingressroutes"] resources: ["ingressroutes"]
verbs: ["watch"] verbs: ["watch", "patch"]
- apiGroups: ["traefik.io"]
resources: ["ingressroutes"]
verbs: ["watch", "patch"]
- apiGroups: ["cert-manager.io"] - apiGroups: ["cert-manager.io"]
resources: ["certificates"] resources: ["certificates"]
verbs: ["get", "create", "delete"] verbs: ["get", "create", "delete"]
@ -56,3 +59,5 @@ spec:
value: ClusterIssuer value: ClusterIssuer
- name: CERT_CLEANUP - name: CERT_CLEANUP
value: "false" value: "false"
- name: PATCH_SECRETNAME
value: "true"