Skip to content

Commit

Permalink
add feature to create ingress A/AAAA instead of CNAME
Browse files Browse the repository at this point in the history
Signed-off-by: n-marton <marton@natko.hu>
  • Loading branch information
n-marton committed Feb 12, 2024
1 parent f71a822 commit 1f3c350
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 32 deletions.
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 @@ -427,6 +428,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
55 changes: 53 additions & 2 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 @@ -264,10 +268,30 @@ func testEndpointsFromIngress(t *testing.T) {
expected: []*endpoint.Endpoint{},
ignoreIngressRulesSpec: true,
},
{
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 @@ -366,7 +390,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 @@ -389,6 +413,7 @@ func testIngressEndpoints(t *testing.T) {
ignoreIngressRulesSpec bool
ingressLabelSelector labels.Selector
ingressClassNames []string
resolveIngress bool
}{
{
title: "no ingress",
Expand Down Expand Up @@ -1381,6 +1406,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 @@ -1409,6 +1459,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

0 comments on commit 1f3c350

Please sign in to comment.