initial code

This commit is contained in:
Rob Kooper 2022-10-09 16:18:29 -05:00
parent 53b95fad2c
commit 1db7c29c2f
6 changed files with 285 additions and 0 deletions

10
CHANGELOG.md Normal file
View file

@ -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.

10
Dockerfile Normal file
View file

@ -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

77
README.md Normal file
View file

@ -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
```

129
main.py Normal file
View file

@ -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()

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
kubernetes

58
traefik-certmanager.yaml Normal file
View file

@ -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"