Skip to content

Commit d38f7bf

Browse files
committed
Don't publish duplicate DNS records
Before attempting to publish a domain to a zone, check if that domain is already being published to the same zone.
1 parent 86f19bd commit d38f7bf

File tree

2 files changed

+131
-4
lines changed

2 files changed

+131
-4
lines changed

pkg/operator/controller/dns/controller.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ const (
6262
kubeCloudConfigName = "kube-cloud-config"
6363
// cloudCABundleKey is the key in the kube cloud config ConfigMap where the custom CA bundle is located
6464
cloudCABundleKey = "ca-bundle.pem"
65+
// dnsRecordIndexFieldName is the key for the DNSRecord index, used to identify conflicting domain names.
66+
dnsRecordIndexFieldName = "Spec.DNSName"
6567
)
6668

6769
var log = logf.Logger.WithName(controllerName)
@@ -81,6 +83,16 @@ func New(mgr manager.Manager, config Config) (runtimecontroller.Controller, erro
8183
if err := c.Watch(source.Kind[client.Object](operatorCache, &iov1.DNSRecord{}, &handler.EnqueueRequestForObject{}, predicate.GenerationChangedPredicate{})); err != nil {
8284
return nil, err
8385
}
86+
// When a DNS record is deleted, there may be a conflicting record that should be published. Watch exclusively for
87+
// deletes, and queue a reconcile request for the appropriate conflicting record, if applicable.
88+
if err := c.Watch(source.Kind[client.Object](operatorCache, &iov1.DNSRecord{}, handler.EnqueueRequestsFromMapFunc(reconciler.MapOnRecordDelete), predicate.Funcs{
89+
CreateFunc: func(e event.CreateEvent) bool { return false },
90+
DeleteFunc: func(e event.DeleteEvent) bool { return true },
91+
UpdateFunc: func(e event.UpdateEvent) bool { return false },
92+
GenericFunc: func(e event.GenericEvent) bool { return false },
93+
})); err != nil {
94+
return nil, err
95+
}
8496
if err := c.Watch(source.Kind[client.Object](operatorCache, &configv1.DNS{}, handler.EnqueueRequestsFromMapFunc(reconciler.ToDNSRecords))); err != nil {
8597
return nil, err
8698
}
@@ -102,6 +114,12 @@ func New(mgr manager.Manager, config Config) (runtimecontroller.Controller, erro
102114
})); err != nil {
103115
return nil, err
104116
}
117+
if err := operatorCache.IndexField(context.Background(), &iov1.DNSRecord{}, dnsRecordIndexFieldName, func(o client.Object) []string {
118+
dnsRecord := o.(*iov1.DNSRecord)
119+
return []string{dnsRecord.Spec.DNSName}
120+
}); err != nil {
121+
return nil, err
122+
}
105123
return c, nil
106124
}
107125

@@ -341,8 +359,9 @@ func (r *reconciler) publishRecordToZones(zones []configv1.DNSZone, record *iov1
341359

342360
var err error
343361
var condition iov1.DNSZoneCondition
362+
var isDomainPublished bool
344363
if dnsPolicy == iov1.UnmanagedDNS {
345-
log.Info("DNS record not published", "record", record.Spec)
364+
log.Info("DNS record not published: DNS management policy is unmanaged", "record", record.Spec)
346365
condition = iov1.DNSZoneCondition{
347366
Message: "DNS record is currently not being managed by the operator",
348367
Reason: "UnmanagedDNS",
@@ -352,6 +371,24 @@ func (r *reconciler) publishRecordToZones(zones []configv1.DNSZone, record *iov1
352371
}
353372
} else if isRecordPublished {
354373
condition, err = r.replacePublishedRecord(zones[i], record)
374+
} else if isDomainPublished, err = domainIsAlreadyPublishedToZone(context.Background(), r.cache, record, &zones[i]); err != nil {
375+
log.Error(err, "failed to validate DNS record", "record", record.Spec)
376+
condition = iov1.DNSZoneCondition{
377+
Message: "Pre-publish validation failed",
378+
Reason: "InternalError",
379+
Status: string(operatorv1.ConditionFalse),
380+
Type: iov1.DNSRecordPublishedConditionType,
381+
LastTransitionTime: metav1.Now(),
382+
}
383+
} else if isDomainPublished {
384+
log.Info("DNS record not published: domain name already used by another DNS record", "record", record.Spec)
385+
condition = iov1.DNSZoneCondition{
386+
Message: "Domain name is already in use",
387+
Reason: "DomainAlreadyInUse",
388+
Status: string(operatorv1.ConditionFalse),
389+
Type: iov1.DNSRecordPublishedConditionType,
390+
LastTransitionTime: metav1.Now(),
391+
}
355392
} else {
356393
condition, err = r.publishRecord(zones[i], record)
357394
}
@@ -390,6 +427,35 @@ func recordIsAlreadyPublishedToZone(record *iov1.DNSRecord, zoneToPublish *confi
390427
return false
391428
}
392429

430+
// domainIsAlreadyPublishedToZone returns true if the domain name in the provided DNSRecord is already published by
431+
// another existing dnsRecord.
432+
func domainIsAlreadyPublishedToZone(ctx context.Context, cache cache.Cache, record *iov1.DNSRecord, zone *configv1.DNSZone) (bool, error) {
433+
records := iov1.DNSRecordList{}
434+
if err := cache.List(ctx, &records, client.MatchingFields{dnsRecordIndexFieldName: record.Spec.DNSName}); err != nil {
435+
return false, err
436+
}
437+
438+
if len(records.Items) == 0 {
439+
return false, nil
440+
}
441+
442+
for _, existingRecord := range records.Items {
443+
// we only care if the domain name is published by a different record, so ignore the matching record if it
444+
// already exists.
445+
// TODO: There's got to be a better way to match the same object
446+
if (record.Name == existingRecord.Name) && (record.Namespace == existingRecord.Namespace) {
447+
continue
448+
}
449+
if record.Spec.DNSName != existingRecord.Spec.DNSName {
450+
continue
451+
}
452+
if recordIsAlreadyPublishedToZone(&existingRecord, zone) {
453+
return true, nil
454+
}
455+
}
456+
return false, nil
457+
}
458+
393459
func (r *reconciler) delete(record *iov1.DNSRecord) error {
394460
var errs []error
395461
for i := range record.Status.Zones {
@@ -580,6 +646,37 @@ func (r *reconciler) ToDNSRecords(ctx context.Context, o client.Object) []reconc
580646
return requests
581647
}
582648

649+
func (r *reconciler) MapOnRecordDelete(ctx context.Context, o client.Object) []reconcile.Request {
650+
deletedRecord, ok := o.(*iov1.DNSRecord)
651+
if !ok {
652+
log.Info("failed to read DNS record")
653+
return []reconcile.Request{}
654+
}
655+
otherRecords := iov1.DNSRecordList{}
656+
if err := r.cache.List(ctx, &otherRecords, client.MatchingFields{dnsRecordIndexFieldName: deletedRecord.Spec.DNSName}); err != nil {
657+
log.Error(err, "failed to list DNS records")
658+
return []reconcile.Request{}
659+
}
660+
if len(otherRecords.Items) == 0 {
661+
// Nothing to do.
662+
return []reconcile.Request{}
663+
}
664+
oldestExistingRecord := iov1.DNSRecord{}
665+
for _, existingRecord := range otherRecords.Items {
666+
if oldestExistingRecord.CreationTimestamp.IsZero() || existingRecord.CreationTimestamp.Before(&oldestExistingRecord.CreationTimestamp) {
667+
oldestExistingRecord = existingRecord
668+
}
669+
}
670+
return []reconcile.Request{
671+
reconcile.Request{
672+
NamespacedName: types.NamespacedName{
673+
Name: oldestExistingRecord.Name,
674+
Namespace: oldestExistingRecord.Namespace,
675+
},
676+
},
677+
}
678+
}
679+
583680
// createDNSProvider creates a DNS manager compatible with the given cluster
584681
// configuration.
585682
func (r *reconciler) createDNSProvider(dnsConfig *configv1.DNS, platformStatus *configv1.PlatformStatus, infraStatus *configv1.InfrastructureStatus, creds *corev1.Secret, AzureWorkloadIdentityEnabled bool) (dns.Provider, error) {

pkg/operator/controller/dns/controller_test.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import (
99
operatorv1 "github.com/openshift/api/operator/v1"
1010
iov1 "github.com/openshift/api/operatoringress/v1"
1111
"github.com/openshift/cluster-ingress-operator/pkg/dns"
12+
testutil "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/test/util"
1213
corev1 "k8s.io/api/core/v1"
1314
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1415
"k8s.io/apimachinery/pkg/runtime"
16+
"sigs.k8s.io/controller-runtime/pkg/cache/informertest"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
1518
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1619
)
1720

@@ -117,6 +120,7 @@ func Test_publishRecordToZones(t *testing.T) {
117120
r := &reconciler{
118121
// TODO To write a fake provider that can return errors and add more test cases.
119122
dnsProvider: &dns.FakeProvider{},
123+
cache: buildFakeCache(t),
120124
}
121125

122126
_, actual := r.publishRecordToZones(test.zones, record)
@@ -128,9 +132,9 @@ func Test_publishRecordToZones(t *testing.T) {
128132
}
129133
}
130134

131-
// TestPublishRecordToZonesMergesStatus verifies that publishRecordToZones
135+
// Test_publishRecordToZonesMergesStatus verifies that publishRecordToZones
132136
// correctly merges status updates.
133-
func TestPublishRecordToZonesMergesStatus(t *testing.T) {
137+
func Test_publishRecordToZonesMergesStatus(t *testing.T) {
134138
testCases := []struct {
135139
description string
136140
oldZoneStatuses []iov1.DNSZoneStatus
@@ -215,7 +219,10 @@ func TestPublishRecordToZonesMergesStatus(t *testing.T) {
215219
},
216220
Status: iov1.DNSRecordStatus{Zones: tc.oldZoneStatuses},
217221
}
218-
r := &reconciler{dnsProvider: &dns.FakeProvider{}}
222+
r := &reconciler{
223+
dnsProvider: &dns.FakeProvider{},
224+
cache: buildFakeCache(t),
225+
}
219226
zone := []configv1.DNSZone{{ID: "zone2"}}
220227
oldStatuses := record.Status.DeepCopy().Zones
221228
_, newStatuses := r.publishRecordToZones(zone, record)
@@ -840,3 +847,26 @@ func Test_customCABundle(t *testing.T) {
840847
})
841848
}
842849
}
850+
851+
func buildFakeCache(t *testing.T) *testutil.FakeCache {
852+
t.Helper()
853+
scheme := runtime.NewScheme()
854+
iov1.AddToScheme(scheme)
855+
fakeClient := fake.NewClientBuilder().
856+
WithScheme(scheme).
857+
WithIndex(&iov1.DNSRecord{}, dnsRecordIndexFieldName, func(o client.Object) []string {
858+
dnsRecord := o.(*iov1.DNSRecord)
859+
return []string{dnsRecord.Spec.DNSName}
860+
}).
861+
Build()
862+
cl := &testutil.FakeClientRecorder{
863+
Client: fakeClient,
864+
T: t,
865+
Added: []client.Object{},
866+
Updated: []client.Object{},
867+
Deleted: []client.Object{},
868+
}
869+
informer := informertest.FakeInformers{Scheme: scheme}
870+
cache := testutil.FakeCache{Informers: &informer, Reader: cl}
871+
return &cache
872+
}

0 commit comments

Comments
 (0)