Label filtering for Ingress, Service, Openshift Route sources

Currently the `--label-filter` flag can only be used to filter CRDs
which match the label selector passed through that flag. This change
extends the functionality to the Ingress, Service and Openshift Route
type objects. When the flag is not specified the default value is
`labels.Everything()` which is an empty string, the same as before.
Annotation based filter is inefficient because the filtering has to be
done in the controller instead of the API server like with label
filtering.
This commit is contained in:
Arjun Naik 2021-10-13 09:51:28 +02:00
parent 3676ea0b79
commit d91b7e6b8f
13 changed files with 920 additions and 1126 deletions

View File

@ -255,7 +255,7 @@ The internal one should provision hostnames used on the internal network (perhap
one to expose DNS to the internet. one to expose DNS to the internet.
To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to
an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external` an instance of an ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external`
then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-internal)` then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-internal)`
and one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)`. and one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)`.
@ -265,6 +265,11 @@ If you need to search for multiple values of said annotation, you can provide a
Beware when using multiple sources, e.g. `--source=service --source=ingress`, `--annotation-filter` will filter every given source objects. Beware when using multiple sources, e.g. `--source=service --source=ingress`, `--annotation-filter` will filter every given source objects.
If you need to filter only one specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`. If you need to filter only one specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`.
**Note:** Filtering based on annotation means that the external-dns controller will receive all resources of that kind and then filter on the client-side.
In larger clusters with many resources which change frequently this can cause performance issues. If only some resources need to be managed by an instance
of external-dns then label filtering can be used instead of annotation filtering. This means that only those resources which match the selector specified
in `--label-filter` will be passed to the controller.
### How do I specify that I want the DNS record to point to either the Node's public or private IP when it has both? ### How do I specify that I want the DNS record to point to either the Node's public or private IP when it has both?
If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other. If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other.

View File

@ -26,6 +26,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/labels"
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"sigs.k8s.io/external-dns/controller" "sigs.k8s.io/external-dns/controller"
@ -99,11 +100,14 @@ func main() {
go serveMetrics(cfg.MetricsAddress) go serveMetrics(cfg.MetricsAddress)
go handleSigterm(cancel) go handleSigterm(cancel)
// error is explicitly ignored because the filter is already validated in validation.ValidateConfig
labelSelector, _ := labels.Parse(cfg.LabelFilter)
// Create a source.Config from the flags passed by the user. // Create a source.Config from the flags passed by the user.
sourceCfg := &source.Config{ sourceCfg := &source.Config{
Namespace: cfg.Namespace, Namespace: cfg.Namespace,
AnnotationFilter: cfg.AnnotationFilter, AnnotationFilter: cfg.AnnotationFilter,
LabelFilter: cfg.LabelFilter, LabelFilter: labelSelector,
FQDNTemplate: cfg.FQDNTemplate, FQDNTemplate: cfg.FQDNTemplate,
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation, CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation, IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,

View File

@ -23,6 +23,8 @@ import (
"strconv" "strconv"
"time" "time"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"github.com/alecthomas/kingpin" "github.com/alecthomas/kingpin"
@ -185,7 +187,7 @@ var defaultConfig = &Config{
Sources: nil, Sources: nil,
Namespace: "", Namespace: "",
AnnotationFilter: "", AnnotationFilter: "",
LabelFilter: "", LabelFilter: labels.Everything().String(),
FQDNTemplate: "", FQDNTemplate: "",
CombineFQDNAndAnnotation: false, CombineFQDNAndAnnotation: false,
IgnoreHostnameAnnotation: false, IgnoreHostnameAnnotation: false,
@ -361,7 +363,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress") app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress")
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) 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) 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)
app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently only supported by source CRD").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently supported by source types CRD, ingress, service and openshift-route").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate) app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation) app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation) app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)

View File

@ -20,6 +20,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/pkg/apis/externaldns"
) )
@ -110,5 +112,9 @@ func ValidateConfig(cfg *externaldns.Config) error {
return errors.New("txt-prefix and txt-suffix are mutual exclusive") return errors.New("txt-prefix and txt-suffix are mutual exclusive")
} }
_, err := labels.Parse(cfg.LabelFilter)
if err != nil {
return errors.New("--label-filter does not specify a valid label selector")
}
return nil return nil
} }

View File

@ -43,7 +43,7 @@ type crdSource struct {
crdResource string crdResource string
codec runtime.ParameterCodec codec runtime.ParameterCodec
annotationFilter string annotationFilter string
labelFilter string labelSelector labels.Selector
} }
func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error { func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error {
@ -103,12 +103,12 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiS
} }
// NewCRDSource creates a new crdSource with the given config. // NewCRDSource creates a new crdSource with the given config.
func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelFilter string, scheme *runtime.Scheme) (Source, error) { func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFilter string, labelSelector labels.Selector, scheme *runtime.Scheme) (Source, error) {
return &crdSource{ return &crdSource{
crdResource: strings.ToLower(kind) + "s", crdResource: strings.ToLower(kind) + "s",
namespace: namespace, namespace: namespace,
annotationFilter: annotationFilter, annotationFilter: annotationFilter,
labelFilter: labelFilter, labelSelector: labelSelector,
crdClient: crdClient, crdClient: crdClient,
codec: runtime.NewParameterCodec(scheme), codec: runtime.NewParameterCodec(scheme),
}, nil }, nil
@ -126,11 +126,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
err error err error
) )
if cs.labelFilter != "" { result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelSelector.String()})
result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelFilter})
} else {
result, err = cs.List(ctx, &metav1.ListOptions{})
}
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -30,6 +30,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
@ -381,9 +382,13 @@ func testCRDSourceEndpoints(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
addKnownTypes(scheme, groupVersion) require.NoError(t, addKnownTypes(scheme, groupVersion))
cs, _ := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, ti.labelFilter, scheme) labelSelector, err := labels.Parse(ti.labelFilter)
require.NoError(t, err)
cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme)
require.NoError(t, err)
receivedEndpoints, err := cs.Endpoints(context.Background()) receivedEndpoints, err := cs.Endpoints(context.Background())
if ti.expectError { if ti.expectError {

View File

@ -60,10 +60,11 @@ type ingressSource struct {
ingressInformer netinformers.IngressInformer ingressInformer netinformers.IngressInformer
ignoreIngressTLSSpec bool ignoreIngressTLSSpec bool
ignoreIngressRulesSpec bool ignoreIngressRulesSpec bool
labelSelector labels.Selector
} }
// NewIngressSource creates a new ingressSource with the given config. // NewIngressSource creates a new ingressSource with the given config.
func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool) (Source, error) { func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool, labelSelector labels.Selector) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate) tmpl, err := parseTemplate(fqdnTemplate)
if err != nil { if err != nil {
return nil, err return nil, err
@ -100,6 +101,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
ingressInformer: ingressInformer, ingressInformer: ingressInformer,
ignoreIngressTLSSpec: ignoreIngressTLSSpec, ignoreIngressTLSSpec: ignoreIngressTLSSpec,
ignoreIngressRulesSpec: ignoreIngressRulesSpec, ignoreIngressRulesSpec: ignoreIngressRulesSpec,
labelSelector: labelSelector,
} }
return sc, nil return sc, nil
} }
@ -107,7 +109,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt
// Endpoints returns endpoint objects for each host-target combination that should be processed. // Endpoints returns endpoint objects for each host-target combination that should be processed.
// Retrieves all ingress resources on all namespaces // Retrieves all ingress resources on all namespaces
func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { func (sc *ingressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
ingresses, err := sc.ingressInformer.Lister().Ingresses(sc.namespace).List(labels.Everything()) ingresses, err := sc.ingressInformer.Lister().Ingresses(sc.namespace).List(sc.labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -26,6 +26,7 @@ import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
networkv1 "k8s.io/api/networking/v1" networkv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
@ -63,6 +64,7 @@ func (suite *IngressSuite) SetupTest() {
false, false,
false, false,
false, false,
labels.Everything(),
) )
suite.NoError(err, "should initialize ingress source") suite.NoError(err, "should initialize ingress source")
} }
@ -144,6 +146,7 @@ func TestNewIngressSource(t *testing.T) {
false, false,
false, false,
false, false,
labels.Everything(),
) )
if ti.expectError { if ti.expectError {
assert.Error(t, err) assert.Error(t, err)
@ -358,6 +361,7 @@ func testIngressEndpoints(t *testing.T) {
ignoreHostnameAnnotation bool ignoreHostnameAnnotation bool
ignoreIngressTLSSpec bool ignoreIngressTLSSpec bool
ignoreIngressRulesSpec bool ignoreIngressRulesSpec bool
ingressLabelSelector labels.Selector
}{ }{
{ {
title: "no ingress", title: "no ingress",
@ -1169,6 +1173,41 @@ func testIngressEndpoints(t *testing.T) {
}, },
}, },
}, },
{
ingressLabelSelector: labels.SelectorFromSet(labels.Set{"app": "web-external"}),
title: "ingress with matching labels",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
dnsnames: []string{"example.org"},
ips: []string{"8.8.8.8"},
labels: map[string]string{"app": "web-external", "name": "reverse-proxy"},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "example.org",
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
{
ingressLabelSelector: labels.SelectorFromSet(labels.Set{"app": "web-external"}),
title: "ingress without matching labels",
targetNamespace: "",
ingressItems: []fakeIngress{
{
name: "fake1",
namespace: namespace,
dnsnames: []string{"example.org"},
ips: []string{"8.8.8.8"},
labels: map[string]string{"app": "web-internal", "name": "reverse-proxy"},
},
},
expected: []*endpoint.Endpoint{},
},
} { } {
ti := ti ti := ti
t.Run(ti.title, func(t *testing.T) { t.Run(ti.title, func(t *testing.T) {
@ -1180,6 +1219,11 @@ func testIngressEndpoints(t *testing.T) {
_, err := fakeClient.NetworkingV1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{}) _, err := fakeClient.NetworkingV1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{})
require.NoError(t, err) require.NoError(t, err)
} }
if ti.ingressLabelSelector == nil {
ti.ingressLabelSelector = labels.Everything()
}
source, _ := NewIngressSource( source, _ := NewIngressSource(
fakeClient, fakeClient,
ti.targetNamespace, ti.targetNamespace,
@ -1189,6 +1233,7 @@ func testIngressEndpoints(t *testing.T) {
ti.ignoreHostnameAnnotation, ti.ignoreHostnameAnnotation,
ti.ignoreIngressTLSSpec, ti.ignoreIngressTLSSpec,
ti.ignoreIngressRulesSpec, ti.ignoreIngressRulesSpec,
ti.ingressLabelSelector,
) )
// Informer cache has all of the ingresses. Retrieve and validate their endpoints. // Informer cache has all of the ingresses. Retrieve and validate their endpoints.
res, err := source.Endpoints(context.Background()) res, err := source.Endpoints(context.Background())
@ -1211,6 +1256,7 @@ type fakeIngress struct {
namespace string namespace string
name string name string
annotations map[string]string annotations map[string]string
labels map[string]string
} }
func (ing fakeIngress) Ingress() *networkv1.Ingress { func (ing fakeIngress) Ingress() *networkv1.Ingress {
@ -1219,6 +1265,7 @@ func (ing fakeIngress) Ingress() *networkv1.Ingress {
Namespace: ing.namespace, Namespace: ing.namespace,
Name: ing.name, Name: ing.name,
Annotations: ing.annotations, Annotations: ing.annotations,
Labels: ing.labels,
}, },
Spec: networkv1.IngressSpec{ Spec: networkv1.IngressSpec{
Rules: []networkv1.IngressRule{}, Rules: []networkv1.IngressRule{},

View File

@ -48,6 +48,7 @@ type ocpRouteSource struct {
combineFQDNAnnotation bool combineFQDNAnnotation bool
ignoreHostnameAnnotation bool ignoreHostnameAnnotation bool
routeInformer routeInformer.RouteInformer routeInformer routeInformer.RouteInformer
labelSelector labels.Selector
} }
// NewOcpRouteSource creates a new ocpRouteSource with the given config. // NewOcpRouteSource creates a new ocpRouteSource with the given config.
@ -58,6 +59,7 @@ func NewOcpRouteSource(
fqdnTemplate string, fqdnTemplate string,
combineFQDNAnnotation bool, combineFQDNAnnotation bool,
ignoreHostnameAnnotation bool, ignoreHostnameAnnotation bool,
labelSelector labels.Selector,
) (Source, error) { ) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate) tmpl, err := parseTemplate(fqdnTemplate)
if err != nil { if err != nil {
@ -66,11 +68,11 @@ func NewOcpRouteSource(
// Use a shared informer to listen for add/update/delete of Routes in the specified namespace. // Use a shared informer to listen for add/update/delete of Routes in the specified namespace.
// Set resync period to 0, to prevent processing when nothing has changed. // Set resync period to 0, to prevent processing when nothing has changed.
informerFactory := extInformers.NewFilteredSharedInformerFactory(ocpClient, 0, namespace, nil) informerFactory := extInformers.NewSharedInformerFactoryWithOptions(ocpClient, 0, extInformers.WithNamespace(namespace))
routeInformer := informerFactory.Route().V1().Routes() informer := informerFactory.Route().V1().Routes()
// Add default resource event handlers to properly initialize informer. // Add default resource event handlers to properly initialize informer.
routeInformer.Informer().AddEventHandler( informer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{ cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { AddFunc: func(obj interface{}) {
}, },
@ -92,7 +94,8 @@ func NewOcpRouteSource(
fqdnTemplate: tmpl, fqdnTemplate: tmpl,
combineFQDNAnnotation: combineFQDNAnnotation, combineFQDNAnnotation: combineFQDNAnnotation,
ignoreHostnameAnnotation: ignoreHostnameAnnotation, ignoreHostnameAnnotation: ignoreHostnameAnnotation,
routeInformer: routeInformer, routeInformer: informer,
labelSelector: labelSelector,
}, nil }, nil
} }
@ -104,7 +107,7 @@ func (ors *ocpRouteSource) AddEventHandler(ctx context.Context, handler func())
// Retrieves all OpenShift Route resources on all namespaces, unless an explicit namespace // Retrieves all OpenShift Route resources on all namespaces, unless an explicit namespace
// is specified in ocpRouteSource. // is specified in ocpRouteSource.
func (ors *ocpRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { func (ors *ocpRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
ocpRoutes, err := ors.routeInformer.Lister().Routes(ors.namespace).List(labels.Everything()) ocpRoutes, err := ors.routeInformer.Lister().Routes(ors.namespace).List(ors.labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"k8s.io/apimachinery/pkg/labels"
routev1 "github.com/openshift/api/route/v1" routev1 "github.com/openshift/api/route/v1"
fake "github.com/openshift/client-go/route/clientset/versioned/fake" fake "github.com/openshift/client-go/route/clientset/versioned/fake"
@ -48,6 +49,7 @@ func (suite *OCPRouteSuite) SetupTest() {
"{{.Name}}", "{{.Name}}",
false, false,
false, false,
labels.Everything(),
) )
suite.routeWithTargets = &routev1.Route{ suite.routeWithTargets = &routev1.Route{
@ -104,6 +106,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) {
annotationFilter string annotationFilter string
fqdnTemplate string fqdnTemplate string
expectError bool expectError bool
labelFilter string
}{ }{
{ {
title: "invalid template", title: "invalid template",
@ -124,8 +127,15 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) {
expectError: false, expectError: false,
annotationFilter: "kubernetes.io/ingress.class=nginx", annotationFilter: "kubernetes.io/ingress.class=nginx",
}, },
{
title: "valid label selector",
expectError: false,
labelFilter: "app=web-external",
},
} { } {
ti := ti ti := ti
labelSelector, err := labels.Parse(ti.labelFilter)
require.NoError(t, err)
t.Run(ti.title, func(t *testing.T) { t.Run(ti.title, func(t *testing.T) {
t.Parallel() t.Parallel()
@ -136,6 +146,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) {
ti.fqdnTemplate, ti.fqdnTemplate,
false, false,
false, false,
labelSelector,
) )
if ti.expectError { if ti.expectError {
@ -160,6 +171,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
ocpRoute *routev1.Route ocpRoute *routev1.Route
expected []*endpoint.Endpoint expected []*endpoint.Endpoint
expectError bool expectError bool
labelFilter string
}{ }{
{ {
title: "route with basic hostname and route status target", title: "route with basic hostname and route status target",
@ -240,6 +252,61 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
}, },
expectError: false, expectError: false,
}, },
{
title: "route with matching labels",
labelFilter: "app=web-external",
ignoreHostnameAnnotation: false,
ocpRoute: &routev1.Route{
Spec: routev1.RouteSpec{
Host: "my-annotation-domain.com",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "route-with-matching-labels",
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/target": "my.site.foo.com",
},
Labels: map[string]string{
"app": "web-external",
"name": "service-frontend",
},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "my-annotation-domain.com",
Targets: []string{
"my.site.foo.com",
},
},
},
expectError: false,
},
{
title: "route without matching labels",
labelFilter: "app=web-external",
ignoreHostnameAnnotation: false,
ocpRoute: &routev1.Route{
Spec: routev1.RouteSpec{
Host: "my-annotation-domain.com",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "route-without-matching-labels",
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/target": "my.site.foo.com",
},
Labels: map[string]string{
"app": "web-internal",
"name": "service-frontend",
},
},
},
expected: []*endpoint.Endpoint{},
expectError: false,
},
} { } {
tc := tc tc := tc
t.Run(tc.title, func(t *testing.T) { t.Run(tc.title, func(t *testing.T) {
@ -251,6 +318,9 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
_, err := fakeClient.RouteV1().Routes(tc.ocpRoute.Namespace).Create(context.Background(), tc.ocpRoute, metav1.CreateOptions{}) _, err := fakeClient.RouteV1().Routes(tc.ocpRoute.Namespace).Create(context.Background(), tc.ocpRoute, metav1.CreateOptions{})
require.NoError(t, err) require.NoError(t, err)
labelSelector, err := labels.Parse(tc.labelFilter)
require.NoError(t, err)
source, err := NewOcpRouteSource( source, err := NewOcpRouteSource(
fakeClient, fakeClient,
"", "",
@ -258,6 +328,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
"{{.Name}}", "{{.Name}}",
false, false,
false, false,
labelSelector,
) )
require.NoError(t, err) require.NoError(t, err)

View File

@ -63,10 +63,11 @@ type serviceSource struct {
podInformer coreinformers.PodInformer podInformer coreinformers.PodInformer
nodeInformer coreinformers.NodeInformer nodeInformer coreinformers.NodeInformer
serviceTypeFilter map[string]struct{} serviceTypeFilter map[string]struct{}
labelSelector labels.Selector
} }
// NewServiceSource creates a new serviceSource with the given config. // NewServiceSource creates a new serviceSource with the given config.
func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool) (Source, error) { func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool, labelSelector labels.Selector) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate) tmpl, err := parseTemplate(fqdnTemplate)
if err != nil { if err != nil {
return nil, err return nil, err
@ -137,12 +138,13 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt
podInformer: podInformer, podInformer: podInformer,
nodeInformer: nodeInformer, nodeInformer: nodeInformer,
serviceTypeFilter: serviceTypes, serviceTypeFilter: serviceTypes,
labelSelector: labelSelector,
}, nil }, nil
} }
// Endpoints returns endpoint objects for each service that should be processed. // Endpoints returns endpoint objects for each service that should be processed.
func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything()) services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(sc.labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
istioclient "istio.io/client-go/pkg/clientset/versioned" istioclient "istio.io/client-go/pkg/clientset/versioned"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -42,7 +43,7 @@ var ErrSourceNotFound = errors.New("source not found")
type Config struct { type Config struct {
Namespace string Namespace string
AnnotationFilter string AnnotationFilter string
LabelFilter string LabelFilter labels.Selector
FQDNTemplate string FQDNTemplate string
CombineFQDNAndAnnotation bool CombineFQDNAndAnnotation bool
IgnoreHostnameAnnotation bool IgnoreHostnameAnnotation bool
@ -183,13 +184,13 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation) return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter)
case "ingress": case "ingress":
client, err := p.KubeClient() client, err := p.KubeClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec) return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter)
case "pod": case "pod":
client, err := p.KubeClient() client, err := p.KubeClient()
if err != nil { if err != nil {
@ -253,7 +254,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewOcpRouteSource(ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) return NewOcpRouteSource(ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter)
case "fake": case "fake":
return NewFakeSource(cfg.FQDNTemplate) return NewFakeSource(cfg.FQDNTemplate)
case "connector": case "connector":