Skip to content

Commit 2a52eed

Browse files
committed
Allow for override of NS1 zone FQDN
1 parent 6cf328f commit 2a52eed

File tree

6 files changed

+249
-55
lines changed

6 files changed

+249
-55
lines changed

controller/execute.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,13 @@ func buildProvider(
301301
case "ns1":
302302
p, err = ns1.NewNS1Provider(
303303
ns1.NS1Config{
304-
DomainFilter: domainFilter,
305-
ZoneIDFilter: zoneIDFilter,
306-
NS1Endpoint: cfg.NS1Endpoint,
307-
NS1IgnoreSSL: cfg.NS1IgnoreSSL,
308-
DryRun: cfg.DryRun,
309-
MinTTLSeconds: cfg.NS1MinTTLSeconds,
304+
DomainFilter: domainFilter,
305+
ZoneIDFilter: zoneIDFilter,
306+
NS1Endpoint: cfg.NS1Endpoint,
307+
NS1IgnoreSSL: cfg.NS1IgnoreSSL,
308+
DryRun: cfg.DryRun,
309+
MinTTLSeconds: cfg.NS1MinTTLSeconds,
310+
ZoneHandleOverrides: cfg.NS1ZoneHandleMap,
310311
},
311312
)
312313
case "transip":

docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
| `--ns1-endpoint=""` | When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/) |
119119
| `--[no-]ns1-ignoressl` | When using the NS1 provider, specify whether to verify the SSL certificate (default: false) |
120120
| `--ns1-min-ttl=0` | Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this. |
121+
| `--ns1-zone-handle-map=fqdn=handle` | Map FQDN (or suffix) to an NS1 zone handle/ID. Repeatable; k=v form. . |
121122
| `--digitalocean-api-page-size=50` | Configure the page size used when querying the DigitalOcean API. |
122123
| `--godaddy-api-key=""` | When using the GoDaddy provider, specify the API Key (required when --provider=godaddy) |
123124
| `--godaddy-api-secret=""` | When using the GoDaddy provider, specify the API secret (required when --provider=godaddy) |

pkg/apis/externaldns/types.go

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -190,33 +190,36 @@ type Config struct {
190190
NS1Endpoint string
191191
NS1IgnoreSSL bool
192192
NS1MinTTLSeconds int
193-
TransIPAccountName string
194-
TransIPPrivateKeyFile string
195-
DigitalOceanAPIPageSize int
196-
ManagedDNSRecordTypes []string
197-
ExcludeDNSRecordTypes []string
198-
GoDaddyAPIKey string `secure:"yes"`
199-
GoDaddySecretKey string `secure:"yes"`
200-
GoDaddyTTL int64
201-
GoDaddyOTE bool
202-
OCPRouterName string
203-
PiholeServer string
204-
PiholePassword string `secure:"yes"`
205-
PiholeTLSInsecureSkipVerify bool
206-
PiholeApiVersion string
207-
PluralCluster string
208-
PluralProvider string
209-
WebhookProviderURL string
210-
WebhookProviderReadTimeout time.Duration
211-
WebhookProviderWriteTimeout time.Duration
212-
WebhookServer bool
213-
TraefikEnableLegacy bool
214-
TraefikDisableNew bool
215-
NAT64Networks []string
216-
ExcludeUnschedulable bool
217-
EmitEvents []string
218-
ForceDefaultTargets bool
219-
sourceWrappers map[string]bool // map of source wrappers, e.g. "targetfilter", "nat64"
193+
// Accepts repeatable --ns1-zone-handle-map flags or a comma-separated
194+
// EXTERNAL_DNS_NS1_ZONE_HANDLE_MAP env var like: "example.com=corp,dev.example.com=dev"
195+
NS1ZoneHandleMap map[string]string
196+
TransIPAccountName string
197+
TransIPPrivateKeyFile string
198+
DigitalOceanAPIPageSize int
199+
ManagedDNSRecordTypes []string
200+
ExcludeDNSRecordTypes []string
201+
GoDaddyAPIKey string `secure:"yes"`
202+
GoDaddySecretKey string `secure:"yes"`
203+
GoDaddyTTL int64
204+
GoDaddyOTE bool
205+
OCPRouterName string
206+
PiholeServer string
207+
PiholePassword string `secure:"yes"`
208+
PiholeTLSInsecureSkipVerify bool
209+
PiholeApiVersion string
210+
PluralCluster string
211+
PluralProvider string
212+
WebhookProviderURL string
213+
WebhookProviderReadTimeout time.Duration
214+
WebhookProviderWriteTimeout time.Duration
215+
WebhookServer bool
216+
TraefikEnableLegacy bool
217+
TraefikDisableNew bool
218+
NAT64Networks []string
219+
ExcludeUnschedulable bool
220+
EmitEvents []string
221+
ForceDefaultTargets bool
222+
sourceWrappers map[string]bool // map of source wrappers, e.g. "targetfilter", "nat64"
220223
}
221224

222225
var defaultConfig = &Config{
@@ -312,6 +315,7 @@ var defaultConfig = &Config{
312315
NAT64Networks: []string{},
313316
NS1Endpoint: "",
314317
NS1IgnoreSSL: false,
318+
NS1ZoneHandleMap: map[string]string{},
315319
OCIConfigFile: "/etc/kubernetes/oci.yaml",
316320
OCIZoneCacheDuration: 0 * time.Second,
317321
OCIZoneScope: "GLOBAL",
@@ -447,7 +451,8 @@ var allowedSources = []string{
447451
// NewConfig returns new Config object
448452
func NewConfig() *Config {
449453
return &Config{
450-
AWSSDCreateTag: map[string]string{},
454+
AWSSDCreateTag: map[string]string{},
455+
NS1ZoneHandleMap: map[string]string{},
451456
}
452457
}
453458

pkg/apis/externaldns/types_test.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ var (
134134
WebhookProviderReadTimeout: 5 * time.Second,
135135
WebhookProviderWriteTimeout: 10 * time.Second,
136136
ExcludeUnschedulable: true,
137+
NS1ZoneHandleMap: map[string]string{},
137138
}
138139

139140
overriddenConfig = &Config{
@@ -235,18 +236,22 @@ var (
235236
CRDSourceKind: "Endpoint",
236237
NS1Endpoint: "https://api.example.com/v1",
237238
NS1IgnoreSSL: true,
238-
TransIPAccountName: "transip",
239-
TransIPPrivateKeyFile: "/path/to/transip.key",
240-
DigitalOceanAPIPageSize: 100,
241-
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
242-
RFC2136BatchChangeSize: 100,
243-
RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"},
244-
RFC2136LoadBalancingStrategy: "round-robin",
245-
PiholeApiVersion: "6",
246-
WebhookProviderURL: "http://localhost:8888",
247-
WebhookProviderReadTimeout: 5 * time.Second,
248-
WebhookProviderWriteTimeout: 10 * time.Second,
249-
ExcludeUnschedulable: false,
239+
NS1ZoneHandleMap: map[string]string{
240+
"example.com": "corp-prod",
241+
"dev.example.com": "dev-view",
242+
},
243+
TransIPAccountName: "transip",
244+
TransIPPrivateKeyFile: "/path/to/transip.key",
245+
DigitalOceanAPIPageSize: 100,
246+
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
247+
RFC2136BatchChangeSize: 100,
248+
RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"},
249+
RFC2136LoadBalancingStrategy: "round-robin",
250+
PiholeApiVersion: "6",
251+
WebhookProviderURL: "http://localhost:8888",
252+
WebhookProviderReadTimeout: 5 * time.Second,
253+
WebhookProviderWriteTimeout: 10 * time.Second,
254+
ExcludeUnschedulable: false,
250255
}
251256
)
252257

@@ -380,6 +385,8 @@ func TestParseFlags(t *testing.T) {
380385
"--crd-source-kind=Endpoint",
381386
"--ns1-endpoint=https://api.example.com/v1",
382387
"--ns1-ignoressl",
388+
"--ns1-zone-handle-map=example.com=corp-prod",
389+
"--ns1-zone-handle-map=dev.example.com=dev-view",
383390
"--transip-account=transip",
384391
"--transip-keyfile=/path/to/transip.key",
385392
"--digitalocean-api-page-size=100",
@@ -501,6 +508,7 @@ func TestParseFlags(t *testing.T) {
501508
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
502509
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
503510
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
511+
"EXTERNAL_DNS_NS1_ZONE_HANDLE_MAP": "example.com=corp-prod\ndev.example.com=dev-view",
504512
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
505513
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
506514
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",

provider/ns1/ns1.go

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ type NS1Config struct {
9191
NS1IgnoreSSL bool
9292
DryRun bool
9393
MinTTLSeconds int
94+
// Optional: map a zone FQDN (or suffix) to the NS1 zone handle/ID to use
95+
// when looking up that zone. Keys/values are case-insensitive;
96+
// trailing dots on keys are ignored.
97+
// e.g. map[string]string{"example.com":"corp-prod-zone","dev.example.com":"dev-view-handle"}
98+
ZoneHandleOverrides map[string]string
9499
}
95100

96101
// NS1Provider is the NS1 provider
@@ -101,6 +106,8 @@ type NS1Provider struct {
101106
zoneIDFilter provider.ZoneIDFilter
102107
dryRun bool
103108
minTTLSeconds int
109+
// normalized overrides: fqdn (no trailing dot, lowercased) -> handle/ID (lowercased)
110+
zoneHandleOverrides map[string]string
104111
}
105112

106113
// NewNS1Provider creates a new NS1 Provider
@@ -137,10 +144,11 @@ func newNS1ProviderWithHTTPClient(config NS1Config, client *http.Client) (*NS1Pr
137144
apiClient := api.NewClient(client, clientArgs...)
138145

139146
return &NS1Provider{
140-
client: NS1DomainService{apiClient},
141-
domainFilter: config.DomainFilter,
142-
zoneIDFilter: config.ZoneIDFilter,
143-
minTTLSeconds: config.MinTTLSeconds,
147+
client: NS1DomainService{apiClient},
148+
domainFilter: config.DomainFilter,
149+
zoneIDFilter: config.ZoneIDFilter,
150+
minTTLSeconds: config.MinTTLSeconds,
151+
zoneHandleOverrides: normalizeOverrides(config.ZoneHandleOverrides),
144152
}, nil
145153
}
146154

@@ -155,9 +163,20 @@ func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
155163

156164
for _, zone := range zones {
157165
// TODO handle Header Codes
158-
zoneData, _, err := p.client.GetZone(zone.String())
166+
// Prefer lookup via handle/ID if an override exists; fall back to FQDN.
167+
lookup := p.zoneLookupKeyFor(zone.Zone)
168+
zoneData, _, err := p.client.GetZone(lookup)
159169
if err != nil {
160-
return nil, err
170+
if lookup != strings.TrimSuffix(zone.Zone, ".") {
171+
// fallback to FQDN lookup if override missed
172+
zoneData, _, err = p.client.GetZone(zone.Zone)
173+
if err != nil {
174+
return nil, err
175+
}
176+
} else {
177+
return nil, err
178+
}
179+
161180
}
162181

163182
for _, record := range zoneData.Records {
@@ -208,7 +227,7 @@ func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error {
208227
}
209228

210229
// separate into per-zone change sets to be passed to the API.
211-
changesByZone := ns1ChangesByZone(zones, changes)
230+
changesByZone := p.ns1ChangesByZone(zones, changes)
212231
for zoneName, changes := range changesByZone {
213232
for _, change := range changes {
214233
record := p.ns1BuildRecord(zoneName, change)
@@ -302,15 +321,34 @@ func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change {
302321
return changes
303322
}
304323

324+
// normalizeOverrides lowercases keys/values and strips any trailing dot on keys.
325+
func normalizeOverrides(m map[string]string) map[string]string {
326+
if len(m) == 0 {
327+
return map[string]string{}
328+
}
329+
out := make(map[string]string, len(m))
330+
for k, v := range m {
331+
kk := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(k)), ".")
332+
vv := strings.ToLower(strings.TrimSpace(v))
333+
if kk != "" && vv != "" {
334+
out[kk] = vv
335+
}
336+
}
337+
return out
338+
}
339+
305340
// ns1ChangesByZone separates a multi-zone change into a single change per zone.
306-
func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
341+
// The map key becomes the "write key": handle/ID if overridden, else FQDN.
342+
func (p *NS1Provider) ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
307343
changes := make(map[string][]*ns1Change)
308344
zoneNameIDMapper := provider.ZoneIDName{}
345+
309346
for _, z := range zones {
310347
zoneNameIDMapper.Add(z.Zone, z.Zone)
311348
changes[z.Zone] = []*ns1Change{}
312349
}
313350

351+
// group changes by zone FQDN
314352
for _, c := range changeSets {
315353
zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName)
316354
if zone == "" {
@@ -320,5 +358,34 @@ func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*
320358
changes[zone] = append(changes[zone], c)
321359
}
322360

361+
// replace zone FQDN with zone handle if FQDN is overridden
362+
for k, v := range changes {
363+
writeKey := p.zoneLookupKeyFor(k)
364+
365+
if writeKey != k {
366+
changes[writeKey] = v
367+
delete(changes, k)
368+
}
369+
}
370+
323371
return changes
324372
}
373+
374+
// zoneLookupKeyFor returns the preferred key to pass to GetZone:
375+
// if an override exists for fqdn (or a more specific suffix), return its mapped handle/ID;
376+
// otherwise return the normalized FQDN.
377+
func (p *NS1Provider) zoneLookupKeyFor(fqdn string) string {
378+
name := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(fqdn)), ".")
379+
bestKey := ""
380+
for k := range p.zoneHandleOverrides {
381+
if name == k || strings.HasSuffix(name, "."+k) {
382+
if len(k) > len(bestKey) {
383+
bestKey = k // longest (most specific) match wins
384+
}
385+
}
386+
}
387+
if bestKey != "" {
388+
return p.zoneHandleOverrides[bestKey]
389+
}
390+
return name
391+
}

0 commit comments

Comments
 (0)