external-dns/source/service_test.go
2025-02-02 23:42:50 +02:00

3808 lines
121 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"
"net"
"sort"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
)
type ServiceSuite struct {
suite.Suite
sc Source
fooWithTargets *v1.Service
}
func (suite *ServiceSuite) SetupTest() {
fakeClient := fake.NewSimpleClientset()
suite.fooWithTargets = &v1.Service{
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeLoadBalancer,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "foo-with-targets",
Annotations: map[string]string{},
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{IP: "8.8.8.8"},
{Hostname: "foo"},
},
},
},
}
_, err := fakeClient.CoreV1().Services(suite.fooWithTargets.Namespace).Create(context.Background(), suite.fooWithTargets, metav1.CreateOptions{})
suite.NoError(err, "should successfully create service")
suite.sc, err = NewServiceSource(
context.TODO(),
fakeClient,
"",
"",
"{{.Name}}",
false,
"",
false,
false,
false,
[]string{},
false,
labels.Everything(),
false,
false,
)
suite.NoError(err, "should initialize service source")
}
func (suite *ServiceSuite) TestResourceLabelIsSet() {
endpoints, _ := suite.sc.Endpoints(context.Background())
for _, ep := range endpoints {
suite.Equal("service/default/foo-with-targets", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label")
}
}
func TestServiceSource(t *testing.T) {
t.Parallel()
suite.Run(t, new(ServiceSuite))
t.Run("Interface", testServiceSourceImplementsSource)
t.Run("NewServiceSource", testServiceSourceNewServiceSource)
t.Run("Endpoints", testServiceSourceEndpoints)
t.Run("MultipleServices", testMultipleServicesEndpoints)
}
// testServiceSourceImplementsSource tests that serviceSource is a valid Source.
func testServiceSourceImplementsSource(t *testing.T) {
assert.Implements(t, (*Source)(nil), new(serviceSource))
}
// testServiceSourceNewServiceSource tests that NewServiceSource doesn't return an error.
func testServiceSourceNewServiceSource(t *testing.T) {
t.Parallel()
for _, ti := range []struct {
title string
annotationFilter string
fqdnTemplate string
serviceTypesFilter []string
expectError bool
}{
{
title: "invalid template",
expectError: true,
fqdnTemplate: "{{.Name",
},
{
title: "valid empty template",
expectError: false,
},
{
title: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
},
{
title: "non-empty annotation filter label",
expectError: false,
annotationFilter: "kubernetes.io/ingress.class=nginx",
},
{
title: "non-empty service types filter",
expectError: false,
serviceTypesFilter: []string{string(v1.ServiceTypeClusterIP)},
},
} {
ti := ti
t.Run(ti.title, func(t *testing.T) {
t.Parallel()
_, err := NewServiceSource(
context.TODO(),
fake.NewSimpleClientset(),
"",
ti.annotationFilter,
ti.fqdnTemplate,
false,
"",
false,
false,
false,
ti.serviceTypesFilter,
false,
labels.Everything(),
false,
false,
)
if ti.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
// testServiceSourceEndpoints tests that various services generate the correct endpoints.
func testServiceSourceEndpoints(t *testing.T) {
exampleDotComIP4, err := net.DefaultResolver.LookupNetIP(context.Background(), "ip4", "example.com")
assert.NoError(t, err)
exampleDotComIP6, err := net.DefaultResolver.LookupNetIP(context.Background(), "ip6", "example.com")
assert.NoError(t, err)
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
externalIPs []string
lbs []string
serviceTypesFilter []string
expected []*endpoint.Endpoint
expectError bool
serviceLabelSelector string
resolveLoadBalancerHostname bool
}{
{
title: "no annotated services return no endpoints",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "no annotated services return no endpoints when ignoring annotations",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
ignoreHostnameAnnotation: true,
labels: map[string]string{},
annotations: map[string]string{},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "annotated services return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "hostname annotation on services is ignored",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
ignoreHostnameAnnotation: true,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "annotated ClusterIp aren't processed without explicit authorization",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
clusterIP: "1.2.3.4",
externalIPs: []string{},
lbs: []string{},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "FQDN template with multiple hostnames return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com",
labels: map[string]string{},
annotations: map[string]string{},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "FQDN template with multiple hostnames return an endpoint with target IP when ignoring annotations",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com",
ignoreHostnameAnnotation: true,
labels: map[string]string{},
annotations: map[string]string{},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "FQDN template and annotation both with multiple hostnames return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com",
combineFQDNAndAnnotation: true,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "FQDN template and annotation both with multiple hostnames while ignoring annotations will only return FQDN endpoints",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com",
combineFQDNAndAnnotation: true,
ignoreHostnameAnnotation: true,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.fqdn.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "annotated services with multiple hostnames return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org., bar.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "annotated services with multiple hostnames and without trailing period return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org, bar.example.org",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "annotated services return an endpoint with target hostname",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"lb.example.com"}, // Kubernetes omits the trailing dot
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}},
},
},
{
title: "annotated services return an endpoint with hostname then resolve hostname",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"example.com"}, // Use a resolvable hostname for testing.
serviceTypesFilter: []string{},
resolveLoadBalancerHostname: true,
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: testutils.NewTargetsFromAddr(exampleDotComIP4)},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: testutils.NewTargetsFromAddr(exampleDotComIP6)},
},
},
{
title: "annotated services can omit trailing dot",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
},
externalIPs: []string{},
lbs: []string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}},
},
},
{
title: "our controller type is kops dns controller",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
controllerAnnotationKey: controllerAnnotationValue,
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "different controller types are ignored even (with template specified)",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.ext-dns.test.com",
labels: map[string]string{},
annotations: map[string]string{
controllerAnnotationKey: "some-other-tool",
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "services are found in target namespace",
targetNamespace: "testing",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "services that are not in target namespace are ignored",
targetNamespace: "testing",
svcNamespace: "other-testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "services are found in all namespaces",
svcNamespace: "other-testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "valid matching annotation filter expression",
annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "valid non-matching annotation filter expression",
annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
"service.beta.kubernetes.io/external-traffic": "SomethingElse",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "invalid annotation filter expression",
annotationFilter: "service.beta.kubernetes.io/external-traffic in (Global OnlyLocal)",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
expectError: true,
},
{
title: "valid matching annotation filter label",
annotationFilter: "service.beta.kubernetes.io/external-traffic=Global",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
"service.beta.kubernetes.io/external-traffic": "Global",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "valid non-matching annotation filter label",
annotationFilter: "service.beta.kubernetes.io/external-traffic=Global",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "no external entrypoints return no endpoints",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "annotated service with externalIPs returns a single endpoint with multiple targets",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{"10.2.3.4", "11.2.3.4"},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}},
},
},
{
title: "multiple external entrypoints return a single endpoint with multiple targets",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4", "8.8.8.8"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}},
},
},
{
title: "services annotated with legacy mate annotations are ignored in default mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
"zalando.org/dnsname": "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
},
{
title: "services annotated with legacy mate annotations return an endpoint in compatibility mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
compatibility: "mate",
labels: map[string]string{},
annotations: map[string]string{
"zalando.org/dnsname": "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "services annotated with legacy molecule annotations return an endpoint in compatibility mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
compatibility: "molecule",
labels: map[string]string{
"dns": "route53",
},
annotations: map[string]string{
"domainName": "foo.example.org., bar.example.org",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "load balancer services annotated with DNS Controller annotations return an endpoint with A and CNAME targets in compatibility mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
compatibility: "kops-dns-controller",
labels: map[string]string{},
annotations: map[string]string{
kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4", "lb.example.com"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}},
},
},
{
title: "load balancer services annotated with DNS Controller annotations return an endpoint with both annotations in compatibility mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
compatibility: "kops-dns-controller",
labels: map[string]string{},
annotations: map[string]string{
kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "not annotated services with set fqdnTemplate return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.bar.example.com",
labels: map[string]string{},
annotations: map[string]string{},
externalIPs: []string{},
lbs: []string{"1.2.3.4", "elb.com"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com"}},
},
},
{
title: "annotated services with set fqdnTemplate annotation takes precedence",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Name}}.bar.example.com",
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4", "elb.com"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com"}},
},
},
{
title: "compatibility annotated services with tmpl. compatibility takes precedence",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
compatibility: "mate",
fqdnTemplate: "{{.Name}}.bar.example.com",
labels: map[string]string{},
annotations: map[string]string{
"zalando.org/dnsname": "mate.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "mate.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "not annotated services with unknown tmpl field should not return anything",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
fqdnTemplate: "{{.Calibre}}.bar.example.com",
labels: map[string]string{},
annotations: map[string]string{},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{},
expectError: true,
},
{
title: "ttl not annotated should have RecordTTL.IsConfigured set to false",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
},
},
{
title: "ttl annotated but invalid should have RecordTTL.IsConfigured set to false",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "foo",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
},
},
{
title: "ttl annotated and is valid should set Record.TTL",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "10",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)},
},
},
{
title: "ttl annotated (in duration format) and is valid should set Record.TTL",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "1m",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(60)},
},
},
{
title: "Negative ttl is not valid",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
ttlAnnotationKey: "-10",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)},
},
},
{
title: "filter on service types should include matching services",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "filter on service types should exclude non-matching services",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)},
expected: []*endpoint.Endpoint{},
},
{
title: "internal-host annotated and host annotated clusterip services return an endpoint with Cluster IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
internalHostnameAnnotationKey: "foo.internal.example.org.",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.internal.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
},
},
{
title: "internal-host annotated loadbalancer services return an endpoint with Cluster IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
internalHostnameAnnotationKey: "foo.internal.example.org.",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.internal.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
},
},
{
title: "internal-host annotated and host annotated loadbalancer services return an endpoint with Cluster IP and an endpoint with lb IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
internalHostnameAnnotationKey: "foo.internal.example.org.",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
expected: []*endpoint.Endpoint{
{DNSName: "foo.internal.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "service with matching labels and fqdn filter should be included",
svcNamespace: "testing",
svcName: "fqdn",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{
"app": "web-external",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
serviceLabelSelector: "app=web-external",
fqdnTemplate: "{{.Name}}.bar.example.com",
expected: []*endpoint.Endpoint{
{DNSName: "fqdn.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "service with matching labels and hostname annotation should be included",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{
"app": "web-external",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
serviceLabelSelector: "app=web-external",
annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"},
expected: []*endpoint.Endpoint{
{DNSName: "annotation.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "service without matching labels and fqdn filter should be excluded",
svcNamespace: "testing",
svcName: "fqdn",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{
"app": "web-internal",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
serviceLabelSelector: "app=web-external",
fqdnTemplate: "{{.Name}}.bar.example.com",
expected: []*endpoint.Endpoint{},
},
{
title: "service without matching labels and hostname annotation should be excluded",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{
"app": "web-internal",
},
clusterIP: "1.1.1.1",
externalIPs: []string{},
lbs: []string{"1.2.3.4"},
serviceTypesFilter: []string{},
serviceLabelSelector: "app=web-external",
annotations: map[string]string{hostnameAnnotationKey: "annotation.bar.example.com"},
expected: []*endpoint.Endpoint{},
},
{
title: "dual-stack load-balancer service gets both addresses",
svcNamespace: "testing",
svcName: "foobar",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
clusterIP: "1.1.1.2,2001:db8::2",
externalIPs: []string{},
lbs: []string{"1.1.1.1", "2001:db8::1"},
serviceTypesFilter: []string{},
annotations: map[string]string{hostnameAnnotationKey: "foobar.example.org"},
expected: []*endpoint.Endpoint{
{DNSName: "foobar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foobar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}},
},
},
{
title: "IPv6-only load-balancer service gets IPv6 endpoint",
svcNamespace: "testing",
svcName: "foobar-v6",
svcType: v1.ServiceTypeLoadBalancer,
labels: map[string]string{},
clusterIP: "2001:db8::1",
externalIPs: []string{},
lbs: []string{"2001:db8::2"},
serviceTypesFilter: []string{},
annotations: map[string]string{hostnameAnnotationKey: "foobar-v6.example.org"},
expected: []*endpoint.Endpoint{
{DNSName: "foobar-v6.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::2"}},
},
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
// Create a service to test against
ingresses := []v1.LoadBalancerIngress{}
for _, lb := range tc.lbs {
if net.ParseIP(lb) != nil {
ingresses = append(ingresses, v1.LoadBalancerIngress{IP: lb})
} else {
ingresses = append(ingresses, v1.LoadBalancerIngress{Hostname: lb})
}
}
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
ExternalIPs: tc.externalIPs,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.annotations,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: ingresses,
},
},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
var sourceLabel labels.Selector
if tc.serviceLabelSelector != "" {
sourceLabel, err = labels.Parse(tc.serviceLabelSelector)
require.NoError(t, err)
} else {
sourceLabel = labels.Everything()
}
// Create our object under test and get the endpoints.
client, err := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
tc.combineFQDNAndAnnotation,
tc.compatibility,
false,
false,
false,
tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation,
sourceLabel,
tc.resolveLoadBalancerHostname,
false,
)
require.NoError(t, err)
res, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, res, tc.expected)
})
}
}
// testMultipleServicesEndpoints tests that multiple services generate correct merged endpoints
func testMultipleServicesEndpoints(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
combineFQDNAndAnnotation bool
ignoreHostnameAnnotation bool
labels map[string]string
clusterIP string
services map[string]map[string]string
serviceTypesFilter []string
expected []*endpoint.Endpoint
expectError bool
}{
{
"test service returns a correct end point",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]map[string]string{
"1.2.3.4": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}},
},
false,
},
{
"multiple services that share same DNS should be merged into one endpoint",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]map[string]string{
"1.2.3.4": {hostnameAnnotationKey: "foo.example.org"},
"1.2.3.5": {hostnameAnnotationKey: "foo.example.org"},
"1.2.3.6": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5", "1.2.3.6"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}},
},
false,
},
{
"test that services with different hostnames do not get merged together",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]map[string]string{
"1.2.3.5": {hostnameAnnotationKey: "foo.example.org"},
"10.1.1.3": {hostnameAnnotationKey: "bar.example.org"},
"10.1.1.1": {hostnameAnnotationKey: "bar.example.org"},
"1.2.3.4": {hostnameAnnotationKey: "foo.example.org"},
"10.1.1.2": {hostnameAnnotationKey: "bar.example.org"},
"20.1.1.1": {hostnameAnnotationKey: "foobar.example.org"},
"1.2.3.6": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5", "1.2.3.6"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.4"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.1", "10.1.1.2", "10.1.1.3"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo10.1.1.1"}},
{DNSName: "foobar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"20.1.1.1"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo20.1.1.1"}},
},
false,
},
{
"test that services with different set-identifier do not get merged together",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]map[string]string{
"1.2.3.5": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "a"},
"10.1.1.3": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "b"},
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.5"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo1.2.3.5"}, SetIdentifier: "a"},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.3"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foo10.1.1.3"}, SetIdentifier: "b"},
},
false,
},
{
"test that services with CNAME types do not get merged together",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]map[string]string{
"a.elb.com": {hostnameAnnotationKey: "foo.example.org"},
"b.elb.com": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"a.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/fooa.elb.com"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"b.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foob.elb.com"}},
},
false,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
// Create services to test against
for lb, annotations := range tc.services {
ingresses := []v1.LoadBalancerIngress{}
ingresses = append(ingresses, v1.LoadBalancerIngress{IP: lb})
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName + lb,
Labels: tc.labels,
Annotations: annotations,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: ingresses,
},
},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
}
// Create our object under test and get the endpoints.
client, err := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
tc.combineFQDNAndAnnotation,
tc.compatibility,
false,
false,
false,
tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
false,
)
require.NoError(t, err)
res, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, res, tc.expected)
// Test that endpoint resourceLabelKey matches desired endpoint
sort.SliceStable(res, func(i, j int) bool {
return strings.Compare(res[i].DNSName, res[j].DNSName) < 0
})
sort.SliceStable(tc.expected, func(i, j int) bool {
return strings.Compare(tc.expected[i].DNSName, tc.expected[j].DNSName) < 0
})
for i := range res {
if res[i].Labels[endpoint.ResourceLabelKey] != tc.expected[i].Labels[endpoint.ResourceLabelKey] {
t.Errorf("expected %s, got %s", tc.expected[i].Labels[endpoint.ResourceLabelKey], res[i].Labels[endpoint.ResourceLabelKey])
}
}
})
}
}
// testServiceSourceEndpoints tests that various services generate the correct endpoints.
func TestClusterIpServices(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
expected []*endpoint.Endpoint
expectError bool
labelSelector string
}{
{
title: "hostname annotated ClusterIp services return an endpoint with Cluster IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
title: "target annotated ClusterIp services return an endpoint with the specified A",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "4.3.2.1",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.3.2.1"}},
},
},
{
title: "target annotated ClusterIp services return an endpoint with the specified CNAME",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "bar.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org"}},
},
},
{
title: "target annotated ClusterIp services return an endpoint with the specified AAAA",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "2001:DB8::1",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}},
},
},
{
title: "multiple target annotated ClusterIp services return an endpoint with the specified CNAMES",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "bar.example.org.,baz.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org", "baz.example.org"}},
},
},
{
title: "multiple target annotated ClusterIp services return two endpoints with the specified CNAMES and AAAA",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "bar.example.org.,baz.example.org.,2001:DB8::1",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org", "baz.example.org"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}},
},
},
{
title: "hostname annotated ClusterIp services are ignored",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
ignoreHostnameAnnotation: true,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{},
},
{
title: "hostname and target annotated ClusterIp services are ignored",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
ignoreHostnameAnnotation: true,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "bar.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{},
},
{
title: "hostname and target annotated ClusterIp services return an endpoint with the specified CNAME",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "bar.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org"}},
},
},
{
title: "non-annotated ClusterIp services with set fqdnTemplate return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
fqdnTemplate: "{{.Name}}.bar.example.com",
clusterIP: "4.5.6.7",
expected: []*endpoint.Endpoint{
{DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.5.6.7"}},
},
},
{
title: "Headless services do not generate endpoints",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
clusterIP: v1.ClusterIPNone,
expected: []*endpoint.Endpoint{},
},
{
title: "Headless services generate endpoints when target is specified",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
targetAnnotationKey: "bar.example.org.",
},
clusterIP: v1.ClusterIPNone,
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.org"}},
},
},
{
title: "ClusterIP service with matching label generates an endpoint",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
fqdnTemplate: "{{.Name}}.bar.example.com",
labels: map[string]string{"app": "web-internal"},
clusterIP: "4.5.6.7",
expected: []*endpoint.Endpoint{
{DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.5.6.7"}},
},
labelSelector: "app=web-internal",
},
{
title: "ClusterIP service with matching label and target generates a CNAME endpoint",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
fqdnTemplate: "{{.Name}}.bar.example.com",
labels: map[string]string{"app": "web-internal"},
annotations: map[string]string{targetAnnotationKey: "bar.example.com."},
clusterIP: "4.5.6.7",
expected: []*endpoint.Endpoint{
{DNSName: "foo.bar.example.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"bar.example.com"}},
},
labelSelector: "app=web-internal",
},
{
title: "ClusterIP service without matching label generates an endpoint",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
fqdnTemplate: "{{.Name}}.bar.example.com",
labels: map[string]string{"app": "web-internal"},
clusterIP: "4.5.6.7",
expected: []*endpoint.Endpoint{},
labelSelector: "app=web-external",
},
{
title: "invalid hostname does not generate endpoints",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeClusterIP,
annotations: map[string]string{
hostnameAnnotationKey: "this-is-an-exceedingly-long-label-that-external-dns-should-reject.example.org.",
},
clusterIP: "1.2.3.4",
expected: []*endpoint.Endpoint{},
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
// Create a service to test against
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.annotations,
},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
var labelSelector labels.Selector
if tc.labelSelector != "" {
labelSelector, err = labels.Parse(tc.labelSelector)
require.NoError(t, err)
} else {
labelSelector = labels.Everything()
}
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
false,
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
labelSelector,
false,
false,
)
require.NoError(t, err)
endpoints, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
})
}
}
// testNodePortServices tests that various services generate the correct endpoints.
func TestServiceSourceNodePortServices(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
annotationFilter string
svcNamespace string
svcName string
svcType v1.ServiceType
svcTrafficPolicy v1.ServiceExternalTrafficPolicyType
compatibility string
fqdnTemplate string
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
lbs []string
expected []*endpoint.Endpoint
expectError bool
nodes []*v1.Node
podNames []string
nodeIndex []int
phases []v1.PodPhase
conditions []v1.PodCondition
labelSelector labels.Selector
deletionTimestamp []*metav1.Time
}{
{
title: "annotated NodePort services return an endpoint with IP addresses of the cluster's nodes",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "hostname annotated NodePort services are ignored",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
ignoreHostnameAnnotation: true,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
expected: []*endpoint.Endpoint{},
},
{
title: "non-annotated NodePort services with set fqdnTemplate return an endpoint with target IP",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
fqdnTemplate: "{{.Name}}.bar.example.com",
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "annotated NodePort services return an endpoint with IP addresses of the private cluster's nodes",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "annotated NodePort services with ExternalTrafficPolicy=Local return an endpoint with IP addresses of the cluster's nodes where pods is running only",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
podNames: []string{"pod-0"},
nodeIndex: []int{1},
phases: []v1.PodPhase{v1.PodRunning},
conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionFalse}},
deletionTimestamp: []*metav1.Time{{}},
},
{
title: "annotated NodePort services with ExternalTrafficPolicy=Local and multiple pods on a single node return an endpoint with unique IP addresses of the cluster's nodes where pods is running only",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
podNames: []string{"pod-0", "pod-1"},
nodeIndex: []int{1, 1},
phases: []v1.PodPhase{v1.PodRunning, v1.PodRunning},
conditions: []v1.PodCondition{
{Type: v1.PodReady, Status: v1.ConditionFalse},
{Type: v1.PodReady, Status: v1.ConditionFalse},
},
deletionTimestamp: []*metav1.Time{{}, {}},
},
{
title: "annotated NodePort services with ExternalTrafficPolicy=Local return pods in Ready & Running state",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
},
},
}},
podNames: []string{"pod-0", "pod-1"},
nodeIndex: []int{0, 1},
phases: []v1.PodPhase{v1.PodRunning, v1.PodRunning},
conditions: []v1.PodCondition{
{Type: v1.PodReady, Status: v1.ConditionTrue},
{Type: v1.PodReady, Status: v1.ConditionFalse},
},
deletionTimestamp: []*metav1.Time{{}, {}},
},
{
title: "annotated NodePort services with ExternalTrafficPolicy=Local return pods in Ready & Running state & not in Terminating",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node3",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.3"},
{Type: v1.NodeInternalIP, Address: "10.0.1.3"},
},
},
}},
podNames: []string{"pod-0", "pod-1", "pod-2"},
nodeIndex: []int{0, 1, 2},
phases: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning},
conditions: []v1.PodCondition{
{Type: v1.PodReady, Status: v1.ConditionTrue},
{Type: v1.PodReady, Status: v1.ConditionFalse},
{Type: v1.PodReady, Status: v1.ConditionTrue},
},
deletionTimestamp: []*metav1.Time{nil, nil, {}},
},
{
title: "access=private annotation NodePort services return an endpoint with private IP addresses of the cluster's nodes",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
accessAnnotationKey: "private",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "access=public annotation NodePort services return an endpoint with public IP addresses of the cluster's nodes",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
labels: map[string]string{},
annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
accessAnnotationKey: "public",
},
expected: []*endpoint.Endpoint{
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "node port services annotated DNS Controller annotations return an endpoint where all targets has the node role",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
compatibility: "kops-dns-controller",
labels: map[string]string{},
annotations: map[string]string{
kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
},
expected: []*endpoint.Endpoint{
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1"}},
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}},
{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1"}},
{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1"}},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
Labels: map[string]string{
"node-role.kubernetes.io/control-plane": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "node port services annotated with internal DNS Controller annotations return an endpoint in compatibility mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
compatibility: "kops-dns-controller",
annotations: map[string]string{
kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
},
expected: []*endpoint.Endpoint{
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}},
{DNSName: "internal.foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}},
{DNSName: "internal.bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "node port services annotated with external DNS Controller annotations return an endpoint in compatibility mode",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
compatibility: "kops-dns-controller",
annotations: map[string]string{
kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
},
expected: []*endpoint.Endpoint{
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}},
{DNSName: "foo.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}},
{DNSName: "bar.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}},
},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
{
title: "node port services annotated with both kops dns controller annotations return an empty set of addons",
svcNamespace: "testing",
svcName: "foo",
svcType: v1.ServiceTypeNodePort,
svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
compatibility: "kops-dns-controller",
labels: map[string]string{},
annotations: map[string]string{
kopsDNSControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
kopsDNSControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
},
expected: []*endpoint.Endpoint{},
nodes: []*v1.Node{{
ObjectMeta: metav1.ObjectMeta{
Name: "node1",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::1"},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{
Name: "node2",
Labels: map[string]string{
"node-role.kubernetes.io/node": "",
},
},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
{Type: v1.NodeInternalIP, Address: "2001:DB8::2"},
},
},
}},
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
// Create the nodes
for _, node := range tc.nodes {
if _, err := kubernetes.CoreV1().Nodes().Create(context.Background(), node, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
}
// Create pods
for i, podname := range tc.podNames {
pod := &v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{},
Hostname: podname,
NodeName: tc.nodes[tc.nodeIndex[i]].Name,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: podname,
Labels: tc.labels,
Annotations: tc.annotations,
DeletionTimestamp: tc.deletionTimestamp[i],
},
Status: v1.PodStatus{
Phase: tc.phases[i],
Conditions: []v1.PodCondition{tc.conditions[i]},
},
}
_, err := kubernetes.CoreV1().Pods(tc.svcNamespace).Create(context.Background(), pod, metav1.CreateOptions{})
require.NoError(t, err)
}
// Create a service to test against
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ExternalTrafficPolicy: tc.svcTrafficPolicy,
Ports: []v1.ServicePort{
{
NodePort: 30192,
},
},
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.annotations,
},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
tc.annotationFilter,
tc.fqdnTemplate,
false,
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
false,
)
require.NoError(t, err)
endpoints, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
})
}
}
// TestHeadlessServices tests that headless services generate the correct endpoints.
func TestHeadlessServices(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
ignoreHostnameAnnotation bool
labels map[string]string
svcAnnotations map[string]string
podAnnotations map[string]string
clusterIP string
podIPs []string
hostIPs []string
selector map[string]string
lbs []string
podnames []string
hostnames []string
podsReady []bool
publishNotReadyAddresses bool
nodes []v1.Node
expected []*endpoint.Endpoint
expectError bool
}{
{
"annotated Headless services return IPv4 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{"1.1.1.1", "1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
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",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
true,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
map[string]string{},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.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{},
false,
},
{
"annotated Headless services return IPv4 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{"1.1.1.1", "1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
},
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",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
map[string]string{},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]bool{true, false},
false,
[]v1.Node{},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
{
"annotated Headless services return endpoints for all Pod if publishNotReadyAddresses is set",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
map[string]string{},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]bool{true, false},
true,
[]v1.Node{},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
{
"annotated Headless services return endpoints for pods missing hostname",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
map[string]string{},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"", ""},
[]bool{true, true},
false,
[]v1.Node{},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
{
"annotated Headless services return only a unique set of IPv4 targets",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
map[string]string{},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.1", "1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
{
"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",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
map[string]string{
targetAnnotationKey: "1.2.3.4",
},
v1.ClusterIPNone,
[]string{"1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
{
"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",
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",
},
},
},
},
},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
{
"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",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
endpointsTypeAnnotationKey: EndpointsTypeHostIP,
},
map[string]string{},
v1.ClusterIPNone,
[]string{"1.1.1.1"},
[]string{"1.2.3.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.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
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) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
Selector: tc.selector,
PublishNotReadyAddresses: tc.publishNotReadyAddresses,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.svcAnnotations,
},
Status: v1.ServiceStatus{},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
var addresses, notReadyAddresses []v1.EndpointAddress
for i, podname := range tc.podnames {
pod := &v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{},
Hostname: tc.hostnames[i],
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: podname,
Labels: tc.labels,
Annotations: tc.podAnnotations,
},
Status: v1.PodStatus{
PodIP: tc.podIPs[i],
HostIP: tc.hostIPs[i],
},
}
_, err = kubernetes.CoreV1().Pods(tc.svcNamespace).Create(context.Background(), pod, metav1.CreateOptions{})
require.NoError(t, err)
address := v1.EndpointAddress{
IP: tc.podIPs[i],
TargetRef: &v1.ObjectReference{
APIVersion: "",
Kind: "Pod",
Name: podname,
},
}
if tc.podsReady[i] {
addresses = append(addresses, address)
} else {
notReadyAddresses = append(notReadyAddresses, address)
}
}
endpointsObject := &v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
},
Subsets: []v1.EndpointSubset{
{
Addresses: addresses,
NotReadyAddresses: notReadyAddresses,
},
},
}
_, err = kubernetes.CoreV1().Endpoints(tc.svcNamespace).Create(context.Background(), endpointsObject, metav1.CreateOptions{})
require.NoError(t, err)
for _, node := range tc.nodes {
_, err = kubernetes.CoreV1().Nodes().Create(context.Background(), &node, metav1.CreateOptions{})
require.NoError(t, err)
}
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
"",
tc.fqdnTemplate,
false,
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
false,
)
require.NoError(t, err)
endpoints, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
})
}
}
// TestHeadlessServices tests that headless services generate the correct endpoints.
func TestHeadlessServicesHostIP(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
hostIPs []string
selector map[string]string
lbs []string
podnames []string
hostnames []string
podsReady []bool
targetRefs []*v1.ObjectReference
publishNotReadyAddresses bool
expected []*endpoint.Endpoint
expectError bool
}{
{
"annotated Headless services return IPv4 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{"1.1.1.1", "1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
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",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
true,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.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{},
false,
},
{
"annotated Headless services return IPv4 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{"1.1.1.1", "1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
},
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",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]bool{true, false},
[]*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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
{
"annotated Headless services return endpoints for all Pod if publishNotReadyAddresses is set",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]bool{true, false},
[]*v1.ObjectReference{
{APIVersion: "", Kind: "Pod", Name: "foo-0"},
{APIVersion: "", Kind: "Pod", Name: "foo-1"},
},
true,
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo-1.service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.2"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
{
"annotated Headless services return IPv4 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{"1.1.1.1", "1.1.1.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.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
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",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0"},
[]string{"foo-0"},
[]bool{true, true},
[]*v1.ObjectReference{nil},
false,
[]*endpoint.Endpoint{},
false,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
Selector: tc.selector,
PublishNotReadyAddresses: tc.publishNotReadyAddresses,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.annotations,
},
Status: v1.ServiceStatus{},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
var addresses []v1.EndpointAddress
var notReadyAddresses []v1.EndpointAddress
for i, podname := range tc.podnames {
pod := &v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{},
Hostname: tc.hostnames[i],
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: podname,
Labels: tc.labels,
Annotations: tc.annotations,
},
Status: v1.PodStatus{
HostIP: tc.hostIPs[i],
},
}
_, err = kubernetes.CoreV1().Pods(tc.svcNamespace).Create(context.Background(), pod, metav1.CreateOptions{})
require.NoError(t, err)
address := v1.EndpointAddress{
IP: "4.3.2.1",
TargetRef: tc.targetRefs[i],
}
if tc.podsReady[i] {
addresses = append(addresses, address)
} else {
notReadyAddresses = append(notReadyAddresses, address)
}
}
endpointsObject := &v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
},
Subsets: []v1.EndpointSubset{
{
Addresses: addresses,
NotReadyAddresses: notReadyAddresses,
},
},
}
_, err = kubernetes.CoreV1().Endpoints(tc.svcNamespace).Create(context.Background(), endpointsObject, metav1.CreateOptions{})
require.NoError(t, err)
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
"",
tc.fqdnTemplate,
false,
tc.compatibility,
true,
true,
false,
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
false,
)
require.NoError(t, err)
endpoints, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
})
}
}
// TestExternalServices tests that external services generate the correct endpoints.
func TestExternalServices(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
externalName string
externalIPs []string
expected []*endpoint.Endpoint
expectError bool
}{
{
"external services return an A endpoint for the external name that is an IPv4 address",
"",
"testing",
"foo",
v1.ServiceTypeExternalName,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
"111.111.111.111",
[]string{},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"111.111.111.111"}, RecordType: endpoint.RecordTypeA},
},
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",
[]string{},
[]*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",
"",
"testing",
"foo",
v1.ServiceTypeExternalName,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
"remote.example.com",
[]string{},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"remote.example.com"}, RecordType: endpoint.RecordTypeCNAME},
},
false,
},
{
"annotated ExternalName service with externalIPs returns a single endpoint with multiple targets",
"",
"testing",
"foo",
v1.ServiceTypeExternalName,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
"service.example.org",
[]string{"10.2.3.4", "11.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}},
},
false,
},
{
"annotated ExternalName service with externalIPs of dualstack addresses returns 2 endpoints with multiple targets",
"",
"testing",
"foo",
v1.ServiceTypeExternalName,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
"service.example.org",
[]string{"10.2.3.4", "11.2.3.4", "2001:db8::1", "2001:db8::2"},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}},
{DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}},
},
false,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ExternalName: tc.externalName,
ExternalIPs: tc.externalIPs,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.annotations,
},
Status: v1.ServiceStatus{},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(t, err)
// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
context.TODO(),
kubernetes,
tc.targetNamespace,
"",
tc.fqdnTemplate,
false,
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
labels.Everything(),
false,
false,
)
require.NoError(t, err)
endpoints, err := client.Endpoints(context.Background())
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
})
}
}
func BenchmarkServiceEndpoints(b *testing.B) {
kubernetes := fake.NewSimpleClientset()
service := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "testing",
Name: "foo",
Annotations: map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{IP: "1.2.3.4"},
{IP: "8.8.8.8"},
},
},
},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{})
require.NoError(b, err)
client, err := NewServiceSource(
context.TODO(),
kubernetes,
v1.NamespaceAll,
"",
"",
false,
"",
false,
false,
false,
[]string{},
false,
labels.Everything(),
false,
false,
)
require.NoError(b, err)
for i := 0; i < b.N; i++ {
_, err := client.Endpoints(context.Background())
require.NoError(b, err)
}
}