external-dns/source/source.go
Maxim Ivanov 14ccac056c feat(service): optionally create DNS entry for each pod
headless service already supported mode where DNS entry is created for
each pod when `spec.hostName` is present. Not all controllers populate that
field, but it is sometimes desireable to create DNS entry per pod nevertheless.

This commit adds new annotation `service-pod-endpoints` , which when set
on a headless service makes external-dns to create endpoint per pod using pod name as a prefix.

diff --git c/docs/sources/service.md i/docs/sources/service.md
index ec39aa7f..7f03426e 100644
--- c/docs/sources/service.md
+++ i/docs/sources/service.md
@@ -33,6 +33,10 @@ a Pod that has a non-empty `spec.hostname` field, additional DNS entries are cre
 For each domain name created for the Service, the additional DNS entry for the Pod has that domain name prefixed with
 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.
+
+
 ## Targets

 If the Service has an `external-dns.alpha.kubernetes.io/target` annotation, uses
diff --git c/source/service.go i/source/service.go
index 2c33dcea..0186a102 100644
--- c/source/service.go
+++ i/source/service.go
@@ -279,6 +279,8 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri

 	endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations)

+	_, perPodDNS := svc.Annotations[servicePodEndpointsKey]
+
 	targetsByHeadlessDomainAndType := make(map[endpoint.EndpointKey]endpoint.Targets)
 	for _, subset := range endpointsObject.Subsets {
 		addresses := subset.Addresses
@@ -309,6 +311,10 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 				headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname))
 			}

+			if perPodDNS {
+				headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
+			}
+
 			for _, headlessDomain := range headlessDomains {
 				targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
 				if len(targets) == 0 {
diff --git c/source/service_test.go i/source/service_test.go
index 5e9948ab..c25e2f29 100644
--- c/source/service_test.go
+++ i/source/service_test.go
@@ -2892,6 +2892,61 @@ func TestHeadlessServices(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"annotated Headless services create DNS name for each pod",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"",
+			false,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				servicePodEndpointsKey:     "true",
+				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.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo2.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo2.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo3.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo3.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+			},
+			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 414ff1ad..0df189bb 100644
--- c/source/source.go
+++ i/source/source.go
@@ -46,6 +46,7 @@ const (
 	ingressHostnameSourceKey      = annotations.IngressHostnameSourceKey
 	controllerAnnotationValue     = annotations.ControllerValue
 	internalHostnameAnnotationKey = annotations.InternalHostnameKey
+	servicePodEndpointsKey        = annotations.ServicePodEndpoints

 	EndpointsTypeNodeExternalIP = "NodeExternalIP"
 	EndpointsTypeHostIP         = "HostIP"

# Conflicts:
#	source/service_test.go

diff --git c/docs/sources/service.md i/docs/sources/service.md
index ec39aa7f..7f03426e 100644
--- c/docs/sources/service.md
+++ i/docs/sources/service.md
@@ -33,6 +33,10 @@ a Pod that has a non-empty `spec.hostname` field, additional DNS entries are cre
 For each domain name created for the Service, the additional DNS entry for the Pod has that domain name prefixed with
 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.
+
+
 ## Targets

 If the Service has an `external-dns.alpha.kubernetes.io/target` annotation, uses
diff --git c/source/annotations/annotations.go i/source/annotations/annotations.go
index abfce135..9fd2cd4c 100644
--- c/source/annotations/annotations.go
+++ i/source/annotations/annotations.go
@@ -55,4 +55,6 @@ const (
 	ControllerValue = "dns-controller"
 	// The annotation used for defining the desired hostname
 	InternalHostnameKey = AnnotationKeyPrefix + "internal-hostname"
+	// When set on a service, per-pod DNS entries will be created.
+	ServicePodEndpoints = AnnotationKeyPrefix + "service-pod-endpoints"
 )
diff --git c/source/service.go i/source/service.go
index a339b192..2bb23e11 100644
--- c/source/service.go
+++ i/source/service.go
@@ -323,6 +323,8 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 	publishPodIPs := endpointsType != EndpointsTypeNodeExternalIP && endpointsType != EndpointsTypeHostIP && !sc.publishHostIP
 	publishNotReadyAddresses := svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses

+	_, perPodDNS := svc.Annotations[servicePodEndpointsKey]
+
 	targetsByHeadlessDomainAndType := make(map[endpoint.EndpointKey]endpoint.Targets)
 	for _, endpointSlice := range endpointSlices {
 		for _, ep := range endpointSlice.Endpoints {
@@ -359,6 +361,10 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
 				headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname))
 			}

+			if perPodDNS {
+				headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
+			}
+
 			for _, headlessDomain := range headlessDomains {
 				targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
 				if len(targets) == 0 {
diff --git c/source/service_test.go i/source/service_test.go
index dd21331e..f8f14f4d 100644
--- c/source/service_test.go
+++ i/source/service_test.go
@@ -3036,6 +3036,62 @@ func TestHeadlessServices(t *testing.T) {
 			},
 			false,
 		},
+		{
+			"annotated Headless services create DNS name for each pod",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeClusterIP,
+			"",
+			"",
+			false,
+			true,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				servicePodEndpointsKey:     "true",
+				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.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo2.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo2.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+				{DNSName: "foo3.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
+				{DNSName: "foo3.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}},
+			},
+			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 aaa2d1dc..6be7d031 100644
--- c/source/source.go
+++ i/source/source.go
@@ -38,6 +38,7 @@ const (
 	ingressHostnameSourceKey      = annotations.IngressHostnameSourceKey
 	controllerAnnotationValue     = annotations.ControllerValue
 	internalHostnameAnnotationKey = annotations.InternalHostnameKey
+	servicePodEndpointsKey        = annotations.ServicePodEndpoints

 	EndpointsTypeNodeExternalIP = "NodeExternalIP"
 	EndpointsTypeHostIP         = "HostIP"
2025-07-22 13:11:19 +01:00

84 lines
2.7 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"
)
// 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() }