Allow setting Cloudflare proxying by annotation

This commit is contained in:
Erik Swets 2018-04-06 12:12:11 +02:00 committed by Arjan Molenaar
parent 5fea7534b3
commit e0e7a9defd
13 changed files with 179 additions and 72 deletions

View File

@ -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

View File

@ -33,7 +33,14 @@ spec:
labels:
type: object
providerSpecific:
items:
properties:
name:
type: string
value:
type: string
type: object
type: array
recordTTL:
format: int64
type: integer

View File

@ -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.

View File

@ -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)
}

View File

@ -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)
}

View File

@ -58,7 +58,9 @@ func ExampleSameEndpoints() {
{
DNSName: "example.org",
Targets: endpoint.Targets{"load-balancer.org"},
ProviderSpecific: endpoint.ProviderSpecific{"foo": "bar"},
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 []
}

View File

@ -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 != "" {

View File

@ -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,
},
},
},
}
@ -834,7 +837,12 @@ func TestAWSisAWSAlias(t *testing.T) {
ep := &endpoint.Endpoint{
Targets: endpoint.Targets{tc.target},
RecordType: tc.recordType,
ProviderSpecific: map[string]string{"alias": tc.alias},
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{
Name: "alias",
Value: tc.alias,
},
},
}
addrs := []*endpoint.Endpoint{
{

View File

@ -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 (
@ -95,7 +97,7 @@ type CloudFlareProvider struct {
// only consider hosted zones managing domains ending in this suffix
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
proxied bool
proxiedByDefault bool
DryRun bool
}
@ -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 {
@ -117,7 +119,7 @@ func NewCloudFlareProvider(domainFilter DomainFilter, zoneIDFilter ZoneIDFilter,
Client: zoneService{config},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
proxied: proxied,
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
}

View File

@ -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

View File

@ -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
}

View File

@ -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 {
@ -301,6 +303,7 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nod
Labels: endpoint.NewLabels(),
Targets: make(endpoint.Targets, 0, defaultTargetsCapacity),
DNSName: hostname,
ProviderSpecific: providerSpecific,
}
epCNAME := &endpoint.Endpoint{
@ -309,6 +312,7 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nod
Labels: endpoint.NewLabels(),
Targets: make(endpoint.Targets, 0, defaultTargetsCapacity),
DNSName: hostname,
ProviderSpecific: providerSpecific,
}
var endpoints []*endpoint.Endpoint

View File

@ -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.