From 0cec08d0ec908fc3e270f9bcecba9e7aa09e6f16 Mon Sep 17 00:00:00 2001
From: Tom Neuber <tomneuber@web.de>
Date: Tue, 21 Jan 2025 21:29:51 +0100
Subject: [PATCH] feat(go-traefik-certmanager): initial commit

---
 .woodpecker/.build.yaml          |  41 ++++++++
 .woodpecker/.deploy.yaml         |  58 +++++++++++
 .woodpecker/.lint.yaml           |  20 ++++
 Dockerfile                       |  25 +++++
 go.mod                           |  39 ++++++++
 go.sum                           | 146 ++++++++++++++++++++++++++++
 internal/cmd/cli.go              |  36 +++++++
 internal/config/config.go        |   8 ++
 main.go                          |  51 ++++++++++
 pkg/certmanager/certificate.go   | 153 +++++++++++++++++++++++++++++
 pkg/certmanager/client.go        |  41 ++++++++
 pkg/ingressroute/client.go       |  37 +++++++
 pkg/ingressroute/ingressroute.go | 161 +++++++++++++++++++++++++++++++
 renovate.json                    |  19 ++++
 14 files changed, 835 insertions(+)
 create mode 100644 .woodpecker/.build.yaml
 create mode 100644 .woodpecker/.deploy.yaml
 create mode 100644 .woodpecker/.lint.yaml
 create mode 100644 Dockerfile
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 internal/cmd/cli.go
 create mode 100644 internal/config/config.go
 create mode 100644 main.go
 create mode 100644 pkg/certmanager/certificate.go
 create mode 100644 pkg/certmanager/client.go
 create mode 100644 pkg/ingressroute/client.go
 create mode 100644 pkg/ingressroute/ingressroute.go
 create mode 100644 renovate.json

diff --git a/.woodpecker/.build.yaml b/.woodpecker/.build.yaml
new file mode 100644
index 0000000..a287d04
--- /dev/null
+++ b/.woodpecker/.build.yaml
@@ -0,0 +1,41 @@
+steps:
+- name: docker
+  image: woodpeckerci/plugin-docker-buildx
+  settings:
+    registry: git.ar21.de
+    username:
+      from_secret: REGISTRY_USER
+    password:
+      from_secret: REGISTRY_PASS
+    repo: git.ar21.de/yolokube/go-traefik-certmanager
+    platforms:
+      - linux/amd64
+      - linux/arm64
+    tags:
+      - latest
+      - ${CI_PIPELINE_NUMBER}
+  when:
+    - branch: main
+      event: [push, manual]
+- name: docker-staging
+  image: woodpeckerci/plugin-docker-buildx
+  settings:
+    registry: git.ar21.de
+    username:
+      from_secret: REGISTRY_USER
+    password:
+      from_secret: REGISTRY_PASS
+    repo: git.ar21.de/yolokube/go-traefik-certmanager
+    platforms:
+      - linux/amd64
+      - linux/arm64
+    tags:
+      - staging
+      - staging-${CI_PIPELINE_NUMBER}
+    dry_run: true
+  when:
+    - branch:
+        exclude: main
+      event: [push, manual]
+depends_on:
+  - lint
diff --git a/.woodpecker/.deploy.yaml b/.woodpecker/.deploy.yaml
new file mode 100644
index 0000000..508680f
--- /dev/null
+++ b/.woodpecker/.deploy.yaml
@@ -0,0 +1,58 @@
+skip_clone: true
+steps:
+- name: bump tag in deployment-repo (prod)
+  image: git.ar21.de/aaron/kustomize-ci
+  commands:
+    - git clone https://git.ar21.de/yolokube/core-deployments.git deployment-repo
+    - cd deployment-repo/traefik-certmanager/overlay
+    - kustomize edit set image git.ar21.de/yolokube/go-traefik-certmanager=git.ar21.de/yolokube/go-traefik-certmanager:${CI_PIPELINE_NUMBER}
+  when:
+    - branch: main
+      event: [push, manual]
+- name: push new tag to deployment-repo (prod)
+  image: appleboy/drone-git-push
+  settings:
+    branch: "${CI_PIPELINE_NUMBER}_traefik-certmanager_prod"
+    remote: ssh://git@git.ar21.de:2222/yolokube/core-deployments.git
+    path: deployment-repo
+    force: false
+    commit: true
+    commit_message: "traefik-certmanager: update image tag to ${CI_PIPELINE_NUMBER} (done automagically via Woodpecker pipeline)"
+    ssh_key:
+      from_secret: FORGEJO_SSH_KEY
+  when:
+    - branch: main
+      event: [push, manual]
+- name: create pull request (prod)
+  image: git.ar21.de/tom/push-message-randomizer:latest
+  pull: true
+  settings:
+    gitea_address: https://git.ar21.de
+    gitea_token:
+      from_secret: FORGEJO_API
+    owner: ${CI_REPO_OWNER}
+    repo: core-deployments
+    branch: "${CI_PIPELINE_NUMBER}_traefik-certmanager_prod"
+    base_branch: main
+    pr_title: "traefik-certmanager: update image tag to ${CI_PIPELINE_NUMBER}"
+    pr_body: |
+      ### ℹ traefik-certmanager image update
+
+      {- random-pr-message -}
+
+      ### Configuration
+
+      📅 **Schedule**: Branch creation - At any time, Automerge - At any time.
+
+      🚦 **Automerge**: Enabled.
+    skip_on_missing_branch: true
+    close_pr_if_empty: true
+    delete_branch_if_pr_empty: true
+    merge_when_checks_succeed: true
+    delete_branch_after_merge: true
+  when:
+    - branch: main
+      event: [push, manual]
+depends_on:
+  - build
+  - lint
diff --git a/.woodpecker/.lint.yaml b/.woodpecker/.lint.yaml
new file mode 100644
index 0000000..166959e
--- /dev/null
+++ b/.woodpecker/.lint.yaml
@@ -0,0 +1,20 @@
+steps:
+- name: gofmt
+  image: golang:1.23.5
+  commands:
+    - gofmt -l -s .
+  when:
+    - event: [push, manual]
+- name: vuln-check
+  image: golang:1.23.5
+  commands:
+    - go install golang.org/x/vuln/cmd/govulncheck@latest
+    - govulncheck ./...
+  when:
+    - event: [push, manual]
+- name: golangci-linter
+  image: golangci/golangci-lint:v1.63.4
+  commands:
+    - golangci-lint run ./...
+  when:
+    - event: [push, manual]
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..03f4630
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+FROM golang:1.23.5-bookworm AS build
+
+# Create build workspace folder
+WORKDIR /workspace
+ADD . /workspace
+
+# Install updates and build tools
+RUN apt-get update --yes && \
+    apt-get install --yes build-essential
+
+# Build the actual binary
+RUN CGO_ENABLED=0 go build -o go-traefik-certmanager main.go
+
+# -- -- -- -- -- --
+
+# Set up image to run the tool
+FROM alpine
+
+# Create main app folder to run from
+WORKDIR /app
+
+# Copy built binary from build image
+COPY --from=build /workspace/go-traefik-certmanager /app
+
+ENTRYPOINT ["/app/go-traefik-certmanager"]
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3d4c05f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,39 @@
+module git.ar21.de/yolokube/go-traefik-certmanager
+
+go 1.23.4
+
+require (
+	github.com/alecthomas/kong v1.6.0
+	github.com/cert-manager/cert-manager v1.16.2
+	k8s.io/api v0.32.0
+	k8s.io/apimachinery v0.32.0
+	k8s.io/client-go v0.32.0
+)
+
+require (
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/google/go-cmp v0.6.0 // indirect
+	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	golang.org/x/net v0.30.0 // indirect
+	golang.org/x/oauth2 v0.23.0 // indirect
+	golang.org/x/sys v0.26.0 // indirect
+	golang.org/x/term v0.25.0 // indirect
+	golang.org/x/text v0.19.0 // indirect
+	golang.org/x/time v0.7.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	k8s.io/apiextensions-apiserver v0.31.1 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
+	sigs.k8s.io/gateway-api v1.1.0 // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
+	sigs.k8s.io/yaml v1.4.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..bc3fff0
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,146 @@
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE=
+github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/cert-manager/cert-manager v1.16.2 h1:c9UU2E+8XWGruyvC/mdpc1wuLddtgmNr8foKdP7a8Jg=
+github.com/cert-manager/cert-manager v1.16.2/go.mod h1:MfLVTL45hFZsqmaT1O0+b2ugaNNQQZttSFV9hASHUb0=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
+github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE=
+k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0=
+k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
+k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
+k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg=
+k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8=
+k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
+sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/internal/cmd/cli.go b/internal/cmd/cli.go
new file mode 100644
index 0000000..8177ba5
--- /dev/null
+++ b/internal/cmd/cli.go
@@ -0,0 +1,36 @@
+package cmd
+
+import (
+	"git.ar21.de/yolokube/go-traefik-certmanager/internal/config"
+	"github.com/alecthomas/kong"
+)
+
+//nolint:lll // ignore line length
+type CLI struct {
+	CertIssuerName  string `name:"cert-issuer-name" env:"CERT_ISSUER_NAME" help:"Name for the certificate issuer" default:"${default_cert_issuer_name}"`
+	CertIssuerKind  string `name:"cert-issuer-kind" env:"CERT_ISSUER_KIND" help:"Kind for the certificate issuer" default:"${default_cert_issuer_kind}"`
+	CertCleanup     bool   `name:"cert-cleanup" env:"CERT_CLEANUP" help:"Delete outdated / removed certificates" default:"${default_cert_cleanup}"`
+	PatchSecretName bool   `name:"patch-secret-name" env:"PATCH_SECRET_NAME" help:"Adjust secret names" default:"${default_patch_secret_name}"`
+}
+
+func (c *CLI) Parse() *config.AppSettings {
+	_ = kong.Parse(
+		c,
+		kong.Vars{
+			"default_cert_issuer_name":  "letsencrypt",
+			"default_cert_issuer_kind":  "ClusterIssuer",
+			"default_cert_cleanup":      "false",
+			"default_patch_secret_name": "false",
+		},
+		kong.Name("go-traefik-certmanager"),
+		kong.Description("🚀 Start a simple service to sync certificates for traefik CRs"),
+		kong.UsageOnError(),
+	)
+
+	return &config.AppSettings{
+		CertIssuerName:  c.CertIssuerName,
+		CertIssuerKind:  c.CertIssuerKind,
+		CertCleanup:     c.CertCleanup,
+		PatchSecretName: c.PatchSecretName,
+	}
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..c1bdc90
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,8 @@
+package config
+
+type AppSettings struct {
+	CertIssuerName  string
+	CertIssuerKind  string
+	CertCleanup     bool
+	PatchSecretName bool
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..bc97e42
--- /dev/null
+++ b/main.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"git.ar21.de/yolokube/go-traefik-certmanager/internal/cmd"
+	"git.ar21.de/yolokube/go-traefik-certmanager/pkg/certmanager"
+	"git.ar21.de/yolokube/go-traefik-certmanager/pkg/ingressroute"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/rest"
+)
+
+func main() {
+	cli := cmd.CLI{}
+	appSettings := cli.Parse()
+
+	config, err := rest.InClusterConfig()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	client, err := dynamic.NewForConfig(config)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	cmClient := certmanager.NewClient(
+		*client,
+		certmanager.WithCertIssuerKind(appSettings.CertIssuerKind),
+		certmanager.WithCertIssuerName(appSettings.CertIssuerName),
+	)
+
+	irClient := ingressroute.NewClient(
+		*client,
+		cmClient,
+		ingressroute.WithCertCleanup(),
+	)
+
+	stopCh := make(chan struct{})
+	defer close(stopCh)
+
+	go irClient.IngressRoutes.Watch(stopCh)
+
+	signalCh := make(chan os.Signal, 1)
+	signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
+	<-signalCh
+	log.Print("Shutting down gracefully")
+}
diff --git a/pkg/certmanager/certificate.go b/pkg/certmanager/certificate.go
new file mode 100644
index 0000000..a5c2441
--- /dev/null
+++ b/pkg/certmanager/certificate.go
@@ -0,0 +1,153 @@
+package certmanager
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+
+	cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
+	cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+const (
+	certGroup    = "cert-manager.io"
+	certVersion  = "v1"
+	certKind     = "Certificate"
+	certResource = "certificates"
+)
+
+var (
+	ErrCertificateAlreadyExist   = errors.New("certificate already exists")
+	ErrCertificateToUnstructured = errors.New("certificate cannot get converted to unstructured")
+	ErrCertificateCreation       = errors.New("certificate creation error")
+	ErrCertificateToJSON         = errors.New("certificate cannot get converted to JSON")
+)
+
+type certificateClient struct {
+	client *Client
+	gvr    schema.GroupVersionResource
+}
+
+func newCertificateClient(client *Client) certificateClient {
+	return certificateClient{
+		client: client,
+		gvr: schema.GroupVersionResource{
+			Group:    certGroup,
+			Version:  certVersion,
+			Resource: certResource,
+		},
+	}
+}
+
+func (c *certificateClient) Create(
+	ctx context.Context,
+	namespace, secretName string,
+	routes []map[string]interface{},
+) error {
+	_, err := c.client.crdClient.Resource(c.gvr).Namespace(namespace).Get(ctx, secretName, metav1.GetOptions{})
+	if err == nil {
+		return ErrCertificateAlreadyExist
+	}
+
+	hosts := extractHosts(routes)
+
+	cert := cmv1.Certificate{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       certKind,
+			APIVersion: fmt.Sprintf("%s/%s", certGroup, certVersion),
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: secretName,
+		},
+		Spec: cmv1.CertificateSpec{
+			DNSNames:   hosts,
+			SecretName: secretName,
+			IssuerRef: cmmetav1.ObjectReference{
+				Name: c.client.certIssuerName,
+				Kind: c.client.certIssuerKind,
+			},
+		},
+	}
+
+	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(cert)
+	if err != nil {
+		return errors.Join(ErrCertificateToUnstructured, err)
+	}
+
+	_, err = c.client.crdClient.Resource(c.gvr).Namespace(namespace).Create(
+		ctx,
+		&unstructured.Unstructured{Object: obj},
+		metav1.CreateOptions{},
+	)
+	if err != nil {
+		return errors.Join(ErrCertificateCreation, err)
+	}
+	return nil
+}
+
+func (c *certificateClient) Delete(ctx context.Context, namespace, name string) error {
+	return c.client.crdClient.Resource(c.gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
+}
+
+func (c *certificateClient) Patch(ctx context.Context, namespace, name string, cert cmv1.Certificate) error {
+	data, err := json.Marshal(cert)
+	if err != nil {
+		return errors.Join(ErrCertificateToJSON, err)
+	}
+
+	_, err = c.client.crdClient.Resource(c.gvr).Namespace(namespace).Patch(
+		ctx,
+		name,
+		types.JSONPatchType,
+		data,
+		metav1.PatchOptions{},
+	)
+	return err
+}
+
+func (c *certificateClient) PatchSecretName(ctx context.Context, namespace, name, secretName string) error {
+	cert := cmv1.Certificate{
+		Spec: cmv1.CertificateSpec{
+			SecretName: secretName,
+		},
+	}
+
+	return c.Patch(ctx, namespace, name, cert)
+}
+
+func extractHosts(routes []map[string]interface{}) []string {
+	var hosts []string
+	re := regexp.MustCompile(`Host\(([^)]*)\)`)
+
+	for _, route := range routes {
+		var (
+			kind  string
+			match string
+			ok    bool
+		)
+
+		kind, ok = route["kind"].(string)
+		if !ok || kind != "Rule" {
+			continue
+		}
+
+		if match, ok = route["match"].(string); ok {
+			hostMatches := re.FindAllStringSubmatch(match, -1)
+			for _, match := range hostMatches {
+				if len(match) > 1 {
+					hosts = append(hosts, strings.Split(match[1], ",")...)
+				}
+			}
+		}
+	}
+
+	return hosts
+}
diff --git a/pkg/certmanager/client.go b/pkg/certmanager/client.go
new file mode 100644
index 0000000..3d1701f
--- /dev/null
+++ b/pkg/certmanager/client.go
@@ -0,0 +1,41 @@
+package certmanager
+
+import (
+	"k8s.io/client-go/dynamic"
+)
+
+type Client struct {
+	crdClient      dynamic.DynamicClient
+	certIssuerName string
+	certIssuerKind string
+
+	Certificates certificateClient
+}
+
+type ClientOption func(*Client)
+
+func WithCertIssuerName(name string) ClientOption {
+	return func(c *Client) {
+		c.certIssuerName = name
+	}
+}
+
+func WithCertIssuerKind(kind string) ClientOption {
+	return func(c *Client) {
+		c.certIssuerKind = kind
+	}
+}
+
+func NewClient(crdClient dynamic.DynamicClient, options ...ClientOption) *Client {
+	client := &Client{
+		crdClient: crdClient,
+	}
+
+	for _, option := range options {
+		option(client)
+	}
+
+	client.Certificates = newCertificateClient(client)
+
+	return client
+}
diff --git a/pkg/ingressroute/client.go b/pkg/ingressroute/client.go
new file mode 100644
index 0000000..e591a39
--- /dev/null
+++ b/pkg/ingressroute/client.go
@@ -0,0 +1,37 @@
+package ingressroute
+
+import (
+	"git.ar21.de/yolokube/go-traefik-certmanager/pkg/certmanager"
+	"k8s.io/client-go/dynamic"
+)
+
+type Client struct {
+	crdClient   dynamic.DynamicClient
+	certmanager *certmanager.Client
+	certCleanup bool
+
+	IngressRoutes ingressRouteClient
+}
+
+type ClientOption func(*Client)
+
+func WithCertCleanup() ClientOption {
+	return func(c *Client) {
+		c.certCleanup = true
+	}
+}
+
+func NewClient(crdClient dynamic.DynamicClient, cmClient *certmanager.Client, options ...ClientOption) *Client {
+	client := &Client{
+		crdClient:   crdClient,
+		certmanager: cmClient,
+	}
+
+	for _, option := range options {
+		option(client)
+	}
+
+	client.IngressRoutes = ingressRouteClient{client: client}
+
+	return client
+}
diff --git a/pkg/ingressroute/ingressroute.go b/pkg/ingressroute/ingressroute.go
new file mode 100644
index 0000000..3fd089d
--- /dev/null
+++ b/pkg/ingressroute/ingressroute.go
@@ -0,0 +1,161 @@
+package ingressroute
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"time"
+
+	"git.ar21.de/yolokube/go-traefik-certmanager/pkg/certmanager"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/util/workqueue"
+)
+
+const (
+	group    = "traefik.io"
+	version  = "v1alpha1"
+	resource = "ingressroutes"
+)
+
+type ingressRouteClient struct {
+	client *Client
+}
+
+func (i *ingressRouteClient) Watch(stopCh chan struct{}) {
+	gvr := schema.GroupVersionResource{
+		Group:    group,
+		Version:  version,
+		Resource: resource,
+	}
+
+	listWatch := &cache.ListWatch{
+		ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
+			return i.client.crdClient.Resource(gvr).Namespace(corev1.NamespaceAll).List(context.Background(), options)
+		},
+		WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
+			return i.client.crdClient.Resource(gvr).Namespace(corev1.NamespaceAll).Watch(context.Background(), options)
+		},
+	}
+
+	queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]())
+
+	informer := cache.NewSharedInformer(listWatch, &unstructured.Unstructured{}, 0)
+	_, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc: func(obj interface{}) {
+			key, err := cache.MetaNamespaceKeyFunc(obj)
+			if err != nil {
+				return
+			}
+
+			convObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+			if err != nil {
+				return
+			}
+
+			rawRoutes, found, err := unstructured.NestedSlice(convObj, "spec", "routes")
+			if err != nil || !found {
+				return
+			}
+
+			routes, err := routeInterfaceToMapSlice(rawRoutes)
+			if err != nil {
+				return
+			}
+
+			queue.Add(event{key: key, eventType: watch.Added, routes: routes})
+		},
+		UpdateFunc: func(_, newObj interface{}) {
+			key, err := cache.MetaNamespaceKeyFunc(newObj)
+			if err == nil {
+				queue.Add(event{key: key, eventType: watch.Modified})
+			}
+		},
+		DeleteFunc: func(obj interface{}) {
+			if !i.client.certCleanup {
+				return
+			}
+
+			key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
+			if err == nil {
+				queue.Add(event{key: key, eventType: watch.Deleted})
+			}
+		},
+	})
+	if err != nil {
+		log.Printf("Cannot add event handler: %v", err)
+	}
+
+	go informer.Run(stopCh)
+
+	wait.Until(func() {
+		for i.processNextItem(queue) {
+		}
+	}, time.Second, stopCh)
+}
+
+func (i *ingressRouteClient) processNextItem(queue workqueue.TypedRateLimitingInterface[any]) bool {
+	item, quit := queue.Get()
+	if quit {
+		return false
+	}
+	defer queue.Done(item)
+
+	log.Printf("Processing key %v", item)
+
+	event, ok := item.(event)
+	if !ok {
+		log.Printf("Invalid data struct: %v", item)
+		return true
+	}
+
+	namespace, name, err := cache.SplitMetaNamespaceKey(event.key)
+	if err != nil {
+		log.Printf("Failed to split namespace and name: %v", err)
+		return true
+	}
+
+	//nolint:exhaustive // ignore missing switch cases
+	switch event.eventType {
+	case watch.Added, watch.Modified:
+		createErr := i.client.certmanager.Certificates.Create(context.Background(), namespace, name, event.routes)
+		if createErr != nil {
+			if errors.Is(createErr, certmanager.ErrCertificateAlreadyExist) {
+				log.Printf("Certificate %s already exists", event.key)
+			} else {
+				log.Printf("Failed to create certificate %s: %v", event.key, createErr)
+			}
+		}
+	case watch.Deleted:
+		if deleteErr := i.client.certmanager.Certificates.Delete(context.Background(), namespace, name); deleteErr != nil {
+			log.Printf("Failed to delete certificate %s: %v", event.key, deleteErr)
+		}
+	}
+
+	return true
+}
+
+type event struct {
+	key       string
+	eventType watch.EventType
+	routes    []map[string]interface{}
+}
+
+func routeInterfaceToMapSlice(input []interface{}) ([]map[string]interface{}, error) {
+	var result []map[string]interface{}
+	for _, item := range input {
+		match, ok := item.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("item is not of type map[string]interface{}: %v", item)
+		}
+		result = append(result, match)
+	}
+	return result, nil
+}
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..6efaf68
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,19 @@
+{
+  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+  "extends": [
+    "config:base",
+    "regexManagers:dockerfileVersions"
+  ],
+  "packageRules": [
+    {
+      "matchPackagePatterns": ["*"],
+      "automerge": true,
+      "automergeType": "pr",
+      "platformAutomerge": true,
+      "dependencyDashboard": true
+    }
+  ],
+  "postUpdateOptions": [
+    "gomodTidy"
+  ]
+}