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.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). | `""` |
| `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
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`.
### Limited supported
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`.
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`.
The annotation `external-dns.alpha.kubernetes.io/endpoints-type: NodeExternalIP` is not supported.

View File

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

View File

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

View File

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

View File

@ -166,6 +166,7 @@ type Config struct {
CFAPIEndpoint string
CFUsername string
CFPassword string
ResolveLoadBalancerHostname bool
RFC2136Host string
RFC2136Port int
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("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("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
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 (
"context"
"fmt"
"net"
"sort"
"strings"
"text/template"
@ -57,6 +58,7 @@ type serviceSource struct {
publishInternal bool
publishHostIP bool
alwaysPublishNotReadyAddresses bool
resolveLoadBalancerHostname bool
serviceInformer coreinformers.ServiceInformer
endpointsInformer coreinformers.EndpointsInformer
podInformer coreinformers.PodInformer
@ -66,7 +68,7 @@ type serviceSource struct {
}
// 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)
if err != nil {
return nil, err
@ -137,6 +139,7 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
nodeInformer: nodeInformer,
serviceTypeFilter: serviceTypes,
labelSelector: labelSelector,
resolveLoadBalancerHostname: resolveLoadBalancerHostname,
}, nil
}
@ -480,7 +483,7 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
if useClusterIP {
targets = append(targets, extractServiceIps(svc)...)
} else {
targets = append(targets, extractLoadBalancerTargets(svc)...)
targets = append(targets, extractLoadBalancerTargets(svc, sc.resolveLoadBalancerHostname)...)
}
case v1.ServiceTypeClusterIP:
if sc.publishInternal {
@ -540,7 +543,7 @@ func extractServiceExternalName(svc *v1.Service) endpoint.Targets {
return endpoint.Targets{svc.Spec.ExternalName}
}
func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
func extractLoadBalancerTargets(svc *v1.Service, resolveLoadBalancerHostname bool) endpoint.Targets {
var (
targets endpoint.Targets
externalIPs endpoint.Targets
@ -552,7 +555,18 @@ func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
targets = append(targets, lb.IP)
}
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{},
false,
labels.Everything(),
false,
)
suite.NoError(err, "should initialize service source")
}
@ -158,6 +159,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
ti.serviceTypesFilter,
false,
labels.Everything(),
false,
)
if ti.expectError {
@ -174,25 +176,26 @@ func testServiceSourceEndpoints(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
externalIPs []string
lbs []string
serviceTypesFilter []string
expected []*endpoint.Endpoint
expectError bool
serviceLabelSelector string
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
externalIPs []string
lbs []string
serviceTypesFilter []string
expected []*endpoint.Endpoint
expectError bool
serviceLabelSelector string
resolveLoadBalancerHostname bool
}{
{
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"}},
},
},
{
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",
svcNamespace: "testing",
@ -1086,6 +1107,7 @@ func testServiceSourceEndpoints(t *testing.T) {
tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation,
sourceLabel,
tc.resolveLoadBalancerHostname,
)
require.NoError(t, err)
@ -1275,6 +1297,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
)
require.NoError(t, err)
@ -1440,6 +1463,7 @@ func TestClusterIpServices(t *testing.T) {
[]string{},
tc.ignoreHostnameAnnotation,
labelSelector,
false,
)
require.NoError(t, err)
@ -2010,6 +2034,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
)
require.NoError(t, err)
@ -2483,6 +2508,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
)
require.NoError(t, err)
@ -2840,6 +2866,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
)
require.NoError(t, err)
@ -2952,6 +2979,7 @@ func TestExternalServices(t *testing.T) {
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
)
require.NoError(t, err)
@ -3006,6 +3034,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
[]string{},
false,
labels.Everything(),
false,
)
require.NoError(b, err)

View File

@ -73,6 +73,7 @@ type Config struct {
DefaultTargets []string
OCPRouterName string
UpdateEvents bool
ResolveLoadBalancerHostname bool
}
// ClientGenerator provides clients
@ -215,7 +216,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
if err != nil {
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":
client, err := p.KubeClient()
if err != nil {