38 Commits

Author SHA1 Message Date
tim
680725f599 .gitea/workflows/helm-chart.yaml aktualisiert 2024-12-05 22:13:26 +01:00
7c9c43d747 right branch 2024-12-05 22:04:01 +01:00
c8c65b2205 changed variables 2024-12-05 22:03:23 +01:00
5c71a4e99c added helm chart workflow 2024-12-05 22:00:13 +01:00
e8684cac14 add helm-chart workflow 2024-12-05 21:48:51 +01:00
J Mozdzen
d2a1b4f4f4 syntax fix
Signed-off-by: J Mozdzen <31672367+jmozd@users.noreply.github.com>
2022-08-09 10:33:55 +02:00
J Mozdzen
964ee68c7e added release workflow
Signed-off-by: J Mozdzen <31672367+jmozd@users.noreply.github.com>
2022-08-09 10:29:47 +02:00
J Mozdzen
451e2a34f9 added workflow to create Docker image
Signed-off-by: J Mozdzen <31672367+jmozd@users.noreply.github.com>
2022-08-08 19:39:20 +02:00
Jens-U. Mozdzen
47f7d29be0 deleted extra line of junk 2022-08-05 16:46:43 +02:00
Jens-U. Mozdzen
3712279d2a changed name of binary to ignore 2022-08-05 16:43:58 +02:00
Jens-U. Mozdzen
45d0073087 Finalizations and clean-up 2022-08-05 16:42:23 +02:00
Jens-U. Mozdzen
db6a2f76d8 fixed spelling 2022-07-04 00:03:32 +02:00
Jens-U. Mozdzen
e17c1e502d updated formatting 2022-07-04 00:00:54 +02:00
Jens-U. Mozdzen
6f180147ac updated Dockerfile to use latest Alpine image, optimized logging and updated documentation 2022-07-03 23:48:49 +02:00
Jens-U. Mozdzen
ecfaaacf38 created webhook for Variomedia AG API (2019+) 2022-07-03 20:33:07 +02:00
jetstack-bot
97ebc9b1dd Merge pull request #41 from SgtCoDFish/logo
Update to small logo
2022-05-31 16:40:12 +01:00
Ashley Davis
bf6742ece8 update to small logo
Signed-off-by: Ashley Davis <ashley.davis@jetstack.io>
2022-05-31 16:37:22 +01:00
jetstack-bot
bcd7e427a5 Merge pull request #40 from SgtCoDFish/logo
Add logo + fix link in README
2022-04-27 11:04:02 +01:00
Ashley Davis
74a6e5e9a2 tweak logo + link in README
Signed-off-by: Ashley Davis <ashley.davis@jetstack.io>
2022-04-26 18:18:18 +01:00
jetstack-bot
be0eb944c4 Merge pull request #36 from identiq-protocol/cert-manager-pr
update cert-manager version and kubebuilder
2022-02-10 11:18:58 +00:00
roytev
28063781c2 export kubectl path
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-10 11:22:49 +02:00
roytev
20cde28508 Missing dependency
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-08 14:25:10 +02:00
roytev
ba75aea4d1 Comment once fix merged
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-08 14:03:18 +02:00
roytev
20f1353521 GO 1.17
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-08 11:10:40 +02:00
roytev
2c8a3cba02 make it build
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-08 11:00:10 +02:00
roytev
7db2a3e28c fixes from review
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-08 10:47:55 +02:00
roytev
e4ab1da942 update version and fix
Signed-off-by: Roi Teveth <roytev@gmail.com>
2022-02-08 10:24:16 +02:00
jetstack-bot
9440683e53 Merge pull request #23 from jakexks/import-path
Update import path for repo move
2021-02-24 14:19:01 +00:00
jetstack-bot
8d3a914514 Merge pull request #22 from jakexks/readme-update
README update
2021-02-24 14:15:02 +00:00
Jake Sanders
4364e0fa77 Update README
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-24 14:06:00 +00:00
Jake Sanders
40a6864511 Add OWNERS file
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-23 17:26:29 +00:00
Jake Sanders
328f01ec87 Update import path for repo move
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-23 17:09:19 +00:00
jetstack-bot
af909124d3 Merge pull request #21 from jakexks/smoke_tests
Update to cert-manager 1.2 and prepare for automated testing.
2021-02-22 17:10:48 +01:00
Jake Sanders
13383858a8 Tidy example package imports
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-22 16:05:04 +00:00
Jake Sanders
1f895be0fe Tidy example package
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-22 16:02:41 +00:00
Jake Sanders
4f51af7d86 Add example server that passes the test fixtures
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-22 15:11:18 +00:00
Jake Sanders
9b61d24773 Update dependencies and prepare for testing
Signed-off-by: Jake Sanders <i@am.so-aweso.me>
2021-02-22 15:10:52 +00:00
jetstack-bot
7a722fd851 Merge pull request #17 from jetstack/apiVersion
Change apiVersion of cert-manager in APIService to the up to date one
2021-01-13 14:43:56 +01:00
33 changed files with 2551 additions and 534 deletions

View File

@@ -0,0 +1,42 @@
name: Publish Helm Chart
on:
push:
branches:
- master
jobs:
build-and-publish:
name: Build and Publish Helm Chart
runs-on: ubuntu-latest
steps:
# Checkout Code
- name: Checkout Code
uses: actions/checkout@v3
# Set up Helm CLI
- name: Install Helm CLI
run: |
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Build Helm Chart
- name: Package Helm Chart
run: |
helm package helm/cert-manager-webhook-variomedia -d ./packages/
# Login to Gitea OCI Registry
- name: Login to Gitea OCI Registry
env:
OCI_USERNAME: ${{ secrets.USER }}
OCI_PASSWORD: ${{ secrets.TOKEN }}
run: |
helm registry login -u "$OCI_USERNAME" -p "$OCI_PASSWORD" https://git.unkrig.dev
# Push Chart to OCI Registry
- name: Push Chart to OCI Registry
env:
OCI_USERNAME: ${{ secrets.USER }}
OCI_PASSWORD: ${{ secrets.TOKEN }}
run: |
helm push ./packages/*.tgz oci://git.unkrig.dev/helm-charts

32
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Docker Image CI
on:
push:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:latest
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the Docker image
run: |
echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker.io
docker build . --file Dockerfile --tag docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:$GITHUB_REF_NAME
docker push docker.io/${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPOSITORY }}:$GITHUB_REF_NAME

8
.gitignore vendored
View File

@@ -4,7 +4,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
.idea
# Test binary, build with `go test -c` # Test binary, build with `go test -c`
*.test *.test
@@ -12,4 +12,8 @@
*.out *.out
# Ignore the built binary # Ignore the built binary
cert-manager-webhook-example cert-manager-webhook-variomedia
# Make artifacts
_out
_test

View File

@@ -1,9 +1,8 @@
FROM golang:1.12.4-alpine AS build_deps FROM golang:1.17-alpine AS build_deps
RUN apk add --no-cache git RUN apk add --no-cache git
WORKDIR /workspace WORKDIR /workspace
ENV GO111MODULE=on
COPY go.mod . COPY go.mod .
COPY go.sum . COPY go.sum .
@@ -16,7 +15,7 @@ COPY . .
RUN CGO_ENABLED=0 go build -o webhook -ldflags '-w -extldflags "-static"' . RUN CGO_ENABLED=0 go build -o webhook -ldflags '-w -extldflags "-static"' .
FROM alpine:3.9 FROM alpine
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates

View File

@@ -1,20 +1,45 @@
IMAGE_NAME := "webhook" OS ?= $(shell go env GOOS)
IMAGE_TAG := "latest" ARCH ?= $(shell go env GOARCH)
PROVIDER := "variomedia"
IMAGE_NAME := "${REGISTRY}cert-manager-webhook-${PROVIDER}"
IMAGE_TAG := "v2.0.1"
OUT := $(shell pwd)/_out OUT := $(shell pwd)/_out
$(shell mkdir -p "$(OUT)") KUBE_VERSION=1.21.2
verify: $(shell mkdir -p "$(OUT)")
export TEST_ASSET_ETCD=_test/kubebuilder/bin/etcd
export TEST_ASSET_KUBE_APISERVER=_test/kubebuilder/bin/kube-apiserver
export TEST_ASSET_KUBECTL=_test/kubebuilder/bin/kubectl
test: _test/kubebuilder
go test -v . go test -v .
build: _test/kubebuilder:
curl -fsSL https://go.kubebuilder.io/test-tools/$(KUBE_VERSION)/$(OS)/$(ARCH) -o kubebuilder-tools.tar.gz
mkdir -p _test/kubebuilder
tar -xvf kubebuilder-tools.tar.gz
mv kubebuilder/bin _test/kubebuilder/
rm kubebuilder-tools.tar.gz
rm -R kubebuilder
clean: clean-kubebuilder
clean-kubebuilder:
rm -Rf _test/kubebuilder
build: test
docker build -t "$(IMAGE_NAME):$(IMAGE_TAG)" . docker build -t "$(IMAGE_NAME):$(IMAGE_TAG)" .
push: build
docker push $(IMAGE_NAME):$(IMAGE_TAG)
.PHONY: rendered-manifest.yaml .PHONY: rendered-manifest.yaml
rendered-manifest.yaml: rendered-manifest.yaml:
helm template \ helm template \
--name example-webhook \ --name cert-manager-webhook-${PROVIDER} \
--set image.repository=$(IMAGE_NAME) \ --set image.repository=$(IMAGE_NAME) \
--set image.tag=$(IMAGE_TAG) \ --set image.tag=$(IMAGE_TAG) \
deploy/example-webhook > "$(OUT)/rendered-manifest.yaml" deploy/cert-manager-webhook-${PROVIDER} > "$(OUT)/rendered-manifest.yaml"

4
OWNERS Normal file
View File

@@ -0,0 +1,4 @@
approvers:
- jmozd
reviewers:
- jmozd

147
README.md
View File

@@ -1,27 +1,44 @@
# ACME webhook example <p align="center">
<img src="https://raw.githubusercontent.com/cert-manager/cert-manager/d53c0b9270f8cd90d908460d69502694e1838f5f/logo/logo-small.png" height="256" width="256" alt="cert-manager project logo" />
</p>
# ACME webhook for Variomedia AG (post-2018 API)
The ACME issuer type supports an optional 'webhook' solver, which can be used The ACME issuer type supports an optional 'webhook' solver, which can be used
to implement custom DNS01 challenge solving logic. to implement custom DNS01 challenge solving logic.
This is useful if you need to use cert-manager with a DNS provider that is not This is useful if you need to use cert-manager with a DNS provider that is not
officially supported in cert-manager core. officially supported in cert-manager core. This project "cert-manager-webhook-variomedia"
implements a webhook to be used with Variomedia's new (post-2018) API for updating
DNS entries. Support for Variomedia's legacy API can be found in https://github.com/jheyduk/cert-manager-webhook-variomedia
## Why not in core? You can find Variomedia's documentation on the "new" API in https://api.variomedia.de/docs/
(German language only) - please don't confuse this with the legacy "Reseller API", see
https://api.variomedia.de/docs/legacy/ .
As the project & adoption has grown, there has been an influx of DNS provider # Security warning
pull requests to our core codebase. As this number has grown, the test matrix
has become un-maintainable and so, it's not possible for us to certify that
providers work to a sufficient level.
By creating this 'interface' between cert-manager and DNS providers, we allow The API keys provided by Variomedia are currently **not** restrictable to allow for
users to quickly iterate and test out new integrations, and then packaging DNS updates only - if your key is compromised, **any** entry in your Variomedia customer
those up themselves as 'extensions' to cert-manager. profile can be updated by the one having the key.
We can also then provide a standardised 'testing framework', or set of Also note that you are solely responsible for protecting access to not only the key, but also
conformance tests, which allow us to validate the a DNS provider works as to the running webhook: Anyone with access to the webhook will be able to update your DNS entries
expected. at Variomedia, including adding malicious entries, overriding existing entries (even if not DNS-01-related)
and deleting existing entries (even if not DNS-01-related).
## Creating your own webhook By using this software, you agree to not hold responsible the authors of this software
for **any** damage that may occur to you, directly or indirectly, and accept that the
authors of this software make no guarantees on the suitability of this software for any use.
In other words: Use this software at your own risk.
If you find security flaws in the implementation of this software, please report an
according issue at https://github.com/jmozd/cert-manager-webhook-variomedia/issue .
## Development & building
### Origins of the Variomedia webhook
Webhook's themselves are deployed as Kubernetes API services, in order to allow Webhook's themselves are deployed as Kubernetes API services, in order to allow
administrators to restrict access to webhooks with Kubernetes RBAC. administrators to restrict access to webhooks with Kubernetes RBAC.
@@ -29,26 +46,104 @@ administrators to restrict access to webhooks with Kubernetes RBAC.
This is important, as otherwise it'd be possible for anyone with access to your This is important, as otherwise it'd be possible for anyone with access to your
webhook to complete ACME challenge validations and obtain certificates. webhook to complete ACME challenge validations and obtain certificates.
To make the set up of these webhook's easier, we provide a template repository The Variomedia AG webhook implementation is based on the example webhook provided
that can be used to get started quickly. by the cert-manager project (https://github.com/cert-manager/webhook-example). Also,
inspiration was taken from an implementation for the old Variomedia "provider API",
which can be found at https://github.com/jheyduk/cert-manager-webhook-variomedia.
### Creating your own repository ### Using your own repository
### Running the test suite The GitHub version of the Variomedia webhook implementation is currently focussed on providing
an implementation in a decentral container registry, i.e. "Harbor". The Docker image
is currently *not* published on docker.io. This may change at a later time.
All DNS providers **must** run the DNS01 provider conformance testing suite, #### Running the test suite
else they will have undetermined behaviour when used with cert-manager.
**It is essential that you configure and run the test suite when creating a **It is essential that you configure and run the test suite after modifying the
DNS01 webhook.** DNS01 webhook.**
An example Go test file has been provided in [main_test.go]().
You can run the test suite with: You can run the test suite with:
```bash ```bash
$ TEST_ZONE_NAME=example.com go test . $ TEST_ZONE_NAME=example.com. make test
``` ```
The example file has a number of areas you must fill in and replace with your Setting the trailing "." on the zone name (for which you have the Variomedia API key
own options in order for tests to pass. and set up the files in the testdata/my-custom-solver/ subdirectory) is required, the
test run might otherwise fail.
### Pushing the Docker image
Once you have your registry up & running (which is not part of this README description),
you can build and upload your local copy of the software using the following commands:
```bash
# to upload the container image to your registry
$ export REGISTRY='your.registry.company.com/yourproject'
$ docker login $REGISTRY
# build and push the resulting image to your repository
# will invoke via dependencies test -> build -> push
$ TEST_ZONE_NAME=example.com. make push
```
## Installation via Helm chart
We have provided a Helm chart to ease the installation of the Variomedia webhook.
When specifying the groupName parameter, make sure to use a name in your cluster's domain.
If you set that differently from "cluster.local", you'll need to use the proper domain suffix
both as a Helm value and when creating the (Cluster)Issuer (see below).
## Configuration
In addition to installing the webhook, you will also need to configure it and create at least one
cert-manager Issuer.
Configuration of the webhook consists in providing the according secrets for each DNS domain you
intend to generate certificates for (via cert-manager and i.e. "Let's Encrypt!"). This is done by creating
a Kubernetes "secret" for each API key issued by Variomedia to you and then configuring the cert-manager
to reference each according secret per DNS domain handled by the Issuer:
```bash
$ kubectl create secret generic variomedia-credentials-01 --from-literal=api-token='yourApiKeyGoesHere'
$ kubectl create secret generic variomedia-credentials-02 --from-literal=api-token='someOtherApiKeyGoesHere'
$ kubectl apply -f - << EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
namespace: cert-manager
spec:
acme:
# The ACME test server URL
server: https://acme-staging-v02.api.letsencrypt.org/directory
# The ACME production server URL
#server: https://acme-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: yourEmailAsKnownToLE@company.com
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- dns01:
webhook:
groupName: cert-manager-webhook-variomedia.cluster.local
solverName: variomedia-APIv2019
config:
example.com: variomedia-credentials-01
someotherdomain.com: variomedia-credentials-01
somethirddomain.com: variomedia-credentials-02
EOF
```
Although three domains were covered in above example, typically you'll have only a single domain to configure - you then can
omit creating "secret/variomedia-credentials-02" and will have to specify only a single entry in "...:webhook:config".
Variomedia AG published a page describing how to obtain the according API key (the page is in German
only), basically stating that you can contact their support to have a key issued:
https://www.variomedia.de/faq/Wie-bekomme-ich-einen-API-Token/article/326
Please report any problems and errors you experience by using this webhook, via https://github.com/jmozd/cert-manager-webhook-variomedia/issues

View File

@@ -1,7 +1,7 @@
--- ---
# Create a selfsigned Issuer, in order to create a root CA certificate for # Create a selfsigned Issuer, in order to create a root CA certificate for
# signing webhook serving certificates # signing webhook serving certificates
apiVersion: cert-manager.io/v1alpha3 apiVersion: cert-manager.io/v1
kind: Issuer kind: Issuer
metadata: metadata:
name: {{ include "example-webhook.selfSignedIssuer" . }} name: {{ include "example-webhook.selfSignedIssuer" . }}
@@ -17,7 +17,7 @@ spec:
--- ---
# Generate a CA Certificate used to sign certificates for the webhook # Generate a CA Certificate used to sign certificates for the webhook
apiVersion: cert-manager.io/v1alpha3 apiVersion: cert-manager.io/v1
kind: Certificate kind: Certificate
metadata: metadata:
name: {{ include "example-webhook.rootCACertificate" . }} name: {{ include "example-webhook.rootCACertificate" . }}
@@ -38,7 +38,7 @@ spec:
--- ---
# Create an Issuer that uses the above generated CA certificate to issue certs # Create an Issuer that uses the above generated CA certificate to issue certs
apiVersion: cert-manager.io/v1alpha3 apiVersion: cert-manager.io/v1
kind: Issuer kind: Issuer
metadata: metadata:
name: {{ include "example-webhook.rootCAIssuer" . }} name: {{ include "example-webhook.rootCAIssuer" . }}
@@ -55,7 +55,7 @@ spec:
--- ---
# Finally, generate a serving certificate for the webhook to use # Finally, generate a serving certificate for the webhook to use
apiVersion: cert-manager.io/v1alpha3 apiVersion: cert-manager.io/v1
kind: Certificate kind: Certificate
metadata: metadata:
name: {{ include "example-webhook.servingCertificate" . }} name: {{ include "example-webhook.servingCertificate" . }}

69
example/dns.go Normal file
View File

@@ -0,0 +1,69 @@
package example
import (
"fmt"
"github.com/miekg/dns"
)
func (e *exampleSolver) handleDNSRequest(w dns.ResponseWriter, req *dns.Msg) {
msg := new(dns.Msg)
msg.SetReply(req)
switch req.Opcode {
case dns.OpcodeQuery:
for _, q := range msg.Question {
if err := e.addDNSAnswer(q, msg, req); err != nil {
msg.SetRcode(req, dns.RcodeServerFailure)
break
}
}
}
w.WriteMsg(msg)
}
func (e *exampleSolver) addDNSAnswer(q dns.Question, msg *dns.Msg, req *dns.Msg) error {
switch q.Qtype {
// Always return loopback for any A query
case dns.TypeA:
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN A 127.0.0.1", q.Name))
if err != nil {
return err
}
msg.Answer = append(msg.Answer, rr)
return nil
// TXT records are the only important record for ACME dns-01 challenges
case dns.TypeTXT:
e.RLock()
record, found := e.txtRecords[q.Name]
e.RUnlock()
if !found {
msg.SetRcode(req, dns.RcodeNameError)
return nil
}
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN TXT %s", q.Name, record))
if err != nil {
return err
}
msg.Answer = append(msg.Answer, rr)
return nil
// NS and SOA are for authoritative lookups, return obviously invalid data
case dns.TypeNS:
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN NS ns.example-acme-webook.invalid.", q.Name))
if err != nil {
return err
}
msg.Answer = append(msg.Answer, rr)
return nil
case dns.TypeSOA:
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN SOA %s 20 5 5 5 5", "ns.example-acme-webook.invalid.", "ns.example-acme-webook.invalid."))
if err != nil {
return err
}
msg.Answer = append(msg.Answer, rr)
return nil
default:
return fmt.Errorf("unimplemented record type %v", q.Qtype)
}
}

68
example/example.go Normal file
View File

@@ -0,0 +1,68 @@
// package example contains a self-contained example of a webhook that passes the cert-manager
// DNS conformance tests
package example
import (
"fmt"
"os"
"sync"
"github.com/jetstack/cert-manager/pkg/acme/webhook"
acme "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
"github.com/miekg/dns"
"k8s.io/client-go/rest"
)
type exampleSolver struct {
name string
server *dns.Server
txtRecords map[string]string
sync.RWMutex
}
func (e *exampleSolver) Name() string {
return e.name
}
func (e *exampleSolver) Present(ch *acme.ChallengeRequest) error {
e.Lock()
e.txtRecords[ch.ResolvedFQDN] = ch.Key
e.Unlock()
return nil
}
func (e *exampleSolver) CleanUp(ch *acme.ChallengeRequest) error {
e.Lock()
delete(e.txtRecords, ch.ResolvedFQDN)
e.Unlock()
return nil
}
func (e *exampleSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
go func(done <-chan struct{}) {
<-done
if err := e.server.Shutdown(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
}
}(stopCh)
go func() {
if err := e.server.ListenAndServe(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
}()
return nil
}
func New(port string) webhook.Solver {
e := &exampleSolver{
name: "example",
txtRecords: make(map[string]string),
}
e.server = &dns.Server{
Addr: ":" + port,
Net: "udp",
Handler: dns.HandlerFunc(e.handleDNSRequest),
}
return e
}

96
example/example_test.go Normal file
View File

@@ -0,0 +1,96 @@
package example
import (
"crypto/rand"
"math/big"
"testing"
acme "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func TestExampleSolver_Name(t *testing.T) {
port, _ := rand.Int(rand.Reader, big.NewInt(50000))
port = port.Add(port, big.NewInt(15534))
solver := New(port.String())
assert.Equal(t, "example", solver.Name())
}
func TestExampleSolver_Initialize(t *testing.T) {
port, _ := rand.Int(rand.Reader, big.NewInt(50000))
port = port.Add(port, big.NewInt(15534))
solver := New(port.String())
done := make(chan struct{})
err := solver.Initialize(nil, done)
assert.NoError(t, err, "Expected Initialize not to error")
close(done)
}
func TestExampleSolver_Present_Cleanup(t *testing.T) {
port, _ := rand.Int(rand.Reader, big.NewInt(50000))
port = port.Add(port, big.NewInt(15534))
solver := New(port.String())
done := make(chan struct{})
err := solver.Initialize(nil, done)
assert.NoError(t, err, "Expected Initialize not to error")
validTestData := []struct {
hostname string
record string
}{
{"test1.example.com.", "testkey1"},
{"test2.example.com.", "testkey2"},
{"test3.example.com.", "testkey3"},
}
for _, test := range validTestData {
err := solver.Present(&acme.ChallengeRequest{
Action: acme.ChallengeActionPresent,
Type: "dns-01",
ResolvedFQDN: test.hostname,
Key: test.record,
})
assert.NoError(t, err, "Unexpected error while presenting %v", t)
}
// Resolve test data
for _, test := range validTestData {
msg := new(dns.Msg)
msg.Id = dns.Id()
msg.RecursionDesired = true
msg.Question = make([]dns.Question, 1)
msg.Question[0] = dns.Question{dns.Fqdn(test.hostname), dns.TypeTXT, dns.ClassINET}
in, err := dns.Exchange(msg, "127.0.0.1:"+port.String())
assert.NoError(t, err, "Presented record %s not resolvable", test.hostname)
assert.Len(t, in.Answer, 1, "RR response is of incorrect length")
assert.Equal(t, []string{test.record}, in.Answer[0].(*dns.TXT).Txt, "TXT record returned did not match presented record")
}
// Cleanup test data
for _, test := range validTestData {
err := solver.CleanUp(&acme.ChallengeRequest{
Action: acme.ChallengeActionCleanUp,
Type: "dns-01",
ResolvedFQDN: test.hostname,
Key: test.record,
})
assert.NoError(t, err, "Unexpected error while cleaning up %v", t)
}
// Resolve test data
for _, test := range validTestData {
msg := new(dns.Msg)
msg.Id = dns.Id()
msg.RecursionDesired = true
msg.Question = make([]dns.Question, 1)
msg.Question[0] = dns.Question{dns.Fqdn(test.hostname), dns.TypeTXT, dns.ClassINET}
in, err := dns.Exchange(msg, "127.0.0.1:"+port.String())
assert.NoError(t, err, "Presented record %s not resolvable", test.hostname)
assert.Len(t, in.Answer, 0, "RR response is of incorrect length")
assert.Equal(t, dns.RcodeNameError, in.Rcode, "Expexted NXDOMAIN")
}
close(done)
}

104
go.mod
View File

@@ -1,9 +1,103 @@
module github.com/jetstack/cert-manager-webhook-example module github.com/jmozd/cert-manager-webhook-variomedia
go 1.13 go 1.17
require ( require (
github.com/jetstack/cert-manager v0.13.1 github.com/jetstack/cert-manager v1.7.0
k8s.io/apiextensions-apiserver v0.17.0 github.com/miekg/dns v1.1.34
k8s.io/client-go v0.17.0 github.com/stretchr/testify v1.7.0
k8s.io/apiextensions-apiserver v0.23.1
k8s.io/apimachinery v0.23.1
k8s.io/client-go v0.23.1
k8s.io/klog/v2 v2.30.0
)
require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // 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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/spf13/cobra v1.2.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
go.opentelemetry.io/contrib v0.20.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect
go.opentelemetry.io/otel v0.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
go.opentelemetry.io/otel/metric v0.20.0 // indirect
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
go.opentelemetry.io/otel/trace v0.20.0 // indirect
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.23.1 // indirect
k8s.io/apiserver v0.23.1 // indirect
k8s.io/component-base v0.23.1 // indirect
k8s.io/kube-aggregator v0.23.1 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25 // indirect
sigs.k8s.io/controller-runtime v0.11.0 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )

1195
go.sum

File diff suppressed because it is too large Load Diff

1
helm/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
cert-manager-webhook-variomedia*.tgz

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,25 @@
apiVersion: v2
name: cert-manager-webhook-variomedia
description: cert-manager extension for DNS-01 challenges via DNS provider "Variomedia AG" (https://www.variomedia.de).
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.9.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "2.0.1"

View File

@@ -0,0 +1,48 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "cert-manager-webhook-variomedia.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "cert-manager-webhook-variomedia.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "cert-manager-webhook-variomedia.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "cert-manager-webhook-variomedia.selfSignedIssuer" -}}
{{ printf "%s-selfsign" (include "cert-manager-webhook-variomedia.fullname" .) }}
{{- end -}}
{{- define "cert-manager-webhook-variomedia.rootCAIssuer" -}}
{{ printf "%s-ca" (include "cert-manager-webhook-variomedia.fullname" .) }}
{{- end -}}
{{- define "cert-manager-webhook-variomedia.rootCACertificate" -}}
{{ printf "%s-ca" (include "cert-manager-webhook-variomedia.fullname" .) }}
{{- end -}}
{{- define "cert-manager-webhook-variomedia.servingCertificate" -}}
{{ printf "%s-webhook-tls" (include "cert-manager-webhook-variomedia.fullname" .) }}
{{- end -}}

View File

@@ -0,0 +1,19 @@
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.{{ .Values.groupName }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
annotations:
cert-manager.io/inject-ca-from: "{{ .Values.certManager.namespace }}/{{ include "cert-manager-webhook-variomedia.servingCertificate" . }}"
spec:
group: {{ .Values.groupName }}
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
version: v1alpha1

View File

@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
release: {{ .Release.Name }}
spec:
serviceAccountName: {{ include "cert-manager-webhook-variomedia.fullname" . }}
containers:
- name: {{ .Chart.Name }}
{{- if .Values.image.repository }}
image: "{{ .Values.image.repository }}/{{ .Values.image.image }}:{{ .Values.image.tag }}"
{{- else }}
image: "{{ .Values.image.image }}:{{ .Values.image.tag }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- --tls-cert-file=/tls/tls.crt
- --tls-private-key-file=/tls/tls.key
{{- if .Values.logLevel }}
- --v={{ .Values.logLevel }}
{{- end }}
env:
- name: GROUP_NAME
value: {{ .Values.groupName | quote }}
ports:
- name: https
containerPort: 443
protocol: TCP
livenessProbe:
httpGet:
scheme: HTTPS
path: /healthz
port: https
readinessProbe:
httpGet:
scheme: HTTPS
path: /healthz
port: https
volumeMounts:
- name: certs
mountPath: /tls
readOnly: true
resources:
{{ toYaml .Values.resources | indent 12 }}
volumes:
- name: certs
secret:
secretName: {{ include "cert-manager-webhook-variomedia.servingCertificate" . }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}

View File

@@ -0,0 +1,70 @@
---
# Create a selfsigned Issuer, in order to create a root CA certificate for
# signing webhook serving certificates
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: {{ include "cert-manager-webhook-variomedia.selfSignedIssuer" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
selfSigned: {}
---
# Generate a CA Certificate used to sign certificates for the webhook
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "cert-manager-webhook-variomedia.rootCACertificate" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
secretName: {{ include "cert-manager-webhook-variomedia.rootCACertificate" . }}
duration: 43800h # 5y
issuerRef:
name: {{ include "cert-manager-webhook-variomedia.selfSignedIssuer" . }}
commonName: "ca.cert-manager-webhook-variomedia.cert-manager"
isCA: true
---
# Create an Issuer that uses the above generated CA certificate to issue certs
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: {{ include "cert-manager-webhook-variomedia.rootCAIssuer" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
ca:
secretName: {{ include "cert-manager-webhook-variomedia.rootCACertificate" . }}
---
# Finally, generate a serving certificate for the webhook to use
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "cert-manager-webhook-variomedia.servingCertificate" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
secretName: {{ include "cert-manager-webhook-variomedia.servingCertificate" . }}
duration: 8760h # 1y
issuerRef:
name: {{ include "cert-manager-webhook-variomedia.rootCAIssuer" . }}
dnsNames:
- {{ include "cert-manager-webhook-variomedia.fullname" . }}
- {{ include "cert-manager-webhook-variomedia.fullname" . }}.{{ .Values.certManager.namespace }}
- {{ include "cert-manager-webhook-variomedia.fullname" . }}.{{ .Values.certManager.namespace }}.svc

View File

@@ -0,0 +1,165 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
---
# Grant the webhook permission to read the ConfigMap containing the Kubernetes
# apiserver's requestheader-ca-certificate
# This ConfigMap is automatically created by the Kubernetes apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:webhook-authentication-reader
namespace: kube-system
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
---
# apiserver gets the auth-delegator role to delegate auth decisions to
# the core apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:auth-delegator
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote}}
---
# Grant cert-manager permission to validate using our apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:domain-solver
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
rules:
- apiGroups:
- {{ .Values.groupName }}
resources:
- "*"
verbs:
- "create"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:domain-solver
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:domain-solver
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ .Values.certManager.serviceAccountName }}
namespace: {{ .Values.certManager.namespace | quote }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:secret-reader
namespace: {{ .Values.certManager.namespace | quote }}
rules:
- apiGroups:
- ""
resources:
- "secrets"
resourceNames:
- "variomedia-credentials"
verbs:
- "get"
- "watch"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:secret-reader
namespace: {{ .Values.certManager.namespace | quote }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:secret-reader
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
{{- if .Values.features.apiPriorityAndFairness }}
---
# Grant cert-manager-webhook-variomedia permission to read the flow control mechanism (APF)
# API Priority and Fairness is enabled by default in Kubernetes 1.20
# https://kubernetes.io/docs/concepts/cluster-administration/flow-control/
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:flowcontrol-solver
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
rules:
- apiGroups:
- "flowcontrol.apiserver.k8s.io"
resources:
- "prioritylevelconfigurations"
- "flowschemas"
verbs:
- "list"
- "watch"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:flowcontrol-solver
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}:flowcontrol-solver
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
{{- end }}

View File

@@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "cert-manager-webhook-variomedia.fullname" . }}
namespace: {{ .Values.certManager.namespace | quote }}
labels:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
chart: {{ include "cert-manager-webhook-variomedia.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: https
protocol: TCP
name: https
selector:
app: {{ include "cert-manager-webhook-variomedia.name" . }}
release: {{ .Release.Name }}

View File

@@ -0,0 +1,79 @@
# Default values for cert-manager-webhook-variomedia.
# This is a YAML-formatted file.
replicaCount: 1
certManager:
namespace: cert-manager
serviceAccountName: cert-manager
# remember to change "cluster.local" to your cluster's domain, in case it's set differently
groupName: cert-manager-webhook-variomedia.cluster.local
image:
repository: ''
image: cert-manager-webhook-variomedia
tag: "v2.0.1"
pullPolicy: IfNotPresent
imagePullSecrets: []
logLevel: 2
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 443
features:
apiPriorityAndFairness: false
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 1
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

265
main.go
View File

@@ -1,25 +1,55 @@
// cert-manager webhook supporting Variomedia (https://api.variomedia.de)
//
// (c) 2022 NDE Netzdesign und -entwicklung AG, Hamburg, Germany
//
// Written by Jens-U. Mozdzen <jmozdzen@nde.ag>
//
// Licensed under Apache License 2.0 (see https://directory.fsf.org/wiki/License:Apache-2.0)
//
// Use at your own risk. As this code interacts with a paid services provider, the auther
// especially makes no claims regarding suitability of this software for your use case, nor
// that it will not cause any damage. Depending on your provider situation, using this software
// may or may not cause the use of billable services and may or may not lead to charges incurred
// to you by your service provider.
// Use at your own risk.
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"context"
"strings"
extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
//"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/klog/v2"
"github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
"github.com/jetstack/cert-manager/pkg/acme/webhook/cmd" "github.com/jetstack/cert-manager/pkg/acme/webhook/cmd"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
var GroupName = os.Getenv("GROUP_NAME") var GroupName = os.Getenv("GROUP_NAME")
// our DNS entry URL cache: by client domain, by entry name, by key value
var DnsEntryURL map[string]map[string]map[string]string
const (
variomediaMinTtl = 300 // variomedia reports an error for values < this value
)
func main() { func main() {
klog.InitFlags(nil) // initializing the klog flags
klog.V(4).Infof( "main() called")
if GroupName == "" { if GroupName == "" {
panic("GROUP_NAME must be specified") panic("GROUP_NAME must be specified")
} }
DnsEntryURL = make(map[string]map[string]map[string]string)
// This will register our custom DNS provider with the webhook serving // This will register our custom DNS provider with the webhook serving
// library, making it available as an API under the provided GroupName. // library, making it available as an API under the provided GroupName.
// You can register multiple DNS provider implementations with a single // You can register multiple DNS provider implementations with a single
@@ -28,6 +58,7 @@ func main() {
cmd.RunWebhookServer(GroupName, cmd.RunWebhookServer(GroupName,
&customDNSProviderSolver{}, &customDNSProviderSolver{},
) )
klog.V(4).Infof( "main() finished")
} }
// customDNSProviderSolver implements the provider-specific logic needed to // customDNSProviderSolver implements the provider-specific logic needed to
@@ -35,13 +66,7 @@ func main() {
// To do so, it must implement the `github.com/jetstack/cert-manager/pkg/acme/webhook.Solver` // To do so, it must implement the `github.com/jetstack/cert-manager/pkg/acme/webhook.Solver`
// interface. // interface.
type customDNSProviderSolver struct { type customDNSProviderSolver struct {
// If a Kubernetes 'clientset' is needed, you must: client kubernetes.Clientset
// 1. uncomment the additional `client` field in this structure below
// 2. uncomment the "k8s.io/client-go/kubernetes" import at the top of the file
// 3. uncomment the relevant code in the Initialize method below
// 4. ensure your webhook's service account has the required RBAC role
// assigned to it for interacting with the Kubernetes APIs you need.
//client kubernetes.Clientset
} }
// customDNSProviderConfig is a structure that is used to decode into when // customDNSProviderConfig is a structure that is used to decode into when
@@ -49,24 +74,7 @@ type customDNSProviderSolver struct {
// This information is provided by cert-manager, and may be a reference to // This information is provided by cert-manager, and may be a reference to
// additional configuration that's needed to solve the challenge for this // additional configuration that's needed to solve the challenge for this
// particular certificate or issuer. // particular certificate or issuer.
// This typically includes references to Secret resources containing DNS type customDNSProviderConfig map[string]string
// provider credentials, in cases where a 'multi-tenant' DNS solver is being
// created.
// If you do *not* require per-issuer or per-certificate configuration to be
// provided to your webhook, you can skip decoding altogether in favour of
// using CLI flags or similar to provide configuration.
// You should not include sensitive information here. If credentials need to
// be used by your provider here, you should reference a Kubernetes Secret
// resource and fetch these credentials using a Kubernetes clientset.
type customDNSProviderConfig struct {
// Change the two fields below according to the format of the configuration
// to be decoded.
// These fields will be set by users in the
// `issuer.spec.acme.dns01.providers.webhook.config` field.
//Email string `json:"email"`
//APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"`
}
// Name is used as the name for this DNS solver when referencing it on the ACME // Name is used as the name for this DNS solver when referencing it on the ACME
// Issuer resource. // Issuer resource.
@@ -75,36 +83,7 @@ type customDNSProviderConfig struct {
// within a single webhook deployment**. // within a single webhook deployment**.
// For example, `cloudflare` may be used as the name of a solver. // For example, `cloudflare` may be used as the name of a solver.
func (c *customDNSProviderSolver) Name() string { func (c *customDNSProviderSolver) Name() string {
return "my-custom-solver" return "variomedia-APIv2019"
}
// Present is responsible for actually presenting the DNS record with the
// DNS provider.
// This method should tolerate being called multiple times with the same value.
// cert-manager itself will later perform a self check to ensure that the
// solver has correctly configured the DNS provider.
func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
cfg, err := loadConfig(ch.Config)
if err != nil {
return err
}
// TODO: do something more useful with the decoded configuration
fmt.Printf("Decoded configuration %v", cfg)
// TODO: add code that sets a record in the DNS provider's console
return nil
}
// CleanUp should delete the relevant TXT record from the DNS provider console.
// If multiple TXT records exist with the same record name (e.g.
// _acme-challenge.example.com) then **only** the record with the same `key`
// value provided on the ChallengeRequest should be cleaned up.
// This is in order to facilitate multiple DNS validations for the same domain
// concurrently.
func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
// TODO: add code that deletes a record from the DNS provider's console
return nil
} }
// Initialize will be called when the webhook first starts. // Initialize will be called when the webhook first starts.
@@ -117,23 +96,116 @@ func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
// The stopCh can be used to handle early termination of the webhook, in cases // The stopCh can be used to handle early termination of the webhook, in cases
// where a SIGTERM or similar signal is sent to the webhook process. // where a SIGTERM or similar signal is sent to the webhook process.
func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO klog.V(4).Infof( "Initialize() called")
///// YOUR CUSTOM DNS PROVIDER klog.V(5).InfoS("parameters", "config", kubeClientConfig)
//cl, err := kubernetes.NewForConfig(kubeClientConfig) cl, err := kubernetes.NewForConfig(kubeClientConfig)
//if err != nil { if err != nil {
// return err return err
//} }
//
//c.client = cl
///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE if DnsEntryURL == nil {
DnsEntryURL = make(map[string]map[string]map[string]string)
}
c.client = *cl
klog.V(4).Infof( "Initialize() finished")
return nil
}
// Present is responsible for actually presenting the DNS record with the
// DNS provider.
// This method should tolerate being called multiple times with the same value.
// cert-manager itself will later perform a self check to ensure that the
// solver has correctly configured the DNS provider.
func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
klog.V(4).InfoS( "Present() called")
klog.V(5).InfoS("parameters", "challenge", ch)
cfg, err := c.loadApiKeys(ch.Config, ch.ResourceNamespace)
if err != nil {
klog.ErrorS( err, "Present() finished with error while loading API keys")
return err
}
klog.V(6).Infof("decoded configuration %v", cfg)
entry, domain, apiKey, err := c.getDomainAndEntryAndApiKey( ch, &cfg)
if err != nil {
klog.ErrorS( err, "Present() finished with error while determining domain and entry name")
return fmt.Errorf("unable to get domain key for zone %s: %v", ch.ResolvedZone, err)
}
klog.V(4).InfoS( "present", "entry", entry, "domain", domain, "entry", entry, "API key", apiKey)
variomediaClient := NewvariomediaClient(apiKey)
url, err := variomediaClient.UpdateTxtRecord(&domain, &entry, &ch.Key, variomediaMinTtl)
if err != nil {
klog.ErrorS( err, "Present() finished with error while trying to update the DNS record")
return fmt.Errorf("unable to change TXT record: %v", err)
}
// update our cache map... making sure each level of map exists
if _, ok := DnsEntryURL[ domain]; !ok {
DnsEntryURL[ domain] = make( map[string]map[string]string)
}
if _, ok := DnsEntryURL[ domain][ entry]; !ok {
DnsEntryURL[ domain][ entry] = make( map[string]string)
}
DnsEntryURL[ domain][ entry][ ch.Key] = url
klog.V(5).InfoS( "updated DNS entry cache", "cache", DnsEntryURL)
klog.V(4).InfoS( "Present() finished")
return nil
}
// CleanUp should delete the relevant TXT record from the DNS provider console.
// If multiple TXT records exist with the same record name (e.g.
// _acme-challenge.example.com) then **only** the record with the same `key`
// value provided on the ChallengeRequest should be cleaned up.
// This is in order to facilitate multiple DNS validations for the same domain
// concurrently.
func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
klog.V(4).InfoS( "CleanUp() called")
klog.V(5).InfoS("parameters", "challenge", ch)
cfg, err := c.loadApiKeys(ch.Config, ch.ResourceNamespace)
if err != nil {
klog.ErrorS( err, "CleanUp() finished with error while loading API keys")
return err
}
klog.V(6).Infof("decoded configuration %v", cfg)
entry, domain, apiKey, err := c.getDomainAndEntryAndApiKey( ch, &cfg)
if err != nil {
klog.ErrorS( err, "CleanUp() finished with error while determining domain and entry name")
return fmt.Errorf("unable to get domain key for zone %s: %v", ch.ResolvedZone, err)
}
klog.V(4).InfoS( "clean up", "entry", entry, "domain", domain, "entry", entry, "API key", apiKey)
variomediaClient := NewvariomediaClient(apiKey)
url := DnsEntryURL[ domain][ entry][ ch.Key]
err = variomediaClient.DeleteTxtRecord( url, variomediaMinTtl)
if err != nil {
klog.ErrorS( err, "CleanUp() finished with error while trying to delete the DNS record")
return fmt.Errorf("unable to delete TXT record: %v", err)
}
// DNS entry deleted - so we delete our cache entry
delete( DnsEntryURL[ domain][ entry], ch.Key)
klog.V(5).InfoS( "updated DNS entry cache", "cache", DnsEntryURL)
klog.V(4).InfoS( "CleanUp() finished")
return nil return nil
} }
// loadConfig is a small helper function that decodes JSON configuration into // loadConfig is a small helper function that decodes JSON configuration into
// the typed config struct. // the typed config struct.
func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) { func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) {
klog.V(4).InfoS( "loadConfig() called")
cfg := customDNSProviderConfig{} cfg := customDNSProviderConfig{}
// handle the 'base case' where no configuration has been provided // handle the 'base case' where no configuration has been provided
if cfgJSON == nil { if cfgJSON == nil {
@@ -143,5 +215,66 @@ func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) {
return cfg, fmt.Errorf("error decoding solver config: %v", err) return cfg, fmt.Errorf("error decoding solver config: %v", err)
} }
klog.V(4).InfoS( "LoadConfig() finished")
klog.V(5).InfoS("return values", "configuration", cfg)
return cfg, nil return cfg, nil
} }
// loadApiKeys is a small helper function that takes the decoded JSON configuration
// and extracts the according keys.
// It's called as a wrapper to loadConfig()
func (c *customDNSProviderSolver) loadApiKeys(cfgJSON *extapi.JSON, namespace string) ( customDNSProviderConfig, error) {
klog.V(4).InfoS( "loadApiKeys() called")
klog.V(5).InfoS("parameters", "config", cfgJSON, "namespace", namespace)
// retrieve configuration block from Kubernetes Issuer config
cfg, err := loadConfig( cfgJSON)
// no config? Abort.
if err != nil {
return cfg, err
}
for domain, secretName := range cfg {
klog.V(6).Infof("try to load secret `%s` with key `%s`", secretName, "api-token")
sec, err := c.client.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{})
if err != nil {
klog.ErrorS( err, "loadApiKeys() finished with error")
return nil, fmt.Errorf("unable to get secret `%s`; %v", secretName, err)
}
secBytes, ok := sec.Data["api-token"]
if !ok {
klog.ErrorS( err, "loadApiKeys() finished with error")
return nil, fmt.Errorf("key %q not found in secret \"%s/%s\"", "api-token",
secretName, namespace)
}
// replace name of secret with value of apiKey - and trim blanks and newlines
cfg[domain] = strings.TrimRight( string(secBytes), "\r\n ")
klog.V(6).InfoS( "stored API key", "domain", domain, "API key", cfg[domain])
}
klog.V(4).InfoS( "loadApiKeys() finished")
return cfg, nil
}
// determine the appropriate domain, according API key and the actual entry to make
func (c *customDNSProviderSolver) getDomainAndEntryAndApiKey(ch *v1alpha1.ChallengeRequest, cfg *customDNSProviderConfig) (string, string, string, error) {
klog.V(4).InfoS( "getDomainAndEntryAndApiKey() called")
klog.V(5).InfoS("parameters", "challenge", ch, "provider config", cfg)
// Both ch.ResolvedZone and ch.ResolvedFQDN end with a dot: '.'
entry := strings.TrimSuffix(ch.ResolvedFQDN, ch.ResolvedZone)
entry = strings.TrimSuffix(entry, ".")
domain := strings.TrimSuffix(ch.ResolvedZone, ".")
apiKey, ok := (*cfg)[domain]
if !ok {
klog.ErrorS( fmt.Errorf("domain '%s' not found in config.", domain), "getDomainAndEntryAndApiKey() finished with error")
return entry, domain, apiKey, fmt.Errorf("domain '%s' not found in config.", domain)
}
klog.V(4).InfoS( "getDomainAndEntryAndApiKey() finished")
klog.V(5).InfoS("return values", "entry", entry, "domain", domain, "API key", apiKey)
return entry, domain, apiKey, nil
}

View File

@@ -20,7 +20,11 @@ func TestRunsSuite(t *testing.T) {
dns.SetResolvedZone(zone), dns.SetResolvedZone(zone),
dns.SetAllowAmbientCredentials(false), dns.SetAllowAmbientCredentials(false),
dns.SetManifestPath("testdata/my-custom-solver"), dns.SetManifestPath("testdata/my-custom-solver"),
// dns.SetBinariesPath("_test/kubebuilder/bin"),
) )
//need to uncomment and RunConformance delete runBasic and runExtended once https://github.com/cert-manager/cert-manager/pull/4835 is merged
//fixture.RunConformance(t)
fixture.RunBasic(t)
fixture.RunExtended(t)
fixture.RunConformance(t)
} }

View File

@@ -1 +0,0 @@
#!/usr/bin/env bash

2
testdata/my-custom-solver/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
config.json
variomedia-credentials-*.yaml

View File

@@ -1,3 +1,58 @@
# Solver testdata directory # Solver testdata directory
TODO The cert-manager team has provided an "integrated" testing mechanism with their
"example" implementation of the web hook. It can be called via the provided
"makefile" and needs configuration data so the connection to Variomedia can be
tested live. The according config data goes into this directory.
At least two files need to be provided:
- a config file, defining the web hook config. A sample version of the file
is provided, you can rename "config.json.sample" to "config.json" and adjust
the definition provided, to point to your Variomedia domain(s) and according
secret(s).
- a secret's manifest prviding the API key under the secret name you configured
in your config.json. Two files "variomedia-credentials-0[12].yaml.sample" are
provided, you can rename these to "*.yaml" to create secrets matching the sample
config in config.json
The sample file are configured to run three domains using two API keys - of course,
the keys contained in the sample secrets are *not* live Variomedia secrets and
will cause the tests to fail.
## creating your own config
The content of "config.json" represents the part of the later "Issuer" configuration
and as the name implies, needs to be in JSON format.
If you are the owner of a domain "myvariomediadomain.com" and intend to provide the
according API key in a secret called "variomedia-secret", the JSON file needs to
look like
```
{
"myvariomediadomain.com": "variomedia-secret"
}
```
## creating your own secret
The secret manifest will be used by the testing code to create the mandatory
secrets containing the encoded Variomedia API keys. While the file names can
be of your choice (keep the extension ".yaml", though), the name of the secret
needs to match the name given in config.json
You can create the according base64-encoded string via
```
# echo -n "YourVariomediaAPIKeyGoesHere" | base64
WW91clZhcmlvbWVkaWFBUElLZXlHb2VzSGVyZQ==
```
The implementation of this web hook is removing any trailing blanks, new-lines
and carriage-returns. Therefore, you can also use the following call to create
the base64-encoded string:
```
# base64 <<< "YourVariomediaAPIKeyGoesHere"
WW91clZhcmlvbWVkaWFBUElLZXlHb2VzSGVyZQo=
```

View File

@@ -1 +0,0 @@
{}

View File

@@ -0,0 +1,5 @@
{
"example.com": "variomedia-credentials-01",
"someotherdomain.com": "variomedia-credentials-01",
"somethirddomain.com": "variomedia-credentials-02"
}

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: variomedia-credentials-01
type: Opaque
data:
api-token: dmFyaW8ta2V5LTEK

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: variomedia-credentials-02
type: Opaque
data:
api-token: dmFyaW8ta2V5LTIK

387
variomediaclient_api2019.go Normal file
View File

@@ -0,0 +1,387 @@
// client implementation for Variomedia API, 2019+ version (https://api.variomedia.de/docs/)
//
// Written by Jens-Uwe Mozdzen <jmozdzen@nde.ag>
//
// Licensed under LGPL v3
//
// Entry points:
// client = NewvariomediaClient( apikey)
// - create new instance of API client
// in:
// apikey - customer-specific API key issued by Variomedia
// returns:
// client object
//
//
// client.UpdateTxtRecord(&domain, &entry, Key, ttl)
// - update TXT record
// in:
// domain - DNS domain
// entry - host label
// key - valus of TXT record
// ttl - TTL of record
// returns:
// variomediaDNSEntryURL - the URL of the resulting DNS entry
//
// client.DeleteTxtRecord(&domain, &entry)
// - delete TXT record for entry/domain
// in:
// url - DNS entry's URL
// ttl - TTL of record
// returns:
// -
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
"k8s.io/klog/v2"
)
const (
variomediaLiveDnsBaseUrl = "https://api.variomedia.de/dns-records"
statusLookupDelay = 2 * time.Second
)
type variomediaClient struct {
apiKey string
}
type variomediaDnsAttributes struct {
RecordType string `json:"record_type"`
Name string `json:"name"`
Domain string `json:"domain"`
Data string `json:"data"`
Ttl int `json:"ttl"`
} // variomediaDnsAttributes
type variomediaData struct {
RequestType string `json:"type"`
RequestAttr variomediaDnsAttributes `json:"attributes"`
} // variomediaData
type variomediaRequest struct {
variomediaData `json:"data"`
} // variomediaRequest
type variomediaJobData struct {
Type string `json:"type"`
Id string `json:"id"`
Attributes map[string]string `json:"attributes"`
Links map[string]string `json:"links"`
} // variomediaJobData
type variomediaResponse struct {
Data variomediaJobData `json:"data"`
Links map[string]string `json:"links"`
} // variomediaRequest
// NewvariomediaClient()
// create new instance of Variomedia client
func NewvariomediaClient(apiKey string) *variomediaClient {
klog.V(4).InfoS("NewvariomediaClient() called")
klog.V(5).InfoS("parameters", "API key", apiKey)
klog.V(4).InfoS("NewvariomediaClient() finished")
return &variomediaClient{
apiKey: apiKey,
}
}
// client.UpdateTxtRecord(&domain, &entry, Key, ttl)
// - create or update TXT record
// in:
// domain - DNS domain
// entry - host label
// key - valus of TXT record
// ttl - TTL of record
// returns:
// variomediaDNSEntryURL - the URL of the resulting DNS entry
func (c *variomediaClient) UpdateTxtRecord(domain *string, name *string, value *string, ttl int) (string, error) {
klog.V(4).InfoS("UpdateTxtRecord() called")
klog.V(5).InfoS("parameters", "domain", *domain, "name", *name, "value", *value, "TTL", ttl)
var reqData variomediaRequest
reqData = variomediaRequest{ variomediaData{
RequestType: "dns-record",
RequestAttr: variomediaDnsAttributes{
RecordType: "TXT",
Name: *name,
Domain: *domain,
Data: *value,
Ttl: ttl,
},
},
}
// the actual request is encoded in JSON
body, err := json.Marshal( reqData)
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", fmt.Errorf("cannot marshall to json: %v", err)
}
req, err := http.NewRequest("POST", variomediaLiveDnsBaseUrl, bytes.NewReader(body))
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", err
}
// contact Variomedia and check the results
status, respData, err := c.doRequest(req, true)
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.ErrorS( nil, "UpdateTxtRecord() finished with errori 'too many requests' reported by Variomedia")
return "", fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
klog.ErrorS(nil, "UpdateTxtRecord() finished with error reported by server", "status code", status)
return "", fmt.Errorf("failed creating TXT record: server reported status code %d", status)
}
// the request has succeeded - but is the job already finished?
// check the response for an according '' element
var reply variomediaResponse
err = json.Unmarshal( respData, &reply)
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", fmt.Errorf("cannot unmarshall response to json: %v", err)
}
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
// we give it 5 iterations to finish
loopcount := 5
for {
if reply.Data.Attributes[ "status"] == "pending" {
klog.V(2).InfoS( "DNS job still pending")
// inter-loop delay
time.Sleep( statusLookupDelay)
// re-fetch the job status
req, err := http.NewRequest("GET", reply.Data.Links[ "queue-job"], nil)
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", err
}
// contact Variomedia and check the results
status, respData, err := c.doRequest(req, true)
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.ErrorS( nil, "UpdateTxtRecord() finished with errori 'too many requests' reported by Variomedia")
return "", fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
klog.ErrorS(nil, "UpdateTxtRecord() finished with error reported by server", "status code", status)
return "", fmt.Errorf("failed creating TXT record: server reported status code %d", status)
}
// the request has succeeded - but is the job already finished?
// check the response for an according '' element
err = json.Unmarshal( respData, &reply)
if err != nil {
klog.ErrorS(err, "UpdateTxtRecord() finished with error")
return "", fmt.Errorf("cannot unmarshall response to json: %v", err)
}
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
}
loopcount -= 1
if (reply.Data.Attributes[ "status"] == "done") {
klog.V(2).InfoS( "DNS job finished", "retries left", loopcount)
break;
}
if (loopcount == 0) {
klog.ErrorS(nil, "UpdateTxtRecord() finished with error: job timed out", "most recent status", reply.Data.Attributes[ "status"])
return "", fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"])
}
} // emulated do until
klog.V(4).InfoS("UpdateTxtRecord() finished")
klog.V(5).InfoS("return values", "url", reply.Data.Links[ "dns-record"])
return reply.Data.Links[ "dns-record"], nil
} //func UpdateTxtRecord()
// client.DeleteTxtRecord(&domain, &entry, Key, ttl)
// - create or update TXT record
// in:
// url - DNS entry's URL
// ttl - TTL of record
// returns:
// -
func (c *variomediaClient) DeleteTxtRecord(url string, ttl int) error {
klog.V(4).InfoS("DeleteTxtRecord() called")
klog.V(5).InfoS("parameters", "url", url, "TTL", ttl)
// deleting a record happens by sending a HTTP "DELETE" request to the DNS entry's URL
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
klog.ErrorS(err, "DeleteTxtRecord() finished with error")
return err
}
// contact Variomedia and check the results
status, respData, err := c.doRequest(req, true)
if err != nil {
klog.ErrorS(err, "DeleteTxtRecord() finished with error")
return err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.ErrorS( nil, "DeleteTxtRecord() finished with errori 'too many requests' reported by Variomedia")
return fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
klog.ErrorS(nil, "DeleteTxtRecord() finished with error reported by server", "status code", status)
return fmt.Errorf("failed deleting TXT record: %v", err)
}
// the request has succeeded - but is the job already finished?
// check the response for an according '' element
var reply variomediaResponse
err = json.Unmarshal( respData, &reply)
if err != nil {
klog.ErrorS(err, "DeleteTxtRecord() finished with error")
return fmt.Errorf("cannot unmarshall response to json: %v", err)
}
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
// we give it 5 iterations to finish
loopcount := 5
for {
if reply.Data.Attributes[ "status"] == "pending" && status != http.StatusNotFound {
klog.V(2).InfoS( "DNS job still pending")
// inter-loop delay: two seconds
time.Sleep( statusLookupDelay)
// re-fetch the job status
req, err := http.NewRequest("GET", reply.Data.Links[ "queue-job"], nil)
if err != nil {
klog.ErrorS(err, "DeleteTxtRecord() finished with error")
return err
}
// contact Variomedia and check the results
status, respData, err := c.doRequest(req, true)
if err != nil {
klog.ErrorS(err, "DeleteTxtRecord() finished with error")
return err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.ErrorS( nil, "DeleteTxtRecord() finished with errori 'too many requests' reported by Variomedia")
return fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted && status != http.StatusNotFound {
klog.ErrorS(nil, "DeleteTxtRecord() finished with error reported by server", "status code", status)
return fmt.Errorf("failed creating TXT record: %v", err)
}
// the request has succeeded - but is the job already finished?
// check the response for an according '' element
err = json.Unmarshal( respData, &reply)
if err != nil {
klog.ErrorS(err, "DeleteTxtRecord() finished with error")
return fmt.Errorf("cannot unmarshall response to json: %v", err)
}
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
}
// 404 means "DNS record not found" (anymore) - we're fine with that, the record is gone
if status == http.StatusNotFound {
klog.V(4).InfoS("DeleteTxtRecord() finished because DNS record is gone", "retries left", loopcount)
return nil
}
loopcount -= 1
if (reply.Data.Attributes[ "status"] == "done") {
klog.V(2).InfoS( "DNS job finished", "retries left", loopcount)
break;
}
if (loopcount == 0) {
klog.ErrorS(nil, "DeleteTxtRecord() finished with error: job timed out", "most recent status", reply.Data.Attributes[ "status"])
return fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"])
}
} // emulated do until
klog.V(4).InfoS("DeleteTxtRecord() finished")
return nil
} // func DeleteTxtRecord()
func (c *variomediaClient) variomediaRecordsUrl(domain string) string {
klog.V(4).InfoS("variomediaRecordsUrl() called")
klog.V(5).InfoS("parameters", "domain", domain)
urlpart := fmt.Sprintf("%s/dns-records", domain)
klog.V(4).InfoS("variomediaRecordsUrl() finished")
klog.V(5).InfoS("return values", "url part", urlpart)
return urlpart
}
func (c *variomediaClient) doRequest(req *http.Request, readResponseBody bool) (int, []byte, error) {
klog.V(4).InfoS("doRequest() called")
klog.V(5).InfoS("parameters", "request", req, "readResponseBody", readResponseBody)
// Variomedia uses headers for auth, request content type and to signal accepted API versions
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.apiKey))
req.Header.Set("Content-Type", "application/vnd.api+json")
req.Header.Set("Accept", "application/vnd.variomedia.v1+json")
client := http.Client{
Timeout: 30 * time.Second,
}
res, err := client.Do(req)
if err != nil {
klog.ErrorS(err, "doRequest() finished with error")
return 0, nil, err
}
klog.V(5).InfoS( "HTTP request", "response", res)
// check for proper returns
if (res.StatusCode == http.StatusOK || res.StatusCode == http.StatusAccepted) && readResponseBody {
data, err := ioutil.ReadAll(res.Body)
if err != nil {
klog.ErrorS(err, "HTTP request finished with error")
return 0, nil, err
}
klog.V(4).InfoS( "HTTP request succeeded", "status code", res.StatusCode)
klog.V(5).InfoS( "HTTP request result", "data", data)
return res.StatusCode, data, nil
}
klog.V(4).InfoS("doRequest() finished")
klog.V(5).InfoS("return values", "status code", res.StatusCode)
return res.StatusCode, nil, nil
}