Compare commits

..

No commits in common. "master" and "be0eb944c46529137ab47664ac0fbc71e7c97ebd" have entirely different histories.

28 changed files with 465 additions and 812 deletions

2
.gitignore vendored
View File

@ -12,7 +12,7 @@
*.out
# Ignore the built binary
cert-manager-webhook-freedns
cert-manager-webhook-example
# Make artifacts
_out

View File

@ -1,4 +1,4 @@
FROM golang:1.17-alpine AS build_deps
FROM golang:1.16-alpine AS build_deps
RUN apk add --no-cache git

View File

@ -35,7 +35,7 @@ build:
.PHONY: rendered-manifest.yaml
rendered-manifest.yaml:
helm template \
--name freedns-webhook \
--name example-webhook \
--set image.repository=$(IMAGE_NAME) \
--set image.tag=$(IMAGE_TAG) \
deploy/freedns-webhook > "$(OUT)/rendered-manifest.yaml"
deploy/example-webhook > "$(OUT)/rendered-manifest.yaml"

View File

@ -1,59 +1,54 @@
# Introduction
First, [RTFM](https://cert-manager.io/docs/configuration/acme/dns01/).
# ACME webhook example
Have you read it? If you haven't go read it. Cuz I'll keep everything short.
The ACME issuer type supports an optional 'webhook' solver, which can be used
to implement custom DNS01 challenge solving logic.
This is a dns01 solver for [FreeDNS](https://freedns.afraid.org/).
This is useful if you need to use cert-manager with a DNS provider that is not
officially supported in cert-manager core.
Pull requests welcome. I'm completely unfamiliar with golang. I did it by looking at
other webhook repos and this is the result.
## Why not in core?
As the project & adoption has grown, there has been an influx of DNS provider
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
users to quickly iterate and test out new integrations, and then packaging
those up themselves as 'extensions' to cert-manager.
We can also then provide a standardised 'testing framework', or set of
conformance tests, which allow us to validate the a DNS provider works as
expected.
## Creating your own webhook
Webhook's themselves are deployed as Kubernetes API services, in order to allow
administrators to restrict access to webhooks with Kubernetes RBAC.
This is important, as otherwise it'd be possible for anyone with access to your
webhook to complete ACME challenge validations and obtain certificates.
To make the set up of these webhook's easier, we provide a template repository
that can be used to get started quickly.
### Creating your own repository
### Running the test suite
All DNS providers **must** run the DNS01 provider conformance testing 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
DNS01 webhook.**
An example Go test file has been provided in [main_test.go](https://github.com/jetstack/cert-manager-webhook-example/blob/master/main_test.go).
You can run the test suite with:
## Install
```bash
$ cd deploy
$ helm show values freedns-webhook > my-values.yaml
$ edit my-values.yaml
$ helm install -n cert-manager [INSTALLATION_NAME] freedns-webhook/ -f my-values.yaml
$ TEST_ZONE_NAME=example.com. make test
```
## ClusterIssuer for Let's encrypt staging
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: myemail@example.com
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: le-staging
solvers:
- dns01:
webhook:
groupName: acme.freedns.afraid.org
solverName: freedns-solver
config:
secretName: freedns-auth
```
## FreeDNS webhook settings
Normally if you haven't changed anything, the default namespace should be
`cert-manager`. It should be within the same namespace for the webhook when
you do `helm install webhook -n cert-manager`.
```yaml
apiVersion: v1
kind: Secret
metadata:
name: freedns-auth
namespace: cert-manager
data:
username: [YOUR_USERNAME_IN_BASE64]
password: [YOUR_PASSWORD_IN_BASE64]
type: Opaque
```
Additionally, the following names can be customized
* acme.freedns.afraid.org
* freedns-auth
The example file has a number of areas you must fill in and replace with your
own options in order for tests to pass.

View File

@ -1,5 +1,5 @@
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for Kubernetes
name: freedns-webhook
name: example-webhook
version: 0.1.0

View File

@ -2,7 +2,7 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "freedns-webhook.name" -}}
{{- define "example-webhook.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
@ -11,7 +11,7 @@ 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 "freedns-webhook.fullname" -}}
{{- define "example-webhook.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
@ -27,22 +27,22 @@ If release name contains chart name it will be used as a full name.
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "freedns-webhook.chart" -}}
{{- define "example-webhook.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "freedns-webhook.selfSignedIssuer" -}}
{{ printf "%s-selfsign" (include "freedns-webhook.fullname" .) }}
{{- define "example-webhook.selfSignedIssuer" -}}
{{ printf "%s-selfsign" (include "example-webhook.fullname" .) }}
{{- end -}}
{{- define "freedns-webhook.rootCAIssuer" -}}
{{ printf "%s-ca" (include "freedns-webhook.fullname" .) }}
{{- define "example-webhook.rootCAIssuer" -}}
{{ printf "%s-ca" (include "example-webhook.fullname" .) }}
{{- end -}}
{{- define "freedns-webhook.rootCACertificate" -}}
{{ printf "%s-ca" (include "freedns-webhook.fullname" .) }}
{{- define "example-webhook.rootCACertificate" -}}
{{ printf "%s-ca" (include "example-webhook.fullname" .) }}
{{- end -}}
{{- define "freedns-webhook.servingCertificate" -}}
{{ printf "%s-webhook-tls" (include "freedns-webhook.fullname" .) }}
{{- define "example-webhook.servingCertificate" -}}
{{ printf "%s-webhook-tls" (include "example-webhook.fullname" .) }}
{{- end -}}

View File

@ -3,17 +3,17 @@ kind: APIService
metadata:
name: v1alpha1.{{ .Values.groupName }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
annotations:
cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ include "freedns-webhook.servingCertificate" . }}"
cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/{{ include "example-webhook.servingCertificate" . }}"
spec:
group: {{ .Values.groupName }}
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: {{ include "freedns-webhook.fullname" . }}
name: {{ include "example-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}
version: v1alpha1

View File

@ -1,25 +1,25 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "freedns-webhook.fullname" . }}
name: {{ include "example-webhook.fullname" . }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "freedns-webhook.name" . }}
app: {{ include "example-webhook.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ include "freedns-webhook.name" . }}
app: {{ include "example-webhook.name" . }}
release: {{ .Release.Name }}
spec:
serviceAccountName: {{ include "freedns-webhook.fullname" . }}
serviceAccountName: {{ include "example-webhook.fullname" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
@ -53,7 +53,7 @@ spec:
volumes:
- name: certs
secret:
secretName: {{ include "freedns-webhook.servingCertificate" . }}
secretName: {{ include "example-webhook.servingCertificate" . }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}

View File

@ -4,11 +4,11 @@
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: {{ include "freedns-webhook.selfSignedIssuer" . }}
name: {{ include "example-webhook.selfSignedIssuer" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
@ -20,19 +20,19 @@ spec:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "freedns-webhook.rootCACertificate" . }}
name: {{ include "example-webhook.rootCACertificate" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
secretName: {{ include "freedns-webhook.rootCACertificate" . }}
secretName: {{ include "example-webhook.rootCACertificate" . }}
duration: 43800h # 5y
issuerRef:
name: {{ include "freedns-webhook.selfSignedIssuer" . }}
commonName: "ca.freedns-webhook.cert-manager"
name: {{ include "example-webhook.selfSignedIssuer" . }}
commonName: "ca.example-webhook.cert-manager"
isCA: true
---
@ -41,16 +41,16 @@ spec:
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: {{ include "freedns-webhook.rootCAIssuer" . }}
name: {{ include "example-webhook.rootCAIssuer" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
ca:
secretName: {{ include "freedns-webhook.rootCACertificate" . }}
secretName: {{ include "example-webhook.rootCACertificate" . }}
---
@ -58,19 +58,19 @@ spec:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "freedns-webhook.servingCertificate" . }}
name: {{ include "example-webhook.servingCertificate" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
secretName: {{ include "freedns-webhook.servingCertificate" . }}
secretName: {{ include "example-webhook.servingCertificate" . }}
duration: 8760h # 1y
issuerRef:
name: {{ include "freedns-webhook.rootCAIssuer" . }}
name: {{ include "example-webhook.rootCAIssuer" . }}
dnsNames:
- {{ include "freedns-webhook.fullname" . }}
- {{ include "freedns-webhook.fullname" . }}.{{ .Release.Namespace }}
- {{ include "freedns-webhook.fullname" . }}.{{ .Release.Namespace }}.svc
- {{ include "example-webhook.fullname" . }}
- {{ include "example-webhook.fullname" . }}.{{ .Release.Namespace }}
- {{ include "example-webhook.fullname" . }}.{{ .Release.Namespace }}.svc

View File

@ -0,0 +1,90 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "example-webhook.fullname" . }}
labels:
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.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 "example-webhook.fullname" . }}:webhook-authentication-reader
namespace: kube-system
labels:
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.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 "example-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}
---
# 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 "example-webhook.fullname" . }}:auth-delegator
labels:
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.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 "example-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}
---
# Grant cert-manager permission to validate using our apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "example-webhook.fullname" . }}:domain-solver
labels:
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.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 "example-webhook.fullname" . }}:domain-solver
labels:
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "example-webhook.fullname" . }}:domain-solver
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ .Values.certManager.serviceAccountName }}
namespace: {{ .Values.certManager.namespace }}

View File

@ -1,10 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "freedns-webhook.fullname" . }}
name: {{ include "example-webhook.fullname" . }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
app: {{ include "example-webhook.name" . }}
chart: {{ include "example-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
@ -15,5 +15,5 @@ spec:
protocol: TCP
name: https
selector:
app: {{ include "freedns-webhook.name" . }}
app: {{ include "example-webhook.name" . }}
release: {{ .Release.Name }}

View File

@ -1,20 +1,20 @@
# The GroupName here is used to identify your company or business unit that
# created this webhook.
# For freedns, this may be "acme.freedns.afraid.org".
# For example, this may be "acme.mycompany.com".
# This name will need to be referenced in each Issuer's `webhook` stanza to
# inform cert-manager of where to send ChallengePayload resources in order to
# solve the DNS01 challenge.
# This group name should be **unique**, hence using your own company's domain
# here is recommended.
groupName: acme.freedns.afraid.org
groupName: acme.mycompany.com
certManager:
namespace: cert-manager
serviceAccountName: cert-manager
image:
repository: penguinade/cert-manager-webhook-freedns
tag: 2022.03.15
repository: mycompany/webhook-image
tag: latest
pullPolicy: IfNotPresent
nameOverride: ""

View File

@ -1,168 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "freedns-webhook.fullname" . }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "freedns-webhook.fullname" . }}:secret-read
namespace: {{ .Release.Namespace }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
rules:
- apiGroups:
- ''
resources:
- 'secrets'
verbs:
- 'get'
---
# Grant the webhook permission to read the secret
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "freedns-webhook.fullname" . }}:secret-read
namespace: {{ .Release.Namespace }}
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "freedns-webhook.fullname" . }}:secret-read
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "freedns-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}
---
# 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 "freedns-webhook.fullname" . }}:webhook-authentication-reader
namespace: kube-system
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.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 "freedns-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}
---
# 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 "freedns-webhook.fullname" . }}:auth-delegator
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.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 "freedns-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}
---
# Grant cert-manager permission to validate using our apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "freedns-webhook.fullname" . }}:domain-solver
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.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 "freedns-webhook.fullname" . }}:domain-solver
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "freedns-webhook.fullname" . }}:domain-solver
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ .Values.certManager.serviceAccountName }}
namespace: {{ .Values.certManager.namespace }}
---
# Grant pod account permission to validate using our apiserver
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "freedns-webhook.fullname" . }}:flowcontrol
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.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 "freedns-webhook.fullname" . }}:flowcontrol
labels:
app: {{ include "freedns-webhook.name" . }}
chart: {{ include "freedns-webhook.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "freedns-webhook.fullname" . }}:flowcontrol
subjects:
- apiGroup: ""
kind: ServiceAccount
name: {{ include "freedns-webhook.fullname" . }}
namespace: {{ .Release.Namespace }}

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)
}

View File

@ -1,406 +0,0 @@
package freedns
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
logf "github.com/jetstack/cert-manager/pkg/logs"
"golang.org/x/net/html"
)
type FreeDNSOperations interface {
Login()
SelectDomain()
AddRecord()
FindRecord()
DeleteRecord()
}
type FreeDNS struct {
AuthCookie *http.Cookie
DomainId string
LoggedOut bool
}
const URI_LOGIN = "https://freedns.afraid.org/zc.php?step=2"
const URI_DOMAIN = "https://freedns.afraid.org/domain/"
const URI_ADD_RECORD = "https://freedns.afraid.org/subdomain/save.php?step=2"
const URI_SUBDOMAIN = "https://freedns.afraid.org/subdomain/?limit="
const URI_SUBDOMAIN_EDIT = "https://freedns.afraid.org/subdomain/edit.php?data_id="
const URI_LOGOUT = "https://freedns.afraid.org/logout/"
const URI_DELETE_RECORD = "https://freedns.afraid.org/subdomain/delete2.php?data_id[]=%s&submit=delete%%20selected"
func LogInfo(Mesg string) {
// fmt.Println(Mesg)
logf.V(logf.InfoLevel).Info(Mesg)
}
func LogDebug(Mesg string) {
// fmt.Println(Mesg)
logf.V(logf.DebugLevel).Info(Mesg)
}
func GetDomainFromZone(Zone string) string {
_segs := strings.Split(strings.TrimSuffix(Zone, "."), ".")
_segs = _segs[len(_segs)-2:]
return strings.Join(_segs, ".")
}
func _HttpRequest(method string, url string, PostData url.Values, ExCookie *http.Cookie) (*http.Response, string, error) {
client := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
var req *http.Request
var err error
if method == "GET" {
req, err = http.NewRequest(method, url, nil)
} else if method == "POST" {
req, err = http.NewRequest(method, url, strings.NewReader(PostData.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
} else {
return nil, "", errors.New("Method + \"" + method + "\" is not supported")
}
if err != nil {
return nil, "", err
}
req.Header.Set("User-Agent", "github.com/tgckpg/cert-manager-webhook-freedns (2022.03.15)")
if ExCookie != nil {
req.AddCookie(ExCookie)
}
resp, err := client.Do(req)
if err != nil {
return resp, "", err
}
respData, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return resp, "", err
}
return resp, string(respData), nil
}
func (dnsObj *FreeDNS) Login(Username string, Password string) error {
authData := url.Values{}
authData.Set("username", Username)
authData.Set("password", Password)
authData.Set("submit", "Login")
authData.Set("action", "auth")
resp, respString, err := _HttpRequest("POST", URI_LOGIN, authData, nil)
if err != nil {
return err
}
if strings.Contains(respString, "Invalid UserID/Pass") {
return errors.New("Invalid UserID/Pass")
}
for _, cookie := range resp.Cookies() {
if cookie.Name == "dns_cookie" {
dnsObj.AuthCookie = cookie
dnsObj.LoggedOut = false
}
}
return nil
}
func (dnsObj *FreeDNS) Logout() error {
if dnsObj.LoggedOut {
return nil
}
_, _, err := _HttpRequest("GET", URI_LOGOUT, nil, dnsObj.AuthCookie)
if err != nil {
return err
}
dnsObj.LoggedOut = true
return nil
}
func (dnsObj *FreeDNS) SelectDomain(DomainName string) error {
if dnsObj.AuthCookie == nil {
return errors.New("Not logged in")
}
resp, respStr, err := _HttpRequest("GET", URI_DOMAIN, nil, dnsObj.AuthCookie)
if err != nil {
return err
}
if resp.StatusCode == 302 {
return errors.New("dns_cookie maybe expired")
}
htmlTokens := html.NewTokenizer(strings.NewReader(respStr))
inBold := false
lookForA := false
dnsObj.DomainId = ""
// Begin search for domain id
loop:
for {
tt := htmlTokens.Next()
switch tt {
case html.ErrorToken:
break loop
case html.TextToken:
if inBold && strings.TrimSpace(htmlTokens.Token().Data) == DomainName {
LogInfo("Found " + DomainName + ", looking for domain id")
lookForA = true
}
// The [Manage] anchor is next to the bold tag
// <b>DOMAIN_NAME</b> <a href="">[Manage]</a>
case html.StartTagToken:
_t, hasAttr := htmlTokens.TagName()
tagName := string(_t)
inBold = tagName == "b"
if lookForA && tagName == "a" && hasAttr {
for {
attrKey, attrValue, moreAttr := htmlTokens.TagAttr()
_href := string(attrValue)
if string(attrKey) == "href" && strings.HasPrefix(_href, "/subdomain/?limit=") {
dnsObj.DomainId = strings.TrimPrefix(_href, "/subdomain/?limit=")
LogDebug(fmt.Sprintf("Domain id for \"%s\" is %s\n", DomainName, dnsObj.DomainId))
break loop
}
if !moreAttr {
break
}
}
}
}
}
if dnsObj.DomainId == "" {
return errors.New(fmt.Sprintf("Unable to locate domain id for \"%s\" under /domain/ page", DomainName))
}
return nil
}
func (dnsObj *FreeDNS) AddRecord(RecordType string, Subdomain string, Address string, Wildcard bool, ttl string) error {
if dnsObj.DomainId == "" {
return errors.New("No domain selected")
}
recordData := url.Values{}
recordData.Set("type", RecordType)
recordData.Set("domain_id", dnsObj.DomainId)
recordData.Set("subdomain", Subdomain)
recordData.Set("address", Address)
recordData.Set("send", "Save!")
if Wildcard {
recordData.Set("wildcard", "1")
}
resp, respStr, err := _HttpRequest("POST", URI_ADD_RECORD, recordData, dnsObj.AuthCookie)
if err != nil {
return err
}
if resp.StatusCode != 302 {
// Record already exists, treat this as success
if strings.Contains(respStr, "already have another already existent") {
LogInfo("Record already exists")
return nil
}
// Try get a sense of the problem
var errorMesgs []string
lookForNextEl := 0
lookForText := false
_strBuffer := ""
htmlTokens := html.NewTokenizer(strings.NewReader(respStr))
loop:
for {
tt := htmlTokens.Next()
switch tt {
case html.ErrorToken:
break loop
case html.TextToken:
_text := strings.TrimSpace(string(htmlTokens.Text()))
// Search for the "1 error" / "N errors" message
if strings.HasSuffix(_text, "error") || strings.HasSuffix(_text, "errors") {
_text = strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(_text, "s"), "error"))
_n, _ := strconv.ParseInt(_text, 10, 8)
// + 1 because we are already inside a font tag
// The next closing </font> is ourself
lookForNextEl = int(_n) + 1
} else if lookForText {
_strBuffer = _strBuffer + _text
}
case html.StartTagToken:
_t, _ := htmlTokens.TagName()
tagName := string(_t)
if tagName == "font" && 0 < lookForNextEl {
lookForText = true
_strBuffer = ""
}
case html.EndTagToken:
_t, _ := htmlTokens.TagName()
tagName := string(_t)
if tagName == "font" && 0 < lookForNextEl {
lookForText = false
errorMesgs = append(errorMesgs, strings.TrimSpace(_strBuffer))
lookForNextEl--
}
}
}
if 0 < len(errorMesgs) {
return errors.New(strings.Join(errorMesgs, ", "))
}
return errors.New("Unknown error while submitting record")
}
_Location, err := resp.Location()
if err != nil {
return err
}
if strings.HasPrefix(_Location.Path, "/zc.php") {
LogDebug("Error on AddRecord: Cookie expired")
return errors.New("dns_cookie maybe expired")
}
return nil
}
func (dnsObj *FreeDNS) DeleteRecord(RecordId string) error {
resp, _, err := _HttpRequest("GET", fmt.Sprintf(URI_DELETE_RECORD, RecordId), nil, dnsObj.AuthCookie)
if err != nil {
return err
}
if resp.StatusCode != 302 {
return errors.New("Unexpected " + fmt.Sprint(resp.StatusCode) + " from remote while deleting record")
}
_Location, err := resp.Location()
if err != nil {
return err
}
if strings.HasPrefix(_Location.Path, "/zc.php") {
return errors.New("dns_cookie maybe expired")
}
return nil
}
func (dnsObj *FreeDNS) FindRecord(Subdomain string, RecordType string, Address string) (string, error) {
if dnsObj.DomainId == "" {
return "", errors.New("No domain selected")
}
resp, respStr, err := _HttpRequest("GET", URI_SUBDOMAIN+dnsObj.DomainId, nil, dnsObj.AuthCookie)
if err != nil {
return "", err
}
if resp.StatusCode == 302 {
return "", errors.New("dns_cookie maybe expired")
}
var DeepSearchCandidates []string
CurrRecordId := ""
CurrRecordType := ""
CurrRecordAddr := ""
CurrTagName := ""
lookForNextEl := 0
htmlTokens := html.NewTokenizer(strings.NewReader(respStr))
loop:
for {
tt := htmlTokens.Next()
switch tt {
case html.ErrorToken:
break loop
case html.TextToken:
if CurrTagName == "a" && lookForNextEl == 1 && CurrRecordAddr == "" {
CurrRecordAddr = strings.TrimSpace(string(htmlTokens.Text()))
} else if CurrTagName == "td" {
if lookForNextEl == 1 {
CurrRecordType = string(htmlTokens.Text())
lookForNextEl = 2
} else if lookForNextEl == 2 {
_Addr := string(htmlTokens.Text())
if CurrRecordType == RecordType && CurrRecordAddr == Subdomain {
if _Addr == Address {
return CurrRecordId, nil
} else if strings.HasSuffix(_Addr, "...") && strings.HasPrefix(Address, strings.TrimSuffix(_Addr, "...")) {
DeepSearchCandidates = append(DeepSearchCandidates, CurrRecordId)
}
}
lookForNextEl = 0
}
}
/** Each record is displayed with the following structure
* <td bgcolor="#eeeeee">
* <a href="edit.php?data_id=0000000">
* [DOMAIN_NAME]
* </a> (<b><font color="blue">G</font></b>)
* </td>
* <td bgcolor="#eeeeee">TXT</td>
* <td bgcolor="#eeeeee">"google-site-verification=truncated_text...</td>
*/
case html.StartTagToken:
_t, hasAttr := htmlTokens.TagName()
CurrTagName = string(_t)
if CurrTagName == "a" && hasAttr {
for {
attrKey, attrValue, moreAttr := htmlTokens.TagAttr()
_href := string(attrValue)
if string(attrKey) == "href" && strings.Contains(_href, "edit.php?data_id=") {
lookForNextEl = 1
CurrRecordAddr = ""
CurrRecordId = strings.TrimPrefix(_href, "edit.php?data_id=")
break
}
if !moreAttr {
break
}
}
}
}
}
// Begin deep search for truncated records
htmlAddr := strings.ReplaceAll(html.EscapeString(Address), "&#34;", "&quot;")
for _, RecordId := range DeepSearchCandidates {
LogDebug("Searching in " + RecordId)
_, respStr, err := _HttpRequest("GET", URI_SUBDOMAIN_EDIT+RecordId, nil, dnsObj.AuthCookie)
if err != nil {
continue
}
if strings.Contains(respStr, htmlAddr) {
return RecordId, nil
}
}
return "", errors.New("No such record")
}

View File

@ -1,42 +0,0 @@
package freedns
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
/*
func TestHttpRequest(t *testing.T) {
_HttpRequest("GET", "http://127.0.0.1:12345", nil, nil)
}
*/
func TestGetDomainFromZone(t *testing.T) {
assert.Equal(t, GetDomainFromZone("a.b.example.com"), "example.com")
assert.Equal(t, GetDomainFromZone("example.com"), "example.com")
}
func TestOperations(t *testing.T) {
freeDNS := FreeDNS{}
var UserName = os.Getenv("FREEDNS_USERNAME")
var Password = os.Getenv("FREEDNS_PASSWORD")
var SelectedDomain = os.Getenv("FREEDNS_DOMAIN")
require.NotEmpty(t, UserName, "Please set the env vars for FREEDNS_USERNAME")
require.NotEmpty(t, Password, "Please set the env vars for FREEDNS_PASSWORD")
require.NotEmpty(t, SelectedDomain, "Please set the env vars for FREEDNS_DOMAIN")
require.Nil(t, freeDNS.Login(UserName, Password))
require.Nil(t, freeDNS.SelectDomain(SelectedDomain))
require.Nil(t, freeDNS.AddRecord("TXT", "", "\"TEST\"", false, ""))
id, _ := freeDNS.FindRecord(SelectedDomain, "TXT", "\"TEST\"")
require.NotEmpty(t, id)
require.Nil(t, freeDNS.DeleteRecord(id))
require.Nil(t, freeDNS.Logout())
require.Equal(t, freeDNS.LoggedOut, true)
}

4
go.mod
View File

@ -1,9 +1,9 @@
module github.com/cert-manager/webhook-freedns
module github.com/cert-manager/webhook-example
go 1.17
require (
github.com/jetstack/cert-manager v1.7.1
github.com/jetstack/cert-manager v1.7.0
github.com/miekg/dns v1.1.34
github.com/stretchr/testify v1.7.0
k8s.io/apiextensions-apiserver v0.23.1

2
go.sum
View File

@ -305,8 +305,6 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jetstack/cert-manager v1.7.0 h1:XLLDmREJ8MlBg/Z1bySjHdyzT4yYTzGlbBsxqddJzxU=
github.com/jetstack/cert-manager v1.7.0/go.mod h1:xj0TPp31HE0Jub5mNOnF3Fp3XvhIsiP+tsPZVOmU/Qs=
github.com/jetstack/cert-manager v1.7.1 h1:qIIP0RN5FzBChJLJ3uGCGJmdAAonwDMdcsJExATa64I=
github.com/jetstack/cert-manager v1.7.1/go.mod h1:xj0TPp31HE0Jub5mNOnF3Fp3XvhIsiP+tsPZVOmU/Qs=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=

82
main.go
View File

@ -1,18 +1,14 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
//"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"github.com/cert-manager/webhook-freedns/freedns"
"github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
"github.com/jetstack/cert-manager/pkg/acme/webhook/cmd"
)
@ -45,8 +41,7 @@ type customDNSProviderSolver struct {
// 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
freedns *freedns.FreeDNS
//client kubernetes.Clientset
}
// customDNSProviderConfig is a structure that is used to decode into when
@ -70,8 +65,7 @@ type customDNSProviderConfig struct {
// `issuer.spec.acme.dns01.providers.webhook.config` field.
//Email string `json:"email"`
SecretRef string `json:"secretName"`
//APIKeySecretRef v1.SecretKeySelector `json:"apiKeySecretRef"`
//APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"`
}
// Name is used as the name for this DNS solver when referencing it on the ACME
@ -81,7 +75,7 @@ type customDNSProviderConfig struct {
// within a single webhook deployment**.
// For example, `cloudflare` may be used as the name of a solver.
func (c *customDNSProviderSolver) Name() string {
return "freedns-solver"
return "my-custom-solver"
}
// Present is responsible for actually presenting the DNS record with the
@ -95,40 +89,10 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
return err
}
secretName := cfg.SecretRef
secretObj, err := c.client.CoreV1().Secrets(ch.ResourceNamespace).Get(context.Background(), secretName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Unable to get secret `%s/%s`; %v", secretName, ch.ResourceNamespace, err)
}
// TODO: do something more useful with the decoded configuration
fmt.Printf("Decoded configuration %v", cfg)
username := string(secretObj.Data["username"])
password := string(secretObj.Data["password"])
dnsObj := freedns.FreeDNS{}
err = dnsObj.Login(username, password)
if err != nil {
return err
}
domainName := freedns.GetDomainFromZone(ch.ResolvedZone)
err = dnsObj.SelectDomain(domainName)
if err != nil {
return err
}
_zone := strings.TrimRight(ch.ResolvedFQDN, ".")
_zone = strings.TrimSuffix(_zone, domainName)
_zone = strings.TrimRight(_zone, ".")
_key := "\"" + ch.Key + "\""
freedns.LogInfo(fmt.Sprintf("ADD %s %s", _zone, _key))
err = dnsObj.AddRecord("TXT", _zone, _key, false, "")
if err != nil {
return err
}
c.freedns = &dnsObj
// TODO: add code that sets a record in the DNS provider's console
return nil
}
@ -139,27 +103,8 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
// This is in order to facilitate multiple DNS validations for the same domain
// concurrently.
func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
if c.freedns.AuthCookie == nil {
// TODO: add code that deletes a record from the DNS provider's console
return nil
}
dnsObj := c.freedns
_addr := strings.TrimRight(ch.ResolvedFQDN, ".")
_key := "\"" + ch.Key + "\""
_id, err := c.freedns.FindRecord(_addr, "TXT", _key)
freedns.LogInfo(fmt.Sprintf("DEL %s %s", _addr, _key))
if _id != "" {
err = c.freedns.DeleteRecord(_id)
if err != nil {
return err
}
}
return dnsObj.Logout()
}
// Initialize will be called when the webhook first starts.
@ -175,11 +120,12 @@ func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stop
///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO
///// YOUR CUSTOM DNS PROVIDER
cl, err := kubernetes.NewForConfig(kubeClientConfig)
if err != nil {
return err
}
c.client = cl
//cl, err := kubernetes.NewForConfig(kubeClientConfig)
//if err != nil {
// return err
//}
//
//c.client = cl
///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE
return nil

View File

@ -5,6 +5,8 @@ import (
"testing"
"github.com/jetstack/cert-manager/test/acme/dns"
"github.com/cert-manager/webhook-example/example"
)
var (
@ -18,11 +20,22 @@ func TestRunsSuite(t *testing.T) {
//
// Uncomment the below fixture when implementing your custom DNS provider
fixture := dns.NewFixture(&customDNSProviderSolver{},
dns.SetResolvedZone(zone),
dns.SetAllowAmbientCredentials(false),
dns.SetManifestPath("testdata/freedns-solver"),
//fixture := dns.NewFixture(&customDNSProviderSolver{},
// dns.SetResolvedZone(zone),
// 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.SetDNSServer("127.0.0.1:59351"),
dns.SetUseAuthoritative(false),
)
fixture.RunConformance(t)
//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)
}

View File

@ -1,3 +0,0 @@
{
"secretName": "freedns-auth"
}

View File

@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: freedns-auth
data:
username: ZXhhbXBsZQ==
password: MTIzNA==

3
testdata/my-custom-solver/README.md vendored Normal file
View File

@ -0,0 +1,3 @@
# Solver testdata directory
TODO

1
testdata/my-custom-solver/config.json vendored Normal file
View File

@ -0,0 +1 @@
{}