diff --git a/docs/sources/sources.md b/docs/sources/sources.md index 4c9dc8b93..b15662491 100644 --- a/docs/sources/sources.md +++ b/docs/sources/sources.md @@ -18,7 +18,7 @@ | istio-gateway | Gateway.networking.istio.io | Yes | | | istio-virtualservice | VirtualService.networking.istio.io | Yes | | | kong-tcpingress | TCPIngress.configuration.konghq.com | Yes | | -| node | Node | Yes | | +| node | Node | Yes | Yes | | openshift-route | Route.route.openshift.io | Yes | Yes | | pod | Pod | | | | [service](service.md) | Service | Yes | Yes | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index f8138e86a..19f869f5d 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -422,7 +422,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName) app.Flag("namespace", "Limit resources queried for endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter resources queried for endpoints by annotation, using label selector semantics").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) - app.Flag("label-filter", "Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, openshift-route, and service").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) + app.Flag("label-filter", "Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, and service").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) app.Flag("ingress-class", "Require an Ingress to have this class name (defaults to any class; specify multiple times to allow more than one class)").StringsVar(&cfg.IngressClassNames) 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) diff --git a/source/node.go b/source/node.go index 223ec2982..261002057 100644 --- a/source/node.go +++ b/source/node.go @@ -38,10 +38,11 @@ type nodeSource struct { annotationFilter string fqdnTemplate *template.Template nodeInformer coreinformers.NodeInformer + labelSelector labels.Selector } // NewNodeSource creates a new nodeSource with the given config. -func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string) (Source, error) { +func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string, labelSelector labels.Selector) (Source, error) { tmpl, err := parseTemplate(fqdnTemplate) if err != nil { return nil, err @@ -73,12 +74,13 @@ func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotat annotationFilter: annotationFilter, fqdnTemplate: tmpl, nodeInformer: nodeInformer, + labelSelector: labelSelector, }, nil } // Endpoints returns endpoint objects for each service that should be processed. func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { - nodes, err := ns.nodeInformer.Lister().List(labels.Everything()) + nodes, err := ns.nodeInformer.Lister().List(ns.labelSelector) if err != nil { return nil, err } diff --git a/source/node_test.go b/source/node_test.go index 885d9f54e..217e4a38a 100644 --- a/source/node_test.go +++ b/source/node_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" 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 testNodeSourceNewNodeSource(t *testing.T) { fake.NewSimpleClientset(), ti.annotationFilter, ti.fqdnTemplate, + labels.Everything(), ) if ti.expectError { @@ -93,6 +95,7 @@ func testNodeSourceEndpoints(t *testing.T) { for _, tc := range []struct { title string annotationFilter string + labelSelector string fqdnTemplate string nodeName string nodeAddresses []v1.NodeAddress @@ -102,294 +105,223 @@ func testNodeSourceEndpoints(t *testing.T) { expectError bool }{ { - "node with short hostname returns one endpoint", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with short hostname returns one endpoint", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "node with fqdn returns one endpoint", - "", - "", - "node1.example.org", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with fqdn returns one endpoint", + nodeName: "node1.example.org", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "ipv6 node with fqdn returns one endpoint", - "", - "", - "node1.example.org", - []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "ipv6 node with fqdn returns one endpoint", + nodeName: "node1.example.org", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + expected: []*endpoint.Endpoint{ {RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}}, }, - false, }, { - "node with fqdn template returns endpoint with expanded hostname", - "", - "{{.Name}}.example.org", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with fqdn template returns endpoint with expanded hostname", + fqdnTemplate: "{{.Name}}.example.org", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "node with fqdn and fqdn template returns one endpoint", - "", - "{{.Name}}.example.org", - "node1.example.org", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with fqdn and fqdn template returns one endpoint", + fqdnTemplate: "{{.Name}}.example.org", + nodeName: "node1.example.org", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1.example.org.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "node with fqdn template returns two endpoints with multiple IP addresses and expanded hostname", - "", - "{{.Name}}.example.org", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeExternalIP, Address: "5.6.7.8"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with fqdn template returns two endpoints with multiple IP addresses and expanded hostname", + fqdnTemplate: "{{.Name}}.example.org", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeExternalIP, Address: "5.6.7.8"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4", "5.6.7.8"}}, }, - false, }, { - "node with fqdn template returns two endpoints with dual-stack IP addresses and expanded hostname", - "", - "{{.Name}}.example.org", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with fqdn template returns two endpoints with dual-stack IP addresses and expanded hostname", + fqdnTemplate: "{{.Name}}.example.org", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}}, }, - false, }, { - "node with both external and internal IP returns an endpoint with external IP", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with both external and internal IP returns an endpoint with external IP", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "node with both external, internal, and IPv6 IP returns endpoints with external IPs", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with both external, internal, and IPv6 IP returns endpoints with external IPs", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, {RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}}, }, - false, }, { - "node with only internal IP returns an endpoint with internal IP", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with only internal IP returns an endpoint with internal IP", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}}, }, - false, }, { - "node with only internal IPs returns endpoints with internal IPs", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "node with only internal IPs returns endpoints with internal IPs", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}}, {RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}}, }, - false, }, { - "node with neither external nor internal IP returns no endpoints", - "", - "", - "node1", - []v1.NodeAddress{}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{}, - true, + title: "node with neither external nor internal IP returns no endpoints", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{}, + expectError: true, }, { - "annotated node without annotation filter returns endpoint", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ + title: "annotated node without annotation filter returns endpoint", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "annotated node with matching annotation filter returns endpoint", - "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ + title: "annotated node with matching annotation filter returns endpoint", + annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "annotated node with non-matching annotation filter returns endpoint", - "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ + title: "annotated node with non-matching annotation filter returns nothing", + annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ "service.beta.kubernetes.io/external-traffic": "SomethingElse", }, - []*endpoint.Endpoint{}, - false, + expected: []*endpoint.Endpoint{}, }, { - "our controller type is dns-controller", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ - controllerAnnotationKey: controllerAnnotationValue, + title: "labeled node with matching label selector returns endpoint", + labelSelector: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + labels: map[string]string{ + "service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, }, - false, }, { - "different controller types are ignored", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ + title: "labeled node with non-matching label selector returns nothing", + labelSelector: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + labels: map[string]string{ + "service.beta.kubernetes.io/external-traffic": "SomethingElse", + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "our controller type is dns-controller", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ + controllerAnnotationKey: controllerAnnotationValue, + }, + expected: []*endpoint.Endpoint{ + {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, + }, + }, + { + title: "different controller types are ignored", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ controllerAnnotationKey: "not-dns-controller", }, - []*endpoint.Endpoint{}, - false, + expected: []*endpoint.Endpoint{}, }, { - "ttl not annotated should have RecordTTL.IsConfigured set to false", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{}, - []*endpoint.Endpoint{ + title: "ttl not annotated should have RecordTTL.IsConfigured set to false", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, - false, }, { - "ttl annotated but invalid should have RecordTTL.IsConfigured set to false", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ + title: "ttl annotated but invalid should have RecordTTL.IsConfigured set to false", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ ttlAnnotationKey: "foo", }, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, - false, }, { - "ttl annotated and is valid should set Record.TTL", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - map[string]string{}, - map[string]string{ + title: "ttl annotated and is valid should set Record.TTL", + nodeName: "node1", + nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, + annotations: map[string]string{ ttlAnnotationKey: "10", }, - []*endpoint.Endpoint{ + expected: []*endpoint.Endpoint{ {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)}, }, - false, - }, - { - "node with nil Labels returns valid endpoint", - "", - "", - "node1", - []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}}, - nil, - map[string]string{}, - []*endpoint.Endpoint{ - {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}, Labels: map[string]string{}}, - }, - false, }, } { tc := tc t.Run(tc.title, func(t *testing.T) { t.Parallel() + labelSelector := labels.Everything() + if tc.labelSelector != "" { + var err error + labelSelector, err = labels.Parse(tc.labelSelector) + require.NoError(t, err) + } + // Create a Kubernetes testing client kubernetes := fake.NewSimpleClientset() @@ -413,6 +345,7 @@ func testNodeSourceEndpoints(t *testing.T) { kubernetes, tc.annotationFilter, tc.fqdnTemplate, + labelSelector, ) require.NoError(t, err) diff --git a/source/store.go b/source/store.go index fab0371f6..3599390e8 100644 --- a/source/store.go +++ b/source/store.go @@ -210,7 +210,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg if err != nil { return nil, err } - return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate) + return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter) case "service": client, err := p.KubeClient() if err != nil {