diff --git a/docs/contributing/crd-source.md b/docs/contributing/crd-source.md index 06fbea80a..ca43503d3 100644 --- a/docs/contributing/crd-source.md +++ b/docs/contributing/crd-source.md @@ -14,7 +14,11 @@ Here is typical example of [CRD API type](https://github.com/kubernetes-incubato ```go type TTL int64 type Targets []string -type ProviderSpecific map[string]string +type ProviderSpecificProperty struct { + Name string + Value string +} +type ProviderSpecific []ProviderSpecificProperty type Endpoint struct { // The hostname of the DNS record diff --git a/docs/contributing/crd-source/crd-manifest.yaml b/docs/contributing/crd-source/crd-manifest.yaml index 258404575..00b52f34c 100644 --- a/docs/contributing/crd-source/crd-manifest.yaml +++ b/docs/contributing/crd-source/crd-manifest.yaml @@ -33,7 +33,14 @@ spec: labels: type: object providerSpecific: - type: object + items: + properties: + name: + type: string + value: + type: string + type: object + type: array recordTTL: format: int64 type: integer diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index 308cb8186..2a16d1a35 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -196,3 +196,7 @@ Now that we have verified that ExternalDNS will automatically manage Cloudflare $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml ``` + +## Setting cloudflare-proxied on a per-ingress basis + +Using the `external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"` annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global `--cloudflare-proxied` setting. diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index ca77f36e2..2d292fb0d 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -109,8 +109,14 @@ func (t Targets) IsLess(o Targets) bool { return false } +// ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers +type ProviderSpecificProperty struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} + // ProviderSpecific holds configuration which is specific to individual DNS providers -type ProviderSpecific map[string]string +type ProviderSpecific []ProviderSpecificProperty // Endpoint is a high-level way of a connection between a service and an IP type Endpoint struct { @@ -160,10 +166,21 @@ func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint { if e.ProviderSpecific == nil { e.ProviderSpecific = ProviderSpecific{} } - e.ProviderSpecific[key] = value + + e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value}) return e } +// GetProviderSpecificProperty returns a ProviderSpecificProperty if the property exists. +func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProperty, bool) { + for _, providerSpecific := range e.ProviderSpecific { + if providerSpecific.Name == key { + return providerSpecific, true + } + } + return ProviderSpecificProperty{}, false +} + func (e *Endpoint) String() string { return fmt.Sprintf("%s %d IN %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Targets, e.ProviderSpecific) } diff --git a/internal/testutils/endpoint.go b/internal/testutils/endpoint.go index f13a6b4c7..d804c75ea 100644 --- a/internal/testutils/endpoint.go +++ b/internal/testutils/endpoint.go @@ -17,6 +17,7 @@ limitations under the License. package testutils import ( + "reflect" "sort" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -49,7 +50,7 @@ func SameEndpoint(a, b *endpoint.Endpoint) bool { return a.DNSName == b.DNSName && a.Targets.Same(b.Targets) && a.RecordType == b.RecordType && a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL && a.Labels[endpoint.ResourceLabelKey] == b.Labels[endpoint.ResourceLabelKey] && - SameMap(a.ProviderSpecific, b.ProviderSpecific) + SameProverSpecific(a.ProviderSpecific, b.ProviderSpecific) } // SameEndpoints compares two slices of endpoints regardless of order @@ -81,17 +82,7 @@ func SamePlanChanges(a, b map[string][]*endpoint.Endpoint) bool { SameEndpoints(a["UpdateOld"], b["UpdateOld"]) && SameEndpoints(a["UpdateNew"], b["UpdateNew"]) } -// SameMap verifies that two maps contain the same string/string key/value pairs -func SameMap(a, b map[string]string) bool { - if len(a) != len(b) { - return false - } - - for k, v := range a { - if v != b[k] { - return false - } - } - - return true +// SameProverSpecific verifies that two maps contain the same string/string key/value pairs +func SameProverSpecific(a, b endpoint.ProviderSpecific) bool { + return reflect.DeepEqual(a, b) } diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go index f14ae655c..2f1204fd6 100644 --- a/internal/testutils/endpoint_test.go +++ b/internal/testutils/endpoint_test.go @@ -56,9 +56,11 @@ func ExampleSameEndpoints() { RecordTTL: endpoint.TTL(60), }, { - DNSName: "example.org", - Targets: endpoint.Targets{"load-balancer.org"}, - ProviderSpecific: endpoint.ProviderSpecific{"foo": "bar"}, + DNSName: "example.org", + Targets: endpoint.Targets{"load-balancer.org"}, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{Name: "foo", Value: "bar"}, + }, }, } sort.Sort(byAllFields(eps)) @@ -66,11 +68,11 @@ func ExampleSameEndpoints() { fmt.Println(ep) } // Output: - // abc.com 0 IN A 1.2.3.4 map[] - // abc.com 0 IN TXT something map[] - // bbc.com 0 IN CNAME foo.com map[] - // cbc.com 60 IN CNAME foo.com map[] - // example.org 0 IN load-balancer.org map[] - // example.org 0 IN load-balancer.org map[foo:bar] - // example.org 0 IN TXT load-balancer.org map[] + // abc.com 0 IN A 1.2.3.4 [] + // abc.com 0 IN TXT something [] + // bbc.com 0 IN CNAME foo.com [] + // cbc.com 60 IN CNAME foo.com [] + // example.org 0 IN load-balancer.org [] + // example.org 0 IN load-balancer.org [{foo bar}] + // example.org 0 IN TXT load-balancer.org [] } diff --git a/provider/aws.go b/provider/aws.go index 7ec5e79aa..33fd0b5cc 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -371,8 +371,8 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou if isAWSLoadBalancer(endpoint) { evalTargetHealth := p.evaluateTargetHealth - if _, ok := endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth]; ok { - evalTargetHealth = endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth] == "true" + if prop, ok := endpoint.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok { + evalTargetHealth = prop.Value == "true" } change.ResourceRecordSet.Type = aws.String(route53.RRTypeA) @@ -549,7 +549,7 @@ func isAWSLoadBalancer(ep *endpoint.Endpoint) bool { // isAWSAlias determines if a given hostname belongs to an AWS Alias record by doing an reverse lookup. func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string { - if val, exists := ep.ProviderSpecific["alias"]; ep.RecordType == endpoint.RecordTypeCNAME && exists && val == "true" { + if prop, exists := ep.GetProviderSpecificProperty("alias"); ep.RecordType == endpoint.RecordTypeCNAME && exists && prop.Value == "true" { for _, addr := range addrs { if addr.DNSName == ep.Targets[0] { if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" { diff --git a/provider/aws_test.go b/provider/aws_test.go index 6852b6df2..d1a37cc89 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -781,7 +781,10 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, ProviderSpecific: endpoint.ProviderSpecific{ - providerSpecificEvaluateTargetHealth: key, + endpoint.ProviderSpecificProperty{ + Name: providerSpecificEvaluateTargetHealth, + Value: key, + }, }, }, } @@ -832,9 +835,14 @@ func TestAWSisAWSAlias(t *testing.T) { {"foo.example.org", endpoint.RecordTypeCNAME, "true", ""}, } { ep := &endpoint.Endpoint{ - Targets: endpoint.Targets{tc.target}, - RecordType: tc.recordType, - ProviderSpecific: map[string]string{"alias": tc.alias}, + Targets: endpoint.Targets{tc.target}, + RecordType: tc.recordType, + ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "alias", + Value: tc.alias, + }, + }, } addrs := []*endpoint.Endpoint{ { diff --git a/provider/cloudflare.go b/provider/cloudflare.go index 75e24f414..ffb6bf81c 100644 --- a/provider/cloudflare.go +++ b/provider/cloudflare.go @@ -19,6 +19,7 @@ package provider import ( "fmt" "os" + "strconv" "strings" cloudflare "github.com/cloudflare/cloudflare-go" @@ -26,6 +27,7 @@ import ( "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" + "github.com/kubernetes-incubator/external-dns/source" ) const ( @@ -93,10 +95,10 @@ func (z zoneService) DeleteDNSRecord(zoneID, recordID string) error { type CloudFlareProvider struct { Client cloudFlareDNS // only consider hosted zones managing domains ending in this suffix - domainFilter DomainFilter - zoneIDFilter ZoneIDFilter - proxied bool - DryRun bool + domainFilter DomainFilter + zoneIDFilter ZoneIDFilter + proxiedByDefault bool + DryRun bool } // cloudFlareChange differentiates between ChangActions @@ -106,7 +108,7 @@ type cloudFlareChange struct { } // NewCloudFlareProvider initializes a new CloudFlare DNS based Provider. -func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, proxied bool, dryRun bool) (*CloudFlareProvider, error) { +func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, proxiedByDefault 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 { @@ -114,11 +116,11 @@ func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, } provider := &CloudFlareProvider{ //Client: config, - Client: zoneService{config}, - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - proxied: proxied, - DryRun: dryRun, + Client: zoneService{config}, + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + proxiedByDefault: proxiedByDefault, + DryRun: dryRun, } return provider, nil } @@ -173,11 +175,13 @@ func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) { // ApplyChanges applies a given set of changes in a given zone. func (p *CloudFlareProvider) ApplyChanges(changes *plan.Changes) error { + proxiedByDefault := p.proxiedByDefault + combinedChanges := make([]*cloudFlareChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) - combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create, p.proxied)...) - combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, p.proxied)...) - combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, p.proxied)...) + combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareCreate, changes.Create, proxiedByDefault)...) + combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareUpdate, changes.UpdateNew, proxiedByDefault)...) + combinedChanges = append(combinedChanges, newCloudFlareChanges(cloudFlareDelete, changes.Delete, proxiedByDefault)...) return p.submitChanges(combinedChanges) } @@ -270,21 +274,20 @@ func (p *CloudFlareProvider) getRecordID(records []cloudflare.DNSRecord, record } // newCloudFlareChanges returns a collection of Changes based on the given records and action. -func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint, proxied bool) []*cloudFlareChange { +func newCloudFlareChanges(action string, endpoints []*endpoint.Endpoint, proxiedByDefault bool) []*cloudFlareChange { changes := make([]*cloudFlareChange, 0, len(endpoints)) for _, endpoint := range endpoints { - changes = append(changes, newCloudFlareChange(action, endpoint, proxied)) + changes = append(changes, newCloudFlareChange(action, endpoint, proxiedByDefault)) } return changes } -func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxied bool) *cloudFlareChange { +func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxiedByDefault bool) *cloudFlareChange { ttl := defaultCloudFlareRecordTTL - if proxied && (cloudFlareTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*")) { - proxied = false - } + proxied := shouldBeProxied(endpoint, proxiedByDefault) + if endpoint.RecordTTL.IsConfigured() { ttl = int(endpoint.RecordTTL) } @@ -300,3 +303,24 @@ func newCloudFlareChange(action string, endpoint *endpoint.Endpoint, proxied boo }, } } + +func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { + proxied := proxiedByDefault + + for _, v := range endpoint.ProviderSpecific { + if v.Name == source.CloudflareProxiedKey { + b, err := strconv.ParseBool(v.Value) + if err != nil { + log.Errorf("Failed to parse annotation [%s]: %v", source.CloudflareProxiedKey, err) + } else { + proxied = b + } + break + } + } + + if cloudFlareTypeNotSupported[endpoint.RecordType] || strings.Contains(endpoint.DNSName, "*") { + proxied = false + } + return proxied +} diff --git a/provider/cloudflare_test.go b/provider/cloudflare_test.go index cfa54ca21..00389e9ce 100644 --- a/provider/cloudflare_test.go +++ b/provider/cloudflare_test.go @@ -368,6 +368,36 @@ func TestNewCloudFlareChangeNoProxied(t *testing.T) { assert.False(t, change.ResourceRecordSet.Proxied) } +func TestNewCloudFlareProxiedAnnotationTrue(t *testing.T) { + change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}, ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "true", + }, + }}, false) + assert.True(t, change.ResourceRecordSet.Proxied) +} + +func TestNewCloudFlareProxiedAnnotationFalse(t *testing.T) { + change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}, ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }}, true) + assert.False(t, change.ResourceRecordSet.Proxied) +} + +func TestNewCloudFlareProxiedAnnotationIllegalValue(t *testing.T) { + change := newCloudFlareChange(cloudFlareCreate, &endpoint.Endpoint{DNSName: "new", RecordType: "A", Targets: endpoint.Targets{"target"}, ProviderSpecific: endpoint.ProviderSpecific{ + endpoint.ProviderSpecificProperty{ + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "asdaslkjndaslkdjals", + }, + }}, false) + assert.False(t, change.ResourceRecordSet.Proxied) +} + func TestNewCloudFlareChangeProxiable(t *testing.T) { var cloudFlareTypes = []struct { recordType string diff --git a/source/ingress.go b/source/ingress.go index cbda3cd92..056b8dd6f 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -232,7 +232,6 @@ func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint { for _, hostname := range hostnameList { endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) } - return endpoints } diff --git a/source/service.go b/source/service.go index 28cb1b9cd..1242763fe 100644 --- a/source/service.go +++ b/source/service.go @@ -218,9 +218,10 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service, nodeTargets endp return nil, fmt.Errorf("failed to apply template on service %s: %v", svc.String(), err) } + providerSpecific := getProviderSpecificAnnotations(svc.Annotations) hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",") for _, hostname := range hostnameList { - endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets)...) + endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets, providerSpecific)...) } return endpoints, nil @@ -230,9 +231,10 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service, nodeTargets endp func (sc *serviceSource) endpoints(svc *v1.Service, nodeTargets endpoint.Targets) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint + providerSpecific := getProviderSpecificAnnotations(svc.Annotations) hostnameList := getHostnamesFromAnnotations(svc.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets)...) + endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets, providerSpecific)...) } return endpoints @@ -288,7 +290,7 @@ func (sc *serviceSource) setResourceLabel(service v1.Service, endpoints []*endpo } } -func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nodeTargets endpoint.Targets) []*endpoint.Endpoint { +func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nodeTargets endpoint.Targets, providerSpecific endpoint.ProviderSpecific) []*endpoint.Endpoint { hostname = strings.TrimSuffix(hostname, ".") ttl, err := getTTLFromAnnotations(svc.Annotations) if err != nil { @@ -296,19 +298,21 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nod } epA := &endpoint.Endpoint{ - RecordTTL: ttl, - RecordType: endpoint.RecordTypeA, - Labels: endpoint.NewLabels(), - Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), - DNSName: hostname, + RecordTTL: ttl, + RecordType: endpoint.RecordTypeA, + Labels: endpoint.NewLabels(), + Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), + DNSName: hostname, + ProviderSpecific: providerSpecific, } epCNAME := &endpoint.Endpoint{ - RecordTTL: ttl, - RecordType: endpoint.RecordTypeCNAME, - Labels: endpoint.NewLabels(), - Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), - DNSName: hostname, + RecordTTL: ttl, + RecordType: endpoint.RecordTypeCNAME, + Labels: endpoint.NewLabels(), + Targets: make(endpoint.Targets, 0, defaultTargetsCapacity), + DNSName: hostname, + ProviderSpecific: providerSpecific, } var endpoints []*endpoint.Endpoint diff --git a/source/source.go b/source/source.go index 338397dfc..c4e856fb9 100644 --- a/source/source.go +++ b/source/source.go @@ -41,6 +41,12 @@ const ( controllerAnnotationValue = "dns-controller" ) +// Provider-specific annotations +const ( + // The annotation used for determining if traffic will go through Cloudflare + CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied" +) + const ( ttlMinimum = 1 ttlMaximum = math.MaxUint32 @@ -72,7 +78,6 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string { if !exists { return nil } - return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",") } @@ -82,10 +87,22 @@ func getAliasFromAnnotations(annotations map[string]string) bool { } func getProviderSpecificAnnotations(annotations map[string]string) endpoint.ProviderSpecific { - if getAliasFromAnnotations(annotations) { - return map[string]string{"alias": "true"} + providerSpecificAnnotations := endpoint.ProviderSpecific{} + + v, exists := annotations[CloudflareProxiedKey] + if exists { + providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ + Name: CloudflareProxiedKey, + Value: v, + }) } - return map[string]string{} + if getAliasFromAnnotations(annotations) { + providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ + Name: "alias", + Value: "true", + }) + } + return providerSpecificAnnotations } // getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.