From c10b760c0bfd23dc63ea7236e8eda5458284f2f9 Mon Sep 17 00:00:00 2001 From: Tom Neuber 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..b5df5f3 --- /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 for %s already exists", secretName, 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" + ] +}