created webhook for Variomedia AG API (2019+)

This commit is contained in:
Jens-U. Mozdzen
2022-07-03 20:33:07 +02:00
parent 97ebc9b1dd
commit ecfaaacf38
22 changed files with 1300 additions and 121 deletions

View File

@@ -1,7 +1,8 @@
OS ?= $(shell go env GOOS) OS ?= $(shell go env GOOS)
ARCH ?= $(shell go env GOARCH) ARCH ?= $(shell go env GOARCH)
IMAGE_NAME := "webhook" PROVIDER := "variomedia"
IMAGE_NAME := "${REGISTRY}cert-manager-webhook-${PROVIDER}"
IMAGE_TAG := "latest" IMAGE_TAG := "latest"
OUT := $(shell pwd)/_out OUT := $(shell pwd)/_out
@@ -35,7 +36,7 @@ build:
.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"

16
OWNERS
View File

@@ -1,16 +1,4 @@
approvers: approvers:
- munnerz - jmozd
- joshvanl
- meyskens
- wallrj
- jakexks
- maelvls
- irbekrm
reviewers: reviewers:
- munnerz - jmozd
- joshvanl
- meyskens
- wallrj
- jakexks
- maelvls
- irbekrm

121
README.md
View File

@@ -2,30 +2,43 @@
<img src="https://raw.githubusercontent.com/cert-manager/cert-manager/d53c0b9270f8cd90d908460d69502694e1838f5f/logo/logo-small.png" height="256" width="256" alt="cert-manager project logo" /> <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> </p>
# ACME webhook example # 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* damange 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.
@@ -33,21 +46,30 @@ 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).
### Creating your own repository ### Creating your own repository
### Running the test suite The GitHub version of the Variomedia webhook implementation is focussed on providing
an implementation in a decentral container registry, i.e. "Harbor". The Docker image
is currently *not* published on docker.io.
All DNS providers **must** run the DNS01 provider conformance testing suite, Once you have your registry up & running (which is not part of this README description),
else they will have undetermined behaviour when used with cert-manager. you can build your local copy of the software using the following commands:
**It is essential that you configure and run the test suite when creating a ```bash
# to upload the container image to your registry
export REGISTRY='your.registry.company.com'
docker login $REGISTRY
make build
```
#### Running the test suite
**It is essential that you configure and run the test suite after creating the
DNS01 webhook.** DNS01 webhook.**
An example Go test file has been provided in [main_test.go](https://github.com/cert-manager/webhook-example/blob/master/main_test.go).
You can run the test suite with: You can run the test suite with:
```bash ```bash
@@ -56,3 +78,58 @@ $ TEST_ZONE_NAME=example.com. make test
The example file has a number of areas you must fill in and replace with your The example file has a number of areas you must fill in and replace with your
own options in order for tests to pass. own options in order for tests to pass.
## Installation via Helm chart
We have provided a Helm chart to ease the installation of the Variomedia webhook.
## Configuration
In addition to installing the webhook, you will also need to configure the according webhook and
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: acme.cert-manager-webhook-variomedia.local
solverName: variomedia
config:
example.com: variomedia-credentials-01
someotherdomain.com: variomedia-credentials-01
somethirddomain.com: variomedia-credentials-02
EOF
```
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

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.1
# 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: "1.1.0"

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.
# Declare variables to be passed into your templates.
replicaCount: 1
certManager:
namespace: cert-manager
serviceAccountName: cert-manager
groupName: acme.cert-manager-webhook-variomedia.local
image:
repository: ''
image: cert-manager-webhook-variomedia
tag: "v1.1.0"
pullPolicy: IfNotPresent
imagePullSecrets: []
logLevel: 6
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: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

262
main.go
View File

@@ -1,25 +1,54 @@
// 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/v1" 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.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 +57,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 +65,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 +73,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 +82,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 +95,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.V(2).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.V(2).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.V(2).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.V(2).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.V(2).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.V(2).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 +214,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.V(2).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.V(2).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.V(2).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

@@ -5,8 +5,6 @@ import (
"testing" "testing"
"github.com/jetstack/cert-manager/test/acme/dns" "github.com/jetstack/cert-manager/test/acme/dns"
"github.com/cert-manager/webhook-example/example"
) )
var ( var (
@@ -17,21 +15,12 @@ func TestRunsSuite(t *testing.T) {
// The manifest path should contain a file named config.json that is a // The manifest path should contain a file named config.json that is a
// snippet of valid configuration that should be included on the // snippet of valid configuration that should be included on the
// ChallengeRequest passed as part of the test cases. // ChallengeRequest passed as part of the test cases.
//
// Uncomment the below fixture when implementing your custom DNS provider fixture := dns.NewFixture(&customDNSProviderSolver{},
//fixture := dns.NewFixture(&customDNSProviderSolver{}, dns.SetResolvedZone(zone),
// dns.SetResolvedZone(zone), dns.SetAllowAmbientCredentials(false),
// dns.SetAllowAmbientCredentials(false),
// dns.SetManifestPath("testdata/my-custom-solver"),
// dns.SetBinariesPath("_test/kubebuilder/bin"),
//)
solver := example.New("59351")
fixture := dns.NewFixture(solver,
dns.SetResolvedZone("example.com."),
dns.SetManifestPath("testdata/my-custom-solver"), dns.SetManifestPath("testdata/my-custom-solver"),
dns.SetDNSServer("127.0.0.1:59351"), // dns.SetBinariesPath("_test/kubebuilder/bin"),
dns.SetUseAuthoritative(false),
) )
//need to uncomment and RunConformance delete runBasic and runExtended once https://github.com/cert-manager/cert-manager/pull/4835 is merged //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.RunConformance(t)

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,62 @@
# 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=
```
config.json config.json.sample README.md variomedia-credentials-01.yaml.sample variomedia-credentials-02.yaml.sample variomedia-credentials-ndeag.yaml

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.V(2).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.V(2).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.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
return "", err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.V(2).ErrorS( fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "UpdateTxtRecord() finished with error")
return "", fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
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
var reply variomediaResponse
err = json.Unmarshal( respData, &reply)
if err != nil {
klog.V(2).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.V(2).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.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
return "", err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.V(2).ErrorS(fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "UpdateTxtRecord() finished with error")
return "", fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
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.V(2).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.V(2).ErrorS(fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"]), "UpdateTxtRecord() finished with error")
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.V(2).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.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
return err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.V(2).ErrorS(fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "DeleteTxtRecord() finished with error")
return fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
}
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
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.V(2).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.V(2).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.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
return err
}
// have we hit the rate limit?
if status == http.StatusTooManyRequests {
klog.V(2).ErrorS(fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "DeleteTxtRecord() finished with error")
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.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
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.V(2).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.V(2).ErrorS(fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"]), "DeleteTxtRecord() finished with error")
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.V(2).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.V(2).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
}