diff --git a/docs/faq.md b/docs/faq.md index 67662a782b..30be7afc25 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -255,7 +255,7 @@ The internal one should provision hostnames used on the internal network (perhap 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 -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)` 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. 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? If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other. diff --git a/main.go b/main.go index 5843a583f9..167adae018 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/labels" _ "k8s.io/client-go/plugin/pkg/client/auth" "sigs.k8s.io/external-dns/controller" @@ -99,11 +100,14 @@ func main() { go serveMetrics(cfg.MetricsAddress) 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. sourceCfg := &source.Config{ Namespace: cfg.Namespace, AnnotationFilter: cfg.AnnotationFilter, - LabelFilter: cfg.LabelFilter, + LabelFilter: labelSelector, FQDNTemplate: cfg.FQDNTemplate, CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation, IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 89e053b865..ed85480621 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -23,6 +23,8 @@ import ( "strconv" "time" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/external-dns/endpoint" "github.com/alecthomas/kingpin" @@ -185,7 +187,7 @@ var defaultConfig = &Config{ Sources: nil, Namespace: "", AnnotationFilter: "", - LabelFilter: "", + LabelFilter: labels.Everything().String(), FQDNTemplate: "", CombineFQDNAndAnnotation: 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("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("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("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) diff --git a/pkg/apis/externaldns/validation/validation.go b/pkg/apis/externaldns/validation/validation.go index 9f8c3889a5..c5ebc38451 100644 --- a/pkg/apis/externaldns/validation/validation.go +++ b/pkg/apis/externaldns/validation/validation.go @@ -20,6 +20,8 @@ import ( "errors" "fmt" + "k8s.io/apimachinery/pkg/labels" + "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") } + _, err := labels.Parse(cfg.LabelFilter) + if err != nil { + return errors.New("--label-filter does not specify a valid label selector") + } return nil } diff --git a/source/crd.go b/source/crd.go index a897d45aa1..705d770bc0 100644 --- a/source/crd.go +++ b/source/crd.go @@ -43,7 +43,7 @@ type crdSource struct { crdResource string codec runtime.ParameterCodec annotationFilter string - labelFilter string + labelSelector labels.Selector } 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. -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{ crdResource: strings.ToLower(kind) + "s", namespace: namespace, annotationFilter: annotationFilter, - labelFilter: labelFilter, + labelSelector: labelSelector, crdClient: crdClient, codec: runtime.NewParameterCodec(scheme), }, nil @@ -126,11 +126,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error err error ) - if cs.labelFilter != "" { - result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelFilter}) - } else { - result, err = cs.List(ctx, &metav1.ListOptions{}) - } + result, err = cs.List(ctx, &metav1.ListOptions{LabelSelector: cs.labelSelector.String()}) if err != nil { return nil, err } diff --git a/source/crd_test.go b/source/crd_test.go index a0b054ec2b..a88fb46165 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -381,9 +382,13 @@ func testCRDSourceEndpoints(t *testing.T) { require.NoError(t, err) 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()) if ti.expectError { diff --git a/source/ingress.go b/source/ingress.go index e81f524d14..9890f1fe0b 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -60,10 +60,11 @@ type ingressSource struct { ingressInformer netinformers.IngressInformer ignoreIngressTLSSpec bool ignoreIngressRulesSpec bool + labelSelector labels.Selector } // 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) if err != nil { return nil, err @@ -100,6 +101,7 @@ func NewIngressSource(kubeClient kubernetes.Interface, namespace, annotationFilt ingressInformer: ingressInformer, ignoreIngressTLSSpec: ignoreIngressTLSSpec, ignoreIngressRulesSpec: ignoreIngressRulesSpec, + labelSelector: labelSelector, } 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. // Retrieves all ingress resources on all namespaces 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 { return nil, err } diff --git a/source/ingress_test.go b/source/ingress_test.go index c9d35494c8..32805b7adc 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -26,6 +26,7 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -63,6 +64,7 @@ func (suite *IngressSuite) SetupTest() { false, false, false, + labels.Everything(), ) suite.NoError(err, "should initialize ingress source") } @@ -144,6 +146,7 @@ func TestNewIngressSource(t *testing.T) { false, false, false, + labels.Everything(), ) if ti.expectError { assert.Error(t, err) @@ -358,6 +361,7 @@ func testIngressEndpoints(t *testing.T) { ignoreHostnameAnnotation bool ignoreIngressTLSSpec bool ignoreIngressRulesSpec bool + ingressLabelSelector labels.Selector }{ { 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 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{}) require.NoError(t, err) } + + if ti.ingressLabelSelector == nil { + ti.ingressLabelSelector = labels.Everything() + } + source, _ := NewIngressSource( fakeClient, ti.targetNamespace, @@ -1189,6 +1233,7 @@ func testIngressEndpoints(t *testing.T) { ti.ignoreHostnameAnnotation, ti.ignoreIngressTLSSpec, ti.ignoreIngressRulesSpec, + ti.ingressLabelSelector, ) // Informer cache has all of the ingresses. Retrieve and validate their endpoints. res, err := source.Endpoints(context.Background()) @@ -1211,6 +1256,7 @@ type fakeIngress struct { namespace string name string annotations map[string]string + labels map[string]string } func (ing fakeIngress) Ingress() *networkv1.Ingress { @@ -1219,6 +1265,7 @@ func (ing fakeIngress) Ingress() *networkv1.Ingress { Namespace: ing.namespace, Name: ing.name, Annotations: ing.annotations, + Labels: ing.labels, }, Spec: networkv1.IngressSpec{ Rules: []networkv1.IngressRule{}, diff --git a/source/openshift_route.go b/source/openshift_route.go index 39da5945e4..4f27918fc8 100644 --- a/source/openshift_route.go +++ b/source/openshift_route.go @@ -48,6 +48,7 @@ type ocpRouteSource struct { combineFQDNAnnotation bool ignoreHostnameAnnotation bool routeInformer routeInformer.RouteInformer + labelSelector labels.Selector } // NewOcpRouteSource creates a new ocpRouteSource with the given config. @@ -58,6 +59,7 @@ func NewOcpRouteSource( fqdnTemplate string, combineFQDNAnnotation bool, ignoreHostnameAnnotation bool, + labelSelector labels.Selector, ) (Source, error) { tmpl, err := parseTemplate(fqdnTemplate) 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. // Set resync period to 0, to prevent processing when nothing has changed. - informerFactory := extInformers.NewFilteredSharedInformerFactory(ocpClient, 0, namespace, nil) - routeInformer := informerFactory.Route().V1().Routes() + informerFactory := extInformers.NewSharedInformerFactoryWithOptions(ocpClient, 0, extInformers.WithNamespace(namespace)) + informer := informerFactory.Route().V1().Routes() // Add default resource event handlers to properly initialize informer. - routeInformer.Informer().AddEventHandler( + informer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { }, @@ -92,7 +94,8 @@ func NewOcpRouteSource( fqdnTemplate: tmpl, combineFQDNAnnotation: combineFQDNAnnotation, ignoreHostnameAnnotation: ignoreHostnameAnnotation, - routeInformer: routeInformer, + routeInformer: informer, + labelSelector: labelSelector, }, 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 // is specified in ocpRouteSource. 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 { return nil, err } diff --git a/source/openshift_route_test.go b/source/openshift_route_test.go index c586b2eda9..f2307a5e37 100644 --- a/source/openshift_route_test.go +++ b/source/openshift_route_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "k8s.io/apimachinery/pkg/labels" routev1 "github.com/openshift/api/route/v1" fake "github.com/openshift/client-go/route/clientset/versioned/fake" @@ -48,6 +49,7 @@ func (suite *OCPRouteSuite) SetupTest() { "{{.Name}}", false, false, + labels.Everything(), ) suite.routeWithTargets = &routev1.Route{ @@ -104,6 +106,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) { annotationFilter string fqdnTemplate string expectError bool + labelFilter string }{ { title: "invalid template", @@ -124,8 +127,15 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) { expectError: false, annotationFilter: "kubernetes.io/ingress.class=nginx", }, + { + title: "valid label selector", + expectError: false, + labelFilter: "app=web-external", + }, } { ti := ti + labelSelector, err := labels.Parse(ti.labelFilter) + require.NoError(t, err) t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -136,6 +146,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) { ti.fqdnTemplate, false, false, + labelSelector, ) if ti.expectError { @@ -160,6 +171,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) { ocpRoute *routev1.Route expected []*endpoint.Endpoint expectError bool + labelFilter string }{ { title: "route with basic hostname and route status target", @@ -240,6 +252,61 @@ func testOcpRouteSourceEndpoints(t *testing.T) { }, 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 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{}) require.NoError(t, err) + labelSelector, err := labels.Parse(tc.labelFilter) + require.NoError(t, err) + source, err := NewOcpRouteSource( fakeClient, "", @@ -258,6 +328,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) { "{{.Name}}", false, false, + labelSelector, ) require.NoError(t, err) diff --git a/source/service.go b/source/service.go index 31c4f26a53..b9707be349 100644 --- a/source/service.go +++ b/source/service.go @@ -63,10 +63,11 @@ type serviceSource struct { podInformer coreinformers.PodInformer nodeInformer coreinformers.NodeInformer serviceTypeFilter map[string]struct{} + labelSelector labels.Selector } // 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) if err != nil { return nil, err @@ -137,12 +138,13 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt podInformer: podInformer, nodeInformer: nodeInformer, serviceTypeFilter: serviceTypes, + labelSelector: labelSelector, }, nil } // Endpoints returns endpoint objects for each service that should be processed. 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 { return nil, err } diff --git a/source/service_test.go b/source/service_test.go index 9cf6d8f49d..38bf2a2d96 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/suite" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -75,6 +76,7 @@ func (suite *ServiceSuite) SetupTest() { false, []string{}, false, + labels.Everything(), ) suite.NoError(err, "should initialize service source") } @@ -153,6 +155,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { false, ti.serviceTypesFilter, false, + labels.Everything(), ) if ti.expectError { @@ -187,1056 +190,804 @@ func testServiceSourceEndpoints(t *testing.T) { serviceTypesFilter []string expected []*endpoint.Endpoint expectError bool + serviceLabelSelector string }{ { - "no annotated services return no endpoints", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{}, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, - }, - { - "no annotated services return no endpoints when ignoring annotations", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - true, - map[string]string{}, - map[string]string{}, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, - }, - { - "annotated services return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "no annotated services return no endpoints", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{}, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, + }, + { + title: "no annotated services return no endpoints when ignoring annotations", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + ignoreHostnameAnnotation: true, + labels: map[string]string{}, + annotations: map[string]string{}, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, + }, + { + title: "annotated services return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "hostname annotation on services is ignored", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - true, - map[string]string{}, - map[string]string{ + title: "hostname annotation on services is ignored", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + ignoreHostnameAnnotation: true, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "annotated ClusterIp aren't processed without explicit authorization", - "", - "", - "testing", - "foo", - v1.ServiceTypeClusterIP, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated ClusterIp aren't processed without explicit authorization", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "1.2.3.4", - []string{}, - []string{}, - []string{}, - []*endpoint.Endpoint{}, - false, - }, - { - "FQDN template with multiple hostnames return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", - false, - false, - map[string]string{}, - map[string]string{}, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + clusterIP: "1.2.3.4", + externalIPs: []string{}, + lbs: []string{}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, + }, + { + title: "FQDN template with multiple hostnames return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", + labels: map[string]string{}, + annotations: map[string]string{}, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "FQDN template with multiple hostnames return an endpoint with target IP when ignoring annotations", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", - false, - true, - map[string]string{}, - map[string]string{}, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + title: "FQDN template with multiple hostnames return an endpoint with target IP when ignoring annotations", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", + ignoreHostnameAnnotation: true, + labels: map[string]string{}, + annotations: map[string]string{}, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "FQDN template and annotation both with multiple hostnames return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", - true, - false, - map[string]string{}, - map[string]string{ + title: "FQDN template and annotation both with multiple hostnames return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", + combineFQDNAndAnnotation: true, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org., bar.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "FQDN template and annotation both with multiple hostnames while ignoring annotations will only return FQDN endpoints", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", - true, - true, - map[string]string{}, - map[string]string{ + title: "FQDN template and annotation both with multiple hostnames while ignoring annotations will only return FQDN endpoints", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", + combineFQDNAndAnnotation: true, + ignoreHostnameAnnotation: true, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org., bar.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "annotated services with multiple hostnames return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated services with multiple hostnames return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org., bar.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "annotated services with multiple hostnames and without trailing period return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated services with multiple hostnames and without trailing period return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org, bar.example.org", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "annotated services return an endpoint with target hostname", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated services return an endpoint with target hostname", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"lb.example.com"}, // Kubernetes omits the trailing dot - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"lb.example.com"}, // Kubernetes omits the trailing dot + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, }, - false, }, { - "annotated services can omit trailing dot", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated services can omit trailing dot", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted }, - "", - []string{}, - []string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, }, - false, }, { - "our controller type is kops dns controller", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "our controller type is kops dns controller", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ controllerAnnotationKey: controllerAnnotationValue, hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "different controller types are ignored even (with template specified)", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.ext-dns.test.com", - false, - false, - map[string]string{}, - map[string]string{ + title: "different controller types are ignored even (with template specified)", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.ext-dns.test.com", + labels: map[string]string{}, + annotations: map[string]string{ controllerAnnotationKey: "some-other-tool", hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "services are found in target namespace", - "testing", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "services are found in target namespace", + targetNamespace: "testing", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "services that are not in target namespace are ignored", - "testing", - "", - "other-testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "services that are not in target namespace are ignored", + targetNamespace: "testing", + svcNamespace: "other-testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "services are found in all namespaces", - "", - "", - "other-testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "services are found in all namespaces", + svcNamespace: "other-testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "valid matching annotation filter expression", - "", - "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "valid matching annotation filter expression", + annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "valid non-matching annotation filter expression", - "", - "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "valid non-matching annotation filter expression", + annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "SomethingElse", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "invalid annotation filter expression", - "", - "service.beta.kubernetes.io/external-traffic in (Global OnlyLocal)", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "invalid annotation filter expression", + annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global OnlyLocal)", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - true, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, + expectError: true, }, { - "valid matching annotation filter label", - "", - "service.beta.kubernetes.io/external-traffic=Global", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "valid matching annotation filter label", + annotationFilter: "service.beta.kubernetes.io/external-traffic=Global", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "Global", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "valid non-matching annotation filter label", - "", - "service.beta.kubernetes.io/external-traffic=Global", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "valid non-matching annotation filter label", + annotationFilter: "service.beta.kubernetes.io/external-traffic=Global", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "no external entrypoints return no endpoints", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "no external entrypoints return no endpoints", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "annotated service with externalIPs returns a single endpoint with multiple targets", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated service with externalIPs returns a single endpoint with multiple targets", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{"10.2.3.4", "11.2.3.4"}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}}, - }, - false, - }, - { - "multiple external entrypoints return a single endpoint with multiple targets", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ - hostnameAnnotationKey: "foo.example.org.", - }, - "", - []string{}, - []string{"1.2.3.4", "8.8.8.8"}, - []string{}, - []*endpoint.Endpoint{ - {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}}, - }, - false, - }, - { - "services annotated with legacy mate annotations are ignored in default mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + externalIPs: []string{"10.2.3.4", "11.2.3.4"}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}}, + }, + }, + { + title: "multiple external entrypoints return a single endpoint with multiple targets", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + }, + externalIPs: []string{}, + lbs: []string{"1.2.3.4", "8.8.8.8"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ + {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}}, + }, + }, + { + title: "services annotated with legacy mate annotations are ignored in default mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ "zalando.org/dnsname": "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, }, { - "services annotated with legacy mate annotations return an endpoint in compatibility mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "mate", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "services annotated with legacy mate annotations return an endpoint in compatibility mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + compatibility: "mate", + labels: map[string]string{}, + annotations: map[string]string{ "zalando.org/dnsname": "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "services annotated with legacy molecule annotations return an endpoint in compatibility mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "molecule", - "", - false, - false, - map[string]string{ + title: "services annotated with legacy molecule annotations return an endpoint in compatibility mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + compatibility: "molecule", + labels: map[string]string{ "dns": "route53", }, - map[string]string{ + annotations: map[string]string{ "domainName": "foo.example.org., bar.example.org", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "load balancer services annotated with DNS Controller annotations return an endpoint with A and CNAME targets in compatibility mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "kops-dns-controller", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "load balancer services annotated with DNS Controller annotations return an endpoint with A and CNAME targets in compatibility mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + compatibility: "kops-dns-controller", + labels: map[string]string{}, + annotations: map[string]string{ kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org", }, - "", - []string{}, - []string{"1.2.3.4", "lb.example.com"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4", "lb.example.com"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, }, - false, }, { - "load balancer services annotated with DNS Controller annotations return an endpoint with both annotations in compatibility mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "kops-dns-controller", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "load balancer services annotated with DNS Controller annotations return an endpoint with both annotations in compatibility mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + compatibility: "kops-dns-controller", + labels: map[string]string{}, + annotations: map[string]string{ kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, - { - "not annotated services with set fqdnTemplate return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.bar.example.com", - false, - false, - map[string]string{}, - map[string]string{}, - "", - []string{}, - []string{"1.2.3.4", "elb.com"}, - []string{}, - []*endpoint.Endpoint{ + title: "not annotated services with set fqdnTemplate return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.bar.example.com", + labels: map[string]string{}, + annotations: map[string]string{}, + externalIPs: []string{}, + lbs: []string{"1.2.3.4", "elb.com"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"elb.com"}}, }, - false, }, { - "annotated services with set fqdnTemplate annotation takes precedence", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Name}}.bar.example.com", - false, - false, - map[string]string{}, - map[string]string{ + title: "annotated services with set fqdnTemplate annotation takes precedence", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.bar.example.com", + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4", "elb.com"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4", "elb.com"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"elb.com"}}, }, - false, }, { - "compatibility annotated services with tmpl. compatibility takes precedence", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "mate", - "{{.Name}}.bar.example.com", - false, - false, - map[string]string{}, - map[string]string{ + title: "compatibility annotated services with tmpl. compatibility takes precedence", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + compatibility: "mate", + fqdnTemplate: "{{.Name}}.bar.example.com", + labels: map[string]string{}, + annotations: map[string]string{ "zalando.org/dnsname": "mate.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "mate.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "not annotated services with unknown tmpl field should not return anything", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "{{.Calibre}}.bar.example.com", - false, - false, - map[string]string{}, - map[string]string{}, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{}, - true, + title: "not annotated services with unknown tmpl field should not return anything", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Calibre}}.bar.example.com", + labels: map[string]string{}, + annotations: map[string]string{}, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{}, + expectError: true, }, { - "ttl not annotated should have RecordTTL.IsConfigured set to false", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "ttl not annotated should have RecordTTL.IsConfigured set to false", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, - false, }, { - "ttl annotated but invalid should have RecordTTL.IsConfigured set to false", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "ttl annotated but invalid should have RecordTTL.IsConfigured set to false", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "foo", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, - false, }, { - "ttl annotated and is valid should set Record.TTL", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "ttl annotated and is valid should set Record.TTL", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "10", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)}, }, - false, }, { - "ttl annotated (in duration format) and is valid should set Record.TTL", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "ttl annotated (in duration format) and is valid should set Record.TTL", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "1m", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(60)}, }, - false, }, { - "Negative ttl is not valid", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "Negative ttl is not valid", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", ttlAnnotationKey: "-10", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, - false, }, { - "filter on service types should include matching services", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "filter on service types should include matching services", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{string(v1.ServiceTypeLoadBalancer)}, - []*endpoint.Endpoint{ + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "filter on service types should exclude non-matching services", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "filter on service types should exclude non-matching services", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "", - []string{}, - []string{"1.2.3.4"}, - []string{string(v1.ServiceTypeLoadBalancer)}, - []*endpoint.Endpoint{}, - false, + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)}, + expected: []*endpoint.Endpoint{}, }, - { - "internal-host annotated services return an endpoint with Cluster IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + { + title: "internal-host annotated services return an endpoint with Cluster IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ internalHostnameAnnotationKey: "foo.internal.example.org.", }, - "1.1.1.1", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + clusterIP: "1.1.1.1", + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.internal.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, }, - false, }, { - "internal-host annotated and host annotated services return an endpoint with Cluster IP and an endpoint with lb IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeLoadBalancer, - "", - "", - false, - false, - map[string]string{}, - map[string]string{ + title: "internal-host annotated and host annotated services return an endpoint with Cluster IP and an endpoint with lb IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", internalHostnameAnnotationKey: "foo.internal.example.org.", }, - "1.1.1.1", - []string{}, - []string{"1.2.3.4"}, - []string{}, - []*endpoint.Endpoint{ + clusterIP: "1.1.1.1", + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + expected: []*endpoint.Endpoint{ {DNSName: "foo.internal.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, + }, + { + title: "service with matching labels and fqdn filter should be included", + svcNamespace: "testing", + svcName: "fqdn", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{ + "app": "web-external", + }, + clusterIP: "1.1.1.1", + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + serviceLabelSelector: "app=web-external", + fqdnTemplate: "{{.Name}}.bar.example.com", + expected: []*endpoint.Endpoint{ + {DNSName: "fqdn.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, + }, + }, + { + title: "service with matching labels and hostname annotation should be included", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{ + "app": "web-external", + }, + clusterIP: "1.1.1.1", + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + serviceLabelSelector: "app=web-external", + annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"}, + expected: []*endpoint.Endpoint{ + {DNSName: "annotation.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, + }, + }, + { + title: "service without matching labels and fqdn filter should be excluded", + svcNamespace: "testing", + svcName: "fqdn", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{ + "app": "web-internal", + }, + clusterIP: "1.1.1.1", + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + serviceLabelSelector: "app=web-external", + fqdnTemplate: "{{.Name}}.bar.example.com", + expected: []*endpoint.Endpoint{}, + }, + { + title: "service without matching labels and hostname annotation should be excluded", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + labels: map[string]string{ + "app": "web-internal", + }, + clusterIP: "1.1.1.1", + externalIPs: []string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{}, + serviceLabelSelector: "app=web-external", + annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"}, + expected: []*endpoint.Endpoint{}, }, } { tc := tc @@ -1278,6 +1029,14 @@ func testServiceSourceEndpoints(t *testing.T) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{}) require.NoError(t, err) + var sourceLabel labels.Selector + if tc.serviceLabelSelector != "" { + sourceLabel, err = labels.Parse(tc.serviceLabelSelector) + require.NoError(t, err) + } else { + sourceLabel = labels.Everything() + } + // Create our object under test and get the endpoints. client, err := NewServiceSource( kubernetes, @@ -1291,7 +1050,9 @@ func testServiceSourceEndpoints(t *testing.T) { false, tc.serviceTypesFilter, tc.ignoreHostnameAnnotation, + sourceLabel, ) + require.NoError(t, err) res, err := client.Endpoints(context.Background()) @@ -1456,6 +1217,7 @@ func testMultipleServicesEndpoints(t *testing.T) { false, tc.serviceTypesFilter, tc.ignoreHostnameAnnotation, + labels.Everything(), ) require.NoError(t, err) @@ -1502,85 +1264,77 @@ func TestClusterIpServices(t *testing.T) { labels map[string]string annotations map[string]string clusterIP string - lbs []string expected []*endpoint.Endpoint expectError bool + labelSelector string }{ { - "annotated ClusterIp services return an endpoint with Cluster IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeClusterIP, - "", - "", - false, - map[string]string{}, - map[string]string{ + title: "annotated ClusterIp services return an endpoint with Cluster IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "1.2.3.4", - []string{}, - []*endpoint.Endpoint{ + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "hostname annotated ClusterIp services are ignored", - "", - "", - "testing", - "foo", - v1.ServiceTypeClusterIP, - "", - "", - true, - map[string]string{}, - map[string]string{ + title: "hostname annotated ClusterIp services are ignored", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + ignoreHostnameAnnotation: true, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - "1.2.3.4", - []string{}, - []*endpoint.Endpoint{}, - false, + clusterIP: "1.2.3.4", + expected: []*endpoint.Endpoint{}, }, { - "non-annotated ClusterIp services with set fqdnTemplate return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeClusterIP, - "", - "{{.Name}}.bar.example.com", - false, - map[string]string{}, - map[string]string{}, - "4.5.6.7", - []string{}, - []*endpoint.Endpoint{ + title: "non-annotated ClusterIp services with set fqdnTemplate return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + fqdnTemplate: "{{.Name}}.bar.example.com", + clusterIP: "4.5.6.7", + expected: []*endpoint.Endpoint{ {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"4.5.6.7"}}, }, - false, }, { - "Headless services do not generate endpoints", - "", - "", - "testing", - "foo", - v1.ServiceTypeClusterIP, - "", - "", - false, - map[string]string{}, - map[string]string{}, - v1.ClusterIPNone, - []string{}, - []*endpoint.Endpoint{}, - false, + title: "Headless services do not generate endpoints", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + clusterIP: v1.ClusterIPNone, + expected: []*endpoint.Endpoint{}, + }, + { + title: "ClusterIP service with matching label generates an endpoint", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + fqdnTemplate: "{{.Name}}.bar.example.com", + labels: map[string]string{"app": "web-internal"}, + clusterIP: "4.5.6.7", + expected: []*endpoint.Endpoint{ + {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"4.5.6.7"}}, + }, + labelSelector: "app=web-internal", + }, + { + title: "ClusterIP service without matching label generates an endpoint", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeClusterIP, + fqdnTemplate: "{{.Name}}.bar.example.com", + labels: map[string]string{"app": "web-internal"}, + clusterIP: "4.5.6.7", + expected: []*endpoint.Endpoint{}, + labelSelector: "app=web-external", }, } { tc := tc @@ -1591,15 +1345,6 @@ func TestClusterIpServices(t *testing.T) { kubernetes := fake.NewSimpleClientset() // Create a service to test against - ingresses := []v1.LoadBalancerIngress{} - for _, lb := range tc.lbs { - if net.ParseIP(lb) != nil { - ingresses = append(ingresses, v1.LoadBalancerIngress{IP: lb}) - } else { - ingresses = append(ingresses, v1.LoadBalancerIngress{Hostname: lb}) - } - } - service := &v1.Service{ Spec: v1.ServiceSpec{ Type: tc.svcType, @@ -1611,16 +1356,18 @@ func TestClusterIpServices(t *testing.T) { Labels: tc.labels, Annotations: tc.annotations, }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: ingresses, - }, - }, } _, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{}) require.NoError(t, err) + var labelSelector labels.Selector + if tc.labelSelector != "" { + labelSelector, err = labels.Parse(tc.labelSelector) + require.NoError(t, err) + } else { + labelSelector = labels.Everything() + } // Create our object under test and get the endpoints. client, _ := NewServiceSource( kubernetes, @@ -1634,6 +1381,7 @@ func TestClusterIpServices(t *testing.T) { false, []string{}, tc.ignoreHostnameAnnotation, + labelSelector, ) require.NoError(t, err) @@ -1671,32 +1419,25 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected []*endpoint.Endpoint expectError bool nodes []*v1.Node - podnames []string + podNames []string nodeIndex []int phases []v1.PodPhase + labelSelector labels.Selector }{ { - "annotated NodePort services return an endpoint with IP addresses of the cluster's nodes", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "", - "", - false, - map[string]string{}, - map[string]string{ + title: "annotated NodePort services return an endpoint with IP addresses of the cluster's nodes", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1717,29 +1458,18 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "hostname annotated NodePort services are ignored", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "", - "", - true, - map[string]string{}, - map[string]string{ + title: "hostname annotated NodePort services are ignored", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + ignoreHostnameAnnotation: true, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - nil, - []*endpoint.Endpoint{}, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1760,30 +1490,20 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, + expected: []*endpoint.Endpoint{}, }, { - "non-annotated NodePort services with set fqdnTemplate return an endpoint with target IP", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "", - "{{.Name}}.bar.example.com", - false, - map[string]string{}, - map[string]string{}, - nil, - []*endpoint.Endpoint{ + title: "non-annotated NodePort services with set fqdnTemplate return an endpoint with target IP", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + fqdnTemplate: "{{.Name}}.bar.example.com", + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1804,32 +1524,21 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "annotated NodePort services return an endpoint with IP addresses of the private cluster's nodes", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "", - "", - false, - map[string]string{}, - map[string]string{ + title: "annotated NodePort services return an endpoint with IP addresses of the private cluster's nodes", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1848,32 +1557,21 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "annotated NodePort services with ExternalTrafficPolicy=Local return an endpoint with IP addresses of the cluster's nodes where pods is running only", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeLocal, - "", - "", - false, - map[string]string{}, - map[string]string{ + title: "annotated NodePort services with ExternalTrafficPolicy=Local return an endpoint with IP addresses of the cluster's nodes where pods is running only", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1894,32 +1592,25 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{"pod-0"}, - []int{1}, - []v1.PodPhase{v1.PodRunning}, - }, - { - "annotated NodePort services with ExternalTrafficPolicy=Local and multiple pods on a single node return an endpoint with unique IP addresses of the cluster's nodes where pods is running only", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeLocal, - "", - "", - false, - map[string]string{}, - map[string]string{ + podNames: []string{"pod-0"}, + nodeIndex: []int{1}, + phases: []v1.PodPhase{v1.PodRunning}, + }, + { + title: "annotated NodePort services with ExternalTrafficPolicy=Local and multiple pods on a single node return an endpoint with unique IP addresses of the cluster's nodes where pods is running only", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1940,33 +1631,26 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{"pod-0", "pod-1"}, - []int{1, 1}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, - }, - { - "access=private annotation NodePort services return an endpoint with private IP addresses of the cluster's nodes", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "", - "", - false, - map[string]string{}, - map[string]string{ + podNames: []string{"pod-0", "pod-1"}, + nodeIndex: []int{1, 1}, + phases: []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + }, + { + title: "access=private annotation NodePort services return an endpoint with private IP addresses of the cluster's nodes", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", accessAnnotationKey: "private", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -1987,33 +1671,23 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "access=public annotation NodePort services return an endpoint with public IP addresses of the cluster's nodes", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "", - "", - false, - map[string]string{}, - map[string]string{ + title: "access=public annotation NodePort services return an endpoint with public IP addresses of the cluster's nodes", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + labels: map[string]string{}, + annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", accessAnnotationKey: "public", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, @@ -2034,32 +1708,23 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "node port services annotated DNS Controller annotations return an endpoint where all targets has the node role", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "kops-dns-controller", - "", - false, - map[string]string{}, - map[string]string{ + title: "node port services annotated DNS Controller annotations return an endpoint where all targets has the node role", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + compatibility: "kops-dns-controller", + labels: map[string]string{}, + annotations: map[string]string{ kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}}, {DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"10.0.1.1"}}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", Labels: map[string]string{ @@ -2086,32 +1751,22 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "node port services annotated with internal DNS Controller annotations return an endpoint in compatibility mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "kops-dns-controller", - "", - false, - map[string]string{}, - map[string]string{ + title: "node port services annotated with internal DNS Controller annotations return an endpoint in compatibility mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + compatibility: "kops-dns-controller", + annotations: map[string]string{ kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}}, {DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", Labels: map[string]string{ @@ -2138,32 +1793,22 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "node port services annotated with external DNS Controller annotations return an endpoint in compatibility mode", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "kops-dns-controller", - "", - false, - map[string]string{}, - map[string]string{ + title: "node port services annotated with external DNS Controller annotations return an endpoint in compatibility mode", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + compatibility: "kops-dns-controller", + annotations: map[string]string{ kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org", }, - nil, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}}, }, - false, - []*v1.Node{{ + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", Labels: map[string]string{ @@ -2190,30 +1835,21 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, { - "node port services annotated with both kops dns controller annotations return an empty set of addons", - "", - "", - "testing", - "foo", - v1.ServiceTypeNodePort, - v1.ServiceExternalTrafficPolicyTypeCluster, - "kops-dns-controller", - "", - false, - map[string]string{}, - map[string]string{ + title: "node port services annotated with both kops dns controller annotations return an empty set of addons", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + compatibility: "kops-dns-controller", + labels: map[string]string{}, + annotations: map[string]string{ kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org", kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org", }, - nil, - []*endpoint.Endpoint{}, - false, - []*v1.Node{{ + expected: []*endpoint.Endpoint{}, + nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", Labels: map[string]string{ @@ -2240,9 +1876,6 @@ func TestServiceSourceNodePortServices(t *testing.T) { }, }, }}, - []string{}, - []int{}, - []v1.PodPhase{}, }, } { tc := tc @@ -2260,7 +1893,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { } // Create pods - for i, podname := range tc.podnames { + for i, podname := range tc.podNames { pod := &v1.Pod{ Spec: v1.PodSpec{ Containers: []v1.Container{}, @@ -2317,6 +1950,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { false, []string{}, tc.ignoreHostnameAnnotation, + labels.Everything(), ) require.NoError(t, err) @@ -2651,6 +2285,7 @@ func TestHeadlessServices(t *testing.T) { false, []string{}, tc.ignoreHostnameAnnotation, + labels.Everything(), ) require.NoError(t, err) @@ -3006,6 +2641,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, []string{}, tc.ignoreHostnameAnnotation, + labels.Everything(), ) require.NoError(t, err) @@ -3116,6 +2752,7 @@ func TestExternalServices(t *testing.T) { false, []string{}, tc.ignoreHostnameAnnotation, + labels.Everything(), ) require.NoError(t, err) @@ -3156,7 +2793,20 @@ func BenchmarkServiceEndpoints(b *testing.B) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{}) require.NoError(b, err) - client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false, false, []string{}, false) + client, err := NewServiceSource( + kubernetes, + v1.NamespaceAll, + "", + "", + false, + "", + false, + false, + false, + []string{}, + false, + labels.Everything(), + ) require.NoError(b, err) for i := 0; i < b.N; i++ { diff --git a/source/store.go b/source/store.go index 3b83722556..74771a867e 100644 --- a/source/store.go +++ b/source/store.go @@ -29,6 +29,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" istioclient "istio.io/client-go/pkg/clientset/versioned" + "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -42,7 +43,7 @@ var ErrSourceNotFound = errors.New("source not found") type Config struct { Namespace string AnnotationFilter string - LabelFilter string + LabelFilter labels.Selector FQDNTemplate string CombineFQDNAndAnnotation bool IgnoreHostnameAnnotation bool @@ -183,13 +184,13 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err if err != nil { 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": client, err := p.KubeClient() if err != nil { 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": client, err := p.KubeClient() if err != nil { @@ -253,7 +254,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err if err != nil { 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": return NewFakeSource(cfg.FQDNTemplate) case "connector":