From 41c705e471c8447a12965c8eea2d5e55bf270513 Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Sat, 6 May 2023 16:46:01 -0700 Subject: [PATCH 1/2] Support AAAA records from headless services --- source/service.go | 31 ++-- source/service_test.go | 373 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 385 insertions(+), 19 deletions(-) diff --git a/source/service.go b/source/service.go index 58270cdcc..7eac4c511 100644 --- a/source/service.go +++ b/source/service.go @@ -271,7 +271,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations) - targetsByHeadlessDomain := make(map[string]endpoint.Targets) + targetsByHeadlessDomainAndType := make(map[endpointKey]endpoint.Targets) for _, subset := range endpointsObject.Subsets { addresses := subset.Addresses if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses { @@ -324,18 +324,29 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, address.IP) } } - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], targets...) + for _, target := range targets { + key := endpointKey{ + dnsName: headlessDomain, + recordType: suitableType(target), + } + targetsByHeadlessDomainAndType[key] = append(targetsByHeadlessDomainAndType[key], target) + } } } } - headlessDomains := []string{} - for headlessDomain := range targetsByHeadlessDomain { - headlessDomains = append(headlessDomains, headlessDomain) + headlessKeys := []endpointKey{} + for headlessKey := range targetsByHeadlessDomainAndType { + headlessKeys = append(headlessKeys, headlessKey) } - sort.Strings(headlessDomains) - for _, headlessDomain := range headlessDomains { - allTargets := targetsByHeadlessDomain[headlessDomain] + sort.Slice(headlessKeys, func(i, j int) bool { + if headlessKeys[i].dnsName != headlessKeys[j].dnsName { + return headlessKeys[i].dnsName < headlessKeys[j].dnsName + } + return headlessKeys[i].recordType < headlessKeys[j].recordType + }) + for _, headlessKey := range headlessKeys { + allTargets := targetsByHeadlessDomainAndType[headlessKey] targets := []string{} deduppedTargets := map[string]struct{}{} @@ -350,9 +361,9 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri } if ttl.IsConfigured() { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, targets...)) + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessKey.dnsName, headlessKey.recordType, ttl, targets...)) } else { - endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, targets...)) + endpoints = append(endpoints, endpoint.NewEndpoint(headlessKey.dnsName, headlessKey.recordType, targets...)) } } diff --git a/source/service_test.go b/source/service_test.go index 45737adb7..66ef2a830 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -2118,7 +2118,7 @@ func TestHeadlessServices(t *testing.T) { expectError bool }{ { - "annotated Headless services return endpoints for each selected Pod", + "annotated Headless services return IPv4 endpoints for each selected Pod", "", "testing", "foo", @@ -2150,6 +2150,39 @@ func TestHeadlessServices(t *testing.T) { }, false, }, + { + "annotated Headless services return IPv6 endpoints for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + []string{"", ""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, + }, + false, + }, { "hostname annotated Headless services are ignored", "", @@ -2180,7 +2213,7 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return endpoints with TTL for each selected Pod", + "annotated Headless services return IPv4 endpoints with TTL for each selected Pod", "", "testing", "foo", @@ -2213,6 +2246,40 @@ func TestHeadlessServices(t *testing.T) { }, false, }, + { + "annotated Headless services return IPv6 endpoints with TTL for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + ttlAnnotationKey: "1", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + []string{"", ""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, + }, + false, + }, { "annotated Headless services return endpoints for each selected Pod, which are in running state", "", @@ -2310,7 +2377,7 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return only a unique set of targets", + "annotated Headless services return only a unique set of IPv4 targets", "", "testing", "foo", @@ -2341,7 +2408,38 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return targets from pod annotation", + "annotated Headless services return only a unique set of IPv6 targets", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::1", "2001:db8::2"}, + []string{"", "", ""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1", "foo-3"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, + }, + false, + }, + { + "annotated Headless services return IPv4 targets from pod annotation", "", "testing", "foo", @@ -2374,7 +2472,40 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return targets from node external IP if endpoints-type annotation is set", + "annotated Headless services return IPv6 targets from pod annotation", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{ + targetAnnotationKey: "2001:db8::4", + }, + v1.ClusterIPNone, + []string{"2001:db8::1"}, + []string{""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}}, + }, + false, + }, + { + "annotated Headless services return IPv4 targets from node external IP if endpoints-type annotation is set", "", "testing", "foo", @@ -2417,7 +2548,98 @@ func TestHeadlessServices(t *testing.T) { false, }, { - "annotated Headless services return targets from hostIP if endpoints-type annotation is set", + "annotated Headless services return IPv6 targets from node external IP if endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1"}, + []string{""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "2001:db8::4", + }, + }, + }, + }, + }, + []*endpoint.Endpoint{ + {DNSName: "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", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"1.1.1.1"}, + []string{""}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []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"}}, + }, + false, + }, + { + "annotated Headless services return IPv4 targets from hostIP if endpoints-type annotation is set", "", "testing", "foo", @@ -2448,6 +2670,38 @@ func TestHeadlessServices(t *testing.T) { }, false, }, + { + "annotated Headless services return IPv6 targets from hostIP if endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeHostIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"2001:db8::1"}, + []string{"2001:db8::4"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::4"}}, + }, + false, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { @@ -2590,7 +2844,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { expectError bool }{ { - "annotated Headless services return endpoints for each selected Pod", + "annotated Headless services return IPv4 endpoints for each selected Pod", "", "testing", "foo", @@ -2623,6 +2877,40 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, }, + { + "annotated Headless services return IPv6 endpoints for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + []*v1.ObjectReference{ + {APIVersion: "", Kind: "Pod", Name: "foo-0"}, + {APIVersion: "", Kind: "Pod", Name: "foo-1"}, + }, + false, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, + }, + false, + }, { "hostname annotated Headless services are ignored", "", @@ -2654,7 +2942,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, }, { - "annotated Headless services return endpoints with TTL for each selected Pod", + "annotated Headless services return IPv4 endpoints with TTL for each selected Pod", "", "testing", "foo", @@ -2688,6 +2976,41 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, }, + { + "annotated Headless services return IPv6 endpoints with TTL for each selected Pod", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + ttlAnnotationKey: "1", + }, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, true}, + []*v1.ObjectReference{ + {APIVersion: "", Kind: "Pod", Name: "foo-0"}, + {APIVersion: "", Kind: "Pod", Name: "foo-1"}, + }, + false, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}, RecordTTL: endpoint.TTL(1)}, + }, + false, + }, { "annotated Headless services return endpoints for each selected Pod, which are in running state", "", @@ -2756,7 +3079,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, }, { - "annotated Headless services return endpoints for pods missing hostname", + "annotated Headless services return IPv4 endpoints for pods missing hostname", "", "testing", "foo", @@ -2787,6 +3110,38 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, false, }, + { + "annotated Headless services return IPv6 endpoints for pods missing hostname", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + v1.ClusterIPNone, + []string{"2001:db8::1", "2001:db8::2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"", ""}, + []bool{true, true}, + []*v1.ObjectReference{ + {APIVersion: "", Kind: "Pod", Name: "foo-0"}, + {APIVersion: "", Kind: "Pod", Name: "foo-1"}, + }, + false, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, + }, + false, + }, { "annotated Headless services without a targetRef has no endpoints", "", From 47a0f74f61c85a6899487558d3c04c26b23c875b Mon Sep 17 00:00:00 2001 From: John Gardiner Myers Date: Sat, 6 May 2023 16:59:39 -0700 Subject: [PATCH 2/2] Add test for IPv6 ExternalName service --- source/service_test.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/source/service_test.go b/source/service_test.go index 66ef2a830..7f3255dd5 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -3295,7 +3295,7 @@ func TestExternalServices(t *testing.T) { expectError bool }{ { - "external services return an A endpoint for the external name that is an IP address", + "external services return an A endpoint for the external name that is an IPv4 address", "", "testing", "foo", @@ -3313,6 +3313,25 @@ func TestExternalServices(t *testing.T) { }, false, }, + { + "external services return an AAAA endpoint for the external name that is an IPv6 address", + "", + "testing", + "foo", + v1.ServiceTypeExternalName, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + "2001:db8::111", + []*endpoint.Endpoint{ + {DNSName: "service.example.org", Targets: endpoint.Targets{"2001:db8::111"}, RecordType: endpoint.RecordTypeAAAA}, + }, + false, + }, { "external services return a CNAME endpoint for the external name that is a domain", "",