penguin/webhook-freedns

freedns webhook for cert-manager

commit 4f51af7d86f135f566f8156396dd0c89bc6175aa

authorJake Sanders <i@am.so-aweso.me>
date2021-02-22T15:11:18Z
subjectAdd example server that passes the test fixtures
commit 4f51af7d86f135f566f8156396dd0c89bc6175aa
Author: Jake Sanders <i@am.so-aweso.me>
Date:   2021-02-22T15:11:18Z

    Add example server that passes the test fixtures
    
    Signed-off-by: Jake Sanders <i@am.so-aweso.me>
---
 example/dns.go          | 58 ++++++++++++++++++++++++++++++
 example/example.go      | 60 +++++++++++++++++++++++++++++++
 example/example_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++
 main_test.go            | 20 +++++++++--
 4 files changed, 229 insertions(+), 3 deletions(-)

diff --git a/example/dns.go b/example/dns.go
index f7ec372..9b00b79 100644
--- a/example/dns.go
+++ b/example/dns.go
@@ -1 +1,59 @@
 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 {
+			switch q.Qtype {
+			case dns.TypeA:
+				rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN A 127.0.0.1", q.Name))
+				if err != nil {
+					msg.SetRcode(req, dns.RcodeNameError)
+				} else {
+					msg.Answer = append(msg.Answer, rr)
+				}
+			case dns.TypeTXT:
+				// get record
+				e.RLock()
+				record, found := e.txtRecords[q.Name]
+				e.RUnlock()
+				if !found {
+					msg.SetRcode(req, dns.RcodeNameError)
+				} else {
+					rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN TXT %s", q.Name, record))
+					if err != nil {
+						msg.SetRcode(req, dns.RcodeServerFailure)
+						break
+					}
+					msg.Answer = append(msg.Answer, rr)
+				}
+			case dns.TypeNS:
+				rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN NS ns.example-acme-webook.invalid.", q.Name))
+				if err != nil {
+					msg.SetRcode(req, dns.RcodeServerFailure)
+					break
+				} else {
+					msg.Answer = append(msg.Answer, rr)
+				}
+			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 {
+					msg.SetRcode(req, dns.RcodeServerFailure)
+					break
+				}
+				msg.Answer = append(msg.Answer, rr)
+			default:
+				msg.SetRcode(req, dns.RcodeServerFailure)
+				break
+			}
+		}
+	}
+	w.WriteMsg(msg)
+}
diff --git a/example/example.go b/example/example.go
index f7ec372..49eca8f 100644
--- a/example/example.go
+++ b/example/example.go
@@ -1 +1,61 @@
+// package example contains a self-contained example of a webhook that passes the cert-manager
+// DNS conformance tests
 package example
+
+import (
+	"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
+		e.server.Shutdown()
+	}(stopCh)
+	go func() {
+		e.server.ListenAndServe()
+	}()
+	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
+}
diff --git a/example/example_test.go b/example/example_test.go
index f7ec372..2c4a455 100644
--- a/example/example_test.go
+++ b/example/example_test.go
@@ -1 +1,95 @@
 package example
+
+import (
+	"crypto/rand"
+	acme "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
+	"github.com/miekg/dns"
+	"github.com/stretchr/testify/assert"
+	"math/big"
+	"testing"
+)
+
+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)
+}
diff --git a/main_test.go b/main_test.go
index 4e32419..c731751 100644
--- a/main_test.go
+++ b/main_test.go
@@ -5,6 +5,8 @@ import (
 	"testing"
 
 	"github.com/jetstack/cert-manager/test/acme/dns"
+
+	"github.com/jetstack/cert-manager-webhook-example/example"
 )
 
 var (
@@ -15,11 +17,23 @@ func TestRunsSuite(t *testing.T) {
 	// The manifest path should contain a file named config.json that is a
 	// snippet of valid configuration that should be included on the
 	// ChallengeRequest passed as part of the test cases.
+	//
+
+	// Uncomment the below fixture when implementing your custom DNS provider
+	//fixture := dns.NewFixture(&customDNSProviderSolver{},
+	//	dns.SetResolvedZone(zone),
+	//	dns.SetAllowAmbientCredentials(false),
+	//	dns.SetManifestPath("testdata/my-custom-solver"),
+	//	dns.SetBinariesPath("_test/kubebuilder/bin"),
+	//)
 
-	fixture := dns.NewFixture(&customDNSProviderSolver{},
-		dns.SetResolvedZone(zone),
-		dns.SetAllowAmbientCredentials(false),
+	solver := example.New("59351")
+	fixture := dns.NewFixture(solver,
+		dns.SetResolvedZone("example.com."),
 		dns.SetManifestPath("testdata/my-custom-solver"),
+		dns.SetBinariesPath("_test/kubebuilder/bin"),
+		dns.SetDNSServer("127.0.0.1:59351"),
+		dns.SetUseAuthoritative(false),
 	)
 
 	fixture.RunConformance(t)