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 26, 2024
1 parent 2e24974 commit e5e39ae
Show file tree
Hide file tree
Showing 8 changed files with 316 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"`

// 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.

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-26T11:30:25Z"
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
47 changes: 47 additions & 0 deletions bundle/manifests/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,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 @@ -486,6 +486,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 @@ -474,6 +474,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

0 comments on commit e5e39ae

Please sign in to comment.