diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index 161977c33..d6e746952 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -19,7 +19,9 @@ package source import ( "context" "fmt" + "regexp" "sort" + "strings" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -57,6 +59,11 @@ var ( } ) +var ( + traefikHostExtractor = regexp.MustCompile(`(?:HostSNI|HostHeader|Host)\s*\(\s*(\x60.*?\x60)\s*\)`) + traefikValueProcessor = regexp.MustCompile(`\x60([^,\x60]+)\x60`) +) + type traefikSource struct { annotationFilter string dynamicKubeClient dynamic.Interface @@ -459,14 +466,17 @@ func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *traefikV1alpha1 endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) } - // TODO: Implement Traefik router rule logic/regex magic - // if ingressRoute.Spec.Rules != nil { - // for _, rule := range ingressRoute.Spec.Rules { - // if rule.Host != "" { - // endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...) - // } - // } - // } + for _, route := range ingressRoute.Spec.Routes { + match := route.Match + + for _, hostEntry := range traefikHostExtractor.FindAllString(match, -1) { + for _, host := range traefikValueProcessor.FindAllString(hostEntry, -1) { + host = strings.TrimPrefix(host, "`") + host = strings.TrimSuffix(host, "`") + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + } + } + } return endpoints, nil } @@ -487,14 +497,17 @@ func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *traefikV1alp endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) } - // TODO: Implement Traefik router rule logic/regex magic - // if ingressRoute.Spec.Rules != nil { - // for _, rule := range ingressRoute.Spec.Rules { - // if rule.Host != "" { - // endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...) - // } - // } - // } + for _, route := range ingressRoute.Spec.Routes { + match := route.Match + + for _, hostEntry := range traefikHostExtractor.FindAllString(match, -1) { + for _, host := range traefikValueProcessor.FindAllString(hostEntry, -1) { + host = strings.TrimPrefix(host, "`") + host = strings.TrimSuffix(host, "`") + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier)...) + } + } + } return endpoints, nil } @@ -515,15 +528,6 @@ func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *traefikV1alp endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) } - // TODO: Implement Traefik router rule logic/regex magic - // if ingressRoute.Spec.Rules != nil { - // for _, rule := range ingressRoute.Spec.Rules { - // if rule.Host != "" { - // endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...) - // } - // } - // } - return endpoints, nil } @@ -547,7 +551,7 @@ func newTraefikUnstructuredConverter() (*unstructuredConverter, error) { // Add the core types we need uc.scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &traefikV1alpha1.IngressRoute{}, &traefikV1alpha1.IngressRouteList{}) uc.scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteTCP{}, &traefikV1alpha1.IngressRouteTCPList{}) - uc.scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteUDP{}, &traefikV1alpha1.IngressRouteUDP{}) + uc.scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteUDP{}, &traefikV1alpha1.IngressRouteUDPList{}) if err := scheme.AddToScheme(uc.scheme); err != nil { return nil, err } diff --git a/source/traefik_proxy_test.go b/source/traefik_proxy_test.go new file mode 100644 index 000000000..4b9222684 --- /dev/null +++ b/source/traefik_proxy_test.go @@ -0,0 +1,625 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + traefikV1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + fakeDynamic "k8s.io/client-go/dynamic/fake" + fakeKube "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/external-dns/endpoint" +) + +// This is a compile-time validation that traefikSource is a Source. +var _ Source = &traefikSource{} + +const defaultTraefikNamespace = "traefik" + +func TestTraefikProxyIngressRouteEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRoute traefikV1alpha1.IngressRoute + expected []*endpoint.Endpoint + }{ + { + title: "IngressRoute with hostname annotation", + ingressRoute: traefikV1alpha1.IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with host rule", + ingressRoute: traefikV1alpha1.IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteSpec{ + Routes: []traefikV1alpha1.Route{ + { + Match: "Host(`b.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with hostheader rule", + ingressRoute: traefikV1alpha1.IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-hostheader-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteSpec{ + Routes: []traefikV1alpha1.Route{ + { + Match: "HostHeader(`c.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "c.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-hostheader-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with multiple host rules", + ingressRoute: traefikV1alpha1.IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-multi-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteSpec{ + Routes: []traefikV1alpha1.Route{ + { + Match: "Host(`d.example.com`) || Host(`e.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRoute with multiple host rules and annotation", + ingressRoute: traefikV1alpha1.IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroute-multi-host-annotations-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "f.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteSpec{ + Routes: []traefikV1alpha1.Route{ + { + Match: "Host(`g.example.com`, `h.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "f.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroute/traefik/ingressroute-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &traefikV1alpha1.IngressRoute{}, &traefikV1alpha1.IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteTCP{}, &traefikV1alpha1.IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteUDP{}, &traefikV1alpha1.IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRoute) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(ingressrouteGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ingressrouteGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyIngressRouteTCPEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRouteTCP traefikV1alpha1.IngressRouteTCP + expected []*endpoint.Endpoint + }{ + { + title: "IngressRouteTCP with hostname annotation", + ingressRouteTCP: traefikV1alpha1.IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with host sni rule", + ingressRouteTCP: traefikV1alpha1.IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-hostsni-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteTCPSpec{ + Routes: []traefikV1alpha1.RouteTCP{ + { + Match: "HostSNI(`b.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-hostsni-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple host sni rules", + ingressRouteTCP: traefikV1alpha1.IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-multi-host-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteTCPSpec{ + Routes: []traefikV1alpha1.RouteTCP{ + { + Match: "HostSNI(`d.example.com`) || HostSNI(`e.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple host sni rules and annotation", + ingressRouteTCP: traefikV1alpha1.IngressRouteTCP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRouteTCP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressroutetcp-multi-host-annotations-match", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "f.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + Spec: traefikV1alpha1.IngressRouteTCPSpec{ + Routes: []traefikV1alpha1.RouteTCP{ + { + Match: "HostSNI(`g.example.com`, `h.example.com`)", + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "f.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "g.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "h.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressroutetcp/traefik/ingressroutetcp-multi-host-annotations-match", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &traefikV1alpha1.IngressRoute{}, &traefikV1alpha1.IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteTCP{}, &traefikV1alpha1.IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteUDP{}, &traefikV1alpha1.IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRouteTCP) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(ingressrouteTCPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ingressrouteTCPGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} + +func TestTraefikProxyIngressRouteUDPEndpoints(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRouteUDP traefikV1alpha1.IngressRouteUDP + expected []*endpoint.Endpoint + }{ + { + title: "IngressRouteTCP with hostname annotation", + ingressRouteUDP: traefikV1alpha1.IngressRouteUDP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRouteUDP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressrouteudp-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "IngressRouteTCP with multiple hostname annotation", + ingressRouteUDP: traefikV1alpha1.IngressRouteUDP{ + TypeMeta: metav1.TypeMeta{ + APIVersion: traefikV1alpha1.SchemeGroupVersion.String(), + Kind: "IngressRouteUDP", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "ingressrouteudp-multi-annotation", + Namespace: defaultTraefikNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com, b.example.com", + "external-dns.alpha.kubernetes.io/target": "target.domain.tld", + "kubernetes.io/ingress.class": "traefik", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-multi-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "b.example.com", + Targets: []string{"target.domain.tld"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "ingressrouteudp/traefik/ingressrouteudp-multi-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &traefikV1alpha1.IngressRoute{}, &traefikV1alpha1.IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteTCP{}, &traefikV1alpha1.IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &traefikV1alpha1.IngressRouteUDP{}, &traefikV1alpha1.IngressRouteUDPList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + ir := unstructured.Unstructured{} + + ingressRouteAsJSON, err := json.Marshal(ti.ingressRouteUDP) + assert.NoError(t, err) + + assert.NoError(t, ir.UnmarshalJSON(ingressRouteAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(ingressrouteUDPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ingressrouteUDPGVR).Namespace(defaultTraefikNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +}