@@ -5,6 +5,7 @@ package drivers
55import (
66 "context"
77 "fmt"
8+ "net"
89 "sort"
910 "strings"
1011
@@ -28,12 +29,13 @@ type HoverDelegationClient interface {
2829// [ns1.hover.com, ns2.hover.com]; restore-from-stash is deferred to
2930// v0.3.0 because interfaces.ResourceRef has no state channel.
3031type DelegationDriver struct {
31- client HoverDelegationClient
32+ client HoverDelegationClient
33+ nsResolver func (context.Context , string ) ([]string , error )
3234}
3335
3436// NewDelegationDriver returns a DelegationDriver bound to a real *hover.Client.
3537func NewDelegationDriver (c * hover.Client ) * DelegationDriver {
36- return & DelegationDriver {client : c }
38+ return & DelegationDriver {client : c , nsResolver : lookupPublicNameservers }
3739}
3840
3941// NewDelegationDriverWithClient returns a DelegationDriver bound to an
@@ -42,6 +44,12 @@ func NewDelegationDriverWithClient(c HoverDelegationClient) *DelegationDriver {
4244 return & DelegationDriver {client : c }
4345}
4446
47+ // NewDelegationDriverWithClientAndResolver returns a DelegationDriver bound
48+ // to an injected client and public-NS resolver; used by tests.
49+ func NewDelegationDriverWithClientAndResolver (c HoverDelegationClient , resolver func (context.Context , string ) ([]string , error )) * DelegationDriver {
50+ return & DelegationDriver {client : c , nsResolver : resolver }
51+ }
52+
4553func (d * DelegationDriver ) Type () string { return "infra.dns_delegation" }
4654
4755func (d * DelegationDriver ) SensitiveKeys () []string { return nil }
@@ -105,11 +113,15 @@ func parseDelegationSpec(spec interfaces.ResourceSpec) (dnsDelegationSpec, error
105113func nameserversToAny (ns []string ) []any {
106114 out := make ([]any , len (ns ))
107115 for i , s := range ns {
108- out [i ] = s
116+ out [i ] = normalizeNameserverHost ( s )
109117 }
110118 return out
111119}
112120
121+ func normalizeNameserverHost (host string ) string {
122+ return strings .TrimSuffix (strings .TrimSpace (host ), "." )
123+ }
124+
113125// delegationOutput builds the ResourceOutput for a Create/Update result.
114126// v0.2.0 ships without previous_nameservers (no state channel in
115127// interfaces.ResourceRef; v0.3.0 follow-up).
@@ -154,13 +166,34 @@ func (d *DelegationDriver) Read(ctx context.Context, ref interfaces.ResourceRef)
154166 if domain == "" {
155167 domain = ref .Name
156168 }
169+ if d .nsResolver != nil {
170+ if ns , err := d .nsResolver (ctx , domain ); err == nil && len (ns ) > 0 {
171+ return delegationOutput (ref .Name , domain , ns ), nil
172+ }
173+ }
157174 dom , err := d .client .GetDomainDelegation (ctx , domain )
158175 if err != nil {
159176 return nil , fmt .Errorf ("dns_delegation read %q: %w" , ref .Name , err )
160177 }
161178 return delegationOutput (ref .Name , domain , dom .Nameservers ), nil
162179}
163180
181+ func lookupPublicNameservers (ctx context.Context , domain string ) ([]string , error ) {
182+ resolver := net .DefaultResolver
183+ records , err := resolver .LookupNS (ctx , domain )
184+ if err != nil {
185+ return nil , err
186+ }
187+ out := make ([]string , 0 , len (records ))
188+ for _ , record := range records {
189+ host := normalizeNameserverHost (record .Host )
190+ if host != "" {
191+ out = append (out , host )
192+ }
193+ }
194+ return out , nil
195+ }
196+
164197// Update replaces the registrar nameservers. Rejects in-place domain
165198// renames (those must route through Diff → NeedsReplace → Delete-then-Create).
166199func (d * DelegationDriver ) Update (ctx context.Context , ref interfaces.ResourceRef , spec interfaces.ResourceSpec ) (* interfaces.ResourceOutput , error ) {
0 commit comments