diff --git a/docs/faq.md b/docs/faq.md index 9c9173bb6..6153d9b83 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -40,7 +40,9 @@ Services exposed via `type=LoadBalancer`, `type=ExternalName` and for the hostna There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below: -1. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the loadbalancer IP, it also will look for the annotation `external-dns.alpha.kubernetes.io/internal-hostname` on the service and use the service IP. +1. For ingress objects ExternalDNS will create a DNS record based on the hosts specified for the ingress object, as well as the `external-dns.alpha.kubernetes.io/hostname` annotation. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the loadbalancer IP, it also will look for the annotation `external-dns.alpha.kubernetes.io/internal-hostname` on the service and use the service IP. + - For ingresses, you can optionally force ExternalDNS to create records based on _either_ the hosts specified or the `external-dns.alpha.kubernetes.io/hostname` annotation. This behavior is controlled by + setting the `external-dns.alpha.kubernetes.io/ingress-hostname-source` annotation on that ingress to either `defined-hosts-only` or `annotation-only`. 2. If compatibility mode is enabled (e.g. `--compatibility={mate,molecule}` flag), External DNS will parse annotations used by Zalando/Mate, wearemolecule/route53-kubernetes. Compatibility mode with Kops DNS Controller is planned to be added in the future. diff --git a/source/ingress.go b/source/ingress.go index 0ad7f94b9..23bea7c29 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -42,6 +42,10 @@ const ( ALBDualstackAnnotationKey = "alb.ingress.kubernetes.io/ip-address-type" // ALBDualstackAnnotationValue is the value of the ALB dualstack annotation that indicates it is dualstack ALBDualstackAnnotationValue = "dualstack" + + // Possible values for the ingress-hostname-source annotation + IngressHostnameSourceAnnotationOnlyValue = "annotation-only" + IngressHostnameSourceDefinedHostsOnlyValue = "defined-hosts-only" ) // ingressSource is an implementation of Source for Kubernetes ingress objects. @@ -243,8 +247,6 @@ func (sc *ingressSource) setDualstackLabel(ingress *v1beta1.Ingress, endpoints [ // endpointsFromIngress extracts the endpoints from ingress object func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool) []*endpoint.Endpoint { - var endpoints []*endpoint.Endpoint - ttl, err := getTTLFromAnnotations(ing.Annotations) if err != nil { log.Warn(err) @@ -258,11 +260,13 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool, i providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations) + // Gather endpoints defined on hosts sections of the ingress + var definedHostsEndpoints []*endpoint.Endpoint for _, rule := range ing.Spec.Rules { if rule.Host == "" { continue } - endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...) + definedHostsEndpoints = append(definedHostsEndpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...) } // Skip endpoints if we do not want entries from tls spec section @@ -272,18 +276,33 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool, i if host == "" { continue } - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + definedHostsEndpoints = append(definedHostsEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) } } } - // Skip endpoints if we do not want entries from annotations + // Gather endpoints defined on annotations in the ingress + var annotationEndpoints []*endpoint.Endpoint if !ignoreHostnameAnnotation { - hostnameList := getHostnamesFromAnnotations(ing.Annotations) - for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) + for _, hostname := range getHostnamesFromAnnotations(ing.Annotations) { + annotationEndpoints = append(annotationEndpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) } } + + // Determine which hostnames to consider in our final list + hostnameSourceAnnotation, hostnameSourceAnnotationExists := ing.Annotations[ingressHostnameSourceKey] + if !hostnameSourceAnnotationExists { + return append(definedHostsEndpoints, annotationEndpoints...) + } + + // Include endpoints according to the hostname source annotation in our final list + var endpoints []*endpoint.Endpoint + if strings.ToLower(hostnameSourceAnnotation) == IngressHostnameSourceDefinedHostsOnlyValue { + endpoints = append(endpoints, definedHostsEndpoints...) + } + if strings.ToLower(hostnameSourceAnnotation) == IngressHostnameSourceAnnotationOnlyValue { + endpoints = append(endpoints, annotationEndpoints...) + } return endpoints } diff --git a/source/ingress_test.go b/source/ingress_test.go index 4168940e7..0840e241b 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -85,6 +85,7 @@ func (suite *IngressSuite) TestDualstackLabelIsSet() { func TestIngress(t *testing.T) { suite.Run(t, new(IngressSuite)) t.Run("endpointsFromIngress", testEndpointsFromIngress) + t.Run("endpointsFromIngressHostnameSourceAnnotation", testEndpointsFromIngressHostnameSourceAnnotation) t.Run("Endpoints", testIngressEndpoints) } @@ -228,6 +229,98 @@ func testEndpointsFromIngress(t *testing.T) { } } +func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) { + // Host names and host name annotation provided, with various values of the ingress-hostname-source annotation + for _, ti := range []struct { + title string + ingress fakeIngress + expected []*endpoint.Endpoint + }{ + { + title: "No ingress-hostname-source annotation, one rule.host, one annotation host", + ingress: fakeIngress{ + dnsnames: []string{"foo.bar"}, + annotations: map[string]string{hostnameAnnotationKey: "foo.baz"}, + hostnames: []string{"lb.com"}, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "foo.baz", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "No ingress-hostname-source annotation, one rule.host", + ingress: fakeIngress{ + dnsnames: []string{"foo.bar"}, + hostnames: []string{"lb.com"}, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "No ingress-hostname-source annotation, one rule.host, one annotation host", + ingress: fakeIngress{ + dnsnames: []string{"foo.bar"}, + annotations: map[string]string{hostnameAnnotationKey: "foo.baz"}, + hostnames: []string{"lb.com"}, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "foo.baz", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "Ingress-hostname-source=defined-hosts-only, one rule.host, one annotation host", + ingress: fakeIngress{ + dnsnames: []string{"foo.bar"}, + annotations: map[string]string{hostnameAnnotationKey: "foo.baz", ingressHostnameSourceKey: "defined-hosts-only"}, + hostnames: []string{"lb.com"}, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "Ingress-hostname-source=annotation-only, one rule.host, one annotation host", + ingress: fakeIngress{ + dnsnames: []string{"foo.bar"}, + annotations: map[string]string{hostnameAnnotationKey: "foo.baz", ingressHostnameSourceKey: "annotation-only"}, + hostnames: []string{"lb.com"}, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.baz", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + } { + t.Run(ti.title, func(t *testing.T) { + realIngress := ti.ingress.Ingress() + validateEndpoints(t, endpointsFromIngress(realIngress, false, false), ti.expected) + }) + } +} + func testIngressEndpoints(t *testing.T) { namespace := "testing" for _, ti := range []struct { diff --git a/source/source.go b/source/source.go index 069ee4753..53e550e9f 100644 --- a/source/source.go +++ b/source/source.go @@ -46,6 +46,9 @@ const ( ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl" // The annotation used for switching to the alias record types e. g. AWS Alias records instead of a normal CNAME aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias" + // The annotation used to determine the source of hostnames for ingresses. This is an optional field - all + // available hostname sources are used if not specified. + ingressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source" // The value of the controller annotation so that we feel responsible controllerAnnotationValue = "dns-controller" // The annotation used for defining the desired hostname