diff --git a/source/ambassador_host.go b/source/ambassador_host.go index 244a9c1b02..e13780085a 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -138,7 +138,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp } // Filter Ambassador Hosts - ambassadorHosts, err = sc.filterByAnnotations(ambassadorHosts) + ambassadorHosts, err = annotations.Filter(ambassadorHosts, sc.annotationFilter) if err != nil { return nil, fmt.Errorf("failed to filter Ambassador Hosts by annotation: %w", err) } @@ -290,28 +290,3 @@ func newUnstructuredConverter() (*unstructuredConverter, error) { return uc, nil } - -// Filter a list of Ambassador Host Resources to only return the ones that -// contain the required External-DNS annotation filter -func (sc *ambassadorHostSource) filterByAnnotations(ambassadorHosts []*ambassador.Host) ([]*ambassador.Host, error) { - selector, err := annotations.ParseFilter(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list of Ambassador Hosts - if selector.Empty() { - return ambassadorHosts, nil - } - - // Return a filtered list of Ambassador Hosts - filteredList := []*ambassador.Host{} - for _, host := range ambassadorHosts { - // include Ambassador Host if its annotations match the annotation filter - if selector.Matches(labels.Set(host.Annotations)) { - filteredList = append(filteredList, host) - } - } - - return filteredList, nil -} diff --git a/source/annotations/filter.go b/source/annotations/filter.go new file mode 100644 index 0000000000..be8c444898 --- /dev/null +++ b/source/annotations/filter.go @@ -0,0 +1,50 @@ +/* +Copyright 2025 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package annotations + +import ( + "strings" + + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/labels" +) + +// AnnotatedObject represents any Kubernetes object with annotations +type AnnotatedObject interface { + GetAnnotations() map[string]string +} + +// Filter filters a slice of objects by annotation selector. +// Returns all items if annotationFilter is empty. +func Filter[T AnnotatedObject](items []T, filter string) ([]T, error) { + if filter == "" || strings.TrimSpace(filter) == "" { + return items, nil + } + selector, err := ParseFilter(filter) + if err != nil { + return nil, err + } + if selector.Empty() { + return items, nil + } + + filtered := make([]T, 0, len(items)) + for _, item := range items { + if selector.Matches(labels.Set(item.GetAnnotations())) { + filtered = append(filtered, item) + } + } + log.Debugf("filtered '%d' services out of '%d' with annotation filter '%s'", len(filtered), len(items), filter) + return filtered, nil +} diff --git a/source/annotations/filter_test.go b/source/annotations/filter_test.go new file mode 100644 index 0000000000..d5f2c00e1a --- /dev/null +++ b/source/annotations/filter_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2025 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package annotations + +import ( + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/external-dns/internal/testutils" +) + +// Mock object implementing AnnotatedObject +type mockObj struct { + annotations map[string]string +} + +func (m mockObj) GetAnnotations() map[string]string { + return m.annotations +} + +func TestFilter(t *testing.T) { + tests := []struct { + name string + items []mockObj + filter string + expected []mockObj + expectError bool + }{ + { + name: "Empty filter returns all", + items: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + filter: "", + expected: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + }, + { + name: "Matching items", + items: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"foo": "baz"}}, + }, + filter: "foo=bar", + expected: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + }, + }, + { + name: "No matching items", + items: []mockObj{ + {annotations: map[string]string{"foo": "baz"}}, + }, + filter: "foo=bar", + expected: []mockObj{}, + }, + { + name: "Whitespace filter returns all", + items: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + filter: " ", + expected: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + }, + { + name: "empty filter returns all", + items: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + filter: "", + expected: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + }, + { + name: "invalid filter returns error", + items: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + filter: "=invalid", + expected: []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"baz": "qux"}}, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Filter(tt.items, tt.filter) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestFilter_LogOutput(t *testing.T) { + hook := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t) + + items := []mockObj{ + {annotations: map[string]string{"foo": "bar"}}, + {annotations: map[string]string{"foo": "baz"}}, + } + filter := "foo=bar" + _, _ = Filter(items, filter) + + testutils.TestHelperLogContains("filtered '1' services out of '2' with annotation filter 'foo=bar'", hook, t) +} diff --git a/source/contour_httpproxy.go b/source/contour_httpproxy.go index c8916de7eb..420968a159 100644 --- a/source/contour_httpproxy.go +++ b/source/contour_httpproxy.go @@ -127,7 +127,7 @@ func (sc *httpProxySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, httpProxies = append(httpProxies, hpConverted) } - httpProxies, err = sc.filterByAnnotations(httpProxies) + httpProxies, err = annotations.Filter(httpProxies, sc.annotationFilter) if err != nil { return nil, fmt.Errorf("failed to filter HTTPProxies: %w", err) } @@ -209,30 +209,6 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP return endpoints, nil } -// filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTTPProxy) ([]*projectcontour.HTTPProxy, error) { - selector, err := annotations.ParseFilter(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return httpProxies, nil - } - - var filteredList []*projectcontour.HTTPProxy - - for _, httpProxy := range httpProxies { - // include HTTPProxy if its annotations match the selector - if selector.Matches(labels.Set(httpProxy.Annotations)) { - filteredList = append(filteredList, httpProxy) - } - } - - return filteredList, nil -} - // endpointsFromHTTPProxyConfig extracts the endpoints from a Contour HTTPProxy object func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTPProxy) ([]*endpoint.Endpoint, error) { resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name) diff --git a/source/crd.go b/source/crd.go index f328e9f1e7..ddffa82414 100644 --- a/source/crd.go +++ b/source/crd.go @@ -111,7 +111,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, labelSelector labels.Selector, scheme *runtime.Scheme, startInformer bool) (Source, error) { +func NewCRDSource( + crdClient rest.Interface, + namespace, kind, annotationFilter string, + labelSelector labels.Selector, + scheme *runtime.Scheme, + startInformer bool) (Source, error) { sourceCrd := crdSource{ crdResource: strings.ToLower(kind) + "s", namespace: namespace, @@ -174,12 +179,17 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error return nil, err } - result, err = cs.filterByAnnotations(result) + itemPtrs := make([]*apiv1alpha1.DNSEndpoint, len(result.Items)) + for i := range result.Items { + itemPtrs[i] = &result.Items[i] + } + + filtered, err := annotations.Filter(itemPtrs, cs.annotationFilter) if err != nil { return nil, err } - for _, dnsEndpoint := range result.Items { + for _, dnsEndpoint := range filtered { var crdEndpoints []*endpoint.Endpoint for _, ep := range dnsEndpoint.Spec.Endpoints { if (ep.RecordType == endpoint.RecordTypeCNAME || ep.RecordType == endpoint.RecordTypeA || ep.RecordType == endpoint.RecordTypeAAAA) && len(ep.Targets) < 1 { @@ -214,7 +224,7 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error dnsEndpoint.Status.ObservedGeneration = dnsEndpoint.Generation // Update the ObservedGeneration - _, err = cs.UpdateStatus(ctx, &dnsEndpoint) + _, err = cs.UpdateStatus(ctx, dnsEndpoint) if err != nil { log.Warnf("Could not update ObservedGeneration of the CRD: %v", err) } @@ -253,26 +263,3 @@ func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *apiv1alpha1. Do(ctx). Into(result) } - -// filterByAnnotations filters a list of dnsendpoints by a given annotation selector. -func (cs *crdSource) filterByAnnotations(dnsendpoints *apiv1alpha1.DNSEndpointList) (*apiv1alpha1.DNSEndpointList, error) { - selector, err := annotations.ParseFilter(cs.annotationFilter) - if err != nil { - return nil, err - } - // empty filter returns original list - if selector.Empty() { - return dnsendpoints, nil - } - - filteredList := apiv1alpha1.DNSEndpointList{} - - for _, dnsendpoint := range dnsendpoints.Items { - // include dnsendpoint if its annotations match the selector - if selector.Matches(labels.Set(dnsendpoint.Annotations)) { - filteredList.Items = append(filteredList.Items, dnsendpoint) - } - } - - return &filteredList, nil -} diff --git a/source/f5_transportserver.go b/source/f5_transportserver.go index 817affbd38..d4c59de52c 100644 --- a/source/f5_transportserver.go +++ b/source/f5_transportserver.go @@ -120,7 +120,7 @@ func (ts *f5TransportServerSource) Endpoints(ctx context.Context) ([]*endpoint.E transportServers = append(transportServers, transportServer) } - transportServers, err = ts.filterByAnnotations(transportServers) + transportServers, err = annotations.Filter(transportServers, ts.annotationFilter) if err != nil { return nil, fmt.Errorf("failed to filter TransportServers: %w", err) } @@ -183,30 +183,6 @@ func newTSUnstructuredConverter() (*unstructuredConverter, error) { return uc, nil } -// filterByAnnotations filters a list of TransportServers by a given annotation selector. -func (ts *f5TransportServerSource) filterByAnnotations(transportServers []*f5.TransportServer) ([]*f5.TransportServer, error) { - selector, err := annotations.ParseFilter(ts.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return transportServers, nil - } - - filteredList := []*f5.TransportServer{} - - for _, ts := range transportServers { - // include TransportServer if its annotations match the selector - if selector.Matches(labels.Set(ts.Annotations)) { - filteredList = append(filteredList, ts) - } - } - - return filteredList, nil -} - func hasValidTransportServerIP(vs *f5.TransportServer) bool { normalizedAddress := strings.ToLower(vs.Status.VSAddress) return normalizedAddress != "none" && normalizedAddress != "" diff --git a/source/f5_virtualserver.go b/source/f5_virtualserver.go index 2a9958f839..16d73cdd26 100644 --- a/source/f5_virtualserver.go +++ b/source/f5_virtualserver.go @@ -120,7 +120,7 @@ func (vs *f5VirtualServerSource) Endpoints(ctx context.Context) ([]*endpoint.End virtualServers = append(virtualServers, virtualServer) } - virtualServers, err = vs.filterByAnnotations(virtualServers) + virtualServers, err = annotations.Filter(virtualServers, vs.annotationFilter) if err != nil { return nil, fmt.Errorf("failed to filter VirtualServers: %w", err) } @@ -195,30 +195,6 @@ func newVSUnstructuredConverter() (*unstructuredConverter, error) { return uc, nil } -// filterByAnnotations filters a list of VirtualServers by a given annotation selector. -func (vs *f5VirtualServerSource) filterByAnnotations(virtualServers []*f5.VirtualServer) ([]*f5.VirtualServer, error) { - selector, err := annotations.ParseFilter(vs.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return virtualServers, nil - } - - filteredList := []*f5.VirtualServer{} - - for _, vs := range virtualServers { - // include VirtualServer if its annotations match the selector - if selector.Matches(labels.Set(vs.Annotations)) { - filteredList = append(filteredList, vs) - } - } - - return filteredList, nil -} - func hasValidVirtualServerIP(vs *f5.VirtualServer) bool { normalizedAddress := strings.ToLower(vs.Status.VSAddress) return normalizedAddress != "none" && normalizedAddress != "" diff --git a/source/ingress.go b/source/ingress.go index 59bb5870e1..84c1a329d2 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -131,7 +131,7 @@ func (sc *ingressSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err if err != nil { return nil, err } - ingresses, err = sc.filterByAnnotations(ingresses) + ingresses, err = annotations.Filter(ingresses, sc.annotationFilter) if err != nil { return nil, err } @@ -203,30 +203,6 @@ func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpo return endpoints, nil } -// filterByAnnotations filters a list of ingresses by a given annotation selector. -func (sc *ingressSource) filterByAnnotations(ingresses []*networkv1.Ingress) ([]*networkv1.Ingress, error) { - selector, err := getLabelSelector(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return ingresses, nil - } - - filteredList := []*networkv1.Ingress{} - - for _, ingress := range ingresses { - // include ingress if its annotations match the selector - if matchLabelSelector(selector, ingress.Annotations) { - filteredList = append(filteredList, ingress) - } - } - - return filteredList, nil -} - // filterByIngressClass filters a list of ingresses based on a required ingress // class func (sc *ingressSource) filterByIngressClass(ingresses []*networkv1.Ingress) ([]*networkv1.Ingress, error) { diff --git a/source/istio_gateway.go b/source/istio_gateway.go index cc8dce1834..1d4e16c419 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -30,7 +30,6 @@ import ( networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" kubeinformers "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" netinformers "k8s.io/client-go/informers/networking/v1" @@ -136,7 +135,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e } gateways := gwList.Items - gateways, err = sc.filterByAnnotations(gateways) + gateways, err = annotations.Filter(gateways, sc.annotationFilter) if err != nil { return nil, err } @@ -209,30 +208,6 @@ func (sc *gatewaySource) AddEventHandler(_ context.Context, handler func()) { _, _ = sc.gatewayInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) } -// filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1beta1.Gateway) ([]*networkingv1beta1.Gateway, error) { - selector, err := annotations.ParseFilter(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return gateways, nil - } - - var filteredList []*networkingv1beta1.Gateway - - for _, gw := range gateways { - // include if the annotations match the selector - if selector.Matches(labels.Set(gw.Annotations)) { - filteredList = append(filteredList, gw) - } - } - - return filteredList, nil -} - func (sc *gatewaySource) targetsFromIngress(ingressStr string, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) { namespace, name, err := ParseIngress(ingressStr) if err != nil { diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index ffcdd001be..9877e795da 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -139,7 +139,7 @@ func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endp if err != nil { return nil, err } - virtualServices, err = sc.filterByAnnotations(virtualServices) + virtualServices, err = annotations.Filter(virtualServices, sc.annotationFilter) if err != nil { return nil, err } @@ -251,30 +251,6 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu return endpoints, nil } -// filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *virtualServiceSource) filterByAnnotations(vServices []*v1beta1.VirtualService) ([]*v1beta1.VirtualService, error) { - selector, err := annotations.ParseFilter(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return vServices, nil - } - - var filteredList []*v1beta1.VirtualService - - for _, vs := range vServices { - // include if the annotations match the selector - if selector.Matches(labels.Set(vs.Annotations)) { - filteredList = append(filteredList, vs) - } - } - - return filteredList, nil -} - // append a target to the list of targets unless it's already in the list func appendUnique(targets []string, target string) []string { if slices.Contains(targets, target) { diff --git a/source/kong_tcpingress.go b/source/kong_tcpingress.go index e142bb1c56..9f6cb6049f 100644 --- a/source/kong_tcpingress.go +++ b/source/kong_tcpingress.go @@ -121,7 +121,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp tcpIngresses = append(tcpIngresses, tcpIngress) } - tcpIngresses, err = sc.filterByAnnotations(tcpIngresses) + tcpIngresses, err = annotations.Filter(tcpIngresses, sc.annotationFilter) if err != nil { return nil, fmt.Errorf("failed to filter TCPIngresses: %w", err) } @@ -162,30 +162,6 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp return endpoints, nil } -// filterByAnnotations filters a list of TCPIngresses by a given annotation selector. -func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) { - selector, err := annotations.ParseFilter(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return tcpIngresses, nil - } - - var filteredList []*TCPIngress - - for _, tcpIngress := range tcpIngresses { - // include TCPIngress if its annotations match the selector - if selector.Matches(labels.Set(tcpIngress.Annotations)) { - filteredList = append(filteredList, tcpIngress) - } - } - - return filteredList, nil -} - // endpointsFromTCPIngress extracts the endpoints from a TCPIngress object func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint diff --git a/source/node.go b/source/node.go index bc78beaf4a..39e059268c 100644 --- a/source/node.go +++ b/source/node.go @@ -96,7 +96,7 @@ func (ns *nodeSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) return nil, err } - nodes, err = ns.filterByAnnotations(nodes) + nodes, err = annotations.Filter(nodes, ns.annotationFilter) if err != nil { return nil, err } @@ -202,30 +202,6 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) { return nil, fmt.Errorf("could not find node address for %s", node.Name) } -// filterByAnnotations filters a list of nodes by a given annotation selector. -func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) { - selector, err := annotations.ParseFilter(ns.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return nodes, nil - } - - var filteredList []*v1.Node - - for _, node := range nodes { - // include a node if its annotations match the selector - if selector.Matches(labels.Set(node.Annotations)) { - filteredList = append(filteredList, node) - } - } - - return filteredList, nil -} - // collectDNSNames returns a set of DNS names associated with the given Kubernetes Node. // If an FQDN template is configured, it renders the template using the Node object // to generate one or more DNS names. diff --git a/source/openshift_route.go b/source/openshift_route.go index 66180b70ad..a81e3fa5c9 100644 --- a/source/openshift_route.go +++ b/source/openshift_route.go @@ -122,7 +122,7 @@ func (ors *ocpRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, return nil, err } - ocpRoutes, err = ors.filterByAnnotations(ocpRoutes) + ocpRoutes, err = annotations.Filter(ocpRoutes, ors.annotationFilter) if err != nil { return nil, err } @@ -195,29 +195,6 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en return endpoints, nil } -func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*routev1.Route, error) { - selector, err := annotations.ParseFilter(ors.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return ocpRoutes, nil - } - - var filteredList []*routev1.Route - - for _, ocpRoute := range ocpRoutes { - // include ocpRoute if its annotations match the selector - if selector.Matches(labels.Set(ocpRoute.Annotations)) { - filteredList = append(filteredList, ocpRoute) - } - } - - return filteredList, nil -} - // endpointsFromOcpRoute extracts the endpoints from a OpenShift Route object func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignoreHostnameAnnotation bool) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint diff --git a/source/service.go b/source/service.go index f978e3294a..338e749efe 100644 --- a/source/service.go +++ b/source/service.go @@ -239,7 +239,7 @@ func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err // filter on service types if at least one has been provided services = sc.filterByServiceType(services) - services, err = sc.filterByAnnotations(services) + services, err = annotations.Filter(services, sc.annotationFilter) if err != nil { return nil, err } @@ -572,30 +572,6 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint { return endpoints } -// filterByAnnotations filters a list of services by a given annotation selector. -func (sc *serviceSource) filterByAnnotations(services []*v1.Service) ([]*v1.Service, error) { - selector, err := annotations.ParseFilter(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return services, nil - } - - var filteredList []*v1.Service - - for _, service := range services { - // include service if its annotations match the selector - if selector.Matches(labels.Set(service.Annotations)) { - filteredList = append(filteredList, service) - } - } - log.Debugf("filtered %d services out of %d with annotation filter", len(filteredList), len(services)) - return filteredList, nil -} - // filterByServiceType filters services according to their types func (sc *serviceSource) filterByServiceType(services []*v1.Service) []*v1.Service { if !sc.serviceTypeFilter.enabled || len(services) == 0 { diff --git a/source/skipper_routegroup.go b/source/skipper_routegroup.go index fc5737cb80..98a06d0fe8 100644 --- a/source/skipper_routegroup.go +++ b/source/skipper_routegroup.go @@ -246,13 +246,14 @@ func (sc *routeGroupSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint log.Errorf("Failed to get RouteGroup list: %v", err) return nil, err } - rgList, err = sc.filterByAnnotations(rgList) + + filtered, err := annotations.Filter(rgList.Items, sc.annotationFilter) if err != nil { return nil, err } endpoints := []*endpoint.Endpoint{} - for _, rg := range rgList.Items { + for _, rg := range filtered { // Check controller annotation to see if we are responsible. controller, ok := rg.Metadata.Annotations[annotations.ControllerKey] if ok && controller != annotations.ControllerValue { @@ -364,30 +365,6 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint. return endpoints } -// filterByAnnotations filters a list of routeGroupList by a given annotation selector. -func (sc *routeGroupSource) filterByAnnotations(rgs *routeGroupList) (*routeGroupList, error) { - selector, err := getLabelSelector(sc.annotationFilter) - if err != nil { - return nil, err - } - - // empty filter returns original list - if selector.Empty() { - return rgs, nil - } - - var filteredList []*routeGroup - for _, rg := range rgs.Items { - // include ingress if its annotations match the selector - if matchLabelSelector(selector, rg.Metadata.Annotations) { - filteredList = append(filteredList, rg) - } - } - rgs.Items = filteredList - - return rgs, nil -} - func targetsFromRouteGroupStatus(status routeGroupStatus) endpoint.Targets { var targets endpoint.Targets @@ -443,3 +420,7 @@ type routeGroupLoadBalancer struct { IP string `json:"ip,omitempty"` Hostname string `json:"hostname,omitempty"` } + +func (rg *routeGroup) GetAnnotations() map[string]string { + return rg.Metadata.Annotations +} diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index 8deca9d851..86c38fe9dd 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -240,7 +240,7 @@ func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) { typed := &IngressRoute{} return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil) }, - ts.filterIngressRouteByAnnotation, + ts.annotationFilter, func(r *IngressRoute, targets endpoint.Targets) []*endpoint.Endpoint { return ts.endpointsFromIngressRoute(r, targets) }, @@ -271,7 +271,7 @@ func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error ingressRouteTCPs = append(ingressRouteTCPs, ingressRouteTCP) } - ingressRouteTCPs, err = ts.filterIngressRouteTcpByAnnotations(ingressRouteTCPs) + ingressRouteTCPs, err = annotations.Filter(ingressRouteTCPs, ts.annotationFilter) if err != nil { return nil, fmt.Errorf("failed to filter IngressRouteTCP: %w", err) } @@ -305,7 +305,7 @@ func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error typed := &IngressRouteUDP{} return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil) }, - ts.filterIngressRouteUdpByAnnotations, + ts.annotationFilter, ts.endpointsFromIngressRouteUDP, ) } @@ -319,7 +319,7 @@ func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error typed := &IngressRoute{} return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil) }, - ts.filterIngressRouteByAnnotation, + ts.annotationFilter, func(r *IngressRoute, targets endpoint.Targets) []*endpoint.Endpoint { return ts.endpointsFromIngressRoute(r, targets) }, @@ -335,7 +335,7 @@ func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, er typed := &IngressRouteTCP{} return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil) }, - ts.filterIngressRouteTcpByAnnotations, + ts.annotationFilter, ts.endpointsFromIngressRouteTCP, ) } @@ -349,32 +349,11 @@ func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, er typed := &IngressRouteUDP{} return typed, ts.unstructuredConverter.scheme.Convert(u, typed, nil) }, - ts.filterIngressRouteUdpByAnnotations, + ts.annotationFilter, ts.endpointsFromIngressRouteUDP, ) } -// filterIngressRouteByAnnotation filters a list of IngressRoute by a given annotation selector. -func (ts *traefikSource) filterIngressRouteByAnnotation(input []*IngressRoute) ([]*IngressRoute, error) { - return filterResourcesByAnnotations(input, ts.annotationFilter, func(ir *IngressRoute) map[string]string { - return ir.Annotations - }) -} - -// filterIngressRouteTcpByAnnotations filters a list of IngressRouteTCP by a given annotation selector. -func (ts *traefikSource) filterIngressRouteTcpByAnnotations(input []*IngressRouteTCP) ([]*IngressRouteTCP, error) { - return filterResourcesByAnnotations(input, ts.annotationFilter, func(ir *IngressRouteTCP) map[string]string { - return ir.Annotations - }) -} - -// filterIngressRouteUdpByAnnotations filters a list of IngressRoute by a given annotation selector. -func (ts *traefikSource) filterIngressRouteUdpByAnnotations(input []*IngressRouteUDP) ([]*IngressRouteUDP, error) { - return filterResourcesByAnnotations(input, ts.annotationFilter, func(ir *IngressRouteUDP) map[string]string { - return ir.Annotations - }) -} - // endpointsFromIngressRoute extracts the endpoints from a IngressRoute object func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, targets endpoint.Targets) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint @@ -842,19 +821,34 @@ func (in *IngressRouteUDPList) DeepCopyObject() runtime.Object { return nil } +// GetAnnotations returns the annotations of the IngressRoute. +func (in *IngressRoute) GetAnnotations() map[string]string { + return in.Annotations +} + +// GetAnnotations returns the annotations of the IngressRouteTCP. +func (in *IngressRouteTCP) GetAnnotations() map[string]string { + return in.Annotations +} + +// GetAnnotations returns the annotations of the IngressRouteUDP. +func (in *IngressRouteUDP) GetAnnotations() map[string]string { + return in.Annotations +} + // extractEndpoints is a generic function that extracts endpoints from Kubernetes resources. // It performs the following steps: // 1. Lists all objects in the specified namespace using the provided informer. // 2. Converts the unstructured objects to the desired type using the convertFunc. -// 3. Filters the converted objects based on the provided filterFunc. +// 3. Filters the converted objects based on the annotation filter. // 4. Generates endpoints for each filtered object using the generateEndpoints function. // Returns a list of generated endpoints or an error if any step fails. -func extractEndpoints[T any]( +func extractEndpoints[T annotations.AnnotatedObject]( informer cache.GenericLister, namespace string, - convertFunc func(*unstructured.Unstructured) (*T, error), - filterFunc func([]*T) ([]*T, error), - generateEndpoints func(*T, endpoint.Targets) []*endpoint.Endpoint, + convertFunc func(*unstructured.Unstructured) (T, error), + annotationFilter string, + generateEndpoints func(T, endpoint.Targets) []*endpoint.Endpoint, ) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint @@ -863,7 +857,7 @@ func extractEndpoints[T any]( return nil, err } - var typedObjs []*T + var typedObjs []T for _, obj := range objs { unstructuredObj, ok := obj.(*unstructured.Unstructured) if !ok { @@ -877,13 +871,13 @@ func extractEndpoints[T any]( typedObjs = append(typedObjs, typed) } - typedObjs, err = filterFunc(typedObjs) + typedObjs, err = annotations.Filter(typedObjs, annotationFilter) if err != nil { return nil, err } for _, item := range typedObjs { - targets := annotations.TargetsFromTargetAnnotation(getAnnotations(item)) + targets := annotations.TargetsFromTargetAnnotation(item.GetAnnotations()) name := getObjectFullName(item) ingressEndpoints := generateEndpoints(item, targets) @@ -900,46 +894,6 @@ func extractEndpoints[T any]( return endpoints, nil } -// filterResourcesByAnnotations filters a list of resources based on a given annotation selector. -// It performs the following steps: -// 1. Parses the annotation filter into a label selector. -// 2. Converts the label selector into a Kubernetes selector. -// 3. If the selector is empty, returns the original list of resources. -// 4. Iterates through the resources and matches their annotations against the selector. -// 5. Returns the filtered list of resources or an error if any step fails. -func filterResourcesByAnnotations[T any](resources []*T, annotationFilter string, getAnnotations func(*T) map[string]string) ([]*T, error) { - selector, err := annotations.ParseFilter(annotationFilter) - if err != nil { - return nil, err - } - - if selector.Empty() { - return resources, nil - } - - var filteredList []*T - for _, resource := range resources { - if selector.Matches(labels.Set(getAnnotations(resource))) { - filteredList = append(filteredList, resource) - } - } - - return filteredList, nil -} - -func getAnnotations(obj any) map[string]string { - switch o := obj.(type) { - case *IngressRouteUDP: - return o.Annotations - case *IngressRoute: - return o.Annotations - case *IngressRouteTCP: - return o.Annotations - default: - return nil - } -} - func getObjectFullName(obj any) string { switch o := obj.(type) { case *IngressRouteUDP: