diff --git a/docs/tutorials/traefik-proxy.md b/docs/tutorials/traefik-proxy.md new file mode 100644 index 000000000..7857be1b4 --- /dev/null +++ b/docs/tutorials/traefik-proxy.md @@ -0,0 +1,98 @@ +# Configuring ExternalDNS to use the Traefik Proxy Source + +This tutorial describes how to configure ExternalDNS to use the Traefik Proxy source. +It is meant to supplement the other provider-specific setup tutorials. + +## Manifest (for clusters without RBAC enabled) + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + # update this to the desired external-dns version + image: k8s.gcr.io/external-dns/external-dns:v0.12.2 + args: + - --source=traefik-proxy + - --provider=aws + - --registry=txt + - --txt-owner-id=my-identifier +``` + +## Manifest (for clusters with RBAC enabled) + +Could be change if you have mulitple sources + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list","watch"] +- apiGroups: ["traefik.containo.us"] + resources: ["ingressroutes", "ingressroutestcps", "ingressroutesudps"] + verbs: ["get","watch","list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + # update this to the desired external-dns version + image: k8s.gcr.io/external-dns/external-dns:v0.12.2 + args: + - --source=traefik-proxy + - --provider=aws + - --registry=txt + - --txt-owner-id=my-identifier +``` diff --git a/go.mod b/go.mod index de66742d3..30b591550 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.599 github.com/transip/gotransip/v6 v6.19.0 github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 - github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 + github.com/vinyldns/go-vinyldns v0.9.16 github.com/vultr/govultr/v2 v2.17.2 go.etcd.io/etcd/api/v3 v3.5.5 go.etcd.io/etcd/client/v3 v3.5.5 @@ -167,6 +167,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/terra-farm/udnssdk v1.3.5 // indirect + github.com/traefik/paerser v0.1.9 // indirect github.com/vektah/gqlparser/v2 v2.5.0 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.5 // indirect go.mongodb.org/mongo-driver v1.5.1 // indirect @@ -189,6 +190,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/resty.v1 v1.12.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect diff --git a/go.sum b/go.sum index bb032b1b7..e80e8b7fb 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bodgit/tsig v1.2.0 h1:wNfc7yTk2OuWh/s7nEFa9h+SkIfTn7e4xlFtf1Sgvr4= github.com/bodgit/tsig v1.2.0/go.mod h1:bsN2ntwGE/s3EeoawjAoKUcAfO4Fr0nGKC72vNF/cqM= @@ -197,6 +198,7 @@ github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8n github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -797,6 +799,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= @@ -1171,6 +1175,8 @@ github.com/vektah/gqlparser/v2 v2.5.0 h1:GwEwy7AJsqPWrey0bHnn+3JLaHLZVT66wY/+O+T github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 h1:UbVjBjgJUYGD8MlobEdOR+yTeNqaNa2Gf1/nskVNCSE= github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg= +github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= +github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go index ffbb54fb2..efa472f89 100644 --- a/internal/testutils/endpoint_test.go +++ b/internal/testutils/endpoint_test.go @@ -19,11 +19,12 @@ package testutils import ( "fmt" "sort" + "testing" "sigs.k8s.io/external-dns/endpoint" ) -func ExampleSameEndpoints() { +func TestExampleSameEndpoints(t *testing.T) { eps := []*endpoint.Endpoint{ { DNSName: "example.org", diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 12759ab32..688dc0e98 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -404,7 +404,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion) // Flags related to processing source - app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver") + app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "traefik-proxy") app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName) app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) diff --git a/source/store.go b/source/store.go index 9f5d24b00..52d0dbf93 100644 --- a/source/store.go +++ b/source/store.go @@ -289,6 +289,16 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg return nil, err } return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespace) + case "traefik-proxy": + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) case "openshift-route": ocpClient, err := p.OpenShiftClient() if err != nil { diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go new file mode 100644 index 000000000..200f63e07 --- /dev/null +++ b/source/traefik_proxy.go @@ -0,0 +1,547 @@ +/* +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" + "fmt" + "sort" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/endpoint" + + traefikV1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" +) + +var ( + ingressrouteGVR = schema.GroupVersionResource{ + Group: traefikV1alpha1.SchemeGroupVersion.Group, + Version: traefikV1alpha1.SchemeGroupVersion.Version, + Resource: "ingressroutes", + } + ingressrouteTCPGVR = schema.GroupVersionResource{ + Group: traefikV1alpha1.SchemeGroupVersion.Group, + Version: traefikV1alpha1.SchemeGroupVersion.Version, + Resource: "ingressroutetcps", + } + ingressrouteUDPGVR = schema.GroupVersionResource{ + Group: traefikV1alpha1.SchemeGroupVersion.Group, + Version: traefikV1alpha1.SchemeGroupVersion.Version, + Resource: "ingressrouteudps", + } +) + +type traefikSource struct { + annotationFilter string + dynamicKubeClient dynamic.Interface + ingressRouteInformer informers.GenericInformer + ingressRouteTcpInformer informers.GenericInformer + ingressRouteUdpInformer informers.GenericInformer + kubeClient kubernetes.Interface + namespace string + unstructuredConverter *unstructuredConverter +} + +func NewTraefikSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string) (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) + + // 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{}) {}, + }, + ) + + informerFactory.Start((ctx.Done())) + + // wait for the local cache to be populated. + if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { + return nil, err + } + + uc, err := newTraefikUnstructuredConverter() + if err != nil { + return nil, errors.Wrapf(err, "failed to setup Unstructured Converter") + } + + return &traefikSource{ + annotationFilter: annotationFilter, + dynamicKubeClient: dynamicKubeClient, + ingressRouteInformer: ingressRouteInformer, + ingressRouteTcpInformer: ingressRouteTcpInformer, + ingressRouteUdpInformer: ingressRouteUdpInformer, + kubeClient: kubeClient, + namespace: namespace, + unstructuredConverter: uc, + }, nil +} + +func (ts *traefikSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + ingressRouteEndpoints, err := ts.ingressRouteEndpoints() + if err != nil { + return nil, err + } + ingressRouteTCPEndpoints, err := ts.ingressRouteTCPEndpoints() + if err != nil { + return nil, err + } + ingressRouteUDPEndpoints, err := ts.ingressRouteUDPEndpoints() + if err != nil { + return nil, err + } + + endpoints = append(endpoints, ingressRouteEndpoints...) + endpoints = append(endpoints, ingressRouteTCPEndpoints...) + endpoints = append(endpoints, ingressRouteUDPEndpoints...) + + for _, ep := range endpoints { + sort.Sort(ep.Targets) + } + + return endpoints, nil +} + +func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.ingressRouteInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRoutes []*traefikV1alpha1.IngressRoute + for _, ingressRouteObj := range irs { + unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert") + } + + ingressRoute := &traefikV1alpha1.IngressRoute{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRoutes = append(ingressRoutes, ingressRoute) + } + + ingressRoutes, err = ts.filterByAnnotationsIngressRoute(ingressRoutes) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRoute") + } + + for _, ingressRoute := range ingressRoutes { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRoute(ingressRoute, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRoute(ingressRoute, ingressEndpoints) + ts.setDualstackLabelIngressRoute(ingressRoute, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} +func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.ingressRouteTcpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRoutes []*traefikV1alpha1.IngressRouteTCP + for _, ingressRouteObj := range irs { + unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert") + } + + ingressRoute := &traefikV1alpha1.IngressRouteTCP{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRoutes = append(ingressRoutes, ingressRoute) + } + + ingressRoutes, err = ts.filterByAnnotationsIngressRouteTCP(ingressRoutes) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRoute") + } + + for _, ingressRoute := range ingressRoutes { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRouteTCP(ingressRoute, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRouteTCP(ingressRoute, ingressEndpoints) + ts.setDualstackLabelIngressRouteTCP(ingressRoute, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} +func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + irs, err := ts.ingressRouteUdpInformer.Lister().ByNamespace(ts.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var ingressRoutes []*traefikV1alpha1.IngressRouteUDP + for _, ingressRouteObj := range irs { + unstructuredHost, ok := ingressRouteObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert") + } + + ingressRoute := &traefikV1alpha1.IngressRouteUDP{} + err := ts.unstructuredConverter.scheme.Convert(unstructuredHost, ingressRoute, nil) + if err != nil { + return nil, err + } + ingressRoutes = append(ingressRoutes, ingressRoute) + } + + ingressRoutes, err = ts.filterByAnnotationsIngressRouteUDP(ingressRoutes) + if err != nil { + return nil, errors.Wrap(err, "failed to filter IngressRoute") + } + + for _, ingressRoute := range ingressRoutes { + var targets endpoint.Targets + + targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) + + fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) + + ingressEndpoints, err := ts.endpointsFromIngressRouteUDP(ingressRoute, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from IngressRoute: %s: %v", fullname, ingressEndpoints) + ts.setResourceLabelIngressRouteUDP(ingressRoute, ingressEndpoints) + ts.setDualstackLabelIngressRouteUDP(ingressRoute, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + return endpoints, nil +} + +// filterByAnnotations filters a list of IngressRoute by a given annotation selector. +func (ts *traefikSource) filterByAnnotationsIngressRoute(ingressRoutes []*traefikV1alpha1.IngressRoute) ([]*traefikV1alpha1.IngressRoute, error) { + labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*traefikV1alpha1.IngressRoute{} + + for _, ingressRoute := range ingressRoutes { + // convert the IngressRoute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include IngressRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +// filterByAnnotations filters a list of IngressRouteTCP by a given annotation selector. +func (ts *traefikSource) filterByAnnotationsIngressRouteTCP(ingressRoutes []*traefikV1alpha1.IngressRouteTCP) ([]*traefikV1alpha1.IngressRouteTCP, error) { + labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*traefikV1alpha1.IngressRouteTCP{} + + for _, ingressRoute := range ingressRoutes { + // convert the IngressRoute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include IngressRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +// filterByAnnotations filters a list of IngressRoute by a given annotation selector. +func (ts *traefikSource) filterByAnnotationsIngressRouteUDP(ingressRoutes []*traefikV1alpha1.IngressRouteUDP) ([]*traefikV1alpha1.IngressRouteUDP, error) { + labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*traefikV1alpha1.IngressRouteUDP{} + + for _, ingressRoute := range ingressRoutes { + // convert the IngressRoute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include IngressRoute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +func (ts *traefikSource) setResourceLabelIngressRoute(ingressroute *traefikV1alpha1.IngressRoute, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressroute/%s/%s", ingressroute.Namespace, ingressroute.Name) + } +} +func (ts *traefikSource) setResourceLabelIngressRouteTCP(ingressroute *traefikV1alpha1.IngressRouteTCP, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressroutetcp/%s/%s", ingressroute.Namespace, ingressroute.Name) + } +} +func (ts *traefikSource) setResourceLabelIngressRouteUDP(ingressroute *traefikV1alpha1.IngressRouteUDP, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressrouteudp/%s/%s", ingressroute.Namespace, ingressroute.Name) + } +} + +func (ts *traefikSource) setDualstackLabelIngressRoute(ingressRoute *traefikV1alpha1.IngressRoute, endpoints []*endpoint.Endpoint) { + val, ok := ingressRoute.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to IngressRoute %s/%s.", ingressRoute.Namespace, ingressRoute.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} +func (ts *traefikSource) setDualstackLabelIngressRouteTCP(ingressRoute *traefikV1alpha1.IngressRouteTCP, endpoints []*endpoint.Endpoint) { + val, ok := ingressRoute.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to IngressRouteTCP %s/%s.", ingressRoute.Namespace, ingressRoute.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} +func (ts *traefikSource) setDualstackLabelIngressRouteUDP(ingressRoute *traefikV1alpha1.IngressRouteUDP, endpoints []*endpoint.Endpoint) { + val, ok := ingressRoute.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to IngressRouteUDP %s/%s.", ingressRoute.Namespace, ingressRoute.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} + +// endpointsFromIngressRoute extracts the endpoints from a IngressRoute object +func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *traefikV1alpha1.IngressRoute, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + 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 +} +func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *traefikV1alpha1.IngressRouteTCP, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + 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 +} +func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *traefikV1alpha1.IngressRouteUDP, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + 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 +} + +func (ts *traefikSource) AddEventHandler(ctx context.Context, handler func()) { + // Right now there is no way to remove event handler from informer, see: + // https://github.com/kubernetes/kubernetes/issues/79610 + log.Debug("Adding event handler for IngressRoute") + ts.ingressRouteInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + log.Debug("Adding event handler for IngressRouteTCP") + ts.ingressRouteTcpInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + log.Debug("Adding event handler for IngressRouteUDP") + ts.ingressRouteUdpInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) +} + +// newTraefikUnstructuredConverter returns a new unstructuredConverter initialized +func newTraefikUnstructuredConverter() (*unstructuredConverter, error) { + uc := &unstructuredConverter{ + scheme: runtime.NewScheme(), + } + + // 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{}) + if err := scheme.AddToScheme(uc.scheme); err != nil { + return nil, err + } + + return uc, nil +}