external-dns/source/source.go
Maxim Ivanov 63802fda51 feat(service): add support for fqdn-template when generating per-pod DNS entries.
Allows further customization of a generated per-pod DNS name
with `--fqdn-template` parameter if it is set.

diff --git c/docs/sources/service.md i/docs/sources/service.md
index 7f03426e..1c30eba7 100644
--- c/docs/sources/service.md
+++ i/docs/sources/service.md
@@ -34,7 +34,8 @@ For each domain name created for the Service, the additional DNS entry for the P
 the value of the Pod's `spec.hostname` field and a `.`.

 Another way to create per-pod DNS entries is to annotate headless service with
-`external-dns.alpha.kubernetes.io/service-pod-endpoints: true`,  this will prefix service domain name with pod name.
+`external-dns.alpha.kubernetes.io/service-pod-endpoints` and values `pod-name` or `fqdn-template`. Former prefixes
+service domain name with pod name, latter uses `--fqdn-template` to generate the domain name for each pod in the service.

 ## Targets
diff --git c/source/service.go i/source/service.go
index 0186a102..d913cfe1 100644
--- c/source/service.go
+++ i/source/service.go
@@ -279,7 +279,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri

 	endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations)

-	_, perPodDNS := svc.Annotations[servicePodEndpointsKey]
+	perPodDNSMode, perPodDNS := svc.Annotations[servicePodEndpointsKey]

 	targetsByHeadlessDomainAndType := make(map[endpoint.EndpointKey]endpoint.Targets)
 	for _, subset := range endpointsObject.Subsets {
@@ -312,7 +312,18 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 			}

 			if perPodDNS {
-				headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
+				if perPodDNSMode == ServicePodEndpointsPodName {
+					headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
+				} else if perPodDNSMode == ServicePodEndpointsFqdnTemplate {
+					if hostnames, err := execTemplate(sc.fqdnTemplate, pod); err == nil {
+						headlessDomains = append(headlessDomains, hostnames...)
+					} else {
+						log.Errorf("Error executing template for pod %s: %v", pod.Name, err)
+					}
+				} else {
+					log.Errorf("Unknown `service-pod-endpoints` value %s", perPodDNSMode)
+					return endpoints
+				}
 			}

 			for _, headlessDomain := range headlessDomains {
diff --git c/source/service_test.go i/source/service_test.go
index c25e2f29..c64adcc4 100644
--- c/source/service_test.go
+++ i/source/service_test.go
@@ -2903,7 +2903,7 @@ func TestHeadlessServices(t *testing.T) {
 			false,
 			map[string]string{"component": "foo"},
 			map[string]string{
-				servicePodEndpointsKey:     "true",
+				servicePodEndpointsKey:     ServicePodEndpointsPodName,
 				hostnameAnnotationKey:      "service.example.org",
 				endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP,
 			},
@@ -2947,6 +2947,91 @@ func TestHeadlessServices(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"annotated Headless services create DNS name for each pod using fqdn template",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"{{ .Name }}-{{ .Namespace }}.example.org",
+			false,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				servicePodEndpointsKey:     ServicePodEndpointsFqdnTemplate,
+				hostnameAnnotationKey:      "service.example.org",
+				endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP,
+			},
+			map[string]string{},
+			v1.ClusterIPNone,
+			[]string{"1.1.1.1", "1.1.1.2", "1.1.1.3"},
+			[]string{"", "", ""},
+			map[string]string{
+				"component": "foo",
+			},
+			[]string{},
+			[]string{"foo1", "foo2", "foo3"},
+			[]string{"", "", ""},
+			[]bool{true, true, true},
+			false,
+			[]v1.Node{
+				{
+					Status: v1.NodeStatus{
+						Addresses: []v1.NodeAddress{
+							{
+								Type:    v1.NodeExternalIP,
+								Address: "1.2.3.4",
+							},
+							{
+								Type:    v1.NodeInternalIP,
+								Address: "2001:db8::4",
+							},
+						},
+					},
+				},
+			},
+			[]*endpoint.Endpoint{
+				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo1-testing.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo1-testing.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo2-testing.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo2-testing.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo3-testing.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo3-testing.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+			},
+			false,
+		},
+		{
+			"annotated Headless service returns error if incorrect `service-pod-endpoints` value is set",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"",
+			false,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				servicePodEndpointsKey: "not-valid",
+				hostnameAnnotationKey:  "service.example.org",
+			},
+			map[string]string{},
+			v1.ClusterIPNone,
+			[]string{"1.1.1.1", "1.1.1.2", "1.1.1.3"},
+			[]string{"", "", ""},
+			map[string]string{
+				"component": "foo",
+			},
+			[]string{},
+			[]string{"foo1", "foo2", "foo3"},
+			[]string{"", "", ""},
+			[]bool{true, true, true},
+			false,
+			[]v1.Node{{}},
+			[]*endpoint.Endpoint{},
+			false,
+		},
 		{
 			"annotated Headless services return dual-stack targets from node external IP if endpoints-type annotation is set",
 			"",
diff --git c/source/source.go i/source/source.go
index 0df189bb..37f9016e 100644
--- c/source/source.go
+++ i/source/source.go
@@ -52,6 +52,11 @@ const (
 	EndpointsTypeHostIP         = "HostIP"
 )

+const (
+	ServicePodEndpointsPodName      = "pod-name"
+	ServicePodEndpointsFqdnTemplate = "fqdn-template"
+)
+
 // Source defines the interface Endpoint sources should implement.
 type Source interface {
 	Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)

diff --git c/docs/sources/service.md i/docs/sources/service.md
index 7f03426e..1c30eba7 100644
--- c/docs/sources/service.md
+++ i/docs/sources/service.md
@@ -34,7 +34,8 @@ For each domain name created for the Service, the additional DNS entry for the P
 the value of the Pod's `spec.hostname` field and a `.`.

 Another way to create per-pod DNS entries is to annotate headless service with
-`external-dns.alpha.kubernetes.io/service-pod-endpoints: true`,  this will prefix service domain name with pod name.
+`external-dns.alpha.kubernetes.io/service-pod-endpoints` and values `pod-name` or `fqdn-template`. Former prefixes
+service domain name with pod name, latter uses `--fqdn-template` to generate the domain name for each pod in the service.

 ## Targets
diff --git c/source/service.go i/source/service.go
index 8adb25ea..12e295db 100644
--- c/source/service.go
+++ i/source/service.go
@@ -281,7 +281,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri

 	endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations)

-	_, perPodDNS := svc.Annotations[servicePodEndpointsKey]
+	perPodDNSMode, perPodDNS := svc.Annotations[servicePodEndpointsKey]

 	targetsByHeadlessDomainAndType := make(map[endpoint.EndpointKey]endpoint.Targets)
 	for _, subset := range endpointsObject.Subsets {
@@ -314,7 +314,18 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 			}

 			if perPodDNS {
-				headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
+				if perPodDNSMode == ServicePodEndpointsPodName {
+					headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
+				} else if perPodDNSMode == ServicePodEndpointsFqdnTemplate {
+					if hostnames, err := execTemplate(sc.fqdnTemplate, pod); err == nil {
+						headlessDomains = append(headlessDomains, hostnames...)
+					} else {
+						log.Errorf("Error executing template for pod %s: %v", pod.Name, err)
+					}
+				} else {
+					log.Errorf("Unknown `service-pod-endpoints` value %s", perPodDNSMode)
+					return endpoints
+				}
 			}

 			for _, headlessDomain := range headlessDomains {
diff --git c/source/service_test.go i/source/service_test.go
index ddfc2b59..b759aeac 100644
--- c/source/service_test.go
+++ i/source/service_test.go
@@ -2975,7 +2975,7 @@ func TestHeadlessServices(t *testing.T) {
 			true,
 			map[string]string{"component": "foo"},
 			map[string]string{
-				servicePodEndpointsKey:     "true",
+				servicePodEndpointsKey:     ServicePodEndpointsPodName,
 				hostnameAnnotationKey:      "service.example.org",
 				endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP,
 			},
@@ -3019,6 +3019,93 @@ func TestHeadlessServices(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"annotated Headless services create DNS name for each pod using fqdn template",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"{{ .Name }}-{{ .Namespace }}.example.org",
+			false,
+			true,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				servicePodEndpointsKey:     ServicePodEndpointsFqdnTemplate,
+				hostnameAnnotationKey:      "service.example.org",
+				endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP,
+			},
+			map[string]string{},
+			v1.ClusterIPNone,
+			[]string{"1.1.1.1", "1.1.1.2", "1.1.1.3"},
+			[]string{"", "", ""},
+			map[string]string{
+				"component": "foo",
+			},
+			[]string{},
+			[]string{"foo1", "foo2", "foo3"},
+			[]string{"", "", ""},
+			[]bool{true, true, true},
+			false,
+			[]v1.Node{
+				{
+					Status: v1.NodeStatus{
+						Addresses: []v1.NodeAddress{
+							{
+								Type:    v1.NodeExternalIP,
+								Address: "1.2.3.4",
+							},
+							{
+								Type:    v1.NodeInternalIP,
+								Address: "2001:db8::4",
+							},
+						},
+					},
+				},
+			},
+			[]*endpoint.Endpoint{
+				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo1-testing.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo1-testing.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo2-testing.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo2-testing.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo3-testing.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo3-testing.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+			},
+			false,
+		},
+		{
+			"annotated Headless service returns error if incorrect `service-pod-endpoints` value is set",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"",
+			false,
+			true,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				servicePodEndpointsKey: "not-valid",
+				hostnameAnnotationKey:  "service.example.org",
+			},
+			map[string]string{},
+			v1.ClusterIPNone,
+			[]string{"1.1.1.1", "1.1.1.2", "1.1.1.3"},
+			[]string{"", "", ""},
+			map[string]string{
+				"component": "foo",
+			},
+			[]string{},
+			[]string{"foo1", "foo2", "foo3"},
+			[]string{"", "", ""},
+			[]bool{true, true, true},
+			false,
+			[]v1.Node{{}},
+			[]*endpoint.Endpoint{},
+			false,
+		},
 		{
 			"annotated Headless services return dual-stack targets from node external IP if endpoints-type annotation is set and exposeInternalIPv6 flag set",
 			"",
diff --git c/source/source.go i/source/source.go
index bcb96fb7..b93304a7 100644
--- c/source/source.go
+++ i/source/source.go
@@ -46,6 +46,9 @@ const (

 	EndpointsTypeNodeExternalIP = "NodeExternalIP"
 	EndpointsTypeHostIP         = "HostIP"
+
+	ServicePodEndpointsPodName      = "pod-name"
+	ServicePodEndpointsFqdnTemplate = "fqdn-template"
 )

 // Source defines the interface Endpoint sources should implement.
2025-07-22 13:11:22 +01:00

87 lines
2.8 KiB
Go

/*
Copyright 2017 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 source
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
const (
controllerAnnotationKey = annotations.ControllerKey
hostnameAnnotationKey = annotations.HostnameKey
accessAnnotationKey = annotations.AccessKey
endpointsTypeAnnotationKey = annotations.EndpointsTypeKey
targetAnnotationKey = annotations.TargetKey
ttlAnnotationKey = annotations.TtlKey
aliasAnnotationKey = annotations.AliasKey
ingressHostnameSourceKey = annotations.IngressHostnameSourceKey
controllerAnnotationValue = annotations.ControllerValue
internalHostnameAnnotationKey = annotations.InternalHostnameKey
servicePodEndpointsKey = annotations.ServicePodEndpoints
EndpointsTypeNodeExternalIP = "NodeExternalIP"
EndpointsTypeHostIP = "HostIP"
ServicePodEndpointsPodName = "pod-name"
ServicePodEndpointsFqdnTemplate = "fqdn-template"
)
// Source defines the interface Endpoint sources should implement.
type Source interface {
Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)
// AddEventHandler adds an event handler that should be triggered if something in source changes
AddEventHandler(context.Context, func())
}
type kubeObject interface {
runtime.Object
metav1.Object
}
func getAccessFromAnnotations(input map[string]string) string {
return input[accessAnnotationKey]
}
func getEndpointsTypeFromAnnotations(annotations map[string]string) string {
return annotations[endpointsTypeAnnotationKey]
}
func getLabelSelector(annotationFilter string) (labels.Selector, error) {
labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
if err != nil {
return nil, err
}
return metav1.LabelSelectorAsSelector(labelSelector)
}
func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool {
return selector.Matches(labels.Set(srcAnnotations))
}
type eventHandlerFunc func()
func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() }
func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() }
func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() }