Skip to content

Commit

Permalink
gh-205 add related endpoints to DNSRecord status
Browse files Browse the repository at this point in the history
Signed-off-by: Maskym Vavilov <[email protected]>
  • Loading branch information
maksymvavilov committed Sep 19, 2024
1 parent 8089766 commit 967cd55
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 1 deletion.
3 changes: 3 additions & 0 deletions api/v1alpha1/dnsrecord_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ type DNSRecordStatus struct {
// endpoints are the last endpoints that were successfully published to the provider zone
Endpoints []*externaldns.Endpoint `json:"endpoints,omitempty"`

// RelatedEndpoints are endpoints for the same DNSRecordSpec.RootHost that are not defined in this DNSRecord CR
RelatedEndpoints []string `json:"relatedEndpoints,omitempty"`

HealthCheck *HealthCheckStatus `json:"healthCheck,omitempty"`

// ownerID is a unique string used to identify the owner of this record.
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bundle/manifests/dns-operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ metadata:
capabilities: Basic Install
categories: Integration & Delivery
containerImage: quay.io/kuadrant/dns-operator:latest
createdAt: "2024-09-11T15:34:57Z"
createdAt: "2024-09-19T13:19:01Z"
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
Expand Down
6 changes: 6 additions & 0 deletions bundle/manifests/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ spec:
reconciliation
format: date-time
type: string
relatedEndpoints:
description: RelatedEndpoints are endpoints for the same DNSRecordSpec.RootHost
that are not defined in this DNSRecord CR
items:
type: string
type: array
validFor:
description: ValidFor indicates duration since the last reconciliation
we consider data in the record to be valid
Expand Down
6 changes: 6 additions & 0 deletions config/crd/bases/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ spec:
reconciliation
format: date-time
type: string
relatedEndpoints:
description: RelatedEndpoints are endpoints for the same DNSRecordSpec.RootHost
that are not defined in this DNSRecord CR
items:
type: string
type: array
validFor:
description: ValidFor indicates duration since the last reconciliation
we consider data in the record to be valid
Expand Down
91 changes: 91 additions & 0 deletions internal/controller/dnsrecord_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -504,6 +505,11 @@ func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alp
return false, fmt.Errorf("adjusting statusEndpoints: %w", err)
}

// add related endpoints to the record
dnsRecord.Status.RelatedEndpoints = mergeZoneEndpoints(
dnsRecord.Status.RelatedEndpoints,
filterEndpoints(rootDomainName, zoneEndpoints, specEndpoints))

//Note: All endpoint lists should be in the same provider specific format at this point
logger.V(1).Info("applyChanges", "zoneEndpoints", zoneEndpoints,
"specEndpoints", specEndpoints, "statusEndpoints", statusEndpoints)
Expand All @@ -525,3 +531,88 @@ func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alp
}
return false, nil
}

// filterEndpoints takes a list of zoneEndpoints and removes from it all specEndpoints
// and endpoints that does not belong to the rootDomainName (some.example.com does belong to the example.com domain).
// it is not using ownerID of this record as well as domainOwners from the status for filtering
func filterEndpoints(rootDomainName string, zoneEndpoints, specEndpoints []*externaldnsendpoint.Endpoint) []string {
// these are records that share domain but are not defined in the spec of DNSRecord
var relatedEndpoints []*externaldnsendpoint.Endpoint

// setup domain filter since we can't be sure that zone records are sharing domain with DNSRecord
rootDomain, _ := strings.CutPrefix(rootDomainName, v1alpha1.WildcardPrefix)
rootDomainFilter := externaldnsendpoint.NewDomainFilter([]string{rootDomain})

// use a map as a filter to remove records that are defined by this DNSRecord
filter := make(map[string]struct{}, len(specEndpoints))

// use dns name as a filter - this is unique
for _, specEndpoint := range specEndpoints {
filter[specEndpoint.DNSName] = struct{}{}
}

// go through all EPs in the zone. It is not checking for the
for _, zoneEndpoint := range zoneEndpoints {
// if zoneEndpoint does not exist in the filter, it is not defined in the spec
// If it also shares domain it must be added to related EPs
if _, ok := filter[zoneEndpoint.DNSName]; !ok && rootDomainFilter.Match(zoneEndpoint.DNSName) {
relatedEndpoints = append(relatedEndpoints, zoneEndpoint)
}
}
return formatZoneEndpoints(relatedEndpoints)
}

// formatZoneEndpoints consumes a list endpoints and returns them as a string array.
// An example of formatted array:
// - *.example.com 60 A 172.0.0.1 (owners: foo)
// - *.example.com 60 A 172.0.0.2 (owners: foo, bar)
// - ie.example.com 360 CNAME pat.the.cat.com (owners: bar)
//
// Following pattern of: [Host TTL RecordType Target (owners: owner1, ...)].
// If a singe endpoint has multiple targets, it will be split in several array elements
func formatZoneEndpoints(endpoints []*externaldnsendpoint.Endpoint) []string {
var formattedEndpoints []string

for _, endpoint := range endpoints {
sep := " "

ownersArray := strings.Split(endpoint.Labels[externaldnsendpoint.OwnerLabelKey], externaldnsplan.OwnerLabelDeliminator)
owners := "(owners:"
for _, owner := range ownersArray {
owners += sep + owner
}
owners += ")"

for _, target := range endpoint.Targets {
formattedEndpoints = append(formattedEndpoints,
endpoint.DNSName+sep+
strconv.Itoa(int(endpoint.RecordTTL))+sep+
endpoint.RecordType+sep+
target+sep+
owners)
}
}
return formattedEndpoints
}

// mergeZoneEndpoints merges existing endpoints with new and ensures there are no duplicates
func mergeZoneEndpoints(currentEndpoints, newEndpoints []string) []string {
// map to use as filter
combinedMap := make(map[string]struct{})
// return struct
var combinedArray []string

// Add each element in an array as a key. Ensures no duplicates
for _, endpoint := range currentEndpoints {
combinedMap[endpoint] = struct{}{}
}
for _, endpoint := range newEndpoints {
combinedMap[endpoint] = struct{}{}
}

// Convert a map into an array
for key := range combinedMap {
combinedArray = append(combinedArray, key)
}
return combinedArray
}
107 changes: 107 additions & 0 deletions internal/controller/dnsrecord_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,113 @@ var _ = Describe("DNSRecordReconciler", func() {
}, TestTimeoutMedium, time.Second).Should(Succeed())
})

It("should report related endpoints correctly", func() {
// this will come in play only for the lb strategy
// in this test I simulate 3 possible scenarios using hand-made simple endpoints
// scenarios:
// 1. Record A in a subdomain of record B. Record B should have endpoints of record 1
// 2. Record A and record B share domain. Endpoints should not be in Spec.RelatedEndpoints as they will be in the Spec.Endpoints
// 3. Record A and record B does not share domain in the zone. They should not have each other's endpoints

// record for testHostname
dnsRecord1 := &v1alpha1.DNSRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-record-1",
Namespace: testNamespace,
},
Spec: v1alpha1.DNSRecordSpec{
RootHost: testHostname,
ProviderRef: v1alpha1.ProviderRef{
Name: dnsProviderSecret.Name,
},
Endpoints: getTestEndpoints(testHostname, "127.0.0.1"),
},
}

// record for sub.testHostname
dnsRecord2 := &v1alpha1.DNSRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-record-2",
Namespace: testNamespace,
},
Spec: v1alpha1.DNSRecordSpec{
RootHost: "sub." + testHostname,
ProviderRef: v1alpha1.ProviderRef{
Name: dnsProviderSecret.Name,
},
Endpoints: getTestEndpoints("sub."+testHostname, "127.0.0.2"),
},
}

// record for testHostname
dnsRecord3 := &v1alpha1.DNSRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-record-3",
Namespace: testNamespace,
},
Spec: v1alpha1.DNSRecordSpec{
RootHost: testHostname,
ProviderRef: v1alpha1.ProviderRef{
Name: dnsProviderSecret.Name,
},
Endpoints: getTestEndpoints(testHostname, "127.0.0.1"),
},
}

// record for testHostname2
testHostname2 := strings.Join([]string{"bar", testZoneDomainName}, ".")
dnsRecord4 := &v1alpha1.DNSRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-record-4",
Namespace: testNamespace,
},
Spec: v1alpha1.DNSRecordSpec{
RootHost: testHostname2,
ProviderRef: v1alpha1.ProviderRef{
Name: dnsProviderSecret.Name,
},
Endpoints: getTestEndpoints(testHostname2, "127.0.0.1"),
},
}

// create all records
Expect(k8sClient.Create(ctx, dnsRecord1)).To(Succeed())
Expect(k8sClient.Create(ctx, dnsRecord2)).To(Succeed())
Expect(k8sClient.Create(ctx, dnsRecord3)).To(Succeed())
Expect(k8sClient.Create(ctx, dnsRecord4)).To(Succeed())

// check first record to have EP from second record and not have EPs from third
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord1), dnsRecord1)).To(Succeed())
g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord2), dnsRecord2)).To(Succeed())
g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord3), dnsRecord3)).To(Succeed())
g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(dnsRecord4), dnsRecord4)).To(Succeed())

g.Expect(dnsRecord1.Status.RelatedEndpoints).ToNot(BeNil())

// Scenario 1
// endpoints from the second record should be present in related EPs as record2 in subdomain of record 1 rootDomain
g.Expect(dnsRecord1.Status.RelatedEndpoints).To(ContainElements(formatZoneEndpoints(dnsRecord2.Status.Endpoints)))
// record1 and 3 share root domain so it should also be case for record 3
g.Expect(dnsRecord3.Status.RelatedEndpoints).To(ContainElements(formatZoneEndpoints(dnsRecord2.Status.Endpoints)))

// Scenario 2
// endpoints from the third record should not be present in RelatedEndpoints as it is in the same rootDomain
g.Expect(dnsRecord1.Status.RelatedEndpoints).ToNot(ContainElements(formatZoneEndpoints(dnsRecord3.Status.Endpoints)))
// the same true to record 3 as well
g.Expect(dnsRecord3.Status.RelatedEndpoints).ToNot(ContainElements(formatZoneEndpoints(dnsRecord1.Status.Endpoints)))
// instead, check equality of status.Endpoints
g.Expect(dnsRecord1.Status.Endpoints).To(ContainElements(dnsRecord3.Status.Endpoints))

// Scenario 3
// endpoints from the forth record should not be present as record 4 have unique rootHosts
g.Expect(dnsRecord1.Status.RelatedEndpoints).ToNot(ContainElements(formatZoneEndpoints(dnsRecord4.Status.Endpoints)))
g.Expect(dnsRecord2.Status.RelatedEndpoints).ToNot(ContainElements(formatZoneEndpoints(dnsRecord4.Status.Endpoints)))
g.Expect(dnsRecord3.Status.RelatedEndpoints).ToNot(ContainElements(formatZoneEndpoints(dnsRecord4.Status.Endpoints)))

}, TestTimeoutMedium, time.Second).Should(Succeed())
})

It("should detect a conflict and the resolution of a conflict", func() {
dnsRecord = &v1alpha1.DNSRecord{
ObjectMeta: metav1.ObjectMeta{
Expand Down

0 comments on commit 967cd55

Please sign in to comment.