Skip to content

Commit 1e54852

Browse files
committed
Allow for override of NS1 zone FQDN
1 parent b31afb4 commit 1e54852

File tree

6 files changed

+249
-54
lines changed

6 files changed

+249
-54
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
@@ -119,6 +119,7 @@
119119
| `--ns1-endpoint=""` | When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/) |
120120
| `--[no-]ns1-ignoressl` | When using the NS1 provider, specify whether to verify the SSL certificate (default: false) |
121121
| `--ns1-min-ttl=NS1-MIN-TTL` | Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this. |
122+
| `--ns1-zone-handle-map=fqdn=handle` | Map FQDN (or suffix) to an NS1 zone handle/ID. Repeatable; k=v form. . |
122123
| `--digitalocean-api-page-size=50` | Configure the page size used when querying the DigitalOcean API. |
123124
| `--godaddy-api-key=""` | When using the GoDaddy provider, specify the API Key (required when --provider=godaddy) |
124125
| `--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 & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -188,32 +188,35 @@ type Config struct {
188188
NS1Endpoint string
189189
NS1IgnoreSSL bool
190190
NS1MinTTLSeconds int
191-
TransIPAccountName string
192-
TransIPPrivateKeyFile string
193-
DigitalOceanAPIPageSize int
194-
ManagedDNSRecordTypes []string
195-
ExcludeDNSRecordTypes []string
196-
GoDaddyAPIKey string `secure:"yes"`
197-
GoDaddySecretKey string `secure:"yes"`
198-
GoDaddyTTL int64
199-
GoDaddyOTE bool
200-
OCPRouterName string
201-
PiholeServer string
202-
PiholePassword string `secure:"yes"`
203-
PiholeTLSInsecureSkipVerify bool
204-
PiholeApiVersion string
205-
PluralCluster string
206-
PluralProvider string
207-
WebhookProviderURL string
208-
WebhookProviderReadTimeout time.Duration
209-
WebhookProviderWriteTimeout time.Duration
210-
WebhookServer bool
211-
TraefikEnableLegacy bool
212-
TraefikDisableNew bool
213-
NAT64Networks []string
214-
ExcludeUnschedulable bool
215-
ForceDefaultTargets bool
216-
sourceWrappers map[string]bool // map of source wrappers, e.g. "targetfilter", "nat64"
191+
// Accepts repeatable --ns1-zone-handle-map flags or a comma-separated
192+
// EXTERNAL_DNS_NS1_ZONE_HANDLE_MAP env var like: "example.com=corp,dev.example.com=dev"
193+
NS1ZoneHandleMap map[string]string
194+
TransIPAccountName string
195+
TransIPPrivateKeyFile string
196+
DigitalOceanAPIPageSize int
197+
ManagedDNSRecordTypes []string
198+
ExcludeDNSRecordTypes []string
199+
GoDaddyAPIKey string `secure:"yes"`
200+
GoDaddySecretKey string `secure:"yes"`
201+
GoDaddyTTL int64
202+
GoDaddyOTE bool
203+
OCPRouterName string
204+
PiholeServer string
205+
PiholePassword string `secure:"yes"`
206+
PiholeTLSInsecureSkipVerify bool
207+
PiholeApiVersion string
208+
PluralCluster string
209+
PluralProvider string
210+
WebhookProviderURL string
211+
WebhookProviderReadTimeout time.Duration
212+
WebhookProviderWriteTimeout time.Duration
213+
WebhookServer bool
214+
TraefikEnableLegacy bool
215+
TraefikDisableNew bool
216+
NAT64Networks []string
217+
ExcludeUnschedulable bool
218+
ForceDefaultTargets bool
219+
sourceWrappers map[string]bool // map of source wrappers, e.g. "targetfilter", "nat64"
217220
}
218221

219222
var defaultConfig = &Config{
@@ -308,6 +311,7 @@ var defaultConfig = &Config{
308311
NAT64Networks: []string{},
309312
NS1Endpoint: "",
310313
NS1IgnoreSSL: false,
314+
NS1ZoneHandleMap: map[string]string{},
311315
OCIConfigFile: "/etc/kubernetes/oci.yaml",
312316
OCIZoneCacheDuration: 0 * time.Second,
313317
OCIZoneScope: "GLOBAL",
@@ -383,7 +387,8 @@ var defaultConfig = &Config{
383387
// NewConfig returns new Config object
384388
func NewConfig() *Config {
385389
return &Config{
386-
AWSSDCreateTag: map[string]string{},
390+
AWSSDCreateTag: map[string]string{},
391+
NS1ZoneHandleMap: map[string]string{},
387392
}
388393
}
389394

@@ -577,6 +582,7 @@ func App(cfg *Config) *kingpin.Application {
577582
app.Flag("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)").Default(defaultConfig.NS1Endpoint).StringVar(&cfg.NS1Endpoint)
578583
app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL)
579584
app.Flag("ns1-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.NS1MinTTLSeconds)
585+
app.Flag("ns1-zone-handle-map", "Map FQDN (or suffix) to an NS1 zone handle/ID. Repeatable; k=v form. .").PlaceHolder("fqdn=handle").StringMapVar(&cfg.NS1ZoneHandleMap)
580586
app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize)
581587
// GoDaddy flags
582588
app.Flag("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)").Default(defaultConfig.GoDaddyAPIKey).StringVar(&cfg.GoDaddyAPIKey)

pkg/apis/externaldns/types_test.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ var (
129129
WebhookProviderReadTimeout: 5 * time.Second,
130130
WebhookProviderWriteTimeout: 10 * time.Second,
131131
ExcludeUnschedulable: true,
132+
NS1ZoneHandleMap: map[string]string{},
132133
}
133134

134135
overriddenConfig = &Config{
@@ -230,18 +231,22 @@ var (
230231
CRDSourceKind: "Endpoint",
231232
NS1Endpoint: "https://api.example.com/v1",
232233
NS1IgnoreSSL: true,
233-
TransIPAccountName: "transip",
234-
TransIPPrivateKeyFile: "/path/to/transip.key",
235-
DigitalOceanAPIPageSize: 100,
236-
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
237-
RFC2136BatchChangeSize: 100,
238-
RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"},
239-
RFC2136LoadBalancingStrategy: "round-robin",
240-
PiholeApiVersion: "6",
241-
WebhookProviderURL: "http://localhost:8888",
242-
WebhookProviderReadTimeout: 5 * time.Second,
243-
WebhookProviderWriteTimeout: 10 * time.Second,
244-
ExcludeUnschedulable: false,
234+
NS1ZoneHandleMap: map[string]string{
235+
"example.com": "corp-prod",
236+
"dev.example.com": "dev-view",
237+
},
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,
245250
}
246251
)
247252

@@ -375,6 +380,8 @@ func TestParseFlags(t *testing.T) {
375380
"--crd-source-kind=Endpoint",
376381
"--ns1-endpoint=https://api.example.com/v1",
377382
"--ns1-ignoressl",
383+
"--ns1-zone-handle-map=example.com=corp-prod",
384+
"--ns1-zone-handle-map=dev.example.com=dev-view",
378385
"--transip-account=transip",
379386
"--transip-keyfile=/path/to/transip.key",
380387
"--digitalocean-api-page-size=100",
@@ -496,6 +503,7 @@ func TestParseFlags(t *testing.T) {
496503
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
497504
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
498505
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
506+
"EXTERNAL_DNS_NS1_ZONE_HANDLE_MAP": "example.com=corp-prod\ndev.example.com=dev-view",
499507
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
500508
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
501509
"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)