From 0aababa74156664680a6dd525ce92a2b03417f7b Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:36:59 +0100 Subject: [PATCH] feat(source/node): fqdn support combineFQDNAnnotation (#5526) Signed-off-by: ivan katliarchuk --- docs/advanced/fqdn-templating.md | 2 +- source/node.go | 85 ++++++++++++++++++++++---------- source/node_fqdn_test.go | 29 +++++++++++ source/node_test.go | 4 ++ source/store.go | 2 +- 5 files changed, 93 insertions(+), 29 deletions(-) diff --git a/docs/advanced/fqdn-templating.md b/docs/advanced/fqdn-templating.md index 82ca6c249..a885f87c6 100644 --- a/docs/advanced/fqdn-templating.md +++ b/docs/advanced/fqdn-templating.md @@ -54,7 +54,7 @@ The template uses the following data from the source object (e.g., a `Service` o | `istio-gateway` | Queries Istio Gateway resources for endpoints. | ✅ | ✅ | | `istio-virtualservice` | Queries Istio VirtualService resources for endpoints. | ✅ | ✅ | | `kong-tcpingress` | Queries Kong TCPIngress resources for endpoints. | ❌ | ❌ | -| `node` | Queries Kubernetes Node resources for endpoints. | ✅ | ❌ | +| `node` | Queries Kubernetes Node resources for endpoints. | ✅ | ✅ | | `openshift-route` | Queries OpenShift Route resources for endpoints. | ✅ | ✅ | | `pod` | Queries Kubernetes Pod resources for endpoints. | ✅ | ✅ | | `service` | Queries Kubernetes Service resources for endpoints. | ✅ | ✅ | diff --git a/source/node.go b/source/node.go index 57c844828..53a438d4c 100644 --- a/source/node.go +++ b/source/node.go @@ -38,9 +38,11 @@ import ( const warningMsg = "The default behavior of exposing internal IPv6 addresses will change in the next minor version. Use --no-expose-internal-ipv6 flag to opt-in to the new behavior." type nodeSource struct { - client kubernetes.Interface - annotationFilter string - fqdnTemplate *template.Template + client kubernetes.Interface + annotationFilter string + fqdnTemplate *template.Template + combineFQDNAnnotation bool + nodeInformer coreinformers.NodeInformer labelSelector labels.Selector excludeUnschedulable bool @@ -48,7 +50,14 @@ type nodeSource struct { } // NewNodeSource creates a new nodeSource with the given config. -func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string, labelSelector labels.Selector, exposeInternalIPv6, excludeUnschedulable bool) (Source, error) { +func NewNodeSource( + ctx context.Context, + kubeClient kubernetes.Interface, + annotationFilter, fqdnTemplate string, + labelSelector labels.Selector, + exposeInternalIPv6, + excludeUnschedulable bool, + combineFQDNAnnotation bool) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { return nil, err @@ -76,18 +85,19 @@ func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotat } return &nodeSource{ - client: kubeClient, - annotationFilter: annotationFilter, - fqdnTemplate: tmpl, - nodeInformer: nodeInformer, - labelSelector: labelSelector, - excludeUnschedulable: excludeUnschedulable, - exposeInternalIPv6: exposeInternalIPv6, + client: kubeClient, + annotationFilter: annotationFilter, + fqdnTemplate: tmpl, + combineFQDNAnnotation: combineFQDNAnnotation, + nodeInformer: nodeInformer, + labelSelector: labelSelector, + excludeUnschedulable: excludeUnschedulable, + exposeInternalIPv6: exposeInternalIPv6, }, nil } // Endpoints returns endpoint objects for each service that should be processed. -func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { +func (ns *nodeSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) { nodes, err := ns.nodeInformer.Lister().List(ns.labelSelector) if err != nil { return nil, err @@ -127,21 +137,9 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro } } - dnsNames := make(map[string]bool) - - if ns.fqdnTemplate != nil { - hostnames, err := fqdn.ExecTemplate(ns.fqdnTemplate, node) - if err != nil { - return nil, err - } - - for _, name := range hostnames { - dnsNames[name] = true - log.Debugf("applied template for %s, converting to %s", node.Name, name) - } - } else { - dnsNames[node.Name] = true - log.Debugf("not applying template for %s", node.Name) + dnsNames, err := ns.collectDNSNames(node) + if err != nil { + return nil, err } for dns := range dnsNames { @@ -233,3 +231,36 @@ func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) 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. +// If combineFQDNAnnotation is enabled, the Node's name is also included alongside +// the templated names. If no FQDN template is provided, the result will include only +// the Node's name. +// +// Returns an error if template rendering fails. +func (ns *nodeSource) collectDNSNames(node *v1.Node) (map[string]bool, error) { + dnsNames := make(map[string]bool) + // If no FQDN template is configured, fallback to the node name + if ns.fqdnTemplate == nil { + dnsNames[node.Name] = true + return dnsNames, nil + } + + names, err := fqdn.ExecTemplate(ns.fqdnTemplate, node) + if err != nil { + return nil, err + } + + for _, name := range names { + dnsNames[name] = true + log.Debugf("applied template for %s, converting to %s", node.Name, name) + } + + if ns.combineFQDNAnnotation { + dnsNames[node.Name] = true + } + + return dnsNames, nil +} diff --git a/source/node_fqdn_test.go b/source/node_fqdn_test.go index 5d93b90c7..726d02efe 100644 --- a/source/node_fqdn_test.go +++ b/source/node_fqdn_test.go @@ -64,6 +64,7 @@ func TestNodeSourceNewNodeSourceWithFqdn(t *testing.T) { labels.Everything(), true, true, + false, ) if tt.expectError { assert.Error(t, err) @@ -80,6 +81,7 @@ func TestNodeSourceFqdnTemplatingExamples(t *testing.T) { nodes []*v1.Node fqdnTemplate string expected []*endpoint.Endpoint + combineFQDN bool }{ { title: "templating expansion with multiple domains", @@ -293,6 +295,32 @@ func TestNodeSourceFqdnTemplatingExamples(t *testing.T) { {DNSName: "node-name-2.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.178"}}, }, }, + { + title: "templating with shared all domain and fqdn combination annotation", + combineFQDN: true, + nodes: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node-name-1"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "243.186.136.160"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "node-name-2"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "243.186.136.178"}}, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.domain.tld,all.example.com", + expected: []*endpoint.Endpoint{ + {DNSName: "all.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.160", "243.186.136.178"}}, + {DNSName: "node-name-1.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.160"}}, + {DNSName: "node-name-2.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.178"}}, + {DNSName: "node-name-1", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.160"}}, + {DNSName: "node-name-2", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.178"}}, + }, + }, } { t.Run(tt.title, func(t *testing.T) { kubeClient := fake.NewClientset() @@ -310,6 +338,7 @@ func TestNodeSourceFqdnTemplatingExamples(t *testing.T) { labels.Everything(), true, true, + tt.combineFQDN, ) require.NoError(t, err) diff --git a/source/node_test.go b/source/node_test.go index 3e738415a..160d5994b 100644 --- a/source/node_test.go +++ b/source/node_test.go @@ -95,6 +95,7 @@ func testNodeSourceNewNodeSource(t *testing.T) { labels.Everything(), true, true, + false, ) if ti.expectError { @@ -440,6 +441,7 @@ func testNodeSourceEndpoints(t *testing.T) { labelSelector, tc.exposeInternalIPv6, tc.excludeUnschedulable, + false, ) require.NoError(t, err) @@ -557,6 +559,7 @@ func testNodeEndpointsWithIPv6(t *testing.T) { labelSelector, tc.exposeInternalIPv6, tc.excludeUnschedulable, + false, ) require.NoError(t, err) @@ -604,6 +607,7 @@ func TestResourceLabelIsSetForEachNodeEndpoint(t *testing.T) { labels.Everything(), false, true, + false, ) require.NoError(t, err) diff --git a/source/store.go b/source/store.go index f78664255..dc5812ad5 100644 --- a/source/store.go +++ b/source/store.go @@ -267,7 +267,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, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable) + return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation) case "service": client, err := p.KubeClient() if err != nil {