mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
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.
87 lines
2.8 KiB
Go
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() }
|