Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add related endpoints to DNSRecord status #241

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/v1alpha1/dnsrecord_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ type DNSRecordStatus struct {
// endpoints are the last endpoints that were successfully published to the provider zone
Endpoints []*externaldns.Endpoint `json:"endpoints,omitempty"`

// ZoneEndpoints are all the endpoints for the DNSRecordSpec.RootHost that are present in the provider
ZoneEndpoints []*externaldns.Endpoint `json:"relatedEndpoints,omitempty"`

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

// ownerID is a unique string used to identify the owner of this record.
Expand Down
11 changes: 11 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.

47 changes: 47 additions & 0 deletions bundle/manifests/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,53 @@ spec:
reconciliation
format: date-time
type: string
relatedEndpoints:
description: ZoneEndpoints are all the endpoints for the DNSRecordSpec.RootHost
that are present in the provider
items:
description: Endpoint is a high-level way of a connection between
a service and an IP
properties:
dnsName:
description: The hostname of the DNS record
type: string
labels:
additionalProperties:
type: string
description: Labels stores labels defined for the Endpoint
type: object
providerSpecific:
description: ProviderSpecific stores provider specific config
items:
description: ProviderSpecificProperty holds the name and value
of a configuration which is specific to individual DNS providers
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
description: TTL for the record
format: int64
type: integer
recordType:
description: RecordType type of record, e.g. CNAME, A, AAAA,
SRV, TXT etc
type: string
setIdentifier:
description: Identifier to distinguish multiple records with
the same name and type (e.g. Route53 records with routing
policies other than 'simple')
type: string
targets:
description: The targets the DNS record points to
items:
type: string
type: array
type: object
type: array
validFor:
description: ValidFor indicates duration since the last reconciliation
we consider data in the record to be valid
Expand Down
47 changes: 47 additions & 0 deletions charts/dns-operator/templates/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,53 @@ spec:
reconciliation
format: date-time
type: string
relatedEndpoints:
description: ZoneEndpoints are all the endpoints for the DNSRecordSpec.RootHost
that are present in the provider
items:
description: Endpoint is a high-level way of a connection between
a service and an IP
properties:
dnsName:
description: The hostname of the DNS record
type: string
labels:
additionalProperties:
type: string
description: Labels stores labels defined for the Endpoint
type: object
providerSpecific:
description: ProviderSpecific stores provider specific config
items:
description: ProviderSpecificProperty holds the name and value
of a configuration which is specific to individual DNS providers
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
description: TTL for the record
format: int64
type: integer
recordType:
description: RecordType type of record, e.g. CNAME, A, AAAA,
SRV, TXT etc
type: string
setIdentifier:
description: Identifier to distinguish multiple records with
the same name and type (e.g. Route53 records with routing
policies other than 'simple')
type: string
targets:
description: The targets the DNS record points to
items:
type: string
type: array
type: object
type: array
validFor:
description: ValidFor indicates duration since the last reconciliation
we consider data in the record to be valid
Expand Down
47 changes: 47 additions & 0 deletions config/crd/bases/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,53 @@ spec:
reconciliation
format: date-time
type: string
relatedEndpoints:
description: ZoneEndpoints are all the endpoints for the DNSRecordSpec.RootHost
that are present in the provider
items:
description: Endpoint is a high-level way of a connection between
a service and an IP
properties:
dnsName:
description: The hostname of the DNS record
type: string
labels:
additionalProperties:
type: string
description: Labels stores labels defined for the Endpoint
type: object
providerSpecific:
description: ProviderSpecific stores provider specific config
items:
description: ProviderSpecificProperty holds the name and value
of a configuration which is specific to individual DNS providers
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
description: TTL for the record
format: int64
type: integer
recordType:
description: RecordType type of record, e.g. CNAME, A, AAAA,
SRV, TXT etc
type: string
setIdentifier:
description: Identifier to distinguish multiple records with
the same name and type (e.g. Route53 records with routing
policies other than 'simple')
type: string
targets:
description: The targets the DNS record points to
items:
type: string
type: array
type: object
type: array
validFor:
description: ValidFor indicates duration since the last reconciliation
we consider data in the record to be valid
Expand Down
48 changes: 48 additions & 0 deletions internal/controller/dnsrecord_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,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.ZoneEndpoints = mergeZoneEndpoints(
dnsRecord.Status.ZoneEndpoints,
filterEndpoints(rootDomainName, zoneEndpoints))

//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 +530,46 @@ func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alp
}
return false, nil
}

// filterEndpoints takes a list of zoneEndpoints and removes from it all endpoints
// that do 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 []*externaldnsendpoint.Endpoint) []*externaldnsendpoint.Endpoint {
// these are records that share domain but are not defined in the spec of DNSRecord
var filteredEndpoints []*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})

// go through all EPs in the zone
for _, zoneEndpoint := range zoneEndpoints {
// if zoneEndpoint matches domain filter, it must be added to related EPs
if rootDomainFilter.Match(zoneEndpoint.DNSName) {
filteredEndpoints = append(filteredEndpoints, zoneEndpoint)
}
}
return filteredEndpoints
}

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

// Use DNSName of EP as unique key. Ensures no duplicates
for _, endpoint := range currentEndpoints {
combinedMap[endpoint.DNSName] = endpoint
}
for _, endpoint := range newEndpoints {
combinedMap[endpoint.DNSName] = endpoint
}

// Convert a map into an array
for _, endpoint := range combinedMap {
combinedEndpoints = append(combinedEndpoints, endpoint)
}
return combinedEndpoints
}
112 changes: 112 additions & 0 deletions internal/controller/dnsrecord_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,118 @@ 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 A and record B
// 2. Record A and record B share domain. Endpoints should be in Spec.ZoneEndpoints 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.ZoneEndpoints).ToNot(BeNil())

// Scenario 1
// endpoints from the record2 should be present in zone EPs as record2 in subdomain of record 1 rootDomain
// record must have it's own endpoints (that are identical to the record3 endpoints)
g.Expect(dnsRecord1.Status.ZoneEndpoints).To(And(
ContainElements(dnsRecord2.Status.Endpoints),
ContainElements(dnsRecord1.Status.Endpoints)))
// record1 and 3 share root domain - all of the above should also apply to this record
g.Expect(dnsRecord3.Status.ZoneEndpoints).To(And(
ContainElements(dnsRecord2.Status.Endpoints),
ContainElements(dnsRecord3.Status.Endpoints)))

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

// Scenario 3
// endpoints from the forth record should not be present as record 4 have unique rootHosts
g.Expect(dnsRecord1.Status.ZoneEndpoints).ToNot(ContainElements(dnsRecord4.Status.Endpoints))
g.Expect(dnsRecord2.Status.ZoneEndpoints).ToNot(ContainElements(dnsRecord4.Status.Endpoints))
g.Expect(dnsRecord3.Status.ZoneEndpoints).ToNot(ContainElements(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
Loading