diff --git a/api/v1alpha1/dnsrecord_types.go b/api/v1alpha1/dnsrecord_types.go index b41d3795..c51c65d5 100644 --- a/api/v1alpha1/dnsrecord_types.go +++ b/api/v1alpha1/dnsrecord_types.go @@ -142,6 +142,9 @@ type DNSRecordStatus struct { // ownerID is a unique string used to identify the owner of this record. OwnerID string `json:"ownerID,omitempty"` + + // DomainOwners is a list of all the owners working against the root domain of this record + DomainOwners []string `json:"domainOwners,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0f306314..ce59068a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -149,6 +149,11 @@ func (in *DNSRecordStatus) DeepCopyInto(out *DNSRecordStatus) { *out = new(HealthCheckStatus) (*in).DeepCopyInto(*out) } + if in.DomainOwners != nil { + in, out := &in.DomainOwners, &out.DomainOwners + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordStatus. diff --git a/bundle/manifests/dns-operator.clusterserviceversion.yaml b/bundle/manifests/dns-operator.clusterserviceversion.yaml index bc87ab97..e35ab174 100644 --- a/bundle/manifests/dns-operator.clusterserviceversion.yaml +++ b/bundle/manifests/dns-operator.clusterserviceversion.yaml @@ -56,7 +56,7 @@ metadata: capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/dns-operator:latest - createdAt: "2024-06-28T14:24:36Z" + createdAt: "2024-08-12T10:16:02Z" description: A Kubernetes Operator to manage the lifecycle of DNS resources operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 diff --git a/bundle/manifests/kuadrant.io_dnsrecords.yaml b/bundle/manifests/kuadrant.io_dnsrecords.yaml index 4b10579c..cf56529e 100644 --- a/bundle/manifests/kuadrant.io_dnsrecords.yaml +++ b/bundle/manifests/kuadrant.io_dnsrecords.yaml @@ -244,6 +244,12 @@ spec: - type type: object type: array + domainOwners: + description: DomainOwners is a list of all the owners working against + the root domain of this record + items: + type: string + type: array endpoints: description: |- endpoints are the last endpoints that were successfully published by the provider diff --git a/config/crd/bases/kuadrant.io_dnsrecords.yaml b/config/crd/bases/kuadrant.io_dnsrecords.yaml index 1c2fd38f..75ee6e9d 100644 --- a/config/crd/bases/kuadrant.io_dnsrecords.yaml +++ b/config/crd/bases/kuadrant.io_dnsrecords.yaml @@ -244,6 +244,12 @@ spec: - type type: object type: array + domainOwners: + description: DomainOwners is a list of all the owners working against + the root domain of this record + items: + type: string + type: array endpoints: description: |- endpoints are the last endpoints that were successfully published by the provider diff --git a/internal/controller/dnsrecord_controller.go b/internal/controller/dnsrecord_controller.go index 67be985f..9f5b62de 100644 --- a/internal/controller/dnsrecord_controller.go +++ b/internal/controller/dnsrecord_controller.go @@ -410,7 +410,7 @@ func (r *DNSRecordReconciler) getDNSProvider(ctx context.Context, managedZone *v func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone, isDelete bool) (bool, error) { logger := log.FromContext(ctx) zoneDomainName, _ := strings.CutPrefix(managedZone.Spec.DomainName, v1alpha1.WildcardPrefix) - rootDomainName, _ := strings.CutPrefix(dnsRecord.Spec.RootHost, v1alpha1.WildcardPrefix) + rootDomainName := dnsRecord.Spec.RootHost zoneDomainFilter := externaldnsendpoint.NewDomainFilter([]string{zoneDomainName}) managedDNSRecordTypes := []string{externaldnsendpoint.RecordTypeA, externaldnsendpoint.RecordTypeAAAA, externaldnsendpoint.RecordTypeCNAME} var excludeDNSRecordTypes []string @@ -465,10 +465,10 @@ func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alp ) plan = plan.Calculate() - if err = plan.Error(); err != nil { return false, err } + dnsRecord.Status.DomainOwners = plan.Owners if plan.Changes.HasChanges() { logger.Info("Applying changes") err = registry.ApplyChanges(ctx, plan.Changes) diff --git a/internal/controller/dnsrecord_controller_test.go b/internal/controller/dnsrecord_controller_test.go index 88c54a62..2c35b5e0 100644 --- a/internal/controller/dnsrecord_controller_test.go +++ b/internal/controller/dnsrecord_controller_test.go @@ -37,12 +37,14 @@ import ( ) var _ = Describe("DNSRecordReconciler", func() { - var dnsRecord *v1alpha1.DNSRecord - var dnsRecord2 *v1alpha1.DNSRecord - var dnsProviderSecret *v1.Secret - var managedZone *v1alpha1.ManagedZone - var brokenZone *v1alpha1.ManagedZone - var testNamespace string + var ( + dnsRecord *v1alpha1.DNSRecord + dnsRecord2 *v1alpha1.DNSRecord + dnsProviderSecret *v1.Secret + managedZone *v1alpha1.ManagedZone + brokenZone *v1alpha1.ManagedZone + testNamespace string + ) BeforeEach(func() { CreateNamespace(&testNamespace) @@ -169,6 +171,7 @@ var _ = Describe("DNSRecordReconciler", func() { "ObservedGeneration": Equal(dnsRecord.Generation), })), ) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) dnsRecord2 = &v1alpha1.DNSRecord{ @@ -258,6 +261,7 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) @@ -307,8 +311,6 @@ var _ = Describe("DNSRecordReconciler", func() { Eventually(func(g Gomega) { err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(dnsRecord.Spec.OwnerID).To(BeEmpty()) - g.Expect(dnsRecord.Status.OwnerID).To(Equal(dnsRecord.GetUIDHash())) g.Expect(dnsRecord.Status.Conditions).To( ContainElement(MatchFields(IgnoreExtras, Fields{ "Type": Equal(string(v1alpha1.ConditionTypeReady)), @@ -320,6 +322,7 @@ var _ = Describe("DNSRecordReconciler", func() { ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) g.Expect(dnsRecord.Status.WriteCounter).To(BeZero()) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) @@ -341,6 +344,7 @@ var _ = Describe("DNSRecordReconciler", func() { ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) g.Expect(dnsRecord.Status.WriteCounter).To(BeZero()) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) //Does not allow ownerID to change once set @@ -356,6 +360,7 @@ var _ = Describe("DNSRecordReconciler", func() { err = k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Status.OwnerID).To(Equal(dnsRecord.GetUIDHash())) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) @@ -391,6 +396,7 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf("owner1")) }, TestTimeoutMedium, time.Second).Should(Succeed()) //Does not allow ownerID to change once set @@ -410,6 +416,7 @@ var _ = Describe("DNSRecordReconciler", func() { err = k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord), dnsRecord) g.Expect(err).NotTo(HaveOccurred()) g.Expect(dnsRecord.Status.OwnerID).To(Equal("owner1")) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf("owner1")) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) @@ -457,6 +464,7 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Finalizers).To(ContainElement(DNSRecordFinalizer)) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, TestTimeoutMedium, time.Second).Should(Succeed()) By("creating dnsrecord " + dnsRecord2.Name + " with endpoint dnsName: `foo.example.com` and target: `127.0.0.2`") @@ -476,6 +484,7 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord.Status.WriteCounter).To(BeNumerically(">", int64(1))) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash(), dnsRecord2.GetUIDHash())) err = k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord2), dnsRecord2) g.Expect(err).NotTo(HaveOccurred()) @@ -489,6 +498,7 @@ var _ = Describe("DNSRecordReconciler", func() { })), ) g.Expect(dnsRecord2.Status.WriteCounter).To(BeNumerically(">", int64(1))) + g.Expect(dnsRecord2.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash(), dnsRecord2.GetUIDHash())) }, TestTimeoutLong, time.Second).Should(Succeed()) By("fixing conflict with dnsrecord " + dnsRecord2.Name + " with endpoint dnsName: `foo.example.com` and target: `127.0.0.1`") @@ -511,6 +521,7 @@ var _ = Describe("DNSRecordReconciler", func() { "Message": Equal("Provider ensured the dns record"), })), ) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash(), dnsRecord2.GetUIDHash())) err = k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord2), dnsRecord2) g.Expect(err).NotTo(HaveOccurred()) @@ -522,6 +533,7 @@ var _ = Describe("DNSRecordReconciler", func() { "Message": Equal("Provider ensured the dns record"), })), ) + g.Expect(dnsRecord2.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash(), dnsRecord2.GetUIDHash())) }, TestTimeoutLong, time.Second).Should(Succeed()) }) diff --git a/internal/external-dns/plan/plan.go b/internal/external-dns/plan/plan.go index ecbc36f8..0d9c55eb 100644 --- a/internal/external-dns/plan/plan.go +++ b/internal/external-dns/plan/plan.go @@ -27,6 +27,8 @@ import ( "sigs.k8s.io/external-dns/endpoint" externaldnsplan "sigs.k8s.io/external-dns/plan" + + "github.com/kuadrant/dns-operator/api/v1alpha1" ) var ( @@ -65,6 +67,9 @@ type Plan struct { Errors []error // RootHost the host dns name being managed by the set of records in the plan. RootHost *string + // Owners list of owners ids contributing to this record set. + // Populated after calling Calculate() + Owners []string logger logr.Logger } @@ -208,7 +213,8 @@ func (p *Plan) Calculate() *Plan { } if p.RootHost != nil { - rootDomainFilter = endpoint.NewDomainFilter([]string{*p.RootHost}) + rootDomainName, _ := strings.CutPrefix(*p.RootHost, v1alpha1.WildcardPrefix) + rootDomainFilter = endpoint.NewDomainFilter([]string{rootDomainName}) p.DomainFilter = append(p.DomainFilter, &rootDomainFilter) } @@ -367,6 +373,13 @@ func (p *Plan) Calculate() *Plan { ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME}, } + if p.RootHost != nil { + p.logger.V(1).Info("plan", "record dnsOwners", managedChanges.dnsNameOwners, "record rootHost", *p.RootHost, "record normalized", normalizeDNSName(*p.RootHost)) + + plan.Owners = managedChanges.dnsNameOwners[normalizeDNSName(*p.RootHost)] + + } + return plan } diff --git a/test/e2e/multi_record_test.go b/test/e2e/multi_record_test.go index 1fa967ae..482222bb 100644 --- a/test/e2e/multi_record_test.go +++ b/test/e2e/multi_record_test.go @@ -119,6 +119,8 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { } By(fmt.Sprintf("checking all dns records become ready within %s", recordsReadyMaxDuration)) + var allOwners = []string{} + var allTargetIps = []string{} Eventually(func(g Gomega, ctx context.Context) { for _, tr := range testRecords { err := tr.cluster.k8sClient.Get(ctx, client.ObjectKeyFromObject(tr.record), tr.record) @@ -129,7 +131,12 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { "Status": Equal(metav1.ConditionTrue), })), ) + allOwners = append(allOwners, tr.record.GetUIDHash()) + allTargetIps = append(allTargetIps, tr.config.testTargetIP) + g.Expect(tr.record.Status.DomainOwners).NotTo(BeEmpty()) + g.Expect(tr.record.Status.DomainOwners).To(ContainElement(tr.record.GetUIDHash())) } + g.Expect(len(allOwners)).To(Equal(len(testRecords))) }, recordsReadyMaxDuration, 5*time.Second, ctx).Should(Succeed()) By("checking provider zone records are created as expected") @@ -140,11 +147,9 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { Expect(err).NotTo(HaveOccurred()) Expect(zoneEndpoints).To(HaveLen(2)) - var allOwners = []string{} - var allTargetIps = []string{} - for i := range testRecords { - allOwners = append(allOwners, testRecords[i].record.Status.OwnerID) - allTargetIps = append(allTargetIps, testRecords[i].config.testTargetIP) + By("checking each record has all owners present") + for _, tr := range testRecords { + Expect(tr.record.Status.DomainOwners).To(ConsistOf(allOwners)) } By("checking all target ips are present") @@ -376,6 +381,7 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { } By(fmt.Sprintf("checking all dns records become ready within %s", recordsReadyMaxDuration)) + var allOwners = []string{} Eventually(func(g Gomega, ctx context.Context) { for _, tr := range testRecords { err := tr.cluster.k8sClient.Get(ctx, client.ObjectKeyFromObject(tr.record), tr.record) @@ -386,7 +392,10 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { "Status": Equal(metav1.ConditionTrue), })), ) + allOwners = append(allOwners, tr.record.GetUIDHash()) + g.Expect(tr.record.Status.DomainOwners).To(Not(BeEmpty())) } + g.Expect(len(allOwners)).To(Equal(len(testRecords))) }, recordsReadyMaxDuration, 5*time.Second, ctx).Should(Succeed()) By("checking provider zone records are created as expected") @@ -405,7 +414,7 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { } var totalEndpointsChecked = 0 - var allOwners = []string{} + var allOwnerMatcher = []types.GomegaMatcher{ ContainSubstring("heritage=external-dns,external-dns/owner="), } @@ -413,10 +422,10 @@ var _ = Describe("Multi Record Test", Labels{"multi_record"}, func() { var geoKlbHostname = map[string]string{} var geoOwnerMatcher = map[string][]types.GomegaMatcher{} for i := range testRecords { - ownerID := testRecords[i].record.Status.OwnerID - allOwners = append(allOwners, ownerID) + underTest := testRecords[i] + ownerID := underTest.record.Status.OwnerID allOwnerMatcher = append(allOwnerMatcher, ContainSubstring(ownerID)) - + Expect(underTest.record.Status.DomainOwners).To(ConsistOf(allOwners)) geoCode := testRecords[i].config.testGeoCode geoOwners[geoCode] = append(geoOwners[geoCode], ownerID) geoKlbHostname[geoCode] = testRecords[i].config.hostnames.geoKlb diff --git a/test/e2e/single_record_test.go b/test/e2e/single_record_test.go index 9caf73ef..543be7bb 100644 --- a/test/e2e/single_record_test.go +++ b/test/e2e/single_record_test.go @@ -108,8 +108,8 @@ var _ = Describe("Single Record Test", func() { "Status": Equal(metav1.ConditionTrue), })), ) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, time.Minute, 10*time.Second, ctx).Should(Succeed()) - By("checking " + dnsRecord.Name + " ownerID is set correctly") Expect(dnsRecord.Spec.OwnerID).To(BeEmpty()) Expect(dnsRecord.Status.OwnerID).ToNot(BeEmpty()) @@ -194,6 +194,7 @@ var _ = Describe("Single Record Test", func() { "Status": Equal(metav1.ConditionTrue), })), ) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, time.Minute, 10*time.Second, ctx).Should(Succeed()) By("checking " + dnsRecord.Name + " ownerID is set correctly") @@ -337,6 +338,7 @@ var _ = Describe("Single Record Test", func() { "Status": Equal(metav1.ConditionTrue), })), ) + g.Expect(dnsRecord.Status.DomainOwners).To(ConsistOf(dnsRecord.GetUIDHash())) }, time.Minute, 10*time.Second, ctx).Should(Succeed()) By("checking " + dnsRecord.Name + " ownerID is set correctly")