version 1.1.0
This commit is contained in:
parent
24e5e587d1
commit
ccb1f91ed4
6 changed files with 89 additions and 33 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
.git
|
||||
venv
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
venv
|
|
@ -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.
|
||||
|
|
|
@ -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 /
|
||||
|
|
100
main.py
100
main.py
|
@ -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))
|
||||
|
||||
print(f"{secretname} : requesting a new certificate for {', '.join(hosts)}")
|
||||
hostmatch = re.findall(r"Host\(([^\)]*)\)", route["match"])
|
||||
hosts = re.findall(r'`([^`]*?)`', ",".join(hostmatch))
|
||||
|
||||
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:
|
||||
if t == 'ADDED':
|
||||
# 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':
|
||||
else:
|
||||
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))
|
||||
|
||||
else:
|
||||
print(t)
|
||||
print(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()
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue