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/)
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
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 \
ISSUER_NAME=letsencrypt \
ISSUER_KIND=ClusterIssuer \
CERT_CLEANUP=false
CERT_CLEANUP=false \
PATCH_SECRETNAME=true
RUN pip install kubernetes
COPY main.py /

98
main.py
View file

@ -1,22 +1,16 @@
# helm install \
# cert-manager jetstack/cert-manager \
# --namespace cert-manager \
# --create-namespace \
# --version v1.9.1 \
# --set installCRDs=true
import json
import logging
import os
import re
import signal
import sys
import threading
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"
@ -24,6 +18,8 @@ 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")
PATCH_SECRETNAME = os.getenv("PATCH_SECRETNAME", "false").lower() in ("yes", "true", "t", "1")
def safe_get(obj, keys, default=None):
"""
@ -43,17 +39,17 @@ def create_certificate(crds, namespace, secretname, routes):
"""
try:
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
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))
hostmatch = re.findall(r"Host\(([^\)]*)\)", route["match"])
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 = {
"apiVersion": f"{CERT_GROUP}/{CERT_VERSION}",
"kind": CERT_KIND,
@ -72,7 +68,7 @@ def create_certificate(crds, namespace, secretname, routes):
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)
logging.exception("Exception when calling CustomObjectsApi->create_namespaced_custom_object:", e)
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.
"""
if CERT_CLEANUP:
print(f"{secretname} : removing certificate")
logging.info(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)
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
"""
@ -96,9 +92,11 @@ def main():
crds = client.CustomObjectsApi()
resource_version = ""
logging.info(f"Watching {group}/{version}/{plural}")
while True:
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)
for event in stream:
t = event["type"]
@ -109,21 +107,61 @@ def main():
# get information about IngressRoute
namespace = safe_get(obj, "metadata.namespace")
name = safe_get(obj, "metadata.name")
secretname = safe_get(obj, "spec.tls.secretName")
routes = safe_get(obj, 'spec.routes')
# create a Certificate if needed
if secretname:
# create or delete certificate based on event type
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)
elif t == 'DELETED':
delete_certificate(crds, namespace, secretname)
else:
print(t)
print(json.dumps(obj, indent=2))
logging.info(f"{namespace}/{name} : no secretName found in IngressRoute, skipping adding")
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__':
logging.basicConfig(level=logging.INFO)
main()

View file

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