2222 requestIDRegexp = regexp .MustCompile (`request id: [^\s]+` )
2323 saxParseExceptionRegexp = regexp .MustCompile (`Invalid XML ; javax.xml.stream.XMLStreamException: org.xml.sax.SAXParseException; lineNumber: [^\s]+; columnNumber: [^\s]+` )
2424
25+ // ErrNoZoneForHost is returned when no DNS zone can be found that matches the requested host.
26+ // This error occurs when:
27+ // - No zones are available from the provider
28+ // - The host is a top-level domain (TLD)
29+ // - The host doesn't match any available zone domains
30+ // - The host has invalid format (single label)
31+ //
32+ // Callers should use errors.Is(err, ErrNoZoneForHost) to check for this error.
2533 ErrNoZoneForHost = fmt .Errorf ("no zone for host" )
2634
35+ // ErrApexDomainNotAllowed is returned when the requested host matches an apex domain
36+ // (zone root) and the provider does not support apex domains (denyApex=true).
37+ // Apex domains can only have A/AAAA records, not CNAME records, so some providers
38+ // restrict their usage.
39+ //
40+ // Example: For zone "example.com", the host "example.com" is the apex domain.
41+ //
42+ // Callers should use errors.Is(err, ErrApexDomainNotAllowed) to check for this error.
43+ ErrApexDomainNotAllowed = fmt .Errorf ("apex domain not allowed" )
44+
45+ // ErrMultipleZonesFound is returned when multiple zones with the same DNS name exist,
46+ // making zone selection ambiguous. This typically indicates a configuration issue
47+ // where zone filters should be used to disambiguate.
48+ //
49+ // Example: Two zones both named "example.com" with different IDs.
50+ //
51+ // Callers should use errors.Is(err, ErrMultipleZonesFound) to check for this error.
52+ ErrMultipleZonesFound = fmt .Errorf ("multiple zones found for host" )
53+
2754 DNSProviderCoreDNS DNSProviderName = "coredns"
2855 DNSProviderAWS DNSProviderName = "aws"
2956 DNSProviderAzure DNSProviderName = "azure"
@@ -116,30 +143,34 @@ func IsWildCardHost(host string) bool {
116143// findDNSZoneForHost will take a host and look for a zone that patches the immediate parent of that host and will continue to step through parents until it either finds a zone or fails. Example *.example.com will look for example.com and other.domain.example.com will step through each subdomain until it hits example.com.
117144func findDNSZoneForHost (originalHost , host string , zones []DNSZone , denyApex bool ) (* DNSZone , string , error ) {
118145 if len (zones ) == 0 {
119- return nil , "" , fmt .Errorf ("%w : %s" , ErrNoZoneForHost , host )
146+ return nil , "" , fmt .Errorf ("%w: %s" , ErrNoZoneForHost , host )
120147 }
121148 host = strings .ToLower (host )
122149 //get the TLD from this host
123150 tld , _ := publicsuffix .PublicSuffix (host )
124151
125152 //The host is a TLD, so we now know `originalHost` can't possibly have a valid `DNSZone` available.
126153 if host == tld {
127- return nil , "" , fmt .Errorf ("no valid zone found for host : %v" , originalHost )
154+ return nil , "" , fmt .Errorf ("%w : %s" , ErrNoZoneForHost , originalHost )
128155 }
129156
130157 // We do not currently support creating records for Apex domains, and a DNSZone represents an Apex domain we cannot setup dns for the host
131- if id , is := IsApexDomain (originalHost , zones ); is && denyApex {
132- return nil , "" , fmt .Errorf ("host %s is an apex domain with zone id %s. Cannot configure DNS for apex domain as apex domains only support A records " , originalHost , id )
158+ if _ , is := IsApexDomain (originalHost , zones ); is && denyApex {
159+ return nil , "" , fmt .Errorf ("%w: %s " , ErrApexDomainNotAllowed , originalHost )
133160 }
134161
135162 hostParts := strings .SplitN (host , "." , 2 )
136163 if len (hostParts ) < 2 {
137- return nil , "" , fmt .Errorf ("no valid zone found for host : %s" , originalHost )
164+ return nil , "" , fmt .Errorf ("%w : %s" , ErrNoZoneForHost , originalHost )
138165 }
139166 parentDomain := hostParts [1 ]
140- // We do not currently support creating records for Apex domains, and a DNSZone represents an Apex domain, as such
141- // we should never be trying to find a zone that matches the `originalHost` exactly. Instead, we just continue
142- // on to the next possible valid host to try i.e. the parent domain.
167+
168+ // When apex domains are denied and we're on the first iteration (host == originalHost),
169+ // skip checking if the host itself is a zone and immediately recurse to the parent domain.
170+ // This prevents matching the host as an apex domain, which would be denied by the check above
171+ // after matching. Instead, we proactively skip to the parent to find a valid parent zone.
172+ // Example: For "example.com" with denyApex=true, skip directly to "com" instead of
173+ // potentially matching "example.com" as a zone and then failing the apex check.
143174 if host == originalHost && denyApex {
144175 return findDNSZoneForHost (originalHost , parentDomain , zones , denyApex )
145176 }
@@ -149,9 +180,14 @@ func findDNSZoneForHost(originalHost, host string, zones []DNSZone, denyApex boo
149180 })
150181 if len (matches ) > 0 {
151182 if len (matches ) > 1 {
152- return nil , "" , fmt .Errorf ("multiple zones found for host: %s" , originalHost )
183+ return nil , "" , fmt .Errorf ("%w: %s" , ErrMultipleZonesFound , originalHost )
184+ }
185+ // Calculate subdomain by removing the zone suffix
186+ // For apex domains (where host == zone), subdomain should be empty
187+ subdomain := ""
188+ if strings .ToLower (originalHost ) != strings .ToLower (matches [0 ].DNSName ) {
189+ subdomain = strings .Replace (strings .ToLower (originalHost ), "." + strings .ToLower (matches [0 ].DNSName ), "" , 1 )
153190 }
154- subdomain := strings .Replace (strings .ToLower (originalHost ), "." + strings .ToLower (matches [0 ].DNSName ), "" , 1 )
155191 return & matches [0 ], subdomain , nil
156192 }
157193
0 commit comments