@@ -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
6769var log = logf .Logger .WithName (controllerName )
@@ -81,6 +83,17 @@ 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. Add a second watch
87+ // exclusively for deletes so that in addition to the normal on-delete cleanup, queue a reconcile for the next
88+ // record with the same domain name (if one exists) so that it can be published.
89+ if err := c .Watch (source .Kind [client.Object ](operatorCache , & iov1.DNSRecord {}, handler .EnqueueRequestsFromMapFunc (reconciler .mapOnRecordDelete ), predicate.Funcs {
90+ CreateFunc : func (e event.CreateEvent ) bool { return false },
91+ DeleteFunc : func (e event.DeleteEvent ) bool { return true },
92+ UpdateFunc : func (e event.UpdateEvent ) bool { return false },
93+ GenericFunc : func (e event.GenericEvent ) bool { return false },
94+ })); err != nil {
95+ return nil , err
96+ }
8497 if err := c .Watch (source .Kind [client.Object ](operatorCache , & configv1.DNS {}, handler .EnqueueRequestsFromMapFunc (reconciler .ToDNSRecords ))); err != nil {
8598 return nil , err
8699 }
@@ -102,6 +115,12 @@ func New(mgr manager.Manager, config Config) (runtimecontroller.Controller, erro
102115 })); err != nil {
103116 return nil , err
104117 }
118+ if err := operatorCache .IndexField (context .Background (), & iov1.DNSRecord {}, dnsRecordIndexFieldName , func (o client.Object ) []string {
119+ dnsRecord := o .(* iov1.DNSRecord )
120+ return []string {dnsRecord .Spec .DNSName }
121+ }); err != nil {
122+ return nil , err
123+ }
105124 return c , nil
106125}
107126
@@ -337,17 +356,33 @@ func (r *reconciler) publishRecordToZones(zones []configv1.DNSZone, record *iov1
337356
338357 var err error
339358 var condition iov1.DNSZoneCondition
359+ var isDomainPublished bool
340360 if dnsPolicy == iov1 .UnmanagedDNS {
341- log .Info ("DNS record not published" , "record" , record .Spec )
361+ log .Info ("DNS record not published: DNS management policy is unmanaged " , "record" , record .Spec )
342362 condition = iov1.DNSZoneCondition {
343- Message : "DNS record is currently not being managed by the operator" ,
344- Reason : "UnmanagedDNS" ,
345- Status : string (operatorv1 .ConditionUnknown ),
346- Type : iov1 .DNSRecordPublishedConditionType ,
347- LastTransitionTime : metav1 .Now (),
363+ Message : "DNS record is currently not being managed by the operator" ,
364+ Reason : "UnmanagedDNS" ,
365+ Status : string (operatorv1 .ConditionUnknown ),
366+ Type : iov1 .DNSRecordPublishedConditionType ,
348367 }
349368 } else if isRecordPublished {
350369 condition , err = r .replacePublishedRecord (zones [i ], record )
370+ } else if isDomainPublished , err = domainIsAlreadyPublishedToZone (context .Background (), r .cache , record , & zones [i ]); err != nil {
371+ log .Error (err , "failed to validate DNS record" , "record" , record .Spec )
372+ condition = iov1.DNSZoneCondition {
373+ Message : "Pre-publish validation failed" ,
374+ Reason : "InternalError" ,
375+ Status : string (operatorv1 .ConditionFalse ),
376+ Type : iov1 .DNSRecordPublishedConditionType ,
377+ }
378+ } else if isDomainPublished {
379+ log .Info ("DNS record not published: domain name already used by another DNS record" , "record" , record .Spec )
380+ condition = iov1.DNSZoneCondition {
381+ Message : "Domain name is already in use" ,
382+ Reason : "DomainAlreadyInUse" ,
383+ Status : string (operatorv1 .ConditionFalse ),
384+ Type : iov1 .DNSRecordPublishedConditionType ,
385+ }
351386 } else {
352387 condition , err = r .publishRecord (zones [i ], record )
353388 }
@@ -386,6 +421,34 @@ func recordIsAlreadyPublishedToZone(record *iov1.DNSRecord, zoneToPublish *confi
386421 return false
387422}
388423
424+ // domainIsAlreadyPublishedToZone returns true if the domain name in the provided DNSRecord is already published by
425+ // another existing dnsRecord.
426+ func domainIsAlreadyPublishedToZone (ctx context.Context , cache cache.Cache , record * iov1.DNSRecord , zone * configv1.DNSZone ) (bool , error ) {
427+ records := iov1.DNSRecordList {}
428+ if err := cache .List (ctx , & records , client.MatchingFields {dnsRecordIndexFieldName : record .Spec .DNSName }); err != nil {
429+ return false , err
430+ }
431+
432+ if len (records .Items ) == 0 {
433+ return false , nil
434+ }
435+
436+ for _ , existingRecord := range records .Items {
437+ // we only care if the domain name is published by a different record, so ignore the matching record if it
438+ // already exists.
439+ if record .UID == existingRecord .UID {
440+ continue
441+ }
442+ if record .Spec .DNSName != existingRecord .Spec .DNSName {
443+ continue
444+ }
445+ if recordIsAlreadyPublishedToZone (& existingRecord , zone ) {
446+ return true , nil
447+ }
448+ }
449+ return false , nil
450+ }
451+
389452func (r * reconciler ) delete (record * iov1.DNSRecord ) error {
390453 var errs []error
391454 for i := range record .Status .Zones {
@@ -576,6 +639,44 @@ func (r *reconciler) ToDNSRecords(ctx context.Context, o client.Object) []reconc
576639 return requests
577640}
578641
642+ // mapOnRecordDelete finds another DNSRecord (if any) that uses the same domain name as the deleted record, and queues a
643+ // reconcile for that record, so that it can be published. If multiple matching records are found, a request for the
644+ // oldest record is returned.
645+ func (r * reconciler ) mapOnRecordDelete (ctx context.Context , o client.Object ) []reconcile.Request {
646+ deletedRecord , ok := o .(* iov1.DNSRecord )
647+ if ! ok {
648+ log .Error (nil , "Got unexpected type of object" , "expected" , "DNSRecord" , "actual" , fmt .Sprintf ("%T" , o ))
649+ return []reconcile.Request {}
650+ }
651+ otherRecords := iov1.DNSRecordList {}
652+ if err := r .cache .List (ctx , & otherRecords , client.MatchingFields {dnsRecordIndexFieldName : deletedRecord .Spec .DNSName }); err != nil {
653+ log .Error (err , "failed to list DNS records" )
654+ return []reconcile.Request {}
655+ }
656+ if len (otherRecords .Items ) == 0 {
657+ // Nothing to do.
658+ return []reconcile.Request {}
659+ }
660+ oldestExistingRecord := iov1.DNSRecord {}
661+ for _ , existingRecord := range otherRecords .Items {
662+ // Exclude records that are marked for deletion.
663+ if ! existingRecord .DeletionTimestamp .IsZero () {
664+ continue
665+ }
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+
579680// createDNSProvider creates a DNS manager compatible with the given cluster
580681// configuration.
581682func (r * reconciler ) createDNSProvider (dnsConfig * configv1.DNS , platformStatus * configv1.PlatformStatus , infraStatus * configv1.InfrastructureStatus , creds * corev1.Secret , AzureWorkloadIdentityEnabled bool ) (dns.Provider , error ) {
0 commit comments