diff --git a/docs/tutorials/traefik-proxy.md b/docs/tutorials/traefik-proxy.md index ed00ae4f94..8bfae7f235 100644 --- a/docs/tutorials/traefik-proxy.md +++ b/docs/tutorials/traefik-proxy.md @@ -138,3 +138,21 @@ Now that we have verified that ExternalDNS will automatically manage Traefik DNS $ kubectl delete -f traefik-ingress.yaml $ kubectl delete -f externaldns.yaml ``` + +## Additional Flags + +| Flag | Description | +| --- | --- | +| --traefik-disable-legacy | Disable listeners on Resources under traefik.containo.us | +| --traefik-disable-new | Disable listeners on Resources under traefik.io | + +### Disabling Resource Listeners + +Traefik has deprecated the legacy API group, traefik.containo.us, in favor of traefik.io. By default the traefik-proxy source will listen for resources under both API groups; however, this may cause timeouts with the following message + +``` +FATA[0060] failed to sync traefik.io/v1alpha1, Resource=ingressroutes: context deadline exceeded +``` + +In this case you can disable one or the other API groups with `--traefik-disable-new` or `--traefik-disable-legacy` + diff --git a/main.go b/main.go index d0756f84d3..3e710575d2 100644 --- a/main.go +++ b/main.go @@ -155,6 +155,8 @@ func main() { OCPRouterName: cfg.OCPRouterName, UpdateEvents: cfg.UpdateEvents, ResolveLoadBalancerHostname: cfg.ResolveServiceLoadBalancerHostname, + TraefikDisableLegacy: cfg.TraefikDisableLegacy, + TraefikDisableNew: cfg.TraefikDisableNew, } // Lookup all the selected sources by names and pass them the desired configuration. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 0ae71197d9..46d46ad978 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -215,6 +215,8 @@ type Config struct { WebhookProviderReadTimeout time.Duration WebhookProviderWriteTimeout time.Duration WebhookServer bool + TraefikDisableLegacy bool + TraefikDisableNew bool } var defaultConfig = &Config{ @@ -369,6 +371,8 @@ var defaultConfig = &Config{ WebhookProviderReadTimeout: 5 * time.Second, WebhookProviderWriteTimeout: 10 * time.Second, WebhookServer: false, + TraefikDisableLegacy: false, + TraefikDisableNew: false, } // NewConfig returns new Config object @@ -456,6 +460,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("default-targets", "Set globally default host/IP that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets) app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter) app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) + app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy) + app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) // Flags related to providers providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "infoblox", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "webhook"} diff --git a/source/store.go b/source/store.go index 5f6b9dc35e..cdb993b6b7 100644 --- a/source/store.go +++ b/source/store.go @@ -74,6 +74,8 @@ type Config struct { OCPRouterName string UpdateEvents bool ResolveLoadBalancerHostname bool + TraefikDisableLegacy bool + TraefikDisableNew bool } // ClientGenerator provides clients @@ -300,7 +302,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg if err != nil { return nil, err } - return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation) + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikDisableLegacy, cfg.TraefikDisableNew) case "openshift-route": ocpClient, err := p.OpenShiftClient() if err != nil { diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index d9140c6401..0c79e95315 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -93,48 +93,54 @@ type traefikSource struct { unstructuredConverter *unstructuredConverter } -func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string, ignoreHostnameAnnotation bool) (Source, error) { +func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string, ignoreHostnameAnnotation bool, disableLegacy bool, disableNew bool) (Source, error) { // Use shared informer to listen for add/update/delete of Host in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) - ingressRouteInformer := informerFactory.ForResource(ingressrouteGVR) - ingressRouteTcpInformer := informerFactory.ForResource(ingressrouteTCPGVR) - ingressRouteUdpInformer := informerFactory.ForResource(ingressrouteUDPGVR) - oldIngressRouteInformer := informerFactory.ForResource(oldIngressrouteGVR) - oldIngressRouteTcpInformer := informerFactory.ForResource(oldIngressrouteTCPGVR) - oldIngressRouteUdpInformer := informerFactory.ForResource(oldIngressrouteUDPGVR) + var ingressRouteInformer, ingressRouteTcpInformer, ingressRouteUdpInformer informers.GenericInformer + var oldIngressRouteInformer, oldIngressRouteTcpInformer, oldIngressRouteUdpInformer informers.GenericInformer // Add default resource event handlers to properly initialize informers. - ingressRouteInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) {}, - }, - ) - ingressRouteTcpInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) {}, - }, - ) - ingressRouteUdpInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) {}, - }, - ) - oldIngressRouteInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) {}, - }, - ) - oldIngressRouteTcpInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) {}, - }, - ) - oldIngressRouteUdpInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) {}, - }, - ) + if !disableNew { + ingressRouteInformer = informerFactory.ForResource(ingressrouteGVR) + ingressRouteTcpInformer = informerFactory.ForResource(ingressrouteTCPGVR) + ingressRouteUdpInformer = informerFactory.ForResource(ingressrouteUDPGVR) + ingressRouteInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + ingressRouteTcpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + ingressRouteUdpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + } + if !disableLegacy { + oldIngressRouteInformer = informerFactory.ForResource(oldIngressrouteGVR) + oldIngressRouteTcpInformer = informerFactory.ForResource(oldIngressrouteTCPGVR) + oldIngressRouteUdpInformer = informerFactory.ForResource(oldIngressrouteUDPGVR) + oldIngressRouteInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + oldIngressRouteTcpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + oldIngressRouteUdpInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) {}, + }, + ) + } informerFactory.Start((ctx.Done())) @@ -167,38 +173,49 @@ func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, func (ts *traefikSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint - ingressRouteEndpoints, err := ts.ingressRouteEndpoints() - if err != nil { - return nil, err + if ts.ingressRouteInformer != nil { + ingressRouteEndpoints, err := ts.ingressRouteEndpoints() + if err != nil { + return nil, err + } + endpoints = append(endpoints, ingressRouteEndpoints...) } - oldIngressRouteEndpoints, err := ts.oldIngressRouteEndpoints() - if err != nil { - return nil, err + if ts.oldIngressRouteInformer != nil { + oldIngressRouteEndpoints, err := ts.oldIngressRouteEndpoints() + if err != nil { + return nil, err + } + endpoints = append(endpoints, oldIngressRouteEndpoints...) } - ingressRouteTCPEndpoints, err := ts.ingressRouteTCPEndpoints() - if err != nil { - return nil, err + if ts.ingressRouteTcpInformer != nil { + ingressRouteTcpEndpoints, err := ts.ingressRouteTCPEndpoints() + if err != nil { + return nil, err + } + endpoints = append(endpoints, ingressRouteTcpEndpoints...) } - oldIngressRouteTCPEndpoints, err := ts.oldIngressRouteTCPEndpoints() - if err != nil { - return nil, err + if ts.oldIngressRouteTcpInformer != nil { + oldIngressRouteTcpEndpoints, err := ts.oldIngressRouteTCPEndpoints() + if err != nil { + return nil, err + } + endpoints = append(endpoints, oldIngressRouteTcpEndpoints...) } - ingressRouteUDPEndpoints, err := ts.ingressRouteUDPEndpoints() - if err != nil { - return nil, err + if ts.ingressRouteUdpInformer != nil { + ingressRouteUdpEndpoints, err := ts.ingressRouteUDPEndpoints() + if err != nil { + return nil, err + } + endpoints = append(endpoints, ingressRouteUdpEndpoints...) } - oldIngressRouteUDPEndpoints, err := ts.oldIngressRouteUDPEndpoints() - if err != nil { - return nil, err + if ts.oldIngressRouteUdpInformer != nil { + oldIngressRouteUdpEndpoints, err := ts.oldIngressRouteUDPEndpoints() + if err != nil { + return nil, err + } + endpoints = append(endpoints, oldIngressRouteUdpEndpoints...) } - endpoints = append(endpoints, ingressRouteEndpoints...) - endpoints = append(endpoints, ingressRouteTCPEndpoints...) - endpoints = append(endpoints, ingressRouteUDPEndpoints...) - endpoints = append(endpoints, oldIngressRouteEndpoints...) - endpoints = append(endpoints, oldIngressRouteTCPEndpoints...) - endpoints = append(endpoints, oldIngressRouteUDPEndpoints...) - for _, ep := range endpoints { sort.Sort(ep.Targets) } diff --git a/source/traefik_proxy_test.go b/source/traefik_proxy_test.go index 4905bb0b7d..730d833a84 100644 --- a/source/traefik_proxy_test.go +++ b/source/traefik_proxy_test.go @@ -19,6 +19,7 @@ package source import ( "context" "encoding/json" + "k8s.io/apimachinery/pkg/runtime/schema" "testing" "github.com/stretchr/testify/assert" @@ -348,7 +349,7 @@ func TestTraefikProxyIngressRouteEndpoints(t *testing.T) { _, 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", ti.ignoreHostnameAnnotation) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -642,7 +643,7 @@ func TestTraefikProxyIngressRouteTCPEndpoints(t *testing.T) { _, 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", ti.ignoreHostnameAnnotation) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -784,7 +785,7 @@ func TestTraefikProxyIngressRouteUDPEndpoints(t *testing.T) { _, 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", ti.ignoreHostnameAnnotation) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1114,7 +1115,7 @@ func TestTraefikProxyOldIngressRouteEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(oldIngressrouteGVR).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", ti.ignoreHostnameAnnotation) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1408,7 +1409,7 @@ func TestTraefikProxyOldIngressRouteTCPEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(oldIngressrouteTCPGVR).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", ti.ignoreHostnameAnnotation) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1550,7 +1551,7 @@ func TestTraefikProxyOldIngressRouteUDPEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(oldIngressrouteUDPGVR).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", ti.ignoreHostnameAnnotation) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1566,3 +1567,166 @@ func TestTraefikProxyOldIngressRouteUDPEndpoints(t *testing.T) { }) } } + +func TestTraefikAPIGroupDisableFlags(t *testing.T) { + t.Parallel() + + for _, ti := range []struct { + title string + ingressRoute IngressRoute + gvr schema.GroupVersionResource + ignoreHostnameAnnotation bool + disableLegacy bool + disableNew bool + expected []*endpoint.Endpoint + }{ + { + title: "IngressRoute.traefik.containo.us with the legacy API group enabled", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().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", + }, + }, + }, + gvr: oldIngressrouteGVR, + disableLegacy: false, + disableNew: false, + 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.traefik.containo.us with the legacy API group disabled", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: oldIngressrouteGVR.GroupVersion().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", + }, + }, + }, + gvr: oldIngressrouteGVR, + disableLegacy: true, + disableNew: false, + }, + { + title: "IngressRoute.traefik.io with the new API group enabled", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().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", + }, + }, + }, + gvr: ingressrouteGVR, + disableLegacy: false, + disableNew: false, + 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.traefik.io with the new API group disabled", + ingressRoute: IngressRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ingressrouteGVR.GroupVersion().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", + }, + }, + }, + gvr: ingressrouteGVR, + disableLegacy: false, + disableNew: true, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + t.Parallel() + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(ingressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(ingressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(ingressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &IngressRouteUDPList{}) + scheme.AddKnownTypes(oldIngressrouteGVR.GroupVersion(), &IngressRoute{}, &IngressRouteList{}) + scheme.AddKnownTypes(oldIngressrouteTCPGVR.GroupVersion(), &IngressRouteTCP{}, &IngressRouteTCPList{}) + scheme.AddKnownTypes(oldIngressrouteUDPGVR.GroupVersion(), &IngressRouteUDP{}, &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(ti.gvr).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", ti.ignoreHostnameAnnotation, ti.disableLegacy, ti.disableNew) + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(ti.gvr).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, ti.expected, endpoints) + }) + } +}