This commit is contained in:
Maxim Ivanov 2025-08-03 04:53:10 -07:00 committed by GitHub
commit 980abef294
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 170 additions and 0 deletions

View File

@ -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` and values `pod-name` or `fqdn-template`. The former prefixes
service domain name with pod name, the latter uses `--fqdn-template` to generate the domain name for each pod in the service.
## Targets
If the Service has an `external-dns.alpha.kubernetes.io/target` annotation, uses

View File

@ -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"
)

View File

@ -314,6 +314,8 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
publishPodIPs := endpointsType != EndpointsTypeNodeExternalIP && endpointsType != EndpointsTypeHostIP && !sc.publishHostIP
publishNotReadyAddresses := svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses
perPodDNSMode, perPodDNS := svc.Annotations[servicePodEndpointsKey]
targetsByHeadlessDomainAndType := make(map[endpoint.EndpointKey]endpoint.Targets)
for _, endpointSlice := range endpointSlices {
for _, ep := range endpointSlice.Endpoints {
@ -350,6 +352,21 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname))
}
if perPodDNS {
switch perPodDNSMode {
case ServicePodEndpointsPodName:
headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
case ServicePodEndpointsFqdnTemplate:
if hostnames, err := fqdn.ExecTemplate(sc.fqdnTemplate, pod); err == nil {
headlessDomains = append(headlessDomains, hostnames...)
} else {
log.Errorf("Error executing template for pod %s: %v", pod.Name, err)
}
default:
log.Errorf("Unknown `service-pod-endpoints` value %s", perPodDNSMode)
return endpoints
}
}
for _, headlessDomain := range headlessDomains {
targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
if len(targets) == 0 {

View File

@ -3053,6 +3053,149 @@ 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: ServicePodEndpointsPodName,
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 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",
"",

View File

@ -38,9 +38,13 @@ const (
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.