Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions controller/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,13 @@ func buildProvider(
case "ns1":
p, err = ns1.NewNS1Provider(
ns1.NS1Config{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
NS1Endpoint: cfg.NS1Endpoint,
NS1IgnoreSSL: cfg.NS1IgnoreSSL,
DryRun: cfg.DryRun,
MinTTLSeconds: cfg.NS1MinTTLSeconds,
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
NS1Endpoint: cfg.NS1Endpoint,
NS1IgnoreSSL: cfg.NS1IgnoreSSL,
DryRun: cfg.DryRun,
MinTTLSeconds: cfg.NS1MinTTLSeconds,
ZoneHandleOverrides: cfg.NS1ZoneHandleMap,
},
)
case "transip":
Expand Down
1 change: 1 addition & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
| `--ns1-endpoint=""` | When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/) |
| `--[no-]ns1-ignoressl` | When using the NS1 provider, specify whether to verify the SSL certificate (default: false) |
| `--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. |
| `--ns1-zone-handle-map=fqdn=handle` | Map FQDN (or suffix) to an NS1 zone handle/ID. Repeatable; k=v form. . |
| `--digitalocean-api-page-size=50` | Configure the page size used when querying the DigitalOcean API. |
| `--godaddy-api-key=""` | When using the GoDaddy provider, specify the API Key (required when --provider=godaddy) |
| `--godaddy-api-secret=""` | When using the GoDaddy provider, specify the API secret (required when --provider=godaddy) |
Expand Down
7 changes: 6 additions & 1 deletion docs/tutorials/ns1.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,18 @@ spec:
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --ns1-zone-handle-map="example.com=corp-prod" # (optional) map example.com FQDN to corp-prod zone handle
- --ns1-zone-handle-map="dev.example.com=dev-view" # (optional) multiple FQDN-zone handle mappings
- --provider=ns1
env:
- name: NS1_APIKEY
- name: NS1_APIKEY
valueFrom:
secretKeyRef:
name: NS1_APIKEY
key: NS1_API_KEY
- name: EXTERNAL_DNS_NS1_ZONE_HANDLE_MAP
value: "example.com=corp-prod\ndev.example.com=dev-view"

```

### Manifest (for clusters with RBAC enabled)
Expand Down
5 changes: 4 additions & 1 deletion pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ type Config struct {
NS1Endpoint string
NS1IgnoreSSL bool
NS1MinTTLSeconds int
NS1ZoneHandleMap map[string]string
TransIPAccountName string
TransIPPrivateKeyFile string
DigitalOceanAPIPageSize int
Expand Down Expand Up @@ -312,6 +313,7 @@ var defaultConfig = &Config{
NAT64Networks: []string{},
NS1Endpoint: "",
NS1IgnoreSSL: false,
NS1ZoneHandleMap: map[string]string{},
OCIConfigFile: "/etc/kubernetes/oci.yaml",
OCIZoneCacheDuration: 0 * time.Second,
OCIZoneScope: "GLOBAL",
Expand Down Expand Up @@ -447,7 +449,8 @@ var allowedSources = []string{
// NewConfig returns new Config object
func NewConfig() *Config {
return &Config{
AWSSDCreateTag: map[string]string{},
AWSSDCreateTag: map[string]string{},
NS1ZoneHandleMap: map[string]string{},
}
}

Expand Down
32 changes: 20 additions & 12 deletions pkg/apis/externaldns/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ var (
WebhookProviderReadTimeout: 5 * time.Second,
WebhookProviderWriteTimeout: 10 * time.Second,
ExcludeUnschedulable: true,
NS1ZoneHandleMap: map[string]string{},
}

overriddenConfig = &Config{
Expand Down Expand Up @@ -235,18 +236,22 @@ var (
CRDSourceKind: "Endpoint",
NS1Endpoint: "https://api.example.com/v1",
NS1IgnoreSSL: true,
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
RFC2136BatchChangeSize: 100,
RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"},
RFC2136LoadBalancingStrategy: "round-robin",
PiholeApiVersion: "6",
WebhookProviderURL: "http://localhost:8888",
WebhookProviderReadTimeout: 5 * time.Second,
WebhookProviderWriteTimeout: 10 * time.Second,
ExcludeUnschedulable: false,
NS1ZoneHandleMap: map[string]string{
"example.com": "corp-prod",
"dev.example.com": "dev-view",
},
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeAAAA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS},
RFC2136BatchChangeSize: 100,
RFC2136Host: []string{"rfc2136-host1", "rfc2136-host2"},
RFC2136LoadBalancingStrategy: "round-robin",
PiholeApiVersion: "6",
WebhookProviderURL: "http://localhost:8888",
WebhookProviderReadTimeout: 5 * time.Second,
WebhookProviderWriteTimeout: 10 * time.Second,
ExcludeUnschedulable: false,
}
)

Expand Down Expand Up @@ -380,6 +385,8 @@ func TestParseFlags(t *testing.T) {
"--crd-source-kind=Endpoint",
"--ns1-endpoint=https://api.example.com/v1",
"--ns1-ignoressl",
"--ns1-zone-handle-map=example.com=corp-prod",
"--ns1-zone-handle-map=dev.example.com=dev-view",
"--transip-account=transip",
"--transip-keyfile=/path/to/transip.key",
"--digitalocean-api-page-size=100",
Expand Down Expand Up @@ -501,6 +508,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint",
"EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1",
"EXTERNAL_DNS_NS1_IGNORESSL": "1",
"EXTERNAL_DNS_NS1_ZONE_HANDLE_MAP": "example.com=corp-prod\ndev.example.com=dev-view",
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
Expand Down
94 changes: 80 additions & 14 deletions provider/ns1/ns1.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) {

// NS1Config passes cli args to the NS1Provider
type NS1Config struct {
DomainFilter *endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
NS1Endpoint string
NS1IgnoreSSL bool
DryRun bool
MinTTLSeconds int
DomainFilter *endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
NS1Endpoint string
NS1IgnoreSSL bool
DryRun bool
MinTTLSeconds int
ZoneHandleOverrides map[string]string
}

// NS1Provider is the NS1 provider
Expand All @@ -101,6 +102,8 @@ type NS1Provider struct {
zoneIDFilter provider.ZoneIDFilter
dryRun bool
minTTLSeconds int
// normalized overrides: fqdn (no trailing dot, lowercased) -> handle/ID (lowercased)
zoneHandleOverrides map[string]string
}

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

return &NS1Provider{
client: NS1DomainService{apiClient},
domainFilter: config.DomainFilter,
zoneIDFilter: config.ZoneIDFilter,
minTTLSeconds: config.MinTTLSeconds,
client: NS1DomainService{apiClient},
domainFilter: config.DomainFilter,
zoneIDFilter: config.ZoneIDFilter,
minTTLSeconds: config.MinTTLSeconds,
zoneHandleOverrides: normalizeOverrides(config.ZoneHandleOverrides),
}, nil
}

Expand All @@ -155,9 +159,19 @@ func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)

for _, zone := range zones {
// TODO handle Header Codes
zoneData, _, err := p.client.GetZone(zone.String())
// Prefer lookup via handle/ID if an override exists; fall back to FQDN.
lookup := p.longestMatch(zone.Zone)
zoneData, _, err := p.client.GetZone(lookup)
if err != nil {
return nil, err
if lookup != strings.TrimSuffix(zone.Zone, ".") {
// fallback to FQDN lookup if override missed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should better fail by an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean return an error instead of falling back?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should better fail by an error.

zoneData, _, err = p.client.GetZone(zone.Zone)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}

for _, record := range zoneData.Records {
Expand Down Expand Up @@ -208,7 +222,7 @@ func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error {
}

// separate into per-zone change sets to be passed to the API.
changesByZone := ns1ChangesByZone(zones, changes)
changesByZone := p.ns1ChangesByZone(zones, changes)
for zoneName, changes := range changesByZone {
for _, change := range changes {
record := p.ns1BuildRecord(zoneName, change)
Expand Down Expand Up @@ -302,15 +316,38 @@ func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change {
return changes
}

// normalizeOverrides lowercases keys/values and strips any trailing dot on keys.
func normalizeOverrides(m map[string]string) map[string]string {
if len(m) == 0 {
return map[string]string{}
}
out := make(map[string]string)
for k, v := range m {
kk := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(k)), ".")
vv := strings.ToLower(strings.TrimSpace(v))

if kk == "" || vv == "" {
log.Debugf("Encountered empty string for zone handle override: key=%s, value=%s", kk, vv)
continue
}

out[kk] = vv
}
return out
}

// ns1ChangesByZone separates a multi-zone change into a single change per zone.
func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
// The map key becomes the "write key": handle/ID if overridden, else FQDN.
func (p *NS1Provider) ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change {
changes := make(map[string][]*ns1Change)
zoneNameIDMapper := provider.ZoneIDName{}

for _, z := range zones {
zoneNameIDMapper.Add(z.Zone, z.Zone)
changes[z.Zone] = []*ns1Change{}
}

// group changes by zone FQDN
for _, c := range changeSets {
zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName)
if zone == "" {
Expand All @@ -320,5 +357,34 @@ func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*
changes[zone] = append(changes[zone], c)
}

// replace zone FQDN with zone handle if FQDN is overridden
for k, v := range changes {
writeKey := p.longestMatch(k)

if writeKey != k {
changes[writeKey] = v
delete(changes, k)
}
}

return changes
}

// longestMatch returns the preferred key to pass to GetZone:
// if an override exists for fqdn (or a more specific suffix), return its mapped handle/ID;
// otherwise return the normalized FQDN.
func (p *NS1Provider) longestMatch(fqdn string) string {
name := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(fqdn)), ".")
bestKey := ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change the name to longestMatch and you can delete the comment below.

for k := range p.zoneHandleOverrides {
if name == k || strings.HasSuffix(name, "."+k) {
if len(k) > len(bestKey) {
bestKey = k
}
}
}
if bestKey != "" {
return p.zoneHandleOverrides[bestKey]
}
return name
}
Loading
Loading