Commit Graph

75 Commits

Author SHA1 Message Date
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
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
ivan katliarchuk
2b7d236734
chore(source): move cache informer to dedicated folder
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
2025-05-25 13:47:16 +01:00
Ivan Ka
e21607254d
chore(codebase): enable errorlint (#5439)
* chore(codebase): enable errorlint

* chore(codebase): enable errorlint

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
2025-05-21 04:14:34 -07:00
Ivan Ka
b20025e311
feat(fqdn): improve ExecTemplate and add more functions (#5406)
* chore(fqdn): fqdn move ExecTemplate to fqdn. add proper tests

* chore(fqdn): fqdn move ExecTemplate to fqdn. add proper tests

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(fqdn): fqdn move ExecTemplate to fqdn. add proper tests

Co-authored-by: Lino Layani <39967417+linoleparquet@users.noreply.github.com>

* chore(fqdn): fqdn move ExecTemplate to fqdn. add proper tests

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

* chore(fqdn): fqdn move ExecTemplate to fqdn. add proper tests

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: Lino Layani <39967417+linoleparquet@users.noreply.github.com>
Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
2025-05-19 12:35:22 -07:00
ivan katliarchuk
fe83c0d2d0
chore(source): code cleanup 2025-05-12 14:21:28 +01:00
Kubernetes Prow Robot
2f165e878c
Merge pull request #5374 from gofogo/code-cleanup-vv0
chore(code-quality): refactoring and linter fixes
2025-05-10 10:57:14 -07:00
Ivan Ka
51d063ad28
chore(fqdn-template): fqdn templating move to specific folder and update documentation (#5354)
* chore(fqdn): fqdn move to specific folder and update documentation

* chore(fqdn): fqdn move to specific folder and update documentation

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

* chore(fqdn): fqdn move to specific folder and update documentation

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(fqdn): fqdn move to specific folder and update documentation

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(fqdn): fqdn move to specific folder and update documentation

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
2025-05-10 05:53:22 -07:00
ivan katliarchuk
9f8f30882b
chore(source): code cleanup
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
2025-05-09 08:21:31 +01:00
ivan katliarchuk
9f427e5622
chore(source): code cleanup 2025-04-16 13:50:04 +01:00
Valentin Flaux
c3f0cd668f
fix cloudflare regional hostnames
Implements create & delete of regional hostnames for A, AAAA & CNAME
records.
Implements "external-dns.alpha.kubernetes.io/cloudflare-region-key"
annotation.
2025-04-12 10:59:47 +02:00
Michel Loiseleur
3835c62bb6 chore(ci): update linter to v2.0.2 2025-04-02 08:53:54 +02:00
Mikhail Rozentsvayg
799ec6d2c9 review suggestions 2025-02-18 14:54:13 -08:00
Mikhail Rozentsvayg
88fd2aa3a7 initial custom hostnames support 2025-02-09 10:01:35 -08:00
Steven Kreitzer
449d27b00e
fix: allow ipv4-mapped ipv6 addresses
Signed-off-by: Steven Kreitzer <skre@skre.me>
2024-12-21 14:16:23 -06:00
Raffaele Di Fazio
c375899f91 test
Signed-off-by: Raffaele Di Fazio <difazio.raffaele@gmail.com>
2024-05-10 20:11:38 +02:00
Raffaele Di Fazio
761d6551d3 Initial support for forwarding wildcard annotations to webhook providers 2024-04-28 14:05:45 +02:00
Eric Bailey
36a6e19dd5 fix(source): use NewEndpointWithTTL and omit nil Endpoints 2024-03-04 15:53:47 +01:00
John Gardiner Myers
17e9637f11
Refactor getTTLFromAnnotations() to not return error (#3939)
* Refactor getTTLFromAnnotations() to not return error

* Improve log messages
2023-10-02 03:56:45 -07:00
John Gardiner Myers
39955e556e Refactor setting the "resource" label into endpointsForHostname 2023-08-29 11:36:27 -07:00
Kubernetes Prow Robot
ae0c06e3e6
Merge pull request #3648 from johngmyers/dynamodb
Add DynamoDB registry implementation
2023-06-23 13:03:07 -07:00
John Gardiner Myers
4417ad4894 Move EndpointKey to endpoints package 2023-06-18 16:47:37 -07:00
Michel Loiseleur
e06ed8cc5d build(deps): bump dev-dependencies group with 40 updates 2023-06-16 17:15:35 +02:00
Kubernetes Prow Robot
8c1be68e87
Merge pull request #3603 from Pluggi/pod-multiple-internal-hostnames
Allow multiple hostnames in internal annotation for pods
2023-05-17 03:12:35 -07:00
Antoine Bardoux
f385139abc Allow multiple hostnames in internal annotation for pods 2023-05-12 12:16:08 +02:00
Gabriel Martinez
9bbebf6c07
Merge remote-tracking branch 'remote/master' into target_annotation_for_service 2023-05-11 11:53:20 +01:00
John Gardiner Myers
4745ddbb0e Address review comment 2023-05-09 19:42:56 -07:00
Gabriel Martinez
3288cc2f98
feat(service): allow using target annotation 2023-05-06 14:48:54 +01:00
John Gardiner Myers
781ee3d71f Address review comments 2023-04-12 22:46:39 -07:00
Skyler Mäntysaari
6f42a8a2da Initial IPv6 support 2023-03-30 17:49:28 -07:00
hzhihui
2270904b9d
Merge branch 'master' into add_ibmcloud_provider 2022-05-09 19:24:03 +08:00
Alfred Krohmer
ea45b03972 Headless service: allow to specify target as NodeExternalIP or by annotation
If external-dns.alpha.kubernetes.io/target annotation is present on a
pod, it's value will be used as the target for the headless service.

If annotation external-dns.alpha.kubernetes.io/access=public is present,
NodeExternalIP of the node running the pod is used as the target for the
headless service.
2022-05-05 19:44:32 +02:00
hzhihui
4703c11741 Add IBM Cloud provider 2022-04-14 11:53:16 +08:00
Andy Bursavich
60c649bf5c source: dedupe wait for cache sync
wait
2021-07-28 13:37:17 -07:00
Andy Bursavich
55637abbef source: dedupe event handlers 2021-07-28 13:36:35 -07:00
Andy Bursavich
b032f2864f source: dedupe template execution 2021-07-28 13:35:51 -07:00
Andy Bursavich
638194fccd source: dedupe template parsing 2021-07-28 07:46:35 -07:00
dan.simone@oracle.com
963282479e Add clarifying comment about ingressHostnameSourceKey 2021-04-14 13:26:34 -07:00
dan.simone@oracle.com
03c3c8fcb8 Address remove comments 2021-04-14 13:21:03 -07:00
dan.simone@oracle.com
fa95e86fb1 Merge remote-tracking branch 'origing/master' into dansimone/support-prefer-ingress-annotations
# Conflicts:
#	docs/faq.md
2021-01-04 15:39:34 -08:00
Bas Toonk
18a60a5175 feat: use ClusterIP with internal-hostname annotation 2020-12-17 15:14:34 +01:00
dan.simone@oracle.com
4d88b47917 Resolve differences 2020-09-25 15:59:18 -07:00
rbtr
54320a16ab
add doc and test 2020-09-16 12:02:19 -04:00
Evan Baker
b75151e3e5
add service annotation to set public/private iface for NodePort
Signed-off-by: Evan Baker <rbtr@users.noreply.github.com>
2020-09-16 11:56:49 -04:00
dan.simone@oracle.com
4cb5a85dd4 Merge remote-tracking branch 'origing/master' into dansimone/support-prefer-ingress-annotations 2020-08-27 12:16:05 -07:00
Tariq Ibrahim
600f4f1ba0
fix goimports local import order and update golangci-lint 2020-08-19 20:05:07 -07:00
Patrik Cyvoct
b1f6c1fa0e
fix scw annotations
Signed-off-by: Patrik Cyvoct <patrik@ptrk.io>
2020-08-19 06:41:47 +02:00
dan.simone@oracle.com
f85cddab58 Change to an annotation-based approach, based on feedback 2020-07-29 11:51:27 -07:00
Joseph Glanville
6efca134f5 dependencies: Upgrade all k8s client-go dependent sources to v1.18.X
This requires pinning grpc for now as istio client-go otherwise pulls in
breaking changes.
2020-07-03 11:29:37 +07:00
Nick Jüttner
3388e3ddf1 improve linter quality for external-dns 2020-06-06 22:29:24 +02:00