mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-09 19:16:56 +02:00
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"
84 lines
2.7 KiB
Go
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() }
|