external-dns/source/utils_test.go
Ivan Ka 83e1bcf39d
refactor(source): inline label selector matching, remove MatchesServiceSelector (#6304)
* refactor(source): inline label selector matching, remove MatchesServiceSelector

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* refactor(source): inline label selector matching, remove MatchesServiceSelector

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
2026-03-26 05:00:16 +05:30

382 lines
13 KiB
Go

/*
Copyright 2025 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 (
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
logtest "sigs.k8s.io/external-dns/internal/testutils/log"
"sigs.k8s.io/external-dns/source/types"
)
func TestParseIngress(t *testing.T) {
tests := []struct {
name string
ingress string
wantNS string
wantName string
wantError bool
}{
{
name: "valid namespace and name",
ingress: "default/test-ingress",
wantNS: "default",
wantName: "test-ingress",
wantError: false,
},
{
name: "only name provided",
ingress: "test-ingress",
wantNS: "",
wantName: "test-ingress",
wantError: false,
},
{
name: "invalid format",
ingress: "default/test/ingress",
wantNS: "",
wantName: "",
wantError: true,
},
{
name: "empty string",
ingress: "",
wantNS: "",
wantName: "",
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNS, gotName, err := ParseIngress(tt.ingress)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.wantNS, gotNS)
assert.Equal(t, tt.wantName, gotName)
})
}
}
func TestMergeEndpoints(t *testing.T) {
tests := []struct {
name string
input []*endpoint.Endpoint
expected []*endpoint.Endpoint
}{
{
name: "nil input returns nil",
input: nil,
expected: nil,
},
{
name: "empty input returns empty",
input: []*endpoint.Endpoint{},
expected: []*endpoint.Endpoint{},
},
{
name: "single endpoint unchanged",
input: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
expected: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
},
{
name: "different keys not merged",
input: []*endpoint.Endpoint{
{DNSName: "a.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "b.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}},
},
expected: []*endpoint.Endpoint{
{DNSName: "a.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "b.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}},
},
},
{
name: "same DNSName different RecordType not merged",
input: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "example.com", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}},
},
expected: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
{DNSName: "example.com", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1"}},
},
},
{
name: "same key merged with sorted targets",
input: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}},
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}},
},
expected: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4", "5.6.7.8"}},
},
},
{
name: "multiple endpoints same key merged",
input: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"3.3.3.3"}},
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2"}},
},
expected: []*endpoint.Endpoint{
{DNSName: "example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "2.2.2.2", "3.3.3.3"}},
},
},
{
name: "mixed merge and no merge",
input: []*endpoint.Endpoint{
{DNSName: "a.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "b.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2"}},
{DNSName: "a.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"3.3.3.3"}},
},
expected: []*endpoint.Endpoint{
{DNSName: "a.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "3.3.3.3"}},
{DNSName: "b.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"2.2.2.2"}},
},
},
{
name: "duplicate targets deduplicated",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4", "1.2.3.4", "5.6.7.8"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4", "5.6.7.8"),
},
},
{
name: "duplicate targets across merged endpoints deduplicated",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4", "5.6.7.8"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4", "5.6.7.8"),
},
},
{
name: "CNAME endpoints not merged",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "b.elb.com"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "b.elb.com"),
},
},
{
name: "CNAME with no targets is skipped",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4"),
},
},
{
name: "identical CNAME endpoints deduplicated",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
},
},
{
name: "same key with different TTL not merged",
input: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, 300, "1.2.3.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, 600, "5.6.7.8"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, 300, "1.2.3.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, 600, "5.6.7.8"),
},
},
{
name: "same DNSName and RecordType with different SetIdentifier not merged",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("us-east-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "5.6.7.8").WithSetIdentifier("eu-west-1"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("us-east-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "5.6.7.8").WithSetIdentifier("eu-west-1"),
},
},
{
name: "same DNSName, RecordType and SetIdentifier targets are merged",
input: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("us-east-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "5.6.7.8").WithSetIdentifier("us-east-1"),
},
expected: []*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeA, "1.2.3.4", "5.6.7.8").WithSetIdentifier("us-east-1"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := MergeEndpoints(tt.input)
assert.ElementsMatch(t, tt.expected, result)
})
}
}
func TestMergeEndpoints_RefObjects(t *testing.T) {
tests := []struct {
name string
input func() []*endpoint.Endpoint
expected func(*testing.T, []*endpoint.Endpoint)
}{
{
name: "empty input",
input: func() []*endpoint.Endpoint { return []*endpoint.Endpoint{} },
expected: func(t *testing.T, ep []*endpoint.Endpoint) {
assert.Empty(t, ep)
},
},
{
name: "single endpoint",
input: func() []*endpoint.Endpoint {
return []*endpoint.Endpoint{
testutils.NewEndpointWithRef("example.com", "1.2.3.4", &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", UID: "123"},
}, types.Service),
}
},
expected: func(t *testing.T, ep []*endpoint.Endpoint) {
assert.Len(t, ep, 1)
assert.Equal(t, types.Service, ep[0].RefObject().Source)
assert.Equal(t, "foo", ep[0].RefObject().Name)
assert.Equal(t, "123", string(ep[0].RefObject().UID))
},
},
{
name: "two endpoints merged and only single refObject preserved",
input: func() []*endpoint.Endpoint {
return []*endpoint.Endpoint{
testutils.NewEndpointWithRef("a.example.com", "1.1.1.1", &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", UID: "123"},
}, types.Service),
testutils.NewEndpointWithRef("a.example.com", "1.1.1.1", &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "ns", UID: "345"},
}, types.Service),
}
},
expected: func(t *testing.T, ep []*endpoint.Endpoint) {
assert.Len(t, ep, 1)
assert.Equal(t, types.Service, ep[0].RefObject().Source)
assert.Equal(t, "foo", ep[0].RefObject().Name)
assert.Equal(t, "123", string(ep[0].RefObject().UID))
assert.NotEqual(t, "345", string(ep[0].RefObject().UID))
},
},
{
name: "two endpoints not merged and two refObject preserved",
input: func() []*endpoint.Endpoint {
return []*endpoint.Endpoint{
testutils.NewEndpointWithRef("a.example.com", "1.1.1.1", &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default", UID: "123"},
}, types.Service),
testutils.NewEndpointWithRef("b.example.com", "1.1.1.2", &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "ns", UID: "345"},
}, types.Service),
}
},
expected: func(t *testing.T, ep []*endpoint.Endpoint) {
assert.Len(t, ep, 2)
assert.NotEqual(t, ep[0], ep[1])
for _, el := range ep {
assert.Equal(t, types.Service, el.RefObject().Source)
assert.Contains(t, []string{"foo", "bar"}, el.RefObject().Name)
assert.Contains(t, []string{"123", "345"}, string(el.RefObject().UID))
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := MergeEndpoints(tt.input())
tt.expected(t, result)
})
}
}
func TestMergeEndpointsLogging(t *testing.T) {
t.Run("warns on CNAME conflict", func(t *testing.T) {
hook := logtest.LogsUnderTestWithLogLevel(log.WarnLevel, t)
MergeEndpoints([]*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "b.elb.com"),
})
logtest.TestHelperLogContainsWithLogLevel("Only one CNAME per name", log.WarnLevel, hook, t)
logtest.TestHelperLogContains("example.com CNAME a.elb.com", hook, t)
logtest.TestHelperLogContains("example.com CNAME b.elb.com", hook, t)
})
t.Run("no warning for identical CNAMEs", func(t *testing.T) {
hook := logtest.LogsUnderTestWithLogLevel(log.WarnLevel, t)
MergeEndpoints([]*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com"),
})
logtest.TestHelperLogNotContains("Only one CNAME per name", hook, t)
})
t.Run("no warning for same DNSName with different SetIdentifier", func(t *testing.T) {
hook := logtest.LogsUnderTestWithLogLevel(log.WarnLevel, t)
MergeEndpoints([]*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "a.elb.com").WithSetIdentifier("weight-1"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "b.elb.com").WithSetIdentifier("weight-2"),
})
logtest.TestHelperLogNotContains("Only one CNAME per name", hook, t)
})
t.Run("debug log for CNAME with no targets", func(t *testing.T) {
hook := logtest.LogsUnderTestWithLogLevel(log.DebugLevel, t)
MergeEndpoints([]*endpoint.Endpoint{
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME),
})
logtest.TestHelperLogContainsWithLogLevel("Skipping CNAME endpoint", log.DebugLevel, hook, t)
logtest.TestHelperLogContains("example.com", hook, t)
})
}