diff --git a/main.go b/main.go index e9879c709..8d00b2ed9 100644 --- a/main.go +++ b/main.go @@ -84,11 +84,12 @@ func main() { endpointsSource := source.NewDedupSource(source.NewMultiSource(sources)) domainFilter := provider.NewDomainFilter(cfg.DomainFilter) + zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) var p provider.Provider switch cfg.Provider { case "aws": - p, err = provider.NewAWSProvider(domainFilter, cfg.DryRun) + p, err = provider.NewAWSProvider(domainFilter, zoneTypeFilter, cfg.DryRun) case "azure": p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, cfg.AzureResourceGroup, cfg.DryRun) case "cloudflare": diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index a880f1920..410046d04 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -38,6 +38,7 @@ type Config struct { Provider string GoogleProject string DomainFilter []string + AWSZoneType string AzureConfigFile string AzureResourceGroup string CloudflareProxied bool @@ -64,6 +65,7 @@ var defaultConfig = &Config{ Provider: "", GoogleProject: "", DomainFilter: []string{}, + AWSZoneType: "", AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", CloudflareProxied: false, @@ -103,8 +105,9 @@ 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, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "inmemory") - 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("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) + 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) app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup) app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 83f235f40..6d7c8f9f4 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -36,6 +36,7 @@ var ( Provider: "google", GoogleProject: "", DomainFilter: []string{""}, + AWSZoneType: "", AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", CloudflareProxied: false, @@ -61,6 +62,7 @@ var ( Provider: "google", GoogleProject: "project", DomainFilter: []string{"example.org", "company.com"}, + AWSZoneType: "private", AzureConfigFile: "azure.json", AzureResourceGroup: "arg", CloudflareProxied: true, @@ -110,6 +112,7 @@ func TestParseFlags(t *testing.T) { "--cloudflare-proxied", "--domain-filter=example.org", "--domain-filter=company.com", + "--aws-zone-type=private", "--policy=upsert-only", "--registry=noop", "--txt-owner-id=owner-1", @@ -140,6 +143,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_AZURE_RESOURCE_GROUP": "arg", "EXTERNAL_DNS_CLOUDFLARE_PROXIED": "1", "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", + "EXTERNAL_DNS_AWS_ZONE_TYPE": "private", "EXTERNAL_DNS_POLICY": "upsert-only", "EXTERNAL_DNS_REGISTRY": "noop", "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", diff --git a/provider/aws.go b/provider/aws.go index 7de5150d2..08bb5d7b3 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -71,10 +71,12 @@ type AWSProvider struct { dryRun bool // only consider hosted zones managing domains ending in this suffix domainFilter DomainFilter + // filter hosted zones by type (e.g. private or public) + zoneTypeFilter ZoneTypeFilter } // NewAWSProvider initializes a new AWS Route53 based Provider. -func NewAWSProvider(domainFilter DomainFilter, dryRun bool) (*AWSProvider, error) { +func NewAWSProvider(domainFilter DomainFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool) (*AWSProvider, error) { config := aws.NewConfig() config = config.WithHTTPClient( @@ -95,9 +97,10 @@ func NewAWSProvider(domainFilter DomainFilter, dryRun bool) (*AWSProvider, error } provider := &AWSProvider{ - client: route53.New(session), - domainFilter: domainFilter, - dryRun: dryRun, + client: route53.New(session), + domainFilter: domainFilter, + zoneTypeFilter: zoneTypeFilter, + dryRun: dryRun, } return provider, nil @@ -109,9 +112,15 @@ 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.domainFilter.Match(aws.StringValue(zone.Name)) { - zones[aws.StringValue(zone.Id)] = zone + if !p.zoneTypeFilter.Match(zone) { + continue } + + if !p.domainFilter.Match(aws.StringValue(zone.Name)) { + continue + } + + zones[aws.StringValue(zone.Id)] = zone } return true diff --git a/provider/aws_test.go b/provider/aws_test.go index 035a8b7e0..d9cc2aad0 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -146,19 +146,15 @@ func (r *Route53APIStub) CreateHostedZone(input *route53.CreateHostedZoneInput) return nil, fmt.Errorf("Error creating hosted DNS zone: %s already exists", id) } r.zones[id] = &route53.HostedZone{ - Id: aws.String(id), - Name: aws.String(name), + Id: aws.String(id), + Name: aws.String(name), + Config: input.HostedZoneConfig, } return &route53.CreateHostedZoneOutput{HostedZone: r.zones[id]}, nil } func TestAWSZones(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), false, []*endpoint.Endpoint{}) - - zones, err := provider.Zones() - require.NoError(t, err) - - validateAWSZones(t, zones, map[string]*route53.HostedZone{ + publicZones := map[string]*route53.HostedZone{ "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.": { Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), @@ -167,15 +163,46 @@ func TestAWSZones(t *testing.T) { Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), }, + } + + privateZones := map[string]*route53.HostedZone{ "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.": { Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), }, - }) + } + + allZones := map[string]*route53.HostedZone{} + for k, v := range publicZones { + allZones[k] = v + } + for k, v := range privateZones { + allZones[k] = v + } + + noZones := map[string]*route53.HostedZone{} + + for _, ti := range []struct { + msg string + 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}, + } { + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneTypeFilter, false, []*endpoint.Endpoint{}) + + zones, err := provider.Zones() + require.NoError(t, err) + + validateAWSZones(t, zones, ti.expectedZones) + } } func TestAWSRecords(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), false, []*endpoint.Endpoint{ + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ endpoint.NewEndpoint("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", "1.2.3.4", endpoint.RecordTypeA), endpoint.NewEndpoint("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA), @@ -196,7 +223,7 @@ func TestAWSRecords(t *testing.T) { } func TestAWSCreateRecords(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), 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), @@ -217,7 +244,7 @@ func TestAWSCreateRecords(t *testing.T) { } func TestAWSUpdateRecords(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), false, []*endpoint.Endpoint{ + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA), endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", "foo.elb.amazonaws.com", endpoint.RecordTypeCNAME), @@ -255,7 +282,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."}), false, originalEndpoints) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, originalEndpoints) require.NoError(t, provider.DeleteRecords(originalEndpoints)) @@ -267,7 +294,7 @@ func TestAWSDeleteRecords(t *testing.T) { } func TestAWSApplyChanges(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), false, []*endpoint.Endpoint{ + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), false, []*endpoint.Endpoint{ endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", "8.8.8.8", endpoint.RecordTypeA), endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", "8.8.4.4", endpoint.RecordTypeA), @@ -341,7 +368,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", "qux.elb.amazonaws.com", endpoint.RecordTypeCNAME), } - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), true, originalEndpoints) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), 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), @@ -493,7 +520,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."}), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), 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}, @@ -518,7 +545,7 @@ func TestAWSCreateRecordsWithCNAME(t *testing.T) { } func TestAWSCreateRecordsWithALIAS(t *testing.T) { - provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), false, []*endpoint.Endpoint{}) + provider := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), 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}, @@ -605,8 +632,9 @@ func TestAWSSuitableZone(t *testing.T) { func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) { params := &route53.CreateHostedZoneInput{ - CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), - Name: zone.Name, + CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"), + Name: zone.Name, + HostedZoneConfig: zone.Config, } if _, err := provider.client.CreateHostedZone(params); err != nil { @@ -671,28 +699,39 @@ func clearAWSRecords(t *testing.T, provider *AWSProvider, zone string) { } } -func newAWSProvider(t *testing.T, domainFilter DomainFilter, dryRun bool, records []*endpoint.Endpoint) *AWSProvider { +func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneTypeFilter ZoneTypeFilter, dryRun bool, records []*endpoint.Endpoint) *AWSProvider { client := NewRoute53APIStub() provider := &AWSProvider{ - client: client, - domainFilter: domainFilter, - dryRun: false, + client: client, + domainFilter: domainFilter, + zoneTypeFilter: zoneTypeFilter, + dryRun: false, } createAWSZone(t, provider, &route53.HostedZone{ - Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), - Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), + Id: aws.String("/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do."), + Name: aws.String("zone-1.ext-dns-test-2.teapot.zalan.do."), + Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, }) createAWSZone(t, provider, &route53.HostedZone{ - Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), - Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), + Id: aws.String("/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), + Name: aws.String("zone-2.ext-dns-test-2.teapot.zalan.do."), + Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, }) createAWSZone(t, provider, &route53.HostedZone{ - Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), - Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), + Id: aws.String("/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."), + Name: aws.String("zone-3.ext-dns-test-2.teapot.zalan.do."), + Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}, + }) + + // filtered out by domain filter + createAWSZone(t, provider, &route53.HostedZone{ + Id: aws.String("/hostedzone/zone-4.ext-dns-test-3.teapot.zalan.do."), + Name: aws.String("zone-4.ext-dns-test-3.teapot.zalan.do."), + Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, }) setupAWSRecords(t, provider, records) diff --git a/provider/domainfilter.go b/provider/domain_filter.go similarity index 100% rename from provider/domainfilter.go rename to provider/domain_filter.go diff --git a/provider/domainfilter_test.go b/provider/domain_filter_test.go similarity index 100% rename from provider/domainfilter_test.go rename to provider/domain_filter_test.go diff --git a/provider/zone_type_filter.go b/provider/zone_type_filter.go new file mode 100644 index 000000000..4ba4af3aa --- /dev/null +++ b/provider/zone_type_filter.go @@ -0,0 +1,62 @@ +/* +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 ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" +) + +const ( + zoneTypePublic = "public" + zoneTypePrivate = "private" +) + +// ZoneTypeFilter holds a zone type to filter for. +type ZoneTypeFilter struct { + zoneType string +} + +// NewZoneTypeFilter returns a new ZoneTypeFilter given a zone type to filter for. +func NewZoneTypeFilter(zoneType string) ZoneTypeFilter { + return ZoneTypeFilter{zoneType: zoneType} +} + +// Match checks whether a zone matches the zone type that's filtered for. +func (f ZoneTypeFilter) Match(zone *route53.HostedZone) bool { + // An empty zone filter includes all hosted zones. + if f.zoneType == "" { + return true + } + + // If the zone has no config we assume it's a public zone since the config's field + // `PrivateZone` is false by default in go. + if zone.Config == nil { + return f.zoneType == zoneTypePublic + } + + // Given a zone type we return true if the given zone matches this type. + switch f.zoneType { + case zoneTypePublic: + return !aws.BoolValue(zone.Config.PrivateZone) + case zoneTypePrivate: + return aws.BoolValue(zone.Config.PrivateZone) + } + + // We return false on any other path, e.g. unknown zone type filter value. + return false +} diff --git a/provider/zone_type_filter_test.go b/provider/zone_type_filter_test.go new file mode 100644 index 000000000..c7d97a8ea --- /dev/null +++ b/provider/zone_type_filter_test.go @@ -0,0 +1,71 @@ +/* +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/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + + "github.com/stretchr/testify/assert" +) + +func TestZoneTypeFilterMatch(t *testing.T) { + publicZone := &route53.HostedZone{Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}} + privateZone := &route53.HostedZone{Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(true)}} + + for _, tc := range []struct { + zoneTypeFilter string + zone *route53.HostedZone + matches bool + }{ + { + "", publicZone, true, + }, + { + "", privateZone, true, + }, + { + "public", publicZone, true, + }, + { + "public", privateZone, false, + }, + { + "private", publicZone, false, + }, + { + "private", privateZone, true, + }, + { + "unknown", publicZone, false, + }, + { + "", &route53.HostedZone{}, true, + }, + { + "public", &route53.HostedZone{}, true, + }, + { + "private", &route53.HostedZone{}, false, + }, + } { + zoneTypeFilter := NewZoneTypeFilter(tc.zoneTypeFilter) + assert.Equal(t, tc.matches, zoneTypeFilter.Match(tc.zone)) + } +}