From 4f51af7d86f135f566f8156396dd0c89bc6175aa Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Mon, 22 Feb 2021 15:11:18 +0000 Subject: [PATCH] Add example server that passes the test fixtures Signed-off-by: Jake Sanders --- 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. + // - fixture := dns.NewFixture(&customDNSProviderSolver{}, - dns.SetResolvedZone(zone), - dns.SetAllowAmbientCredentials(false), + // 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"), + //) + + 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)