diff --git a/pkg/issuer/acme/dns/dns_test.go b/pkg/issuer/acme/dns/dns_test.go new file mode 100644 index 000000000..f8bc7702f --- /dev/null +++ b/pkg/issuer/acme/dns/dns_test.go @@ -0,0 +1,278 @@ +package dns + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kubeinformers "k8s.io/client-go/informers" + kubefake "k8s.io/client-go/kubernetes/fake" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/cloudflare" +) + +type fixture struct { + // Issuer resource this solver is for + Issuer v1alpha1.GenericIssuer + + // Objects here are pre-loaded into the fake client + KubeObjects []runtime.Object + + // Secret objects to store in the fake lister + SecretLister []*corev1.Secret + + // the resourceNamespace to set on the solver + ResourceNamespace string + + // certificate used in the test + Certificate *v1alpha1.Certificate +} + +func (f *fixture) solver() *Solver { + kubeClient := kubefake.NewSimpleClientset(f.KubeObjects...) + sharedInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, 0) + secretsLister := sharedInformerFactory.Core().V1().Secrets().Lister() + for _, s := range f.SecretLister { + sharedInformerFactory.Core().V1().Secrets().Informer().GetIndexer().Add(s) + } + stopCh := make(chan struct{}) + defer close(stopCh) + sharedInformerFactory.Start(stopCh) + return &Solver{ + issuer: f.Issuer, + client: kubeClient, + secretLister: secretsLister, + resourceNamespace: f.ResourceNamespace, + } +} + +func newIssuer(name, namespace string, configs []v1alpha1.ACMEIssuerDNS01Provider) *v1alpha1.Issuer { + return &v1alpha1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.IssuerSpec{ + IssuerConfig: v1alpha1.IssuerConfig{ + ACME: &v1alpha1.ACMEIssuer{ + DNS01: &v1alpha1.ACMEIssuerDNS01Config{ + Providers: configs, + }, + }, + }, + }, + } +} + +func newCertificate(name, namespace, cn string, dnsNames []string, configs []v1alpha1.ACMECertificateDomainConfig) *v1alpha1.Certificate { + return &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + CommonName: cn, + DNSNames: dnsNames, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: configs, + }, + }, + } +} + +func newSecret(name, namespace string, data map[string][]byte) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: data, + } +} + +func TestSolverFor(t *testing.T) { + type testT struct { + f *fixture + domain string + expectErr bool + expectedSolverType reflect.Type + } + tests := map[string]testT{ + "loads secret for cloudflare provider": { + f: &fixture{ + Issuer: newIssuer("test", "default", []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "fake-cloudflare", + Cloudflare: &v1alpha1.ACMEIssuerDNS01ProviderCloudflare{ + Email: "test", + APIKey: v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: "cloudflare-key", + }, + Key: "api-key", + }, + }, + }, + }), + SecretLister: []*corev1.Secret{newSecret("cloudflare-key", "default", map[string][]byte{ + "api-key": []byte("a-cloudflare-api-key"), + })}, + ResourceNamespace: "default", + Certificate: newCertificate("test", "default", "example.com", nil, []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + DNS01: &v1alpha1.ACMECertificateDNS01Config{ + Provider: "fake-cloudflare", + }, + }, + }), + }, + domain: "example.com", + expectedSolverType: reflect.TypeOf(&cloudflare.DNSProvider{}), + }, + "fails to load a cloudflare provider with a missing secret": { + f: &fixture{ + Issuer: newIssuer("test", "default", []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "fake-cloudflare", + Cloudflare: &v1alpha1.ACMEIssuerDNS01ProviderCloudflare{ + Email: "test", + APIKey: v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: "cloudflare-key", + }, + Key: "api-key", + }, + }, + }, + }), + // don't include any secrets in the lister + SecretLister: []*corev1.Secret{}, + ResourceNamespace: "default", + Certificate: newCertificate("test", "default", "example.com", nil, []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + DNS01: &v1alpha1.ACMECertificateDNS01Config{ + Provider: "fake-cloudflare", + }, + }, + }), + }, + domain: "example.com", + expectErr: true, + }, + "fails to load a cloudflare provider with an invalid secret": { + f: &fixture{ + Issuer: newIssuer("test", "default", []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "fake-cloudflare", + Cloudflare: &v1alpha1.ACMEIssuerDNS01ProviderCloudflare{ + Email: "test", + APIKey: v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: "cloudflare-key", + }, + Key: "api-key", + }, + }, + }, + }), + SecretLister: []*corev1.Secret{newSecret("cloudflare-key", "default", map[string][]byte{ + "api-key-oops": []byte("a-cloudflare-api-key"), + })}, + ResourceNamespace: "default", + Certificate: newCertificate("test", "default", "example.com", nil, []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + DNS01: &v1alpha1.ACMECertificateDNS01Config{ + Provider: "fake-cloudflare", + }, + }, + }), + }, + domain: "example.com", + expectErr: true, + }, + "fails to load a provider with no config set for the domain": { + f: &fixture{ + Issuer: newIssuer("test", "default", []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "fake-cloudflare", + Cloudflare: &v1alpha1.ACMEIssuerDNS01ProviderCloudflare{ + Email: "test", + APIKey: v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: "cloudflare-key", + }, + Key: "api-key", + }, + }, + }, + }), + SecretLister: []*corev1.Secret{newSecret("cloudflare-key", "default", map[string][]byte{ + "api-key": []byte("a-cloudflare-api-key"), + })}, + ResourceNamespace: "default", + Certificate: newCertificate("test", "default", "example.com", nil, []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example-oops.com"}, + DNS01: &v1alpha1.ACMECertificateDNS01Config{ + Provider: "fake-cloudflare", + }, + }, + }), + }, + domain: "example.com", + expectErr: true, + }, + "fails to load a provider with a non-existent provider set for the domain": { + f: &fixture{ + Issuer: newIssuer("test", "default", []v1alpha1.ACMEIssuerDNS01Provider{ + { + Name: "fake-cloudflare", + Cloudflare: &v1alpha1.ACMEIssuerDNS01ProviderCloudflare{ + Email: "test", + APIKey: v1alpha1.SecretKeySelector{ + LocalObjectReference: v1alpha1.LocalObjectReference{ + Name: "cloudflare-key", + }, + Key: "api-key", + }, + }, + }, + }), + SecretLister: []*corev1.Secret{newSecret("cloudflare-key", "default", map[string][]byte{ + "api-key": []byte("a-cloudflare-api-key"), + })}, + ResourceNamespace: "default", + Certificate: newCertificate("test", "default", "example.com", nil, []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + DNS01: &v1alpha1.ACMECertificateDNS01Config{ + Provider: "fake-cloudflare-oops", + }, + }, + }), + }, + domain: "example.com", + expectErr: true, + }, + } + testFn := func(test testT) func(*testing.T) { + return func(t *testing.T) { + s := test.f.solver() + dnsSolver, err := s.solverFor(test.f.Certificate, test.domain) + if err != nil && !test.expectErr { + t.Errorf("expected solverFor to not error, but got: %s", err.Error()) + return + } + typeOfSolver := reflect.TypeOf(dnsSolver) + if typeOfSolver != test.expectedSolverType { + t.Errorf("expected solver of type %q but got one of type %q", test.expectedSolverType, typeOfSolver) + return + } + } + } + for name, test := range tests { + t.Run(name, testFn(test)) + } +} diff --git a/pkg/issuer/acme/dns/util/testdata/resolv.conf.1 b/pkg/issuer/acme/dns/util/testdata/resolv.conf.1 new file mode 100644 index 000000000..c6267d799 --- /dev/null +++ b/pkg/issuer/acme/dns/util/testdata/resolv.conf.1 @@ -0,0 +1,5 @@ +domain company.com +nameserver 10.200.3.249 +nameserver 10.200.3.250:5353 +nameserver 2001:4860:4860::8844 +nameserver [10.0.0.1]:5353 \ No newline at end of file diff --git a/pkg/issuer/acme/dns/util/wait_test.go b/pkg/issuer/acme/dns/util/wait_test.go new file mode 100644 index 000000000..c49f6c613 --- /dev/null +++ b/pkg/issuer/acme/dns/util/wait_test.go @@ -0,0 +1,166 @@ +package util + +import ( + "reflect" + "sort" + "strings" + "testing" +) + +var lookupNameserversTestsOK = []struct { + fqdn string + nss []string +}{ + {"books.google.com.ng.", + []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, + }, + {"www.google.com.", + []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, + }, + {"physics.georgetown.edu.", + []string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."}, + }, +} + +var lookupNameserversTestsErr = []struct { + fqdn string + error string +}{ + // invalid tld + {"_null.n0n0.", + "Could not determine the zone", + }, +} + +var findZoneByFqdnTests = []struct { + fqdn string + zone string +}{ + {"mail.google.com.", "google.com."}, // domain is a CNAME + {"foo.google.com.", "google.com."}, // domain is a non-existent subdomain + // TODO: work out why this test doesn't work + //{"example.com.ac.", "ac."}, // domain is a eTLD +} + +var checkAuthoritativeNssTests = []struct { + fqdn, value string + ns []string + ok bool +}{ + // TXT RR w/ expected value + {"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."}, + true, + }, + // No TXT RR + {"ns1.google.com.", "", []string{"ns2.google.com."}, + false, + }, +} + +var checkAuthoritativeNssTestsErr = []struct { + fqdn, value string + ns []string + error string +}{ + // TXT RR /w unexpected value + {"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."}, + "did not return the expected TXT record", + }, + // No TXT RR + {"ns1.google.com.", "fe01=", []string{"ns2.google.com."}, + "did not return the expected TXT record", + }, +} + +var checkResolvConfServersTests = []struct { + fixture string + expected []string + defaults []string +}{ + {"testdata/resolv.conf.1", []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, []string{"127.0.0.1:53"}}, + {"testdata/resolv.conf.nonexistant", []string{"127.0.0.1:53"}, []string{"127.0.0.1:53"}}, +} + +func TestPreCheckDNS(t *testing.T) { + // TODO: find a better TXT record to use in tests + ok, err := PreCheckDNS("google.com.", "v=spf1 include:_spf.google.com ~all") + if err != nil || !ok { + t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org: %s", err.Error()) + } +} + +func TestLookupNameserversOK(t *testing.T) { + for _, tt := range lookupNameserversTestsOK { + nss, err := lookupNameservers(tt.fqdn) + if err != nil { + t.Fatalf("#%s: got %q; want nil", tt.fqdn, err) + } + + sort.Strings(nss) + sort.Strings(tt.nss) + + if !reflect.DeepEqual(nss, tt.nss) { + t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss) + } + } +} + +func TestLookupNameserversErr(t *testing.T) { + for _, tt := range lookupNameserversTestsErr { + _, err := lookupNameservers(tt.fqdn) + if err == nil { + t.Fatalf("#%s: expected %q (error); got ", tt.fqdn, tt.error) + } + + if !strings.Contains(err.Error(), tt.error) { + t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err) + continue + } + } +} + +func TestFindZoneByFqdn(t *testing.T) { + for _, tt := range findZoneByFqdnTests { + res, err := FindZoneByFqdn(tt.fqdn, RecursiveNameservers) + if err != nil { + t.Errorf("FindZoneByFqdn failed for %s: %v", tt.fqdn, err) + } + if res != tt.zone { + t.Errorf("%s: got %s; want %s", tt.fqdn, res, tt.zone) + } + } +} + +func TestCheckAuthoritativeNss(t *testing.T) { + for _, tt := range checkAuthoritativeNssTests { + ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) + if ok != tt.ok { + t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok) + } + } +} + +func TestCheckAuthoritativeNssErr(t *testing.T) { + for _, tt := range checkAuthoritativeNssTestsErr { + _, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns) + if err == nil { + t.Fatalf("#%s: expected %q (error); got ", tt.fqdn, tt.error) + } + if !strings.Contains(err.Error(), tt.error) { + t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err) + continue + } + } +} + +func TestResolveConfServers(t *testing.T) { + for _, tt := range checkResolvConfServersTests { + result := getNameservers(tt.fixture, tt.defaults) + + sort.Strings(result) + sort.Strings(tt.expected) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("#%s: expected %q; got %q", tt.fixture, tt.expected, result) + } + } +}