Skip to content

Commit 0d1108f

Browse files
chore(source/service): restructure code to make service type filters testable (#5485)
* chore(source/service): restructure code with filters are testable * chore(source/service): restructure code with filters are testable Co-authored-by: Michel Loiseleur <[email protected]> * chore(source/service): restructure code with filters are testable Signed-off-by: ivan katliarchuk <[email protected]> --------- Signed-off-by: ivan katliarchuk <[email protected]> Co-authored-by: Michel Loiseleur <[email protected]>
1 parent fbaf15f commit 0d1108f

File tree

4 files changed

+284
-49
lines changed

4 files changed

+284
-49
lines changed

docs/flags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
| `--pod-source-domain=""` | Domain to use for pods records (optional) |
4747
| `--[no-]publish-host-ip` | Allow external-dns to publish host-ip for headless services (optional) |
4848
| `--[no-]publish-internal-services` | Allow external-dns to publish DNS records for ClusterIP services (optional) |
49-
| `--service-type-filter=SERVICE-TYPE-FILTER` | The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName) |
49+
| `--service-type-filter=SERVICE-TYPE-FILTER` | The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName) |
5050
| `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy) |
5151
| `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) |
5252
| `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group |

pkg/apis/externaldns/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ func App(cfg *Config) *kingpin.Application {
480480
app.Flag("pod-source-domain", "Domain to use for pods records (optional)").Default(defaultConfig.PodSourceDomain).StringVar(&cfg.PodSourceDomain)
481481
app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP)
482482
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
483-
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
483+
app.Flag("service-type-filter", "The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").Default(defaultConfig.ServiceTypeFilter...).StringsVar(&cfg.ServiceTypeFilter)
484484
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "f5-transportserver", "traefik-proxy")
485485
app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter)
486486
app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy)

source/service.go

Lines changed: 75 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ package source
1919
import (
2020
"context"
2121
"fmt"
22+
"maps"
2223
"net"
24+
"slices"
2325
"sort"
2426
"strings"
2527
"text/template"
@@ -41,20 +43,28 @@ import (
4143
"sigs.k8s.io/external-dns/source/fqdn"
4244
)
4345

46+
var (
47+
knownServiceTypes = map[v1.ServiceType]struct{}{
48+
v1.ServiceTypeClusterIP: {}, // Default service type exposes the service on a cluster-internal IP.
49+
v1.ServiceTypeNodePort: {}, // Exposes the service on each node's IP at a static port.
50+
v1.ServiceTypeLoadBalancer: {}, // Exposes the service externally using a cloud provider's load balancer.
51+
v1.ServiceTypeExternalName: {}, // Maps the service to an external DNS name.
52+
}
53+
)
54+
4455
// serviceSource is an implementation of Source for Kubernetes service objects.
4556
// It will find all services that are under our jurisdiction, i.e. annotated
4657
// desired hostname and matching or no controller annotation. For each of the
4758
// matched services' entrypoints it will return a corresponding
4859
// Endpoint object.
4960
type serviceSource struct {
50-
client kubernetes.Interface
51-
namespace string
52-
annotationFilter string
61+
client kubernetes.Interface
62+
namespace string
63+
annotationFilter string
64+
labelSelector labels.Selector
65+
fqdnTemplate *template.Template
66+
combineFQDNAnnotation bool
5367

54-
// process Services with legacy annotations
55-
compatibility string
56-
fqdnTemplate *template.Template
57-
combineFQDNAnnotation bool
5868
ignoreHostnameAnnotation bool
5969
publishInternal bool
6070
publishHostIP bool
@@ -65,9 +75,11 @@ type serviceSource struct {
6575
endpointsInformer coreinformers.EndpointsInformer
6676
podInformer coreinformers.PodInformer
6777
nodeInformer coreinformers.NodeInformer
68-
serviceTypeFilter map[string]struct{}
69-
labelSelector labels.Selector
78+
serviceTypeFilter *serviceTypes
7079
exposeInternalIPv6 bool
80+
81+
// process Services with legacy annotations
82+
compatibility string
7183
}
7284

7385
// NewServiceSource creates a new serviceSource with the given config.
@@ -78,7 +90,7 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
7890
}
7991

8092
// Use shared informers to listen for add/update/delete of services/pods/nodes in the specified namespace.
81-
// Set resync period to 0, to prevent processing when nothing has changed
93+
// Set the resync period to 0 to prevent processing when nothing has changed
8294
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
8395
serviceInformer := informerFactory.Core().V1().Services()
8496
endpointsInformer := informerFactory.Core().V1().Endpoints()
@@ -118,11 +130,10 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
118130
return nil, err
119131
}
120132

121-
// Transform the slice into a map so it will
122-
// be way much easier and fast to filter later
123-
serviceTypes := make(map[string]struct{})
124-
for _, serviceType := range serviceTypeFilter {
125-
serviceTypes[serviceType] = struct{}{}
133+
// Transform the slice into a map so it will be way much easier and fast to filter later
134+
sTypesFilter, err := newServiceTypesFilter(serviceTypeFilter)
135+
if err != nil {
136+
return nil, err
126137
}
127138

128139
return &serviceSource{
@@ -140,30 +151,29 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name
140151
endpointsInformer: endpointsInformer,
141152
podInformer: podInformer,
142153
nodeInformer: nodeInformer,
143-
serviceTypeFilter: serviceTypes,
154+
serviceTypeFilter: sTypesFilter,
144155
labelSelector: labelSelector,
145156
resolveLoadBalancerHostname: resolveLoadBalancerHostname,
146157
listenEndpointEvents: listenEndpointEvents,
147158
exposeInternalIPv6: exposeInternalIPv6,
148159
}, nil
149160
}
150161

151-
// Endpoints returns endpoint objects for each service that should be processed.
152-
func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
162+
// Endpoints return endpoint objects for each service that should be processed.
163+
func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) {
153164
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(sc.labelSelector)
154165
if err != nil {
155166
return nil, err
156167
}
168+
169+
// filter on service types if at least one has been provided
170+
services = sc.filterByServiceType(services)
171+
157172
services, err = sc.filterByAnnotations(services)
158173
if err != nil {
159174
return nil, err
160175
}
161176

162-
// filter on service types if at least one has been provided
163-
if len(sc.serviceTypeFilter) > 0 {
164-
services = sc.filterByServiceType(services)
165-
}
166-
167177
endpoints := []*endpoint.Endpoint{}
168178

169179
for _, svc := range services {
@@ -427,11 +437,7 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
427437

428438
// filterByAnnotations filters a list of services by a given annotation selector.
429439
func (sc *serviceSource) filterByAnnotations(services []*v1.Service) ([]*v1.Service, error) {
430-
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
431-
if err != nil {
432-
return nil, err
433-
}
434-
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
440+
selector, err := annotations.ParseFilter(sc.annotationFilter)
435441
if err != nil {
436442
return nil, err
437443
}
@@ -449,21 +455,23 @@ func (sc *serviceSource) filterByAnnotations(services []*v1.Service) ([]*v1.Serv
449455
filteredList = append(filteredList, service)
450456
}
451457
}
452-
458+
log.Debugf("filtered %d services out of %d with annotation filter", len(filteredList), len(services))
453459
return filteredList, nil
454460
}
455461

456-
// filterByServiceType filters services according their types
462+
// filterByServiceType filters services according to their types
457463
func (sc *serviceSource) filterByServiceType(services []*v1.Service) []*v1.Service {
458-
filteredList := []*v1.Service{}
464+
if !sc.serviceTypeFilter.enabled || len(services) == 0 {
465+
return services
466+
}
467+
var result []*v1.Service
459468
for _, service := range services {
460-
// Check if the service is of the given type or not
461-
if _, ok := sc.serviceTypeFilter[string(service.Spec.Type)]; ok {
462-
filteredList = append(filteredList, service)
469+
if _, ok := sc.serviceTypeFilter.types[service.Spec.Type]; ok {
470+
result = append(result, service)
463471
}
464472
}
465-
466-
return filteredList
473+
log.Debugf("filtered %d services out of %d with service types filter %q", len(result), len(services), slices.Collect(maps.Keys(sc.serviceTypeFilter.types)))
474+
return result
467475
}
468476

469477
func (sc *serviceSource) setResourceLabel(service *v1.Service, endpoints []*endpoint.Endpoint) {
@@ -625,9 +633,9 @@ func (sc *serviceSource) nodesExternalTrafficPolicyTypeLocal(svc *v1.Service) []
625633
// If none available, fall back to nodes with ready pods
626634
// If still none, use nodes with any running pods
627635
if len(nodes) > 0 {
628-
// Works same as service endpoints
636+
// Works the same as service endpoints
629637
} else if len(nodesReady) > 0 {
630-
// 2 level of panic modes as safe guard, because old wrong behavior can be used by someone
638+
// 2 level of panic modes as safeguard, because old wrong behavior can be used by someone
631639
// Publish all endpoints not always a bad thing
632640
log.Debugf("All pods in terminating state, use ready")
633641
nodes = nodesReady
@@ -639,7 +647,7 @@ func (sc *serviceSource) nodesExternalTrafficPolicyTypeLocal(svc *v1.Service) []
639647
return nodes
640648
}
641649

642-
// pods retrieves a slice of pods associated with the given Service
650+
// pods retrieve a slice of pods associated with the given Service
643651
func (sc *serviceSource) pods(svc *v1.Service) []*v1.Pod {
644652
labelSelector, err := metav1.ParseToLabelSelector(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String())
645653
if err != nil {
@@ -755,3 +763,31 @@ func (sc *serviceSource) AddEventHandler(_ context.Context, handler func()) {
755763
sc.endpointsInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
756764
}
757765
}
766+
767+
type serviceTypes struct {
768+
enabled bool
769+
types map[v1.ServiceType]bool
770+
}
771+
772+
// newServiceTypesFilter processes a slice of service type filter strings and returns a serviceTypes struct.
773+
// It validates the filter against known Kubernetes service types. If the filter is empty or contains an empty string,
774+
// service type filtering is disabled. If an unknown type is found, an error is returned.
775+
func newServiceTypesFilter(filter []string) (*serviceTypes, error) {
776+
if len(filter) == 0 || slices.Contains(filter, "") {
777+
return &serviceTypes{
778+
enabled: false,
779+
}, nil
780+
}
781+
types := make(map[v1.ServiceType]bool)
782+
for _, serviceType := range filter {
783+
if _, ok := knownServiceTypes[v1.ServiceType(serviceType)]; !ok {
784+
return nil, fmt.Errorf("unsupported service type filter: %q. Supported types are: %q", serviceType, slices.Collect(maps.Keys(knownServiceTypes)))
785+
}
786+
types[v1.ServiceType(serviceType)] = true
787+
}
788+
789+
return &serviceTypes{
790+
enabled: true,
791+
types: types,
792+
}, nil
793+
}

0 commit comments

Comments
 (0)