Skip to content

Commit 5fb78c2

Browse files
authored
fix: read delegation from public NS (#12)
1 parent a35ab4a commit 5fb78c2

2 files changed

Lines changed: 60 additions & 3 deletions

File tree

internal/drivers/delegation.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package drivers
55
import (
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.
3031
type 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.
3537
func 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+
4553
func (d *DelegationDriver) Type() string { return "infra.dns_delegation" }
4654

4755
func (d *DelegationDriver) SensitiveKeys() []string { return nil }
@@ -105,11 +113,15 @@ func parseDelegationSpec(spec interfaces.ResourceSpec) (dnsDelegationSpec, error
105113
func 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).
166199
func (d *DelegationDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) {

internal/drivers/delegation_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import (
1313
type fakeDelegationClient struct {
1414
getResult *hover.DomainDelegation
1515
getErr error
16+
getCalls int
1617
setErr error
1718
lastSetNS []string
1819
}
1920

2021
func (f *fakeDelegationClient) GetDomainDelegation(_ context.Context, _ string) (*hover.DomainDelegation, error) {
22+
f.getCalls++
2123
return f.getResult, f.getErr
2224
}
2325

@@ -140,6 +142,28 @@ func TestDelegationDriver_Read_HappyPath(t *testing.T) {
140142
}
141143
}
142144

145+
func TestDelegationDriver_Read_UsesPublicNSBeforeHoverLogin(t *testing.T) {
146+
fc := &fakeDelegationClient{getErr: errors.New("hover login should not be needed")}
147+
d := NewDelegationDriverWithClientAndResolver(fc, func(_ context.Context, domain string) ([]string, error) {
148+
if domain != "example.com" {
149+
t.Fatalf("resolver domain = %q, want example.com", domain)
150+
}
151+
return []string{"ns1.digitalocean.com.", "ns2.digitalocean.com.", "ns3.digitalocean.com."}, nil
152+
})
153+
154+
out, err := d.Read(context.Background(), interfaces.ResourceRef{Name: "example.com", ProviderID: "example.com"})
155+
if err != nil {
156+
t.Fatalf("Read: %v", err)
157+
}
158+
if fc.getCalls != 0 {
159+
t.Fatalf("GetDomainDelegation called %d times; public NS should avoid Hover login", fc.getCalls)
160+
}
161+
ns, _ := out.Outputs["nameservers"].([]any)
162+
if len(ns) != 3 || ns[0] != "ns1.digitalocean.com" {
163+
t.Fatalf("nameservers = %#v", ns)
164+
}
165+
}
166+
143167
func TestDelegationDriver_Read_PropagatesError(t *testing.T) {
144168
fc := &fakeDelegationClient{getErr: errors.New("API down")}
145169
d := NewDelegationDriverWithClient(fc)

0 commit comments

Comments
 (0)