diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3d4018f..31414db44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Target of DNS record is changed only if corresponding kubernetes resource target changes - If kubernetes resource is deleted, then another resource may acquire DNS name - "Flapping" target issue is resolved by providing a consistent and defined mechanism for choosing a target + - New `--zone-id-filter` parameter allows filtering by zone id ## v0.4.8 - 2017-11-22 diff --git a/main.go b/main.go index 3118fe548..aa51754eb 100644 --- a/main.go +++ b/main.go @@ -88,26 +88,28 @@ func main() { endpointsSource := source.NewDedupSource(source.NewMultiSource(sources)) domainFilter := provider.NewDomainFilter(cfg.DomainFilter) + zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter) zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) var p provider.Provider switch cfg.Provider { case "aws": - p, err = provider.NewAWSProvider(domainFilter, zoneTypeFilter, cfg.DryRun) + p, err = provider.NewAWSProvider(domainFilter, zoneIDFilter, zoneTypeFilter, cfg.DryRun) case "azure": - p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, cfg.AzureResourceGroup, cfg.DryRun) + p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.DryRun) case "cloudflare": - p, err = provider.NewCloudFlareProvider(domainFilter, cfg.CloudflareProxied, cfg.DryRun) + p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareProxied, cfg.DryRun) case "google": - p, err = provider.NewGoogleProvider(cfg.GoogleProject, domainFilter, cfg.DryRun) + p, err = provider.NewGoogleProvider(cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.DryRun) case "digitalocean": p, err = provider.NewDigitalOceanProvider(domainFilter, cfg.DryRun) case "dnsimple": - p, err = provider.NewDnsimpleProvider(domainFilter, cfg.DryRun) + p, err = provider.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun) case "infoblox": p, err = provider.NewInfobloxProvider( provider.InfobloxConfig{ DomainFilter: domainFilter, + ZoneIDFilter: zoneIDFilter, Host: cfg.InfobloxGridHost, Port: cfg.InfobloxWapiPort, Username: cfg.InfobloxWapiUsername, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index a8ad23147..0331efffc 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -41,6 +41,7 @@ type Config struct { Provider string GoogleProject string DomainFilter []string + ZoneIDFilter []string AWSZoneType string AzureConfigFile string AzureResourceGroup string @@ -134,6 +135,7 @@ func (cfg *Config) ParseFlags(args []string) error { // Flags related to providers app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "inmemory") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) + app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index d723427fd..5e3b6a586 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -37,6 +37,7 @@ var ( Provider: "google", GoogleProject: "", DomainFilter: []string{""}, + ZoneIDFilter: []string{""}, AWSZoneType: "", AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", @@ -70,6 +71,7 @@ var ( Provider: "google", GoogleProject: "project", DomainFilter: []string{"example.org", "company.com"}, + ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, AWSZoneType: "private", AzureConfigFile: "azure.json", AzureResourceGroup: "arg", @@ -135,6 +137,8 @@ func TestParseFlags(t *testing.T) { "--no-infoblox-ssl-verify", "--domain-filter=example.org", "--domain-filter=company.com", + "--zone-id-filter=/hostedzone/ZTST1", + "--zone-id-filter=/hostedzone/ZTST2", "--aws-zone-type=private", "--policy=upsert-only", "--registry=noop", @@ -173,6 +177,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0", "EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com", "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", + "EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2", "EXTERNAL_DNS_AWS_ZONE_TYPE": "private", "EXTERNAL_DNS_POLICY": "upsert-only", "EXTERNAL_DNS_REGISTRY": "noop", diff --git a/provider/aws.go b/provider/aws.go index b2815f58d..124b9fbef 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -71,12 +71,14 @@ type AWSProvider struct { dryRun bool // only consider hosted zones managing domains ending in this suffix domainFilter DomainFilter + // filter hosted zones by id + zoneIDFilter ZoneIDFilter // filter hosted zones by type (e.g. private or public) zoneTypeFilter ZoneTypeFilter } // NewAWSProvider initializes a new AWS Route53 based Provider. -func NewAWSProvider(domainFilter DomainFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool) (*AWSProvider, error) { +func NewAWSProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool) (*AWSProvider, error) { config := aws.NewConfig() config = config.WithHTTPClient( @@ -99,6 +101,7 @@ func NewAWSProvider(domainFilter DomainFilter, zoneTypeFilter ZoneTypeFilter, dr provider := &AWSProvider{ client: route53.New(session), domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, zoneTypeFilter: zoneTypeFilter, dryRun: dryRun, } @@ -112,6 +115,10 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) { f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { for _, zone := range resp.HostedZones { + if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) { + continue + } + if !p.zoneTypeFilter.Match(zone) { continue } diff --git a/provider/aws_test.go b/provider/aws_test.go index 81cba6243..90c7c383a 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -183,15 +183,17 @@ func TestAWSZones(t *testing.T) { for _, ti := range []struct { msg string + zoneIDFilter ZoneIDFilter zoneTypeFilter ZoneTypeFilter expectedZones map[string]*route53.HostedZone }{ - {"no filter", NewZoneTypeFilter(""), allZones}, - {"public filter", NewZoneTypeFilter("public"), publicZones}, - {"private filter", NewZoneTypeFilter("private"), privateZones}, - {"unknown filter", NewZoneTypeFilter("unknown"), noZones}, + {"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), allZones}, + {"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), publicZones}, + {"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), privateZones}, + {"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), noZones}, + {"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), privateZones}, } { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneTypeFilter, false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, false, []*endpoint.Endpoint{}) zones, err := provider.Zones() require.NoError(t, err) @@ -201,7 +203,7 @@ func TestAWSZones(t *testing.T) { } func TestAWSRecords(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), @@ -223,7 +225,7 @@ func TestAWSRecords(t *testing.T) { func TestAWSCreateRecords(t *testing.T) { customTTL := endpoint.TTL(60) - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA), @@ -246,7 +248,7 @@ func TestAWSCreateRecords(t *testing.T) { } func TestAWSUpdateRecords(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)), @@ -284,7 +286,7 @@ func TestAWSDeleteRecords(t *testing.T) { endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME), } - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, originalEndpoints) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, originalEndpoints) require.NoError(t, provider.DeleteRecords(originalEndpoints)) @@ -296,7 +298,7 @@ func TestAWSDeleteRecords(t *testing.T) { } func TestAWSApplyChanges(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA, endpoint.TTL(recordTTL)), @@ -370,7 +372,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL)), } - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), true, originalEndpoints) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), true, originalEndpoints) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA), @@ -492,7 +494,7 @@ func TestAWSChangesByZones(t *testing.T) { } func TestAWSsubmitChanges(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) const subnets = 16 const hosts = maxChangeCount / subnets @@ -604,7 +606,7 @@ func validateAWSChangeRecord(t *testing.T, record *route53.Change, expected *rou } func TestAWSCreateRecordsWithCNAME(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.example.org", RecordType: endpoint.RecordTypeCNAME}, @@ -629,7 +631,7 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) { } func TestAWSCreateRecordsWithALIAS(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ {DNSName: "create-test.zone-1.ext-dns-test-2.teapot.zalan.do", Target: "foo.eu-central-1.elb.amazonaws.com", RecordType: endpoint.RecordTypeCNAME}, @@ -764,12 +766,13 @@ func clearAWSRecords(t *testing.T, provider *AWSProvider, zone string) { } } -func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool, records []*endpoint.Endpoint) *AWSProvider { +func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool, records []*endpoint.Endpoint) *AWSProvider { client := NewRoute53APIStub() provider := &AWSProvider{ client: client, domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, zoneTypeFilter: zoneTypeFilter, dryRun: false, } diff --git a/provider/azure.go b/provider/azure.go index 14354d299..67917dae4 100644 --- a/provider/azure.go +++ b/provider/azure.go @@ -66,6 +66,7 @@ type RecordsClient interface { // AzureProvider implements the DNS provider for Microsoft's Azure cloud platform. type AzureProvider struct { domainFilter DomainFilter + zoneIDFilter ZoneIDFilter dryRun bool resourceGroup string zonesClient ZonesClient @@ -75,7 +76,7 @@ type AzureProvider struct { // NewAzureProvider creates a new Azure provider. // // Returns the provider or an error if a provider could not be created. -func NewAzureProvider(configFile string, domainFilter DomainFilter, resourceGroup string, dryRun bool) (*AzureProvider, error) { +func NewAzureProvider(configFile string, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, resourceGroup string, dryRun bool) (*AzureProvider, error) { contents, err := ioutil.ReadFile(configFile) if err != nil { return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) @@ -118,6 +119,7 @@ func NewAzureProvider(configFile string, domainFilter DomainFilter, resourceGrou provider := &AzureProvider{ domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, dryRun: dryRun, resourceGroup: cfg.ResourceGroup, zonesClient: zonesClient, @@ -194,9 +196,19 @@ func (p *AzureProvider) zones() ([]dns.Zone, error) { for list.Value != nil && len(*list.Value) > 0 { for _, zone := range *list.Value { - if zone.Name != nil && p.domainFilter.Match(*zone.Name) { - zones = append(zones, zone) + if zone.Name == nil { + continue } + + if !p.domainFilter.Match(*zone.Name) { + continue + } + + if !p.zoneIDFilter.Match(*zone.ID) { + continue + } + + zones = append(zones, zone) } list, err = p.zonesClient.ListByResourceGroupNextResults(list) diff --git a/provider/azure_test.go b/provider/azure_test.go index 7ff72364a..507dd4dd1 100644 --- a/provider/azure_test.go +++ b/provider/azure_test.go @@ -37,8 +37,9 @@ type mockRecordsClient struct { updatedEndpoints []*endpoint.Endpoint } -func createMockZone(zone string) dns.Zone { +func createMockZone(zone string, id string) dns.Zone { return dns.Zone{ + ID: to.StringPtr(id), Name: to.StringPtr(zone), } } @@ -138,9 +139,10 @@ func (client *mockRecordsClient) CreateOrUpdate(resourceGroupName string, zoneNa return parameters, nil } -func newAzureProvider(domainFilter DomainFilter, dryRun bool, resourceGroup string, zonesClient ZonesClient, recordsClient RecordsClient) *AzureProvider { +func newAzureProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, resourceGroup string, zonesClient ZonesClient, recordsClient RecordsClient) *AzureProvider { return &AzureProvider{ domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, dryRun: dryRun, resourceGroup: resourceGroup, zonesClient: zonesClient, @@ -152,7 +154,7 @@ func TestAzureRecord(t *testing.T) { zonesClient := mockZonesClient{ mockZoneListResult: &dns.ZoneListResult{ Value: &[]dns.Zone{ - createMockZone("example.com"), + createMockZone("example.com", "/dnszones/example.com"), }, }, } @@ -169,7 +171,7 @@ func TestAzureRecord(t *testing.T) { }, } - provider := newAzureProvider(NewDomainFilter([]string{"example.com"}), true, "k8s", &zonesClient, &recordsClient) + provider := newAzureProvider(NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, "k8s", &zonesClient, &recordsClient) actual, err := provider.Records() if err != nil { @@ -225,13 +227,14 @@ func TestAzureApplyChangesDryRun(t *testing.T) { func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordsClient) { provider := newAzureProvider( NewDomainFilter([]string{""}), + NewZoneIDFilter([]string{""}), dryRun, "group", &mockZonesClient{ mockZoneListResult: &dns.ZoneListResult{ Value: &[]dns.Zone{ - createMockZone("example.com"), - createMockZone("other.com"), + createMockZone("example.com", "/dnszones/example.com"), + createMockZone("other.com", "/dnszones/other.com"), }, }, }, diff --git a/provider/cloudflare.go b/provider/cloudflare.go index cef57b164..760327391 100644 --- a/provider/cloudflare.go +++ b/provider/cloudflare.go @@ -92,6 +92,7 @@ type CloudFlareProvider struct { Client cloudFlareDNS // only consider hosted zones managing domains ending in this suffix domainFilter DomainFilter + zoneIDFilter ZoneIDFilter proxied bool DryRun bool } @@ -103,7 +104,7 @@ type cloudFlareChange struct { } // NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. -func NewCloudFlareProvider(domainFilter DomainFilter, proxied bool, dryRun bool) (*CloudFlareProvider, error) { +func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, proxied bool, dryRun bool) (*CloudFlareProvider, error) { // initialize via API email and API key and returns new API object config, err := cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) if err != nil { @@ -113,6 +114,7 @@ func NewCloudFlareProvider(domainFilter DomainFilter, proxied bool, dryRun bool) //Client: config, Client: zoneService{config}, domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, proxied: proxied, DryRun: dryRun, } @@ -129,9 +131,15 @@ func (p *CloudFlareProvider) Zones() ([]cloudflare.Zone, error) { } for _, zone := range zones { - if p.domainFilter.Match(zone.Name) { - result = append(result, zone) + if !p.domainFilter.Match(zone.Name) { + continue } + + if !p.zoneIDFilter.Match(zone.ID) { + continue + } + + result = append(result, zone) } return result, nil diff --git a/provider/cloudflare_test.go b/provider/cloudflare_test.go index d01fd67ab..e42b1bce5 100644 --- a/provider/cloudflare_test.go +++ b/provider/cloudflare_test.go @@ -378,6 +378,7 @@ func TestCloudFlareZones(t *testing.T) { provider := &CloudFlareProvider{ Client: &mockCloudFlareClient{}, domainFilter: NewDomainFilter([]string{"zalando.to."}), + zoneIDFilter: NewZoneIDFilter([]string{""}), } zones, err := provider.Zones() @@ -415,13 +416,13 @@ func TestRecords(t *testing.T) { func TestNewCloudFlareProvider(t *testing.T) { _ = os.Setenv("CF_API_KEY", "xxxxxxxxxxxxxxxxx") _ = os.Setenv("CF_API_EMAIL", "test@test.com") - _, err := NewCloudFlareProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), false, true) + _, err := NewCloudFlareProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), NewZoneIDFilter([]string{""}), false, true) if err != nil { t.Errorf("should not fail, %s", err) } _ = os.Unsetenv("CF_API_KEY") _ = os.Unsetenv("CF_API_EMAIL") - _, err = NewCloudFlareProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), false, true) + _, err = NewCloudFlareProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), NewZoneIDFilter([]string{""}), false, true) if err == nil { t.Errorf("expected to fail") } diff --git a/provider/dnsimple.go b/provider/dnsimple.go index 35ac33859..deb139258 100644 --- a/provider/dnsimple.go +++ b/provider/dnsimple.go @@ -84,6 +84,7 @@ type dnsimpleProvider struct { identity identityService accountID string domainFilter DomainFilter + zoneIDFilter ZoneIDFilter dryRun bool } @@ -99,7 +100,7 @@ const ( ) // NewDnsimpleProvider initializes a new Dnsimple based provider -func NewDnsimpleProvider(domainFilter DomainFilter, dryRun bool) (Provider, error) { +func NewDnsimpleProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (Provider, error) { oauthToken := os.Getenv("DNSIMPLE_OAUTH") if len(oauthToken) == 0 { return nil, fmt.Errorf("No dnsimple oauth token provided") @@ -109,6 +110,7 @@ func NewDnsimpleProvider(domainFilter DomainFilter, dryRun bool) (Provider, erro client: dnsimpleZoneService{service: client.Zones}, identity: identityService{service: client.Identity}, domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, dryRun: dryRun, } whoamiResponse, err := provider.identity.service.Whoami() @@ -119,7 +121,7 @@ func NewDnsimpleProvider(domainFilter DomainFilter, dryRun bool) (Provider, erro return provider, nil } -// Returns a list of Zones that end with the provider's domainFilter +// Returns a list of filtered Zones func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) { zones := make(map[string]dnsimple.Zone) zonesResponse, err := p.client.ListZones(p.accountID, &dnsimple.ZoneListOptions{}) @@ -127,9 +129,15 @@ func (p *dnsimpleProvider) Zones() (map[string]dnsimple.Zone, error) { return nil, err } for _, zone := range zonesResponse.Data { - if p.domainFilter.Match(zone.Name) { - zones[strconv.Itoa(zone.ID)] = zone + if !p.domainFilter.Match(zone.Name) { + continue } + + if !p.zoneIDFilter.Match(strconv.Itoa(zone.ID)) { + continue + } + + zones[strconv.Itoa(zone.ID)] = zone } return zones, nil } diff --git a/provider/dnsimple_test.go b/provider/dnsimple_test.go index e59ac48c2..7a7044b9d 100644 --- a/provider/dnsimple_test.go +++ b/provider/dnsimple_test.go @@ -153,7 +153,7 @@ func testDnsimpleSuitableZone(t *testing.T) { func TestNewDnsimpleProvider(t *testing.T) { os.Setenv("DNSIMPLE_OAUTH", "xxxxxxxxxxxxxxxxxxxxxxxxxx") - _, err := NewDnsimpleProvider(DomainFilter{filters: []string{"example.com"}}, true) + _, err := NewDnsimpleProvider(NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true) if err == nil { t.Errorf("Expected to fail new provider on bad token") } diff --git a/provider/google.go b/provider/google.go index a63df6cbb..22ff184c9 100644 --- a/provider/google.go +++ b/provider/google.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "fmt" "strings" "github.com/linki/instrumented_http" @@ -102,6 +103,8 @@ type GoogleProvider struct { dryRun bool // only consider hosted zones managing domains ending in this suffix domainFilter DomainFilter + // only consider hosted zones ending with this zone id + zoneIDFilter ZoneIDFilter // A client for managing resource record sets resourceRecordSetsClient resourceRecordSetsClientInterface // A client for managing hosted zones @@ -111,7 +114,7 @@ type GoogleProvider struct { } // NewGoogleProvider initializes a new Google CloudDNS based Provider. -func NewGoogleProvider(project string, domainFilter DomainFilter, dryRun bool) (*GoogleProvider, error) { +func NewGoogleProvider(project string, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (*GoogleProvider, error) { gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope) if err != nil { return nil, err @@ -132,6 +135,7 @@ func NewGoogleProvider(project string, domainFilter DomainFilter, dryRun bool) ( provider := &GoogleProvider{ project: project, domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, dryRun: dryRun, resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets}, managedZonesClient: managedZonesService{dnsClient.ManagedZones}, @@ -147,9 +151,15 @@ func (p *GoogleProvider) Zones() (map[string]*dns.ManagedZone, error) { f := func(resp *dns.ManagedZonesListResponse) error { for _, zone := range resp.ManagedZones { - if p.domainFilter.Match(zone.DnsName) { - zones[zone.Name] = zone + if !p.domainFilter.Match(zone.DnsName) { + continue } + + if !p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) { + continue + } + + zones[zone.Name] = zone } return nil diff --git a/provider/google_test.go b/provider/google_test.go index 28406c7d8..50ce92984 100644 --- a/provider/google_test.go +++ b/provider/google_test.go @@ -193,7 +193,7 @@ func hasTrailingDot(target string) bool { } func TestGoogleZones(t *testing.T) { - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) zones, err := provider.Zones() require.NoError(t, err) @@ -212,7 +212,7 @@ func TestGoogleRecords(t *testing.T) { endpoint.NewEndpoint("list-test-alias.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME), } - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, originalEndpoints) + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints) records, err := provider.Records() require.NoError(t, err) @@ -221,7 +221,7 @@ func TestGoogleRecords(t *testing.T) { } func TestGoogleCreateRecords(t *testing.T) { - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) records := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "1.2.3.4", endpoint.RecordTypeA), @@ -242,7 +242,7 @@ func TestGoogleCreateRecords(t *testing.T) { } func TestGoogleUpdateRecords(t *testing.T) { - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, []*endpoint.Endpoint{ + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME), @@ -278,7 +278,7 @@ func TestGoogleDeleteRecords(t *testing.T) { endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "baz.elb.amazonaws.com", endpoint.RecordTypeCNAME), } - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, originalEndpoints) + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, originalEndpoints) require.NoError(t, provider.DeleteRecords(originalEndpoints)) @@ -289,7 +289,7 @@ func TestGoogleDeleteRecords(t *testing.T) { } func TestGoogleApplyChanges(t *testing.T) { - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, []*endpoint.Endpoint{ + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.gcp.zalan.do", "8.8.4.4", endpoint.RecordTypeA), @@ -353,7 +353,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) { endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.gcp.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME), } - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), true, originalEndpoints) + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), true, originalEndpoints) createRecords := []*endpoint.Endpoint{ endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.gcp.zalan.do", "8.8.8.8", endpoint.RecordTypeA), @@ -394,7 +394,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) { } func TestGoogleApplyChangesEmpty(t *testing.T) { - provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), false, []*endpoint.Endpoint{}) + provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) assert.NoError(t, provider.ApplyChanges(&plan.Changes{})) } @@ -501,10 +501,11 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected assert.Equal(t, expected.Type, record.Type) } -func newGoogleProvider(t *testing.T, domainFilter DomainFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { +func newGoogleProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { provider := &GoogleProvider{ project: "zalando-external-dns-test", domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, dryRun: false, resourceRecordSetsClient: &mockResourceRecordSetsClient{}, managedZonesClient: &mockManagedZonesClient{}, diff --git a/provider/infoblox.go b/provider/infoblox.go index 086fd5b36..41973d2bf 100644 --- a/provider/infoblox.go +++ b/provider/infoblox.go @@ -30,6 +30,7 @@ import ( // InfobloxConfig clarifies the method signature type InfobloxConfig struct { DomainFilter DomainFilter + ZoneIDFilter ZoneIDFilter Host string Port int Username string @@ -43,6 +44,7 @@ type InfobloxConfig struct { type InfobloxProvider struct { client ibclient.IBConnector domainFilter DomainFilter + zoneIDFilter ZoneIDFilter dryRun bool } @@ -82,6 +84,7 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro provider := &InfobloxProvider{ client: client, domainFilter: infobloxConfig.DomainFilter, + zoneIDFilter: infobloxConfig.ZoneIDFilter, dryRun: infobloxConfig.DryRun, } @@ -186,9 +189,15 @@ func (p *InfobloxProvider) zones() ([]ibclient.ZoneAuth, error) { } for _, zone := range res { - if p.domainFilter.Match(zone.Fqdn) { - result = append(result, zone) + if !p.domainFilter.Match(zone.Fqdn) { + continue } + + if !p.zoneIDFilter.Match(zone.Ref) { + continue + } + + result = append(result, zone) } return result, nil diff --git a/provider/infoblox_test.go b/provider/infoblox_test.go index add6f6179..746b879df 100644 --- a/provider/infoblox_test.go +++ b/provider/infoblox_test.go @@ -327,10 +327,11 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject return nil } -func newInfobloxProvider(domainFilter DomainFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider { +func newInfobloxProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, client ibclient.IBConnector) *InfobloxProvider { return &InfobloxProvider{ client: client, domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, dryRun: dryRun, } } @@ -351,7 +352,7 @@ func TestInfobloxRecords(t *testing.T) { }, } - provider := newInfobloxProvider(NewDomainFilter([]string{"example.com"}), true, &client) + provider := newInfobloxProvider(NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, &client) actual, err := provider.Records() if err != nil { @@ -425,6 +426,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient provider := newInfobloxProvider( NewDomainFilter([]string{""}), + NewZoneIDFilter([]string{""}), dryRun, client, ) @@ -482,7 +484,7 @@ func TestInfobloxZones(t *testing.T) { mockInfobloxObjects: &[]ibclient.IBObject{}, } - provider := newInfobloxProvider(NewDomainFilter([]string{"example.com"}), true, &client) + provider := newInfobloxProvider(NewDomainFilter([]string{"example.com"}), NewZoneIDFilter([]string{""}), true, &client) zones, _ := provider.zones() assert.Equal(t, provider.findZone(zones, "example.com").Fqdn, "example.com") diff --git a/provider/zone_id_filter.go b/provider/zone_id_filter.go new file mode 100644 index 000000000..066d622f6 --- /dev/null +++ b/provider/zone_id_filter.go @@ -0,0 +1,45 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import "strings" + +// ZoneIDFilter holds a list of zone ids to filter by +type ZoneIDFilter struct { + zoneIDs []string +} + +// NewZoneIDFilter returns a new ZoneIDFilter given a list of zone ids +func NewZoneIDFilter(zoneIDs []string) ZoneIDFilter { + return ZoneIDFilter{zoneIDs} +} + +// Match checks whether a zone matches one of the provided zone ids +func (f ZoneIDFilter) Match(zoneID string) bool { + // An empty filter includes all zones. + if len(f.zoneIDs) == 0 { + return true + } + + for _, id := range f.zoneIDs { + if strings.HasSuffix(zoneID, id) { + return true + } + } + + return false +} diff --git a/provider/zone_id_filter_test.go b/provider/zone_id_filter_test.go new file mode 100644 index 000000000..41e313886 --- /dev/null +++ b/provider/zone_id_filter_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type zoneIDFilterTest struct { + zoneIDFilter []string + zone string + expected bool +} + +func TestZoneIDFilterMatch(t *testing.T) { + zone := "/hostedzone/ZTST1" + + for _, tt := range []zoneIDFilterTest{ + { + []string{}, + zone, + true, + }, + { + []string{"/hostedzone/ZTST1"}, + zone, + true, + }, + { + []string{"/hostedzone/ZTST2"}, + zone, + false, + }, + { + []string{"ZTST1"}, + zone, + true, + }, + { + []string{"ZTST2"}, + zone, + false, + }, + { + []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, + zone, + true, + }, + { + []string{"/hostedzone/ZTST2", "/hostedzone/ZTST3"}, + zone, + false, + }, + { + []string{"/hostedzone/ZTST2", "/hostedzone/ZTST1"}, + zone, + true, + }, + } { + zoneIDFilter := NewZoneIDFilter(tt.zoneIDFilter) + assert.Equal(t, tt.expected, zoneIDFilter.Match(tt.zone)) + } +}