From c0b4a8219c4e688a95b551c0043ae48babc7eaec Mon Sep 17 00:00:00 2001 From: Michael Nairn Date: Wed, 15 May 2024 17:37:27 +0100 Subject: [PATCH] tests: Add provider errors e2e specs Adds a provider errors set of specs to the e2e test suite. The purpose of this is to test each supported provider returns a reasonable response and updates the record status appropriately in cases where a user can misconfigure a DNSRecord and cause a provider error. These mainly focus on what can be set from the DNSPolicy (geo and weight) and the intention isn't to exhaustively test all error scenarios. Added as an e2e test since this can, and will be, different on every provider. Tests also ensure that the records are not falling into a loop of constantly being updated when a provider error is updating the status. --- test/e2e/healthcheck_test.go | 2 +- test/e2e/provider_errors_test.go | 255 +++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 test/e2e/provider_errors_test.go diff --git a/test/e2e/healthcheck_test.go b/test/e2e/healthcheck_test.go index 3ce64e40..ec6af243 100644 --- a/test/e2e/healthcheck_test.go +++ b/test/e2e/healthcheck_test.go @@ -20,7 +20,7 @@ import ( ) // Test Cases covering multiple creation and deletion of health checks -var _ = Describe("Health Check Test", Serial, func() { +var _ = Describe("Health Check Test", Serial, Labels{"health_checks"}, func() { // tests can be run in parallel in the same cluster var testID string // testDomainName generated domain for this test e.g. t-e2e-12345.e2e.hcpapps.net diff --git a/test/e2e/provider_errors_test.go b/test/e2e/provider_errors_test.go new file mode 100644 index 00000000..f70bda25 --- /dev/null +++ b/test/e2e/provider_errors_test.go @@ -0,0 +1,255 @@ +//go:build e2e + +package e2e + +import ( + "context" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + externaldnsendpoint "sigs.k8s.io/external-dns/endpoint" + + "github.com/kuadrant/dns-operator/api/v1alpha1" +) + +// Test Cases covering known provider errors that can be expected with misconfigured DNSRecord resource. +var _ = Describe("DNSRecord Provider Errors", Labels{"provider_errors"}, func() { + // testID is a randomly generated identifier for the test + // it is used to name resources and/or namespaces so different + // tests can be run in parallel in the same cluster + var testID string + // testDomainName generated domain for this test e.g. t-e2e-12345.e2e.hcpapps.net + var testDomainName string + // testHostname generated hostname for this test e.g. t-gw-mgc-12345.t-e2e-12345.e2e.hcpapps.net + var testHostname string + + var dnsRecord *v1alpha1.DNSRecord + + BeforeEach(func(ctx SpecContext) { + testID = "t-errors-" + GenerateName() + testDomainName = strings.Join([]string{testSuiteID, testZoneDomainName}, ".") + testHostname = strings.Join([]string{testID, testDomainName}, ".") + }) + + AfterEach(func(ctx SpecContext) { + if dnsRecord != nil { + err := k8sClient.Delete(ctx, dnsRecord, + client.PropagationPolicy(metav1.DeletePropagationForeground)) + Expect(client.IgnoreNotFound(err)).ToNot(HaveOccurred()) + } + }) + + It("correctly handles invalid geo", func(ctx SpecContext) { + var validGeoCode string + var expectedProviderErr string + if testDNSProvider == "gcp" { + //GCP + expectedProviderErr = "location': 'notageocode', invalid" + validGeoCode = "us-east1" + } else { + //AWS + expectedProviderErr = "Value 'notageocode' with length = '11' is not facet-valid with respect to length '2' for type 'ContinentCode'" + validGeoCode = "US" + } + + invalidEndpoint := &externaldnsendpoint.Endpoint{ + DNSName: testHostname, + Targets: []string{ + "foo.example.com", + }, + RecordType: "CNAME", + RecordTTL: 300, + SetIdentifier: "foo.example.com", + ProviderSpecific: externaldnsendpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "notageocode", //invalid in all providers + }, + }, + } + testEndpoints := []*externaldnsendpoint.Endpoint{ + invalidEndpoint, + } + + dnsRecord = testBuildDNSRecord(testID, testNamespace, testManagedZoneName, "test-owner", testHostname) + dnsRecord.Spec.Endpoints = testEndpoints + + By("creating dnsrecord " + dnsRecord.Name + " with invalid geo endpoint") + err := k8sClient.Create(ctx, dnsRecord) + Expect(err).ToNot(HaveOccurred()) + + By("checking " + dnsRecord.Name + " is not ready and has the expected provider error in the status") + Eventually(func(g Gomega, ctx context.Context) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + "Status": Equal(metav1.ConditionFalse), + "Message": And(ContainSubstring("The DNS provider failed to ensure the record"), ContainSubstring(expectedProviderErr)), + })), + ) + }, 10*time.Second, time.Second, ctx).Should(Succeed()) + + By("checking dnsrecord " + dnsRecord.Name + " is not being updated repeatedly") + tmpRecord := &v1alpha1.DNSRecord{} + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), tmpRecord)).To(Succeed()) + Consistently(func(g Gomega, ctx context.Context) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord)).To(Succeed()) + g.Expect(dnsRecord.ResourceVersion).To(Equal(tmpRecord.ResourceVersion)) + }, 10*time.Second, time.Second, ctx).Should(Succeed()) + + By("updating dnsrecord " + dnsRecord.Name + " with valid geo endpoint") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + invalidEndpoint.ProviderSpecific = externaldnsendpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: validGeoCode, //valid for provider under test + }, + } + dnsRecord.Spec.Endpoints = testEndpoints + err = k8sClient.Update(ctx, dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + }, TestTimeoutMedium, time.Second).Should(Succeed()) + + By("checking dnsrecord " + dnsRecord.Name + " no longer has provider error") + Eventually(func(g Gomega, ctx context.Context) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + // Since this is an e2e test we have no idea how long it might take to become ready, so we can only really + // check that the message is one of the expected ones if it was accepted by the provider + "Message": Or(Equal("Provider ensured the dns record"), Equal("Awaiting validation")), + })), + ) + }, TestTimeoutMedium, time.Second, ctx).Should(Succeed()) + + }) + + It("correctly handles invalid weight", func(ctx SpecContext) { + var expectedProviderErr string + if testDNSProvider == "gcp" { + //GCP + expectedProviderErr = "weight': '-1.0' Reason: backendError, Message: Invalid Value" + } else { + //AWS + expectedProviderErr = "weight' failed to satisfy constraint: Member must have value greater than or equal to 0" + } + validWeight := "100" + + invalidEndpoint := &externaldnsendpoint.Endpoint{ + DNSName: testHostname, + Targets: []string{ + "foo.example.com", + }, + RecordType: "CNAME", + RecordTTL: 300, + SetIdentifier: "foo.example.com", + ProviderSpecific: externaldnsendpoint.ProviderSpecific{ + { + Name: "weight", + Value: "-1", + }, + }, + } + testEndpoints := []*externaldnsendpoint.Endpoint{ + invalidEndpoint, + } + + dnsRecord = testBuildDNSRecord(testID, testNamespace, testManagedZoneName, "test-owner", testHostname) + dnsRecord.Spec.Endpoints = testEndpoints + + By("creating dnsrecord " + dnsRecord.Name + " with invalid weight endpoint") + err := k8sClient.Create(ctx, dnsRecord) + Expect(err).ToNot(HaveOccurred()) + + By("checking " + dnsRecord.Name + " is not ready and has the expected provider error in the status") + Eventually(func(g Gomega, ctx context.Context) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + "Status": Equal(metav1.ConditionFalse), + "Message": And(ContainSubstring("The DNS provider failed to ensure the record"), ContainSubstring(expectedProviderErr)), + })), + ) + }, 10*time.Second, time.Second, ctx).Should(Succeed()) + + By("checking dnsrecord " + dnsRecord.Name + " is not being updated repeatedly") + tmpRecord := &v1alpha1.DNSRecord{} + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), tmpRecord)).To(Succeed()) + Consistently(func(g Gomega, ctx context.Context) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord)).To(Succeed()) + g.Expect(dnsRecord.ResourceVersion).To(Equal(tmpRecord.ResourceVersion)) + }, 10*time.Second, time.Second, ctx).Should(Succeed()) + + By("updating dnsrecord " + dnsRecord.Name + " with valid weight endpoint") + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + invalidEndpoint.ProviderSpecific = externaldnsendpoint.ProviderSpecific{ + { + Name: "weight", + Value: validWeight, //valid for provider under test + }, + } + dnsRecord.Spec.Endpoints = testEndpoints + err = k8sClient.Update(ctx, dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + }, TestTimeoutMedium, time.Second).Should(Succeed()) + + By("checking dnsrecord " + dnsRecord.Name + " no longer has provider error") + Eventually(func(g Gomega, ctx context.Context) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(dnsRecord.Status.Conditions).To( + ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(string(v1alpha1.ConditionTypeReady)), + // Since this is an e2e test we have no idea how long it might take to become ready, so we can only really + // check that the message is one of the expected ones if it was accepted by the provider + "Message": Or(Equal("Provider ensured the dns record"), Equal("Awaiting validation")), + })), + ) + }, TestTimeoutMedium, time.Second, ctx).Should(Succeed()) + + }) + +}) + +// testBuildDNSRecord creates a valid dnsrecord with a single valid endpoint +func testBuildDNSRecord(name, ns, mzName, ownerID, rootHost string) *v1alpha1.DNSRecord { + return &v1alpha1.DNSRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: v1alpha1.DNSRecordSpec{ + OwnerID: ownerID, + RootHost: rootHost, + ManagedZoneRef: &v1alpha1.ManagedZoneReference{ + Name: mzName, + }, + Endpoints: []*externaldnsendpoint.Endpoint{ + { + DNSName: rootHost, + Targets: []string{ + "127.0.0.1", + }, + RecordType: "A", + RecordTTL: 60, + }, + }, + }, + } +}