Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: resolve dns targets in ingress based records to create A/AAAA instead of CNAME #4255

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/sources/ingress.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ the values from that.

2. Otherwise, iterates over the Ingress's `status.loadBalancer.ingress`,
adding each non-empty `ip` and `hostname`.

In the case that `--resolve-ingress-target-hostname` set, it will resolve hostnames in the status to create
A/AAAA records instead of CNAME.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say this is a feature request to the controller that creates the status field.

1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func main() {
OCPRouterName: cfg.OCPRouterName,
UpdateEvents: cfg.UpdateEvents,
ResolveLoadBalancerHostname: cfg.ResolveServiceLoadBalancerHostname,
ResolveIngressTargetHostname: cfg.ResolveIngressTargetHostname,
TraefikDisableLegacy: cfg.TraefikDisableLegacy,
TraefikDisableNew: cfg.TraefikDisableNew,
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ type Config struct {
CFUsername string
CFPassword string
ResolveServiceLoadBalancerHostname bool
ResolveIngressTargetHostname bool
RFC2136Host string
RFC2136Port int
RFC2136Zone []string
Expand Down Expand Up @@ -431,6 +432,7 @@ func (cfg *Config) ParseFlags(args []string) error {
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-service-load-balancer-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.ResolveServiceLoadBalancerHostname)
app.Flag("resolve-ingress-target-hostname", "Resolve the hostname of Ingress target to IP addresses in order to create DNS A/AAAA records instead of CNAMEs").BoolVar(&cfg.ResolveIngressTargetHostname)

// 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)
Expand Down
72 changes: 43 additions & 29 deletions source/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"net"
"sort"
"strings"
"text/template"
Expand Down Expand Up @@ -54,21 +55,22 @@ const (
// Use targetAnnotationKey to explicitly set Endpoint. (useful if the ingress
// controller does not update, or to override with alternative endpoint)
type ingressSource struct {
client kubernetes.Interface
namespace string
annotationFilter string
ingressClassNames []string
fqdnTemplate *template.Template
combineFQDNAnnotation bool
ignoreHostnameAnnotation bool
ingressInformer netinformers.IngressInformer
ignoreIngressTLSSpec bool
ignoreIngressRulesSpec bool
labelSelector labels.Selector
client kubernetes.Interface
namespace string
annotationFilter string
ingressClassNames []string
fqdnTemplate *template.Template
combineFQDNAnnotation bool
ignoreHostnameAnnotation bool
ingressInformer netinformers.IngressInformer
ignoreIngressTLSSpec bool
ignoreIngressRulesSpec bool
labelSelector labels.Selector
resolveIngressTargetHostname bool
}

// NewIngressSource creates a new ingressSource with the given config.
func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string) (Source, error) {
func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector, ingressClassNames []string, resolveIngressTargetHostname bool) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate)
if err != nil {
return nil, err
Expand Down Expand Up @@ -110,17 +112,18 @@ func NewIngressSource(ctx context.Context, kubeClient kubernetes.Interface, name
}

sc := &ingressSource{
client: kubeClient,
namespace: namespace,
annotationFilter: annotationFilter,
ingressClassNames: ingressClassNames,
fqdnTemplate: tmpl,
combineFQDNAnnotation: combineFqdnAnnotation,
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
ingressInformer: ingressInformer,
ignoreIngressTLSSpec: ignoreIngressTLSSpec,
ignoreIngressRulesSpec: ignoreIngressRulesSpec,
labelSelector: labelSelector,
client: kubeClient,
namespace: namespace,
annotationFilter: annotationFilter,
ingressClassNames: ingressClassNames,
fqdnTemplate: tmpl,
combineFQDNAnnotation: combineFqdnAnnotation,
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
ingressInformer: ingressInformer,
ignoreIngressTLSSpec: ignoreIngressTLSSpec,
ignoreIngressRulesSpec: ignoreIngressRulesSpec,
labelSelector: labelSelector,
resolveIngressTargetHostname: resolveIngressTargetHostname,
}
return sc, nil
}
Expand Down Expand Up @@ -153,7 +156,7 @@ func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
continue
}

ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation, sc.ignoreIngressTLSSpec, sc.ignoreIngressRulesSpec)
ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation, sc.ignoreIngressTLSSpec, sc.ignoreIngressRulesSpec, sc.resolveIngressTargetHostname)

// apply template if host is missing on ingress
if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
Expand Down Expand Up @@ -194,7 +197,7 @@ func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpo

targets := getTargetsFromTargetAnnotation(ing.Annotations)
if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status)
targets = targetsFromIngressStatus(ing.Status, sc.resolveIngressTargetHostname)
}

providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
Expand Down Expand Up @@ -285,15 +288,15 @@ func (sc *ingressSource) setDualstackLabel(ingress *networkv1.Ingress, endpoints
}

// endpointsFromIngress extracts the endpoints from ingress object
func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool) []*endpoint.Endpoint {
func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, resolveIngressTargetHostname bool) []*endpoint.Endpoint {
resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)

ttl := getTTLFromAnnotations(ing.Annotations, resource)

targets := getTargetsFromTargetAnnotation(ing.Annotations)

if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status)
targets = targetsFromIngressStatus(ing.Status, resolveIngressTargetHostname)
}

providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
Expand Down Expand Up @@ -347,15 +350,26 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool,
return endpoints
}

func targetsFromIngressStatus(status networkv1.IngressStatus) endpoint.Targets {
func targetsFromIngressStatus(status networkv1.IngressStatus, resolveIngressTargetHostname bool) endpoint.Targets {
var targets endpoint.Targets

for _, lb := range status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
if resolveIngressTargetHostname {
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)
}
}
}

Expand Down
56 changes: 53 additions & 3 deletions source/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (suite *IngressSuite) SetupTest() {
false,
labels.Everything(),
[]string{},
false,
)
suite.NoError(err, "should initialize ingress source")
}
Expand Down Expand Up @@ -103,6 +104,7 @@ func TestNewIngressSource(t *testing.T) {
combineFQDNAndAnnotation bool
expectError bool
ingressClassNames []string
resolveIngress bool
}{
{
title: "invalid template",
Expand Down Expand Up @@ -162,6 +164,7 @@ func TestNewIngressSource(t *testing.T) {
false,
labels.Everything(),
ti.ingressClassNames,
ti.resolveIngress,
)
if ti.expectError {
assert.Error(t, err)
Expand All @@ -182,6 +185,7 @@ func testEndpointsFromIngress(t *testing.T) {
ignoreIngressTLSSpec bool
ignoreIngressRulesSpec bool
expected []*endpoint.Endpoint
resolveIngress bool
}{
{
title: "one rule.host one lb.hostname",
Expand Down Expand Up @@ -267,14 +271,33 @@ func testEndpointsFromIngress(t *testing.T) {
{
title: "invalid hostname does not generate endpoints",
ingress: fakeIngress{
dnsnames: []string{"this-is-an-exceedingly-long-label-that-external-dns-should-reject.example.org"},
dnsnames: []string{"this-is-an-exceedingly-long-label-that-external-dns-should-reject.example.org"},
},
expected: []*endpoint.Endpoint{},
}, {
title: "one rule.host one lb.hostname with resolve true",
resolveIngress: true,
ingress: fakeIngress{
dnsnames: []string{"foo.bar"},
hostnames: []string{"example.org"}, // Use a resolvable hostname for testing.
},
expected: []*endpoint.Endpoint{
{
DNSName: "foo.bar",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"93.184.216.34"},
},
{
DNSName: "foo.bar",
RecordType: endpoint.RecordTypeAAAA,
Targets: endpoint.Targets{"2606:2800:220:1:248:1893:25c8:1946"},
},
},
},
} {
t.Run(ti.title, func(t *testing.T) {
realIngress := ti.ingress.Ingress()
validateEndpoints(t, endpointsFromIngress(realIngress, ti.ignoreHostnameAnnotation, ti.ignoreIngressTLSSpec, ti.ignoreIngressRulesSpec), ti.expected)
validateEndpoints(t, endpointsFromIngress(realIngress, ti.ignoreHostnameAnnotation, ti.ignoreIngressTLSSpec, ti.ignoreIngressRulesSpec, ti.resolveIngress), ti.expected)
})
}
}
Expand Down Expand Up @@ -373,7 +396,7 @@ func testEndpointsFromIngressHostnameSourceAnnotation(t *testing.T) {
} {
t.Run(ti.title, func(t *testing.T) {
realIngress := ti.ingress.Ingress()
validateEndpoints(t, endpointsFromIngress(realIngress, false, false, false), ti.expected)
validateEndpoints(t, endpointsFromIngress(realIngress, false, false, false, false), ti.expected)
})
}
}
Expand All @@ -396,6 +419,7 @@ func testIngressEndpoints(t *testing.T) {
ignoreIngressRulesSpec bool
ingressLabelSelector labels.Selector
ingressClassNames []string
resolveIngress bool
}{
{
title: "no ingress",
Expand Down Expand Up @@ -1388,6 +1412,31 @@ func testIngressEndpoints(t *testing.T) {
},
expected: []*endpoint.Endpoint{},
},
{
title: "simple ingress with resolving",
resolveIngress: true,
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake-with-resolv1",
namespace: namespace,
dnsnames: []string{"foo.bar"},
hostnames: []string{"example.org"}, // Use a resolvable hostname for testing.
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "foo.bar",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"93.184.216.34"},
},
{
DNSName: "foo.bar",
RecordType: endpoint.RecordTypeAAAA,
Targets: endpoint.Targets{"2606:2800:220:1:248:1893:25c8:1946"},
},
},
},
} {
ti := ti
t.Run(ti.title, func(t *testing.T) {
Expand Down Expand Up @@ -1416,6 +1465,7 @@ func testIngressEndpoints(t *testing.T) {
ti.ignoreIngressRulesSpec,
ti.ingressLabelSelector,
ti.ingressClassNames,
ti.resolveIngress,
)
// Informer cache has all of the ingresses. Retrieve and validate their endpoints.
res, err := source.Endpoints(context.Background())
Expand Down
3 changes: 2 additions & 1 deletion source/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type Config struct {
OCPRouterName string
UpdateEvents bool
ResolveLoadBalancerHostname bool
ResolveIngressTargetHostname bool
TraefikDisableLegacy bool
TraefikDisableNew bool
}
Expand Down Expand Up @@ -224,7 +225,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
if err != nil {
return nil, err
}
return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames)
return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames, cfg.ResolveLoadBalancerHostname)
case "pod":
client, err := p.KubeClient()
if err != nil {
Expand Down