Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package endpoint
import (
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -353,7 +354,21 @@ func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
}

// Apply filter to slice of endpoints and return new filtered slice that includes
// UniqueOrderedTargets removes duplicate targets from the Endpoint and sorts them in lexicographical order.
func (e *Endpoint) UniqueOrderedTargets() {
result := make([]string, 0, len(e.Targets))
existing := make(map[string]bool)
for _, target := range e.Targets {
if _, ok := existing[target]; !ok {
result = append(result, target)
existing[target] = true
}
}
slices.Sort(result)
e.Targets = result
}

// FilterEndpointsByOwnerID Apply filter to slice of endpoints and return new filtered slice that includes
// only endpoints that match.
func FilterEndpointsByOwnerID(ownerID string, eps []*Endpoint) []*Endpoint {
filtered := []*Endpoint{}
Expand Down
43 changes: 43 additions & 0 deletions endpoint/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -925,3 +925,46 @@ func TestCheckEndpoint(t *testing.T) {
})
}
}

func TestEndpoint_UniqueOrderedTargets(t *testing.T) {
tests := []struct {
name string
targets []string
expected Targets
want bool
}{
{
name: "no duplicates",
targets: []string{"b.example.com", "a.example.com"},
expected: Targets{"a.example.com", "b.example.com"},
},
{
name: "with duplicates",
targets: []string{"a.example.com", "b.example.com", "a.example.com"},
expected: Targets{"a.example.com", "b.example.com"},
},
{
name: "already sorted",
targets: []string{"a.example.com", "b.example.com"},
expected: Targets{"a.example.com", "b.example.com"},
},
{
name: "all duplicates",
targets: []string{"a.example.com", "a.example.com", "a.example.com"},
expected: Targets{"a.example.com"},
},
{
name: "empty",
targets: []string{},
expected: Targets{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ep := &Endpoint{Targets: tt.targets}
ep.UniqueOrderedTargets()
assert.Equal(t, tt.expected, ep.Targets)
})
}
}
48 changes: 23 additions & 25 deletions source/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,38 +252,36 @@ func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err
sort.Slice(endpoints, func(i, j int) bool {
return endpoints[i].Labels[endpoint.ResourceLabelKey] < endpoints[j].Labels[endpoint.ResourceLabelKey]
})
mergedEndpoints := make(map[endpoint.EndpointKey][]*endpoint.Endpoint)
for _, ep := range endpoints {
key := ep.Key()
if existing, ok := mergedEndpoints[key]; ok {
if existing[0].RecordType == endpoint.RecordTypeCNAME {
log.Debugf("CNAME %s with multiple targets found", ep.DNSName)
mergedEndpoints[key] = append(existing, ep)
continue
}
existing[0].Targets = append(existing[0].Targets, ep.Targets...)
existing[0].UniqueOrderedTargets()
mergedEndpoints[key] = existing
} else {
ep.UniqueOrderedTargets()
mergedEndpoints[key] = []*endpoint.Endpoint{ep}
}
}
processed := make([]*endpoint.Endpoint, 0, len(mergedEndpoints))
for _, ep := range mergedEndpoints {
processed = append(processed, ep...)
}
endpoints = processed

// Use stable sort to not disrupt the order of services
sort.SliceStable(endpoints, func(i, j int) bool {
if endpoints[i].DNSName != endpoints[j].DNSName {
return endpoints[i].DNSName < endpoints[j].DNSName
}
return endpoints[i].RecordType < endpoints[j].RecordType
})
mergedEndpoints := []*endpoint.Endpoint{}
mergedEndpoints = append(mergedEndpoints, endpoints[0])
for i := 1; i < len(endpoints); i++ {
lastMergedEndpoint := len(mergedEndpoints) - 1
if mergedEndpoints[lastMergedEndpoint].DNSName == endpoints[i].DNSName &&
mergedEndpoints[lastMergedEndpoint].RecordType == endpoints[i].RecordType &&
mergedEndpoints[lastMergedEndpoint].RecordType != endpoint.RecordTypeCNAME && // It is against RFC-1034 for CNAME records to have multiple targets, so skip merging
mergedEndpoints[lastMergedEndpoint].SetIdentifier == endpoints[i].SetIdentifier &&
mergedEndpoints[lastMergedEndpoint].RecordTTL == endpoints[i].RecordTTL {
mergedEndpoints[lastMergedEndpoint].Targets = append(mergedEndpoints[lastMergedEndpoint].Targets, endpoints[i].Targets[0])
} else {
mergedEndpoints = append(mergedEndpoints, endpoints[i])
}

if mergedEndpoints[lastMergedEndpoint].DNSName == endpoints[i].DNSName &&
mergedEndpoints[lastMergedEndpoint].RecordType == endpoints[i].RecordType &&
mergedEndpoints[lastMergedEndpoint].RecordType == endpoint.RecordTypeCNAME {
log.Debugf("CNAME %s with multiple targets found", endpoints[i].DNSName)
}
}
endpoints = mergedEndpoints
}

for _, ep := range endpoints {
sort.Sort(ep.Targets)
}

return endpoints, nil
Expand Down
Loading
Loading