feat: resolve LB-type Service hostname to create A/AAAA instead of CNAME

This commit is contained in:
Charles Xu 2023-04-18 21:09:59 -07:00
parent eaabf715fe
commit 1d232c4b86
8 changed files with 79 additions and 26 deletions

View File

@ -83,16 +83,17 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart
| `secretConfiguration.mountPath` | Mount path of secret configuration secret (this can be templated). | `""` | | `secretConfiguration.mountPath` | Mount path of secret configuration secret (this can be templated). | `""` |
| `secretConfiguration.data` | Secret configuration secret data. Could be used to store DNS provider credentials. | `{}` | | `secretConfiguration.data` | Secret configuration secret data. Could be used to store DNS provider credentials. | `{}` |
| `secretConfiguration.subPath` | Sub-path of secret configuration secret (this can be templated). | `""` | | `secretConfiguration.subPath` | Sub-path of secret configuration secret (this can be templated). | `""` |
| `resolveLoadBalancerHostname` | Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs | `false` |
## Namespaced scoped installation ## Namespaced scoped installation
external-dns supports running on a namespaced only scope, too. external-dns supports running on a namespaced only scope, too.
If `namespaced=true` is defined, the helm chart will setup `Roles` and `RoleBindings` instead `ClusterRoles` and `ClusterRoleBindings`. If `namespaced=true` is defined, the helm chart will setup `Roles` and `RoleBindings` instead `ClusterRoles` and `ClusterRoleBindings`.
### Limited supported ### Limited supported
Not all sources are supported in namespaced scope, since some sources depends on cluster-wide resources. Not all sources are supported in namespaced scope, since some sources depends on cluster-wide resources.
For example: Source `node` isn't supported, since `kind: Node` has scope `Cluster`. For example: Source `node` isn't supported, since `kind: Node` has scope `Cluster`.
Sources like `istio-virtualservice` only work, if all resources like `Gateway` and `VirtualService` are present in the same Sources like `istio-virtualservice` only work, if all resources like `Gateway` and `VirtualService` are present in the same
namespaces as `external-dns`. namespaces as `external-dns`.
The annotation `external-dns.alpha.kubernetes.io/endpoints-type: NodeExternalIP` is not supported. The annotation `external-dns.alpha.kubernetes.io/endpoints-type: NodeExternalIP` is not supported.

View File

@ -96,6 +96,9 @@ spec:
- --domain-filter={{ . }} - --domain-filter={{ . }}
{{- end }} {{- end }}
- --provider={{ tpl .Values.provider $ }} - --provider={{ tpl .Values.provider $ }}
{{- if .Values.resolveLoadBalancerHostname }}
- --resolve-load-balancer-hostname
{{- end }}
{{- range .Values.extraArgs }} {{- range .Values.extraArgs }}
- {{ tpl . $ }} - {{ tpl . $ }}
{{- end }} {{- end }}

View File

@ -15,6 +15,8 @@ fullnameOverride: ""
commonLabels: {} commonLabels: {}
resolveLoadBalancerHostname: false
serviceAccount: serviceAccount:
# Specifies whether a service account should be created # Specifies whether a service account should be created
create: true create: true

View File

@ -141,6 +141,7 @@ func main() {
DefaultTargets: cfg.DefaultTargets, DefaultTargets: cfg.DefaultTargets,
OCPRouterName: cfg.OCPRouterName, OCPRouterName: cfg.OCPRouterName,
UpdateEvents: cfg.UpdateEvents, UpdateEvents: cfg.UpdateEvents,
ResolveLoadBalancerHostname: cfg.ResolveLoadBalancerHostname,
} }
// Lookup all the selected sources by names and pass them the desired configuration. // Lookup all the selected sources by names and pass them the desired configuration.

View File

@ -166,6 +166,7 @@ type Config struct {
CFAPIEndpoint string CFAPIEndpoint string
CFUsername string CFUsername string
CFPassword string CFPassword string
ResolveLoadBalancerHostname bool
RFC2136Host string RFC2136Host string
RFC2136Port int RFC2136Port int
RFC2136Zone string RFC2136Zone string
@ -390,6 +391,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("server", "The Kubernetes API server to connect to (default: auto-detect)").Default(defaultConfig.APIServerURL).StringVar(&cfg.APIServerURL) app.Flag("server", "The Kubernetes API server to connect to (default: auto-detect)").Default(defaultConfig.APIServerURL).StringVar(&cfg.APIServerURL)
app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig) app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig)
app.Flag("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout").Default(defaultConfig.RequestTimeout.String()).DurationVar(&cfg.RequestTimeout) app.Flag("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout").Default(defaultConfig.RequestTimeout.String()).DurationVar(&cfg.RequestTimeout)
app.Flag("resolve-lb-hostname", "Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs").BoolVar(&cfg.ResolveLoadBalancerHostname)
// Flags related to cloud foundry // Flags related to cloud foundry
app.Flag("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting").Default(defaultConfig.CFAPIEndpoint).StringVar(&cfg.CFAPIEndpoint) app.Flag("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting").Default(defaultConfig.CFAPIEndpoint).StringVar(&cfg.CFAPIEndpoint)

View File

@ -19,6 +19,7 @@ package source
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"sort" "sort"
"strings" "strings"
"text/template" "text/template"
@ -57,6 +58,7 @@ type serviceSource struct {
publishInternal bool publishInternal bool
publishHostIP bool publishHostIP bool
alwaysPublishNotReadyAddresses bool alwaysPublishNotReadyAddresses bool
resolveLoadBalancerHostname bool
serviceInformer coreinformers.ServiceInformer serviceInformer coreinformers.ServiceInformer
endpointsInformer coreinformers.EndpointsInformer endpointsInformer coreinformers.EndpointsInformer
podInformer coreinformers.PodInformer podInformer coreinformers.PodInformer
@ -66,7 +68,7 @@ type serviceSource struct {
} }
// NewServiceSource creates a new serviceSource with the given config. // NewServiceSource creates a new serviceSource with the given config.
func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector) (Source, error) { func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector, resolveLoadBalancerHostname bool) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate) tmpl, err := parseTemplate(fqdnTemplate)
if err != nil { if err != nil {
return nil, err return nil, err
@ -137,6 +139,7 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
nodeInformer: nodeInformer, nodeInformer: nodeInformer,
serviceTypeFilter: serviceTypes, serviceTypeFilter: serviceTypes,
labelSelector: labelSelector, labelSelector: labelSelector,
resolveLoadBalancerHostname: resolveLoadBalancerHostname,
}, nil }, nil
} }
@ -480,7 +483,7 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
if useClusterIP { if useClusterIP {
targets = append(targets, extractServiceIps(svc)...) targets = append(targets, extractServiceIps(svc)...)
} else { } else {
targets = append(targets, extractLoadBalancerTargets(svc)...) targets = append(targets, extractLoadBalancerTargets(svc, sc.resolveLoadBalancerHostname)...)
} }
case v1.ServiceTypeClusterIP: case v1.ServiceTypeClusterIP:
if sc.publishInternal { if sc.publishInternal {
@ -540,7 +543,7 @@ func extractServiceExternalName(svc *v1.Service) endpoint.Targets {
return endpoint.Targets{svc.Spec.ExternalName} return endpoint.Targets{svc.Spec.ExternalName}
} }
func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets { func extractLoadBalancerTargets(svc *v1.Service, resolveLoadBalancerHostname bool) endpoint.Targets {
var ( var (
targets endpoint.Targets targets endpoint.Targets
externalIPs endpoint.Targets externalIPs endpoint.Targets
@ -552,7 +555,18 @@ func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
targets = append(targets, lb.IP) targets = append(targets, lb.IP)
} }
if lb.Hostname != "" { if lb.Hostname != "" {
targets = append(targets, lb.Hostname) if resolveLoadBalancerHostname {
ips, err := net.LookupIP(lb.Hostname)
if err != nil {
log.Errorf("Unable to resolve %q: %v", lb.Hostname, err)
continue
}
for _, ip := range ips {
targets = append(targets, ip.String())
}
} else {
targets = append(targets, lb.Hostname)
}
} }
} }

View File

@ -78,6 +78,7 @@ func (suite *ServiceSuite) SetupTest() {
[]string{}, []string{},
false, false,
labels.Everything(), labels.Everything(),
false,
) )
suite.NoError(err, "should initialize service source") suite.NoError(err, "should initialize service source")
} }
@ -158,6 +159,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
ti.serviceTypesFilter, ti.serviceTypesFilter,
false, false,
labels.Everything(), labels.Everything(),
false,
) )
if ti.expectError { if ti.expectError {
@ -174,25 +176,26 @@ func testServiceSourceEndpoints(t *testing.T) {
t.Parallel() t.Parallel()
for _, tc := range []struct { for _, tc := range []struct {
title string title string
targetNamespace string targetNamespace string
annotationFilter string annotationFilter string
svcNamespace string svcNamespace string
svcName string svcName string
svcType v1.ServiceType svcType v1.ServiceType
compatibility string compatibility string
fqdnTemplate string fqdnTemplate string
combineFQDNAndAnnotation bool combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool ignoreHostnameAnnotation bool
labels map[string]string labels map[string]string
annotations map[string]string annotations map[string]string
clusterIP string clusterIP string
externalIPs []string externalIPs []string
lbs []string lbs []string
serviceTypesFilter []string serviceTypesFilter []string
expected []*endpoint.Endpoint expected []*endpoint.Endpoint
expectError bool expectError bool
serviceLabelSelector string serviceLabelSelector string
resolveLoadBalancerHostname bool
}{ }{
{ {
title: "no annotated services return no endpoints", title: "no annotated services return no endpoints",
@ -389,6 +392,24 @@ func testServiceSourceEndpoints(t *testing.T) {
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}}, {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}},
}, },
}, },
{
title: "annotated services return an endpoint with hostname then resolve hostname",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"example.com"}, // Use a resolvable hostname for testing.
serviceTypesFilter: []string{},
resolveLoadBalancerHostname: true,
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"93.184.216.34"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2606:2800:220:1:248:1893:25c8:1946"}},
},
},
{ {
title: "annotated services can omit trailing dot", title: "annotated services can omit trailing dot",
svcNamespace: "testing", svcNamespace: "testing",
@ -1086,6 +1107,7 @@ func testServiceSourceEndpoints(t *testing.T) {
tc.serviceTypesFilter, tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
sourceLabel, sourceLabel,
tc.resolveLoadBalancerHostname,
) )
require.NoError(t, err) require.NoError(t, err)
@ -1275,6 +1297,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
tc.serviceTypesFilter, tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
labels.Everything(), labels.Everything(),
false,
) )
require.NoError(t, err) require.NoError(t, err)
@ -1440,6 +1463,7 @@ func TestClusterIpServices(t *testing.T) {
[]string{}, []string{},
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
labelSelector, labelSelector,
false,
) )
require.NoError(t, err) require.NoError(t, err)
@ -2010,6 +2034,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
[]string{}, []string{},
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
labels.Everything(), labels.Everything(),
false,
) )
require.NoError(t, err) require.NoError(t, err)
@ -2483,6 +2508,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{}, []string{},
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
labels.Everything(), labels.Everything(),
false,
) )
require.NoError(t, err) require.NoError(t, err)
@ -2840,6 +2866,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
[]string{}, []string{},
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
labels.Everything(), labels.Everything(),
false,
) )
require.NoError(t, err) require.NoError(t, err)
@ -2952,6 +2979,7 @@ func TestExternalServices(t *testing.T) {
[]string{}, []string{},
tc.ignoreHostnameAnnotation, tc.ignoreHostnameAnnotation,
labels.Everything(), labels.Everything(),
false,
) )
require.NoError(t, err) require.NoError(t, err)
@ -3006,6 +3034,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
[]string{}, []string{},
false, false,
labels.Everything(), labels.Everything(),
false,
) )
require.NoError(b, err) require.NoError(b, err)

View File

@ -73,6 +73,7 @@ type Config struct {
DefaultTargets []string DefaultTargets []string
OCPRouterName string OCPRouterName string
UpdateEvents bool UpdateEvents bool
ResolveLoadBalancerHostname bool
} }
// ClientGenerator provides clients // ClientGenerator provides clients
@ -215,7 +216,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter) return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname)
case "ingress": case "ingress":
client, err := p.KubeClient() client, err := p.KubeClient()
if err != nil { if err != nil {