chore(source): code cleanup

This commit is contained in:
ivan katliarchuk 2025-04-16 13:11:42 +01:00
parent 7ae0405537
commit 9f427e5622
No known key found for this signature in database
GPG Key ID: 90C9B4748A999097
36 changed files with 1787 additions and 857 deletions

View File

@ -254,7 +254,7 @@ func (z zoneTags) filterZonesByTags(p *AWSProvider, zones map[string]*profiledZo
// append adds tags to the ZoneTags for a given zoneID. // append adds tags to the ZoneTags for a given zoneID.
func (z zoneTags) append(id string, tags []route53types.Tag) { func (z zoneTags) append(id string, tags []route53types.Tag) {
zoneId := fmt.Sprintf("/hostedzone/%s", id) zoneId := fmt.Sprintf("/hostedzone/%s", id)
if _, exists := z[zoneId]; !exists { if _, ok := z[zoneId]; !ok {
z[zoneId] = make(map[string]string) z[zoneId] = make(map[string]string)
} }
for _, tag := range tags { for _, tag := range tags {

View File

@ -34,7 +34,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/source" "sigs.k8s.io/external-dns/source/annotations"
) )
const ( const (
@ -736,17 +736,17 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
if proxied { if proxied {
e.RecordTTL = 0 e.RecordTTL = 0
} }
e.SetProviderSpecificProperty(source.CloudflareProxiedKey, strconv.FormatBool(proxied)) e.SetProviderSpecificProperty(annotations.CloudflareProxiedKey, strconv.FormatBool(proxied))
if p.CustomHostnamesConfig.Enabled { if p.CustomHostnamesConfig.Enabled {
// sort custom hostnames in annotation to properly detect changes // sort custom hostnames in annotation to properly detect changes
if customHostnames := getEndpointCustomHostnames(e); len(customHostnames) > 1 { if customHostnames := getEndpointCustomHostnames(e); len(customHostnames) > 1 {
sort.Strings(customHostnames) sort.Strings(customHostnames)
e.SetProviderSpecificProperty(source.CloudflareCustomHostnameKey, strings.Join(customHostnames, ",")) e.SetProviderSpecificProperty(annotations.CloudflareCustomHostnameKey, strings.Join(customHostnames, ","))
} }
} else { } else {
// ignore custom hostnames annotations if not enabled // ignore custom hostnames annotations if not enabled
e.DeleteProviderSpecificProperty(source.CloudflareCustomHostnameKey) e.DeleteProviderSpecificProperty(annotations.CloudflareCustomHostnameKey)
} }
adjustedEndpoints = append(adjustedEndpoints, e) adjustedEndpoints = append(adjustedEndpoints, e)
@ -928,10 +928,10 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool {
proxied := proxiedByDefault proxied := proxiedByDefault
for _, v := range ep.ProviderSpecific { for _, v := range ep.ProviderSpecific {
if v.Name == source.CloudflareProxiedKey { if v.Name == annotations.CloudflareProxiedKey {
b, err := strconv.ParseBool(v.Value) b, err := strconv.ParseBool(v.Value)
if err != nil { if err != nil {
log.Errorf("Failed to parse annotation [%q]: %v", source.CloudflareProxiedKey, err) log.Errorf("Failed to parse annotation [%q]: %v", annotations.CloudflareProxiedKey, err)
} else { } else {
proxied = b proxied = b
} }
@ -951,7 +951,7 @@ func getRegionKey(endpoint *endpoint.Endpoint, defaultRegionKey string) string {
} }
for _, v := range endpoint.ProviderSpecific { for _, v := range endpoint.ProviderSpecific {
if v.Name == source.CloudflareRegionKey { if v.Name == annotations.CloudflareRegionKey {
return v.Value return v.Value
} }
} }
@ -960,7 +960,7 @@ func getRegionKey(endpoint *endpoint.Endpoint, defaultRegionKey string) string {
func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string { func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string {
for _, v := range ep.ProviderSpecific { for _, v := range ep.ProviderSpecific {
if v.Name == source.CloudflareCustomHostnameKey { if v.Name == annotations.CloudflareCustomHostnameKey {
customHostnames := strings.Split(v.Value, ",") customHostnames := strings.Split(v.Value, ",")
return customHostnames return customHostnames
} }
@ -1015,11 +1015,11 @@ func groupByNameAndTypeWithCustomHostnames(records DNSRecordsMap, chs CustomHost
if e == nil { if e == nil {
continue continue
} }
e = e.WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(proxied)) e = e.WithProviderSpecific(annotations.CloudflareProxiedKey, strconv.FormatBool(proxied))
// noop (customHostnames is empty) if custom hostnames feature is not in use // noop (customHostnames is empty) if custom hostnames feature is not in use
if customHostnames, ok := customHostnames[records[0].Name]; ok { if customHostnames, ok := customHostnames[records[0].Name]; ok {
sort.Strings(customHostnames) sort.Strings(customHostnames)
e = e.WithProviderSpecific(source.CloudflareCustomHostnameKey, strings.Join(customHostnames, ",")) e = e.WithProviderSpecific(annotations.CloudflareCustomHostnameKey, strings.Join(customHostnames, ","))
} }
endpoints = append(endpoints, e) endpoints = append(endpoints, e)

View File

@ -312,7 +312,7 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes)
} }
mesh := sets.New[endpoint.EndpointKey]() mesh := sets.New[endpoint.EndpointKey]()
for _, newEndpoint := range changes.Create { for _, newEndpoint := range changes.Create {
if _, exists := curZone[newEndpoint.Key()]; exists { if _, ok := curZone[newEndpoint.Key()]; ok {
return ErrRecordAlreadyExists return ErrRecordAlreadyExists
} }
if err := c.updateMesh(mesh, newEndpoint); err != nil { if err := c.updateMesh(mesh, newEndpoint); err != nil {
@ -320,7 +320,7 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes)
} }
} }
for _, updateEndpoint := range changes.UpdateNew { for _, updateEndpoint := range changes.UpdateNew {
if _, exists := curZone[updateEndpoint.Key()]; !exists { if _, ok := curZone[updateEndpoint.Key()]; !ok {
return ErrRecordNotFound return ErrRecordNotFound
} }
if err := c.updateMesh(mesh, updateEndpoint); err != nil { if err := c.updateMesh(mesh, updateEndpoint); err != nil {
@ -328,12 +328,12 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes)
} }
} }
for _, updateOldEndpoint := range changes.UpdateOld { for _, updateOldEndpoint := range changes.UpdateOld {
if rec, exists := curZone[updateOldEndpoint.Key()]; !exists || rec.Targets[0] != updateOldEndpoint.Targets[0] { if rec, ok := curZone[updateOldEndpoint.Key()]; !ok || rec.Targets[0] != updateOldEndpoint.Targets[0] {
return ErrRecordNotFound return ErrRecordNotFound
} }
} }
for _, deleteEndpoint := range changes.Delete { for _, deleteEndpoint := range changes.Delete {
if rec, exists := curZone[deleteEndpoint.Key()]; !exists || rec.Targets[0] != deleteEndpoint.Targets[0] { if rec, ok := curZone[deleteEndpoint.Key()]; !ok || rec.Targets[0] != deleteEndpoint.Targets[0] {
return ErrRecordNotFound return ErrRecordNotFound
} }
if err := c.updateMesh(mesh, deleteEndpoint); err != nil { if err := c.updateMesh(mesh, deleteEndpoint); err != nil {

View File

@ -1248,7 +1248,7 @@ func (r *DynamoDBStub) BatchExecuteStatement(context context.Context, input *dyn
var key string var key string
assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[0], &key)) assert.Nil(r.t, attributevalue.Unmarshal(statement.Parameters[0], &key))
if code, exists := r.stubConfig.ExpectInsertError[key]; exists { if code, ok := r.stubConfig.ExpectInsertError[key]; ok {
delete(r.stubConfig.ExpectInsertError, key) delete(r.stubConfig.ExpectInsertError, key)
responses = append(responses, dynamodbtypes.BatchStatementResponse{ responses = append(responses, dynamodbtypes.BatchStatementResponse{
Error: &dynamodbtypes.BatchStatementError{ Error: &dynamodbtypes.BatchStatementError{

View File

@ -38,6 +38,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
// ambHostAnnotation is the annotation in the Host that maps to a Service // ambHostAnnotation is the annotation in the Host that maps to a Service
@ -119,7 +120,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
} }
// Get a list of Ambassador Host resources // Get a list of Ambassador Host resources
ambassadorHosts := []*ambassador.Host{} var ambassadorHosts []*ambassador.Host
for _, hostObj := range hosts { for _, hostObj := range hosts {
unstructuredHost, ok := hostObj.(*unstructured.Unstructured) unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
if !ok { if !ok {
@ -140,7 +141,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
return nil, errors.Wrap(err, "failed to filter Ambassador Hosts by annotation") return nil, errors.Wrap(err, "failed to filter Ambassador Hosts by annotation")
} }
endpoints := []*endpoint.Endpoint{} var endpoints []*endpoint.Endpoint
for _, host := range ambassadorHosts { for _, host := range ambassadorHosts {
fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name) fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
@ -152,7 +153,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
continue continue
} }
targets := getTargetsFromTargetAnnotation(host.Annotations) targets := annotations.TargetsFromTargetAnnotation(host.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service) targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service)
if err != nil { if err != nil {
@ -185,11 +186,10 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
// endpointsFromHost extracts the endpoints from a Host object // endpointsFromHost extracts the endpoints from a Host object
func (sc *ambassadorHostSource) endpointsFromHost(host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { func (sc *ambassadorHostSource) endpointsFromHost(host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
annotations := host.Annotations
resource := fmt.Sprintf("host/%s/%s", host.Namespace, host.Name) resource := fmt.Sprintf("host/%s/%s", host.Namespace, host.Name)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(host.Annotations)
ttl := getTTLFromAnnotations(annotations, resource) ttl := annotations.TTLFromAnnotations(host.Annotations, resource)
if host.Spec != nil { if host.Spec != nil {
hostname := host.Spec.Hostname hostname := host.Spec.Hostname

View File

@ -33,6 +33,7 @@ import (
fakeDynamic "k8s.io/client-go/dynamic/fake" fakeDynamic "k8s.io/client-go/dynamic/fake"
fakeKube "k8s.io/client-go/kubernetes/fake" fakeKube "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
const defaultAmbassadorNamespace = "ambassador" const defaultAmbassadorNamespace = "ambassador"
@ -246,8 +247,8 @@ func TestAmbassadorHostSource(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "basic-host", Name: "basic-host",
Annotations: map[string]string{ Annotations: map[string]string{
ambHostAnnotation: hostAnnotation, ambHostAnnotation: hostAnnotation,
CloudflareProxiedKey: "true", annotations.CloudflareProxiedKey: "true",
}, },
}, },
Spec: &ambassador.HostSpec{ Spec: &ambassador.HostSpec{

View File

@ -0,0 +1,54 @@
/*
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 annotations
import (
"math"
)
const (
// CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare
CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
CloudflareRegionKey = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
AWSPrefix = "external-dns.alpha.kubernetes.io/aws-"
SCWPrefix = "external-dns.alpha.kubernetes.io/scw-"
IBMCloudPrefix = "external-dns.alpha.kubernetes.io/ibmcloud-"
WebhookPrefix = "external-dns.alpha.kubernetes.io/webhook-"
CloudflarePrefix = "external-dns.alpha.kubernetes.io/cloudflare-"
TtlKey = "external-dns.alpha.kubernetes.io/ttl"
ttlMinimum = 1
ttlMaximum = math.MaxInt32
SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
AliasKey = "external-dns.alpha.kubernetes.io/alias"
TargetKey = "external-dns.alpha.kubernetes.io/target"
// The annotation used for figuring out which controller is responsible
ControllerKey = "external-dns.alpha.kubernetes.io/controller"
// The annotation used for defining the desired hostname
HostnameKey = "external-dns.alpha.kubernetes.io/hostname"
// The annotation used for specifying whether the public or private interface address is used
AccessKey = "external-dns.alpha.kubernetes.io/access"
// The annotation used for specifying the type of endpoints to use for headless services
EndpointsTypeKey = "external-dns.alpha.kubernetes.io/endpoints-type"
// The annotation used to determine the source of hostnames for ingresses. This is an optional field - all
// available hostname sources are used if not specified.
IngressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source"
// The value of the controller annotation so that we feel responsible
ControllerValue = "dns-controller"
// The annotation used for defining the desired hostname
InternalHostnameKey = "external-dns.alpha.kubernetes.io/internal-hostname"
)

View File

@ -0,0 +1,125 @@
/*
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 annotations
import (
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/external-dns/endpoint"
)
func hasAliasFromAnnotations(annotations map[string]string) bool {
aliasAnnotation, ok := annotations[AliasKey]
return ok && aliasAnnotation == "true"
}
// TTLFromAnnotations extracts the TTL from the annotations of the given resource.
func TTLFromAnnotations(annotations map[string]string, resource string) endpoint.TTL {
ttlNotConfigured := endpoint.TTL(0)
ttlAnnotation, ok := annotations[TtlKey]
if !ok {
return ttlNotConfigured
}
ttlValue, err := parseTTL(ttlAnnotation)
if err != nil {
log.Warnf("%s: %q is not a valid TTL value: %v", resource, ttlAnnotation, err)
return ttlNotConfigured
}
if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum)
return ttlNotConfigured
}
return endpoint.TTL(ttlValue)
}
// parseTTL parses TTL from string, returning duration in seconds.
// parseTTL supports both integers like "600" and durations based
// on Go Duration like "10m", hence "600" and "10m" represent the same value.
//
// Note: for durations like "1.5s" the fraction is omitted (resulting in 1 second for the example).
func parseTTL(s string) (int64, error) {
ttlDuration, errDuration := time.ParseDuration(s)
if errDuration != nil {
ttlInt, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, errDuration
}
return ttlInt, nil
}
return int64(ttlDuration.Seconds()), nil
}
// ParseFilter parses an annotation filter string into a labels.Selector.
// Returns nil if the annotation filter is invalid.
func ParseFilter(annotationFilter string) (labels.Selector, error) {
labelSelector, err := metav1.ParseToLabelSelector(annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
return nil, err
}
return selector, nil
}
// TargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
// Returns empty endpoints array if none are found.
func TargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
var targets endpoint.Targets
// Get the desired hostname of the ingress from the annotation.
targetAnnotation, ok := annotations[TargetKey]
if ok && targetAnnotation != "" {
// splits the hostname annotation and removes the trailing periods
targetsList := SplitHostnameAnnotation(targetAnnotation)
for _, targetHostname := range targetsList {
targetHostname = strings.TrimSuffix(targetHostname, ".")
targets = append(targets, targetHostname)
}
}
return targets
}
// HostnamesFromAnnotations extracts the hostnames from the given annotations map.
// It returns a slice of hostnames if the HostnameKey annotation is present, otherwise it returns nil.
func HostnamesFromAnnotations(input map[string]string) []string {
return extractHostnamesFromAnnotations(input, HostnameKey)
}
// InternalHostnamesFromAnnotations extracts the internal hostnames from the given annotations map.
// It returns a slice of internal hostnames if the InternalHostnameKey annotation is present, otherwise it returns nil.
func InternalHostnamesFromAnnotations(input map[string]string) []string {
return extractHostnamesFromAnnotations(input, InternalHostnameKey)
}
// SplitHostnameAnnotation splits a comma-separated hostname annotation string into a slice of hostnames.
// It trims any leading or trailing whitespace and removes any spaces within the anno
func SplitHostnameAnnotation(input string) []string {
return strings.Split(strings.TrimSpace(strings.ReplaceAll(input, " ", "")), ",")
}
func extractHostnamesFromAnnotations(input map[string]string, key string) []string {
annotation, ok := input[key]
if !ok {
return nil
}
return SplitHostnameAnnotation(annotation)
}

View File

@ -0,0 +1,350 @@
/*
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 annotations
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/external-dns/endpoint"
)
func TestParseAnnotationFilter(t *testing.T) {
tests := []struct {
name string
annotationFilter string
expectedSelector labels.Selector
expectError bool
}{
{
name: "valid annotation filter",
annotationFilter: "key1=value1,key2=value2",
expectedSelector: labels.Set{"key1": "value1", "key2": "value2"}.AsSelector(),
expectError: false,
},
{
name: "invalid annotation filter",
annotationFilter: "key1==value1",
expectedSelector: labels.Set{"key1": "value1"}.AsSelector(),
expectError: false,
},
{
name: "empty annotation filter",
annotationFilter: "",
expectedSelector: labels.Set{}.AsSelector(),
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selector, err := ParseFilter(tt.annotationFilter)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedSelector, selector)
}
})
}
}
func TestTargetsFromTargetAnnotation(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected endpoint.Targets
}{
{
name: "no target annotation",
annotations: map[string]string{},
expected: endpoint.Targets(nil),
},
{
name: "single target annotation",
annotations: map[string]string{
TargetKey: "example.com",
},
expected: endpoint.Targets{"example.com"},
},
{
name: "multiple target annotations",
annotations: map[string]string{
TargetKey: "example.com,example.org",
},
expected: endpoint.Targets{"example.com", "example.org"},
},
{
name: "target annotation with trailing periods",
annotations: map[string]string{
TargetKey: "example.com.,example.org.",
},
expected: endpoint.Targets{"example.com", "example.org"},
},
{
name: "target annotation with spaces",
annotations: map[string]string{
TargetKey: " example.com , example.org ",
},
expected: endpoint.Targets{"example.com", "example.org"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := TargetsFromTargetAnnotation(tt.annotations)
assert.Equal(t, tt.expected, result)
})
}
}
func TestTTLFromAnnotations(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
resource string
expectedTTL endpoint.TTL
}{
{
name: "no TTL annotation",
annotations: map[string]string{},
resource: "test-resource",
expectedTTL: endpoint.TTL(0),
},
{
name: "valid TTL annotation",
annotations: map[string]string{
TtlKey: "600",
},
resource: "test-resource",
expectedTTL: endpoint.TTL(600),
},
{
name: "invalid TTL annotation",
annotations: map[string]string{
TtlKey: "invalid",
},
resource: "test-resource",
expectedTTL: endpoint.TTL(0),
},
{
name: "TTL annotation out of range",
annotations: map[string]string{
TtlKey: "999999",
},
resource: "test-resource",
expectedTTL: endpoint.TTL(999999),
},
{
name: "TTL annotation not present",
annotations: map[string]string{"foo": "bar"},
expectedTTL: endpoint.TTL(0),
},
{
name: "TTL annotation value is not a number",
annotations: map[string]string{TtlKey: "foo"},
expectedTTL: endpoint.TTL(0),
},
{
name: "TTL annotation value is empty",
annotations: map[string]string{TtlKey: ""},
expectedTTL: endpoint.TTL(0),
},
{
name: "TTL annotation value is negative number",
annotations: map[string]string{TtlKey: "-1"},
expectedTTL: endpoint.TTL(0),
},
{
name: "TTL annotation value is too high",
annotations: map[string]string{TtlKey: fmt.Sprintf("%d", 1<<32)},
expectedTTL: endpoint.TTL(0),
},
{
name: "TTL annotation value is set correctly using integer",
annotations: map[string]string{TtlKey: "60"},
expectedTTL: endpoint.TTL(60),
},
{
name: "TTL annotation value is set correctly using duration (whole)",
annotations: map[string]string{TtlKey: "10m"},
expectedTTL: endpoint.TTL(600),
},
{
name: "TTL annotation value is set correctly using duration (fractional)",
annotations: map[string]string{TtlKey: "20.5s"},
expectedTTL: endpoint.TTL(20),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ttl := TTLFromAnnotations(tt.annotations, tt.resource)
assert.Equal(t, tt.expectedTTL, ttl)
})
}
}
func TestGetAliasFromAnnotations(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected bool
}{
{
name: "alias annotation exists and is true",
annotations: map[string]string{AliasKey: "true"},
expected: true,
},
{
name: "alias annotation exists and is false",
annotations: map[string]string{AliasKey: "false"},
expected: false,
},
{
name: "alias annotation does not exist",
annotations: map[string]string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasAliasFromAnnotations(tt.annotations)
assert.Equal(t, tt.expected, result)
})
}
}
func TestHostnamesFromAnnotations(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected []string
}{
{
name: "no hostname annotation",
annotations: map[string]string{},
expected: nil,
},
{
name: "single hostname annotation",
annotations: map[string]string{
HostnameKey: "example.com",
},
expected: []string{"example.com"},
},
{
name: "multiple hostname annotations",
annotations: map[string]string{
HostnameKey: "example.com,example.org",
},
expected: []string{"example.com", "example.org"},
},
{
name: "hostname annotation with spaces",
annotations: map[string]string{
HostnameKey: " example.com , example.org ",
},
expected: []string{"example.com", "example.org"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HostnamesFromAnnotations(tt.annotations)
assert.Equal(t, tt.expected, result)
})
}
}
func TestSplitHostnameAnnotation(t *testing.T) {
tests := []struct {
name string
annotation string
expected []string
}{
{
name: "empty annotation",
annotation: "",
expected: []string{""},
},
{
name: "single hostname",
annotation: "example.com",
expected: []string{"example.com"},
},
{
name: "multiple hostnames",
annotation: "example.com,example.org",
expected: []string{"example.com", "example.org"},
},
{
name: "hostnames with spaces",
annotation: " example.com , example.org ",
expected: []string{"example.com", "example.org"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SplitHostnameAnnotation(tt.annotation)
assert.Equal(t, tt.expected, result)
})
}
}
func TestInternalHostnamesFromAnnotations(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected []string
}{
{
name: "no internal hostname annotation",
annotations: map[string]string{},
expected: nil,
},
{
name: "single internal hostname annotation",
annotations: map[string]string{
InternalHostnameKey: "internal.example.com",
},
expected: []string{"internal.example.com"},
},
{
name: "multiple internal hostname annotations",
annotations: map[string]string{
InternalHostnameKey: "internal.example.com,internal.example.org",
},
expected: []string{"internal.example.com", "internal.example.org"},
},
{
name: "internal hostname annotation with spaces",
annotations: map[string]string{
InternalHostnameKey: " internal.example.com , internal.example.org ",
},
expected: []string{"internal.example.com", "internal.example.org"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := InternalHostnamesFromAnnotations(tt.annotations)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@ -0,0 +1,76 @@
/*
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 annotations
import (
"fmt"
"strings"
"sigs.k8s.io/external-dns/endpoint"
)
func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) {
providerSpecificAnnotations := endpoint.ProviderSpecific{}
if hasAliasFromAnnotations(annotations) {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: "alias",
Value: "true",
})
}
setIdentifier := ""
for k, v := range annotations {
if k == SetIdentifierKey {
setIdentifier = v
} else if strings.HasPrefix(k, AWSPrefix) {
attr := strings.TrimPrefix(k, AWSPrefix)
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("aws/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, SCWPrefix) {
attr := strings.TrimPrefix(k, SCWPrefix)
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("scw/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, IBMCloudPrefix) {
attr := strings.TrimPrefix(k, IBMCloudPrefix)
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("ibmcloud-%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, WebhookPrefix) {
// Support for wildcard annotations for webhook providers
attr := strings.TrimPrefix(k, WebhookPrefix)
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("webhook/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, CloudflarePrefix) {
if strings.Contains(k, CloudflareCustomHostnameKey) {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: CloudflareCustomHostnameKey,
Value: v,
})
} else if strings.Contains(k, CloudflareProxiedKey) {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: CloudflareProxiedKey,
Value: v,
})
}
}
}
return providerSpecificAnnotations, setIdentifier
}

View File

@ -0,0 +1,305 @@
/*
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 annotations
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
)
func TestProviderSpecificAnnotations(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
expected endpoint.ProviderSpecific
setIdentifier string
}{
{
name: "no annotations",
annotations: map[string]string{},
expected: endpoint.ProviderSpecific{},
setIdentifier: "",
},
{
name: "Cloudflare proxied annotation",
annotations: map[string]string{
CloudflareProxiedKey: "true",
},
expected: endpoint.ProviderSpecific{
{Name: CloudflareProxiedKey, Value: "true"},
},
setIdentifier: "",
},
{
name: "Cloudflare custom hostname annotation",
annotations: map[string]string{
CloudflareCustomHostnameKey: "custom.example.com",
},
expected: endpoint.ProviderSpecific{
{Name: CloudflareCustomHostnameKey, Value: "custom.example.com"},
},
setIdentifier: "",
},
{
name: "AWS annotation",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/aws-weight": "100",
},
expected: endpoint.ProviderSpecific{
{Name: "aws/weight", Value: "100"},
},
setIdentifier: "",
},
{
name: "Set identifier annotation",
annotations: map[string]string{
SetIdentifierKey: "identifier",
},
expected: endpoint.ProviderSpecific{},
setIdentifier: "identifier",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, setIdentifier := ProviderSpecificAnnotations(tt.annotations)
assert.Equal(t, tt.expected, result)
assert.Equal(t, tt.setIdentifier, setIdentifier)
})
}
}
func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedKey string
expectedValue bool
}{
{
title: "Cloudflare proxied annotation is set correctly to true",
annotations: map[string]string{CloudflareProxiedKey: "true"},
expectedKey: CloudflareProxiedKey,
expectedValue: true,
},
{
title: "Cloudflare proxied annotation is set correctly to false",
annotations: map[string]string{CloudflareProxiedKey: "false"},
expectedKey: CloudflareProxiedKey,
expectedValue: false,
},
{
title: "Cloudflare proxied annotation among another annotations is set correctly to true",
annotations: map[string]string{
"random annotation 1": "random value 1",
CloudflareProxiedKey: "false",
"random annotation 2": "random value 2",
},
expectedKey: CloudflareProxiedKey,
expectedValue: false,
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == tc.expectedKey {
assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
return
}
}
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
})
}
for _, tc := range []struct {
title string
annotations map[string]string
expectedKey string
expectedValue string
}{
{
title: "Cloudflare custom hostname annotation is set correctly",
annotations: map[string]string{CloudflareCustomHostnameKey: "a.foo.fancybar.com"},
expectedKey: CloudflareCustomHostnameKey,
expectedValue: "a.foo.fancybar.com",
},
{
title: "Cloudflare custom hostname annotation among another annotations is set correctly",
annotations: map[string]string{
"random annotation 1": "random value 1",
CloudflareCustomHostnameKey: "a.foo.fancybar.com",
"random annotation 2": "random value 2"},
expectedKey: CloudflareCustomHostnameKey,
expectedValue: "a.foo.fancybar.com",
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == tc.expectedKey {
assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
return
}
}
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue)
})
}
}
func TestGetProviderSpecificAliasAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedKey string
expectedValue bool
}{
{
title: "alias annotation is set correctly to true",
annotations: map[string]string{AliasKey: "true"},
expectedKey: AliasKey,
expectedValue: true,
},
{
title: "alias annotation among another annotations is set correctly to true",
annotations: map[string]string{
"random annotation 1": "random value 1",
AliasKey: "true",
"random annotation 2": "random value 2",
},
expectedKey: AliasKey,
expectedValue: true,
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == "alias" {
assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
return
}
}
t.Errorf("provider specific annotation alias is not set correctly to %v", tc.expectedValue)
})
}
for _, tc := range []struct {
title string
annotations map[string]string
}{
{
title: "alias annotation is set to false",
annotations: map[string]string{AliasKey: "false"},
},
{
title: "alias annotation is not set",
annotations: map[string]string{
"random annotation 1": "random value 1",
"random annotation 2": "random value 2",
},
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == "alias" {
t.Error("provider specific annotation alias is not expected to be set")
}
}
})
}
}
func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedResult map[string]string
expectedIdentifier string
}{
{
title: "aws- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/aws-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/aws-annotation-2": "value 2",
},
expectedResult: map[string]string{
"aws/annotation-1": "value 1",
"aws/annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
{
title: "scw- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/scw-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/scw-annotation-2": "value 2",
},
expectedResult: map[string]string{
"scw/annotation-1": "value 1",
"scw/annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
{
title: "ibmcloud- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/ibmcloud-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/ibmcloud-annotation-2": "value 2",
},
expectedResult: map[string]string{
"ibmcloud-annotation-1": "value 1",
"ibmcloud-annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
{
title: "webhook- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/webhook-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/webhook-annotation-2": "value 2",
},
expectedResult: map[string]string{
"webhook/annotation-1": "value 1",
"webhook/annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, identifier := ProviderSpecificAnnotations(tc.annotations)
assert.Equal(t, tc.expectedIdentifier, identifier)
for expectedAnnotationKey, expectedAnnotationValue := range tc.expectedResult {
expectedResultFound := false
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == expectedAnnotationKey {
assert.Equal(t, expectedAnnotationValue, providerSpecificAnnotation.Value)
expectedResultFound = true
break
}
}
if !expectedResultFound {
t.Errorf("provider specific annotation %s has not been set", expectedAnnotationKey)
}
}
})
}
}

View File

@ -55,8 +55,8 @@ func legacyEndpointsFromMateService(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// Get the desired hostname of the service from the annotation. // Get the desired hostname of the service from the annotation.
hostname, exists := svc.Annotations[mateAnnotationKey] hostname, ok := svc.Annotations[mateAnnotationKey]
if !exists { if !ok {
return nil return nil
} }
@ -84,8 +84,8 @@ func legacyEndpointsFromMoleculeService(svc *v1.Service) []*endpoint.Endpoint {
} }
// Get the desired hostname of the service from the annotation. // Get the desired hostname of the service from the annotation.
hostnameAnnotation, exists := svc.Annotations[moleculeAnnotationKey] hostnameAnnotation, ok := svc.Annotations[moleculeAnnotationKey]
if !exists { if !ok {
return nil return nil
} }

View File

@ -34,6 +34,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
// HTTPProxySource is an implementation of Source for ProjectContour HTTPProxy objects. // HTTPProxySource is an implementation of Source for ProjectContour HTTPProxy objects.
@ -185,9 +186,9 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP
resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name) resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name)
ttl := getTTLFromAnnotations(httpProxy.Annotations, resource) ttl := annotations.TTLFromAnnotations(httpProxy.Annotations, resource)
targets := getTargetsFromTargetAnnotation(httpProxy.Annotations) targets := annotations.TargetsFromTargetAnnotation(httpProxy.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
for _, lb := range httpProxy.Status.LoadBalancer.Ingress { for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
if lb.IP != "" { if lb.IP != "" {
@ -199,7 +200,7 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP
} }
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(httpProxy.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(httpProxy.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames { for _, hostname := range hostnames {
@ -224,14 +225,11 @@ func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTT
return httpProxies, nil return httpProxies, nil
} }
filteredList := []*projectcontour.HTTPProxy{} var filteredList []*projectcontour.HTTPProxy
for _, httpProxy := range httpProxies { for _, httpProxy := range httpProxies {
// convert the HTTPProxy's annotations to an equivalent label selector
annotations := labels.Set(httpProxy.Annotations)
// include HTTPProxy if its annotations match the selector // include HTTPProxy if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(httpProxy.Annotations)) {
filteredList = append(filteredList, httpProxy) filteredList = append(filteredList, httpProxy)
} }
} }
@ -243,9 +241,9 @@ func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTT
func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTPProxy) ([]*endpoint.Endpoint, error) { func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTPProxy) ([]*endpoint.Endpoint, error) {
resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name) resource := fmt.Sprintf("HTTPProxy/%s/%s", httpProxy.Namespace, httpProxy.Name)
ttl := getTTLFromAnnotations(httpProxy.Annotations, resource) ttl := annotations.TTLFromAnnotations(httpProxy.Annotations, resource)
targets := getTargetsFromTargetAnnotation(httpProxy.Annotations) targets := annotations.TargetsFromTargetAnnotation(httpProxy.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
for _, lb := range httpProxy.Status.LoadBalancer.Ingress { for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
@ -258,7 +256,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
} }
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(httpProxy.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(httpProxy.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
@ -270,7 +268,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
// Skip endpoints if we do not want entries from annotations // Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(httpProxy.Annotations) hostnameList := annotations.HostnamesFromAnnotations(httpProxy.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }

110
source/endpoints.go Normal file
View File

@ -0,0 +1,110 @@
/*
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 (
"fmt"
"k8s.io/apimachinery/pkg/labels"
coreinformers "k8s.io/client-go/informers/core/v1"
"sigs.k8s.io/external-dns/endpoint"
)
func EndpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
var (
endpoints []*endpoint.Endpoint
aTargets endpoint.Targets
aaaaTargets endpoint.Targets
cnameTargets endpoint.Targets
)
for _, t := range targets {
switch suitableType(t) {
case endpoint.RecordTypeA:
aTargets = append(aTargets, t)
case endpoint.RecordTypeAAAA:
aaaaTargets = append(aaaaTargets, t)
default:
cnameTargets = append(cnameTargets, t)
}
}
if len(aTargets) > 0 {
epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
if epA != nil {
epA.ProviderSpecific = providerSpecific
epA.SetIdentifier = setIdentifier
if resource != "" {
epA.Labels[endpoint.ResourceLabelKey] = resource
}
endpoints = append(endpoints, epA)
}
}
if len(aaaaTargets) > 0 {
epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
if epAAAA != nil {
epAAAA.ProviderSpecific = providerSpecific
epAAAA.SetIdentifier = setIdentifier
if resource != "" {
epAAAA.Labels[endpoint.ResourceLabelKey] = resource
}
endpoints = append(endpoints, epAAAA)
}
}
if len(cnameTargets) > 0 {
epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
if epCNAME != nil {
epCNAME.ProviderSpecific = providerSpecific
epCNAME.SetIdentifier = setIdentifier
if resource != "" {
epCNAME.Labels[endpoint.ResourceLabelKey] = resource
}
endpoints = append(endpoints, epCNAME)
}
}
return endpoints
}
func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, namespace string, selector map[string]string) (endpoint.Targets, error) {
targets := endpoint.Targets{}
services, err := svcInformer.Lister().Services(namespace).List(labels.Everything())
if err != nil {
return nil, fmt.Errorf("failed to list labels for services in namespace %q: %w", namespace, err)
}
for _, service := range services {
if !MatchesServiceSelector(selector, service.Spec.Selector) {
continue
}
if len(service.Spec.ExternalIPs) > 0 {
targets = append(targets, service.Spec.ExternalIPs...)
continue
}
for _, lb := range service.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
} else if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
}
return targets, nil
}

255
source/endpoints_test.go Normal file
View File

@ -0,0 +1,255 @@
/*
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 (
"context"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
)
func TestEndpointsForHostname(t *testing.T) {
tests := []struct {
name string
hostname string
targets endpoint.Targets
ttl endpoint.TTL
providerSpecific endpoint.ProviderSpecific
setIdentifier string
resource string
expected []*endpoint.Endpoint
}{
{
name: "A record targets",
hostname: "example.com",
targets: endpoint.Targets{"192.0.2.1", "192.0.2.2"},
ttl: endpoint.TTL(300),
providerSpecific: endpoint.ProviderSpecific{
{Name: "provider", Value: "value"},
},
setIdentifier: "identifier",
resource: "resource",
expected: []*endpoint.Endpoint{
{
DNSName: "example.com",
Targets: endpoint.Targets{"192.0.2.1", "192.0.2.2"},
RecordType: endpoint.RecordTypeA,
RecordTTL: endpoint.TTL(300),
ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
SetIdentifier: "identifier",
Labels: map[string]string{endpoint.ResourceLabelKey: "resource"},
},
},
},
{
name: "AAAA record targets",
hostname: "example.com",
targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"},
ttl: endpoint.TTL(300),
providerSpecific: endpoint.ProviderSpecific{
{Name: "provider", Value: "value"},
},
setIdentifier: "identifier",
resource: "resource",
expected: []*endpoint.Endpoint{
{
DNSName: "example.com",
Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"},
RecordType: endpoint.RecordTypeAAAA,
RecordTTL: endpoint.TTL(300),
ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
SetIdentifier: "identifier",
Labels: map[string]string{endpoint.ResourceLabelKey: "resource"},
},
},
},
{
name: "CNAME record targets",
hostname: "example.com",
targets: endpoint.Targets{"cname.example.com"},
ttl: endpoint.TTL(300),
providerSpecific: endpoint.ProviderSpecific{
{Name: "provider", Value: "value"},
},
setIdentifier: "identifier",
resource: "resource",
expected: []*endpoint.Endpoint{
{
DNSName: "example.com",
Targets: endpoint.Targets{"cname.example.com"},
RecordType: endpoint.RecordTypeCNAME,
RecordTTL: endpoint.TTL(300),
ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}},
SetIdentifier: "identifier",
Labels: map[string]string{endpoint.ResourceLabelKey: "resource"},
},
},
},
{
name: "No targets",
hostname: "example.com",
targets: endpoint.Targets{},
ttl: endpoint.TTL(300),
providerSpecific: endpoint.ProviderSpecific{},
setIdentifier: "",
resource: "",
expected: []*endpoint.Endpoint(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := EndpointsForHostname(tt.hostname, tt.targets, tt.ttl, tt.providerSpecific, tt.setIdentifier, tt.resource)
assert.Equal(t, tt.expected, result)
})
}
}
func TestEndpointTargetsFromServices(t *testing.T) {
tests := []struct {
name string
services []*corev1.Service
namespace string
selector map[string]string
expected endpoint.Targets
wantErr bool
}{
{
name: "no services",
services: []*corev1.Service{},
namespace: "default",
selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{},
wantErr: false,
},
{
name: "matching service with external IPs",
services: []*corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "svc1",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{"app": "nginx"},
ExternalIPs: []string{"192.0.2.1", "158.123.32.23"},
},
},
},
namespace: "default",
selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{"192.0.2.1", "158.123.32.23"},
wantErr: false,
},
{
name: "matching service with load balancer IP",
services: []*corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "svc2",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{"app": "nginx"},
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{IP: "192.0.2.2"},
},
},
},
},
},
namespace: "default",
selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{"192.0.2.2"},
wantErr: false,
},
{
name: "matching service with load balancer hostname",
services: []*corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "svc3",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{"app": "nginx"},
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{Hostname: "lb.example.com"},
},
},
},
},
},
namespace: "default",
selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{"lb.example.com"},
wantErr: false,
},
{
name: "no matching services",
services: []*corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "svc4",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{"app": "apache"},
},
},
},
namespace: "default",
selector: map[string]string{"app": "nginx"},
expected: endpoint.Targets{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := fake.NewSimpleClientset()
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(client, 0,
kubeinformers.WithNamespace(tt.namespace))
serviceInformer := informerFactory.Core().V1().Services()
for _, svc := range tt.services {
_, err := client.CoreV1().Services(tt.namespace).Create(context.Background(), svc, metav1.CreateOptions{})
assert.NoError(t, err)
err = serviceInformer.Informer().GetIndexer().Add(svc)
assert.NoError(t, err)
}
result, err := EndpointTargetsFromServices(serviceInformer, tt.namespace, tt.selector)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}

View File

@ -38,6 +38,7 @@ import (
f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1" f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
var f5TransportServerGVR = schema.GroupVersionResource{ var f5TransportServerGVR = schema.GroupVersionResource{
@ -150,9 +151,9 @@ func (ts *f5TransportServerSource) endpointsFromTransportServers(transportServer
resource := fmt.Sprintf("f5-transportserver/%s/%s", transportServer.Namespace, transportServer.Name) resource := fmt.Sprintf("f5-transportserver/%s/%s", transportServer.Namespace, transportServer.Name)
ttl := getTTLFromAnnotations(transportServer.Annotations, resource) ttl := annotations.TTLFromAnnotations(transportServer.Annotations, resource)
targets := getTargetsFromTargetAnnotation(transportServer.Annotations) targets := annotations.TargetsFromTargetAnnotation(transportServer.Annotations)
if len(targets) == 0 && transportServer.Spec.VirtualServerAddress != "" { if len(targets) == 0 && transportServer.Spec.VirtualServerAddress != "" {
targets = append(targets, transportServer.Spec.VirtualServerAddress) targets = append(targets, transportServer.Spec.VirtualServerAddress)
} }

View File

@ -39,6 +39,7 @@ import (
f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1" f5 "github.com/F5Networks/k8s-bigip-ctlr/v2/config/apis/cis/v1"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
var f5VirtualServerGVR = schema.GroupVersionResource{ var f5VirtualServerGVR = schema.GroupVersionResource{
@ -156,9 +157,9 @@ func (vs *f5VirtualServerSource) endpointsFromVirtualServers(virtualServers []*f
resource := fmt.Sprintf("f5-virtualserver/%s/%s", virtualServer.Namespace, virtualServer.Name) resource := fmt.Sprintf("f5-virtualserver/%s/%s", virtualServer.Namespace, virtualServer.Name)
ttl := getTTLFromAnnotations(virtualServer.Annotations, resource) ttl := annotations.TTLFromAnnotations(virtualServer.Annotations, resource)
targets := getTargetsFromTargetAnnotation(virtualServer.Annotations) targets := annotations.TargetsFromTargetAnnotation(virtualServer.Annotations)
if len(targets) == 0 && virtualServer.Spec.VirtualServerAddress != "" { if len(targets) == 0 && virtualServer.Spec.VirtualServerAddress != "" {
targets = append(targets, virtualServer.Spec.VirtualServerAddress) targets = append(targets, virtualServer.Spec.VirtualServerAddress)
} }

View File

@ -40,6 +40,7 @@ import (
informers_v1beta1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apis/v1beta1" informers_v1beta1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apis/v1beta1"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
const ( const (
@ -236,8 +237,8 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
// Create endpoints from hostnames and targets. // Create endpoints from hostnames and targets.
var routeEndpoints []*endpoint.Endpoint var routeEndpoints []*endpoint.Endpoint
resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name) resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
ttl := getTTLFromAnnotations(annots, resource) ttl := annotations.TTLFromAnnotations(annots, resource)
for host, targets := range hostTargets { for host, targets := range hostTargets {
routeEndpoints = append(routeEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) routeEndpoints = append(routeEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }
@ -373,7 +374,7 @@ func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Tar
if !ok { if !ok {
continue continue
} }
override := getTargetsFromTargetAnnotation(gw.gateway.Annotations) override := annotations.TargetsFromTargetAnnotation(gw.gateway.Annotations)
hostTargets[host] = append(hostTargets[host], override...) hostTargets[host] = append(hostTargets[host], override...)
if len(override) == 0 { if len(override) == 0 {
for _, addr := range gw.gateway.Status.Addresses { for _, addr := range gw.gateway.Status.Addresses {
@ -403,7 +404,7 @@ func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template" // TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
// but other sources don't check if fqdn-template is set. Which should it be? // but other sources don't check if fqdn-template is set. Which should it be?
if !c.src.ignoreHostnameAnnotation { if !c.src.ignoreHostnameAnnotation {
hostnames = append(hostnames, getHostnamesFromAnnotations(rt.Metadata().Annotations)...) hostnames = append(hostnames, annotations.HostnamesFromAnnotations(rt.Metadata().Annotations)...)
} }
// TODO: The combine-fqdn-annotation flag is similarly vague. // TODO: The combine-fqdn-annotation flag is similarly vague.
if c.src.fqdnTemplate != nil && (len(hostnames) == 0 || c.src.combineFQDNAnnotation) { if c.src.fqdnTemplate != nil && (len(hostnames) == 0 || c.src.combineFQDNAnnotation) {

View File

@ -29,6 +29,7 @@ import (
kubefake "k8s.io/client-go/kubernetes/fake" kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/source/annotations"
v1 "sigs.k8s.io/gateway-api/apis/v1" v1 "sigs.k8s.io/gateway-api/apis/v1"
v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake" gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake"
@ -1034,8 +1035,8 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
Name: "provider-annotations", Name: "provider-annotations",
Namespace: "default", Namespace: "default",
Annotations: map[string]string{ Annotations: map[string]string{
SetIdentifierKey: "test-set-identifier", annotations.SetIdentifierKey: "test-set-identifier",
aliasAnnotationKey: "true", aliasAnnotationKey: "true",
}, },
}, },
Spec: v1.HTTPRouteSpec{ Spec: v1.HTTPRouteSpec{

View File

@ -30,6 +30,7 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
var ( var (
@ -134,7 +135,7 @@ func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
} }
log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name) log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name)
proxyTargets := getTargetsFromTargetAnnotation(proxy.Metadata.Annotations) proxyTargets := annotations.TargetsFromTargetAnnotation(proxy.Metadata.Annotations)
if len(proxyTargets) == 0 { if len(proxyTargets) == 0 {
proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns) proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns)
if err != nil { if err != nil {
@ -161,12 +162,12 @@ func (gs *glooSource) generateEndpointsFromProxy(ctx context.Context, proxy *pro
for _, listener := range proxy.Spec.Listeners { for _, listener := range proxy.Spec.Listeners {
for _, virtualHost := range listener.HTTPListener.VirtualHosts { for _, virtualHost := range listener.HTTPListener.VirtualHosts {
annotations, err := gs.annotationsFromProxySource(ctx, virtualHost) ants, err := gs.annotationsFromProxySource(ctx, virtualHost)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ttl := getTTLFromAnnotations(annotations, resource) ttl := annotations.TTLFromAnnotations(ants, resource)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ants)
for _, domain := range virtualHost.Domains { for _, domain := range virtualHost.Domains {
endpoints = append(endpoints, endpointsForHostname(strings.TrimSuffix(domain, "."), targets, ttl, providerSpecific, setIdentifier, "")...) endpoints = append(endpoints, endpointsForHostname(strings.TrimSuffix(domain, "."), targets, ttl, providerSpecific, setIdentifier, "")...)
} }

View File

@ -34,6 +34,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
const ( const (
@ -184,14 +185,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpo
resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name) resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)
ttl := getTTLFromAnnotations(ing.Annotations, resource) ttl := annotations.TTLFromAnnotations(ing.Annotations, resource)
targets := getTargetsFromTargetAnnotation(ing.Annotations) targets := annotations.TargetsFromTargetAnnotation(ing.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ing.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames { for _, hostname := range hostnames {
@ -272,15 +273,15 @@ func (sc *ingressSource) filterByIngressClass(ingresses []*networkv1.Ingress) ([
func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool) []*endpoint.Endpoint { func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, ignoreIngressTLSSpec bool, ignoreIngressRulesSpec bool) []*endpoint.Endpoint {
resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name) resource := fmt.Sprintf("ingress/%s/%s", ing.Namespace, ing.Name)
ttl := getTTLFromAnnotations(ing.Annotations, resource) ttl := annotations.TTLFromAnnotations(ing.Annotations, resource)
targets := getTargetsFromTargetAnnotation(ing.Annotations) targets := annotations.TargetsFromTargetAnnotation(ing.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ing.Annotations)
// Gather endpoints defined on hosts sections of the ingress // Gather endpoints defined on hosts sections of the ingress
var definedHostsEndpoints []*endpoint.Endpoint var definedHostsEndpoints []*endpoint.Endpoint
@ -309,7 +310,7 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool,
// Gather endpoints defined on annotations in the ingress // Gather endpoints defined on annotations in the ingress
var annotationEndpoints []*endpoint.Endpoint var annotationEndpoints []*endpoint.Endpoint
if !ignoreHostnameAnnotation { if !ignoreHostnameAnnotation {
for _, hostname := range getHostnamesFromAnnotations(ing.Annotations) { for _, hostname := range annotations.HostnamesFromAnnotations(ing.Annotations) {
annotationEndpoints = append(annotationEndpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) annotationEndpoints = append(annotationEndpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }
} }

View File

@ -36,6 +36,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
// IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object // IstioGatewayIngressSource is the annotation used to determine if the gateway is implemented by an Ingress object
@ -200,11 +201,7 @@ func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
// filterByAnnotations filters a list of configs by a given annotation selector. // filterByAnnotations filters a list of configs by a given annotation selector.
func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) { func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -217,11 +214,8 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate
var filteredList []*networkingv1alpha3.Gateway var filteredList []*networkingv1alpha3.Gateway
for _, gw := range gateways { for _, gw := range gateways {
// convert the annotations to an equivalent label selector
annotations := labels.Set(gw.Annotations)
// include if the annotations match the selector // include if the annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(gw.Annotations)) {
filteredList = append(filteredList, gw) filteredList = append(filteredList, gw)
} }
} }
@ -229,21 +223,8 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate
return filteredList, nil return filteredList, nil
} }
func parseIngress(ingress string) (namespace, name string, err error) {
parts := strings.Split(ingress, "/")
if len(parts) == 2 {
namespace, name = parts[0], parts[1]
} else if len(parts) == 1 {
name = parts[0]
} else {
err = fmt.Errorf("invalid ingress name (name or namespace/name) found %q", ingress)
}
return
}
func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
namespace, name, err := parseIngress(ingressStr) namespace, name, err := ParseIngress(ingressStr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err)
} }
@ -266,44 +247,24 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri
return return
} }
func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) {
targets = getTargetsFromTargetAnnotation(gateway.Annotations) targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) > 0 { if len(targets) > 0 {
return return targets, nil
} }
ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource] ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
if ok && ingressStr != "" { if ok && ingressStr != "" {
targets, err = sc.targetsFromIngress(ctx, ingressStr, gateway) return sc.targetsFromIngress(ctx, ingressStr, gateway)
return
} }
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything()) targets, err := EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
if err != nil { if err != nil {
log.Error(err) return nil, err
return
} }
for _, service := range services { return targets, nil
if !gatewaySelectorMatchesServiceSelector(gateway.Spec.Selector, service.Spec.Selector) {
continue
}
if len(service.Spec.ExternalIPs) > 0 {
targets = append(targets, service.Spec.ExternalIPs...)
continue
}
for _, lb := range service.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
} else if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
}
return
} }
// endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
@ -313,10 +274,9 @@ func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []s
resource := fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name) resource := fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name)
annotations := gateway.Annotations ttl := annotations.TTLFromAnnotations(gateway.Annotations, resource)
ttl := getTTLFromAnnotations(annotations, resource)
targets := getTargetsFromTargetAnnotation(annotations) targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
targets, err = sc.targetsFromGateway(ctx, gateway) targets, err = sc.targetsFromGateway(ctx, gateway)
if err != nil { if err != nil {
@ -324,7 +284,7 @@ func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []s
} }
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(gateway.Annotations)
for _, host := range hostnames { for _, host := range hostnames {
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
@ -356,7 +316,7 @@ func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gatewa
} }
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnames = append(hostnames, getHostnamesFromAnnotations(gateway.Annotations)...) hostnames = append(hostnames, annotations.HostnamesFromAnnotations(gateway.Annotations)...)
} }
return hostnames, nil return hostnames, nil

View File

@ -37,6 +37,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
// IstioMeshGateway is the built in gateway for all sidecars // IstioMeshGateway is the built in gateway for all sidecars
@ -235,9 +236,9 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu
resource := fmt.Sprintf("virtualservice/%s/%s", virtualService.Namespace, virtualService.Name) resource := fmt.Sprintf("virtualservice/%s/%s", virtualService.Namespace, virtualService.Name)
ttl := getTTLFromAnnotations(virtualService.Annotations, resource) ttl := annotations.TTLFromAnnotations(virtualService.Annotations, resource)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualService.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(virtualService.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames { for _, hostname := range hostnames {
@ -252,11 +253,7 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu
// filterByAnnotations filters a list of configs by a given annotation selector. // filterByAnnotations filters a list of configs by a given annotation selector.
func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkingv1alpha3.VirtualService) ([]*networkingv1alpha3.VirtualService, error) { func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkingv1alpha3.VirtualService) ([]*networkingv1alpha3.VirtualService, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -268,13 +265,10 @@ func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkin
var filteredList []*networkingv1alpha3.VirtualService var filteredList []*networkingv1alpha3.VirtualService
for _, virtualservice := range virtualservices { for _, vs := range virtualservices {
// convert the annotations to an equivalent label selector
annotations := labels.Set(virtualservice.Annotations)
// include if the annotations match the selector // include if the annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(vs.Annotations)) {
filteredList = append(filteredList, virtualservice) filteredList = append(filteredList, vs)
} }
} }
@ -324,11 +318,11 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context,
resource := fmt.Sprintf("virtualservice/%s/%s", virtualservice.Namespace, virtualservice.Name) resource := fmt.Sprintf("virtualservice/%s/%s", virtualservice.Namespace, virtualservice.Name)
ttl := getTTLFromAnnotations(virtualservice.Annotations, resource) ttl := annotations.TTLFromAnnotations(virtualservice.Annotations, resource)
targetsFromAnnotation := getTargetsFromTargetAnnotation(virtualservice.Annotations) targetsFromAnnotation := annotations.TargetsFromTargetAnnotation(virtualservice.Annotations)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(virtualservice.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(virtualservice.Annotations)
for _, host := range virtualservice.Spec.Hosts { for _, host := range virtualservice.Spec.Hosts {
if host == "" || host == "*" { if host == "" || host == "*" {
@ -356,7 +350,7 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context,
// Skip endpoints if we do not want entries from annotations // Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(virtualservice.Annotations) hostnameList := annotations.HostnamesFromAnnotations(virtualservice.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
targets := targetsFromAnnotation targets := targetsFromAnnotation
if len(targets) == 0 { if len(targets) == 0 {
@ -435,7 +429,7 @@ func parseGateway(gateway string) (namespace, name string, err error) {
} }
func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
namespace, name, err := parseIngress(ingressStr) namespace, name, err := ParseIngress(ingressStr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err)
} }
@ -459,7 +453,7 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS
} }
func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
targets = getTargetsFromTargetAnnotation(gateway.Annotations) targets = annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) > 0 { if len(targets) > 0 {
return return
} }

View File

@ -37,6 +37,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
var kongGroupdVersionResource = schema.GroupVersionResource{ var kongGroupdVersionResource = schema.GroupVersionResource{
@ -126,7 +127,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, tcpIngress := range tcpIngresses { for _, tcpIngress := range tcpIngresses {
targets := getTargetsFromTargetAnnotation(tcpIngress.Annotations) targets := annotations.TargetsFromTargetAnnotation(tcpIngress.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
for _, lb := range tcpIngress.Status.LoadBalancer.Ingress { for _, lb := range tcpIngress.Status.LoadBalancer.Ingress {
if lb.IP != "" { if lb.IP != "" {
@ -162,11 +163,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
// filterByAnnotations filters a list of TCPIngresses by a given annotation selector. // filterByAnnotations filters a list of TCPIngresses by a given annotation selector.
func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) { func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -176,14 +173,11 @@ func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress)
return tcpIngresses, nil return tcpIngresses, nil
} }
filteredList := []*TCPIngress{} var filteredList []*TCPIngress
for _, tcpIngress := range tcpIngresses { for _, tcpIngress := range tcpIngresses {
// convert the TCPIngress's annotations to an equivalent label selector
annotations := labels.Set(tcpIngress.Annotations)
// include TCPIngress if its annotations match the selector // include TCPIngress if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(tcpIngress.Annotations)) {
filteredList = append(filteredList, tcpIngress) filteredList = append(filteredList, tcpIngress)
} }
} }
@ -197,12 +191,12 @@ func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress,
resource := fmt.Sprintf("tcpingress/%s/%s", tcpIngress.Namespace, tcpIngress.Name) resource := fmt.Sprintf("tcpingress/%s/%s", tcpIngress.Namespace, tcpIngress.Name)
ttl := getTTLFromAnnotations(tcpIngress.Annotations, resource) ttl := annotations.TTLFromAnnotations(tcpIngress.Annotations, resource)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(tcpIngress.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(tcpIngress.Annotations)
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(tcpIngress.Annotations) hostnameList := annotations.HostnamesFromAnnotations(tcpIngress.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }

View File

@ -23,7 +23,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1" coreinformers "k8s.io/client-go/informers/core/v1"
@ -31,6 +30,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
const warningMsg = "The default behavior of exposing internal IPv6 addresses will change in the next minor version. Use --no-expose-internal-ipv6 flag to opt-in to the new behavior." const warningMsg = "The default behavior of exposing internal IPv6 addresses will change in the next minor version. Use --no-expose-internal-ipv6 flag to opt-in to the new behavior."
@ -115,7 +115,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
log.Debugf("creating endpoint for node %s", node.Name) log.Debugf("creating endpoint for node %s", node.Name)
ttl := getTTLFromAnnotations(node.Annotations, fmt.Sprintf("node/%s", node.Name)) ttl := annotations.TTLFromAnnotations(node.Annotations, fmt.Sprintf("node/%s", node.Name))
// create new endpoint with the information we already have // create new endpoint with the information we already have
ep := &endpoint.Endpoint{ ep := &endpoint.Endpoint{
@ -138,7 +138,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
log.Debugf("not applying template for %s", node.Name) log.Debugf("not applying template for %s", node.Name)
} }
addrs := getTargetsFromTargetAnnotation(node.Annotations) addrs := annotations.TargetsFromTargetAnnotation(node.Annotations)
if len(addrs) == 0 { if len(addrs) == 0 {
addrs, err = ns.nodeAddresses(node) addrs, err = ns.nodeAddresses(node)
if err != nil { if err != nil {
@ -208,11 +208,7 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) {
// filterByAnnotations filters a list of nodes by a given annotation selector. // filterByAnnotations filters a list of nodes by a given annotation selector.
func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) { func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) {
labelSelector, err := metav1.ParseToLabelSelector(ns.annotationFilter) selector, err := annotations.ParseFilter(ns.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -222,14 +218,11 @@ func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error)
return nodes, nil return nodes, nil
} }
filteredList := []*v1.Node{} var filteredList []*v1.Node
for _, node := range nodes { for _, node := range nodes {
// convert the node's annotations to an equivalent label selector
annotations := labels.Set(node.Annotations)
// include node if its annotations match the selector // include node if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(node.Annotations)) {
filteredList = append(filteredList, node) filteredList = append(filteredList, node)
} }
} }

View File

@ -24,16 +24,16 @@ import (
"time" "time"
routev1 "github.com/openshift/api/route/v1" routev1 "github.com/openshift/api/route/v1"
versioned "github.com/openshift/client-go/route/clientset/versioned" "github.com/openshift/client-go/route/clientset/versioned"
extInformers "github.com/openshift/client-go/route/informers/externalversions" extInformers "github.com/openshift/client-go/route/informers/externalversions"
routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1" routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
// ocpRouteSource is an implementation of Source for OpenShift Route objects. // ocpRouteSource is an implementation of Source for OpenShift Route objects.
@ -176,15 +176,15 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en
resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name) resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name)
ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource) ttl := annotations.TTLFromAnnotations(ocpRoute.Annotations, resource)
targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations) targets := annotations.TargetsFromTargetAnnotation(ocpRoute.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
targetsFromRoute, _ := ors.getTargetsFromRouteStatus(ocpRoute.Status) targetsFromRoute, _ := ors.getTargetsFromRouteStatus(ocpRoute.Status)
targets = targetsFromRoute targets = targetsFromRoute
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames { for _, hostname := range hostnames {
@ -194,11 +194,7 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en
} }
func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*routev1.Route, error) { func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*routev1.Route, error) {
labelSelector, err := metav1.ParseToLabelSelector(ors.annotationFilter) selector, err := annotations.ParseFilter(ors.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -211,11 +207,8 @@ func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*r
filteredList := []*routev1.Route{} filteredList := []*routev1.Route{}
for _, ocpRoute := range ocpRoutes { for _, ocpRoute := range ocpRoutes {
// convert the Route's annotations to an equivalent label selector
annotations := labels.Set(ocpRoute.Annotations)
// include ocpRoute if its annotations match the selector // include ocpRoute if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(ocpRoute.Annotations)) {
filteredList = append(filteredList, ocpRoute) filteredList = append(filteredList, ocpRoute)
} }
} }
@ -229,16 +222,16 @@ func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignore
resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name) resource := fmt.Sprintf("route/%s/%s", ocpRoute.Namespace, ocpRoute.Name)
ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource) ttl := annotations.TTLFromAnnotations(ocpRoute.Annotations, resource)
targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations) targets := annotations.TargetsFromTargetAnnotation(ocpRoute.Annotations)
targetsFromRoute, host := ors.getTargetsFromRouteStatus(ocpRoute.Status) targetsFromRoute, host := ors.getTargetsFromRouteStatus(ocpRoute.Status)
if len(targets) == 0 { if len(targets) == 0 {
targets = targetsFromRoute targets = targetsFromRoute
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations)
if host != "" { if host != "" {
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
@ -246,7 +239,7 @@ func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignore
// Skip endpoints if we do not want entries from annotations // Skip endpoints if we do not want entries from annotations
if !ignoreHostnameAnnotation { if !ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ocpRoute.Annotations) hostnameList := annotations.HostnamesFromAnnotations(ocpRoute.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }

View File

@ -28,6 +28,8 @@ import (
coreinformers "k8s.io/client-go/informers/core/v1" coreinformers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/source/annotations"
) )
type podSource struct { type podSource struct {
@ -93,10 +95,10 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
continue continue
} }
targets := getTargetsFromTargetAnnotation(pod.Annotations) targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
if domainAnnotation, ok := pod.Annotations[internalHostnameAnnotationKey]; ok { if domainAnnotation, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation) domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList { for _, domain := range domainList {
if len(targets) == 0 { if len(targets) == 0 {
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP) addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
@ -109,7 +111,7 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
} }
if domainAnnotation, ok := pod.Annotations[hostnameAnnotationKey]; ok { if domainAnnotation, ok := pod.Annotations[hostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation) domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList { for _, domain := range domainList {
if len(targets) == 0 { if len(targets) == 0 {
node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName) node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
@ -130,14 +132,14 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
if ps.compatibility == "kops-dns-controller" { if ps.compatibility == "kops-dns-controller" {
if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok { if domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation) domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList { for _, domain := range domainList {
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP) addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
} }
} }
if domainAnnotation, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok { if domainAnnotation, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation) domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList { for _, domain := range domainList {
node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName) node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
for _, address := range node.Status.Addresses { for _, address := range node.Status.Addresses {

View File

@ -33,6 +33,8 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/source/annotations"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
) )
@ -307,7 +309,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
} }
for _, headlessDomain := range headlessDomains { for _, headlessDomain := range headlessDomains {
targets := getTargetsFromTargetAnnotation(pod.Annotations) targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
if endpointsType == EndpointsTypeNodeExternalIP { if endpointsType == EndpointsTypeNodeExternalIP {
node, err := sc.nodeInformer.Lister().Get(pod.Spec.NodeName) node, err := sc.nodeInformer.Lister().Get(pod.Spec.NodeName)
@ -386,7 +388,7 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
return nil, err return nil, err
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(svc.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames { for _, hostname := range hostnames {
@ -401,16 +403,16 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// Skip endpoints if we do not want entries from annotations // Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(svc.Annotations)
var hostnameList []string var hostnameList []string
var internalHostnameList []string var internalHostnameList []string
hostnameList = getHostnamesFromAnnotations(svc.Annotations) hostnameList = annotations.HostnamesFromAnnotations(svc.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, false)...) endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, false)...)
} }
internalHostnameList = getInternalHostnamesFromAnnotations(svc.Annotations) internalHostnameList = annotations.InternalHostnamesFromAnnotations(svc.Annotations)
for _, hostname := range internalHostnameList { for _, hostname := range internalHostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, true)...) endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, true)...)
} }
@ -434,14 +436,11 @@ func (sc *serviceSource) filterByAnnotations(services []*v1.Service) ([]*v1.Serv
return services, nil return services, nil
} }
filteredList := []*v1.Service{} var filteredList []*v1.Service
for _, service := range services { for _, service := range services {
// convert the service's annotations to an equivalent label selector
annotations := labels.Set(service.Annotations)
// include service if its annotations match the selector // include service if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(service.Annotations)) {
filteredList = append(filteredList, service) filteredList = append(filteredList, service)
} }
} }
@ -473,9 +472,9 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
resource := fmt.Sprintf("service/%s/%s", svc.Namespace, svc.Name) resource := fmt.Sprintf("service/%s/%s", svc.Namespace, svc.Name)
ttl := getTTLFromAnnotations(svc.Annotations, resource) ttl := annotations.TTLFromAnnotations(svc.Annotations, resource)
targets := getTargetsFromTargetAnnotation(svc.Annotations) targets := annotations.TargetsFromTargetAnnotation(svc.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
switch svc.Spec.Type { switch svc.Spec.Type {

View File

@ -33,6 +33,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/source/annotations"
) )
type ServiceSuite struct { type ServiceSuite struct {
@ -1264,8 +1265,8 @@ func testMultipleServicesEndpoints(t *testing.T) {
map[string]string{}, map[string]string{},
"", "",
map[string]map[string]string{ map[string]map[string]string{
"1.2.3.5": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "a"}, "1.2.3.5": {hostnameAnnotationKey: "foo.example.org", annotations.SetIdentifierKey: "a"},
"10.1.1.3": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "b"}, "10.1.1.3": {hostnameAnnotationKey: "foo.example.org", annotations.SetIdentifierKey: "b"},
}, },
[]string{}, []string{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{

View File

@ -36,6 +36,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
const ( const (
@ -303,15 +304,15 @@ func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.E
resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name) resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
// error handled in endpointsFromRouteGroup(), otherwise duplicate log // error handled in endpointsFromRouteGroup(), otherwise duplicate log
ttl := getTTLFromAnnotations(rg.Metadata.Annotations, resource) ttl := annotations.TTLFromAnnotations(rg.Metadata.Annotations, resource)
targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations) targets := annotations.TargetsFromTargetAnnotation(rg.Metadata.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
targets = targetsFromRouteGroupStatus(rg.Status) targets = targetsFromRouteGroupStatus(rg.Status)
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(rg.Metadata.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods // splits the FQDN template and removes the trailing periods
@ -329,9 +330,9 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name) resource := fmt.Sprintf("routegroup/%s/%s", rg.Metadata.Namespace, rg.Metadata.Name)
ttl := getTTLFromAnnotations(rg.Metadata.Annotations, resource) ttl := annotations.TTLFromAnnotations(rg.Metadata.Annotations, resource)
targets := getTargetsFromTargetAnnotation(rg.Metadata.Annotations) targets := annotations.TargetsFromTargetAnnotation(rg.Metadata.Annotations)
if len(targets) == 0 { if len(targets) == 0 {
for _, lb := range rg.Status.LoadBalancer.RouteGroup { for _, lb := range rg.Status.LoadBalancer.RouteGroup {
if lb.IP != "" { if lb.IP != "" {
@ -343,7 +344,7 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
} }
} }
providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(rg.Metadata.Annotations)
for _, src := range rg.Spec.Hosts { for _, src := range rg.Spec.Hosts {
if src == "" { if src == "" {
@ -354,7 +355,7 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
// Skip endpoints if we do not want entries from annotations // Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(rg.Metadata.Annotations) hostnameList := annotations.HostnamesFromAnnotations(rg.Metadata.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }

View File

@ -21,7 +21,6 @@ import (
"testing" "testing"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
) )
@ -836,53 +835,3 @@ func TestResourceLabelIsSet(t *testing.T) {
} }
} }
} }
func TestParseTemplate(t *testing.T) {
for _, tt := range []struct {
name string
annotationFilter string
fqdnTemplate string
combineFQDNAndAnnotation bool
expectError bool
}{
{
name: "invalid template",
expectError: true,
fqdnTemplate: "{{.Name",
},
{
name: "valid empty template",
expectError: false,
},
{
name: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
},
{
name: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
},
{
name: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
combineFQDNAndAnnotation: true,
},
{
name: "non-empty annotation filter label",
expectError: false,
annotationFilter: "kubernetes.io/ingress.class=nginx",
},
} {
t.Run(tt.name, func(t *testing.T) {
_, err := parseTemplate(tt.fqdnTemplate)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -20,68 +20,37 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"math"
"net/netip"
"reflect" "reflect"
"strconv"
"strings" "strings"
"text/template" "text/template"
"time" "time"
"unicode" "unicode"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
const ( const (
// The annotation used for figuring out which controller is responsible controllerAnnotationKey = annotations.ControllerKey
controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller" hostnameAnnotationKey = annotations.HostnameKey
// The annotation used for defining the desired hostname accessAnnotationKey = annotations.AccessKey
hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname" endpointsTypeAnnotationKey = annotations.EndpointsTypeKey
// The annotation used for specifying whether the public or private interface address is used targetAnnotationKey = annotations.TargetKey
accessAnnotationKey = "external-dns.alpha.kubernetes.io/access" ttlAnnotationKey = annotations.TtlKey
// The annotation used for specifying the type of endpoints to use for headless services aliasAnnotationKey = annotations.AliasKey
endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type" ingressHostnameSourceKey = annotations.IngressHostnameSourceKey
// The annotation used for defining the desired ingress/service target controllerAnnotationValue = annotations.ControllerValue
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" internalHostnameAnnotationKey = annotations.InternalHostnameKey
// The annotation used for defining the desired DNS record TTL
ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl"
// The annotation used for switching to the alias record types e. g. AWS Alias records instead of a normal CNAME
aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias"
// The annotation used to determine the source of hostnames for ingresses. This is an optional field - all
// available hostname sources are used if not specified.
ingressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source"
// The value of the controller annotation so that we feel responsible
controllerAnnotationValue = "dns-controller"
// The annotation used for defining the desired hostname
internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname"
)
const (
EndpointsTypeNodeExternalIP = "NodeExternalIP" EndpointsTypeNodeExternalIP = "NodeExternalIP"
EndpointsTypeHostIP = "HostIP" EndpointsTypeHostIP = "HostIP"
) )
// Provider-specific annotations
const (
// The annotation used for determining if traffic will go through Cloudflare
CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied"
CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname"
CloudflareRegionKey = "external-dns.alpha.kubernetes.io/cloudflare-region-key"
SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier"
)
const (
ttlMinimum = 1
ttlMaximum = math.MaxInt32
)
// Source defines the interface Endpoint sources should implement. // Source defines the interface Endpoint sources should implement.
type Source interface { type Source interface {
Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)
@ -89,43 +58,6 @@ type Source interface {
AddEventHandler(context.Context, func()) AddEventHandler(context.Context, func())
} }
func getTTLFromAnnotations(annotations map[string]string, resource string) endpoint.TTL {
ttlNotConfigured := endpoint.TTL(0)
ttlAnnotation, exists := annotations[ttlAnnotationKey]
if !exists {
return ttlNotConfigured
}
ttlValue, err := parseTTL(ttlAnnotation)
if err != nil {
log.Warnf("%s: \"%v\" is not a valid TTL value: %v", resource, ttlAnnotation, err)
return ttlNotConfigured
}
if ttlValue < ttlMinimum || ttlValue > ttlMaximum {
log.Warnf("TTL value %q must be between [%d, %d]", ttlValue, ttlMinimum, ttlMaximum)
return ttlNotConfigured
}
return endpoint.TTL(ttlValue)
}
// parseTTL parses TTL from string, returning duration in seconds.
// parseTTL supports both integers like "600" and durations based
// on Go Duration like "10m", hence "600" and "10m" represent the same value.
//
// Note: for durations like "1.5s" the fraction is omitted (resulting in 1 second
// for the example).
func parseTTL(s string) (ttlSeconds int64, err error) {
ttlDuration, errDuration := time.ParseDuration(s)
if errDuration != nil {
ttlInt, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, errDuration
}
return ttlInt, nil
}
return int64(ttlDuration.Seconds()), nil
}
type kubeObject interface { type kubeObject interface {
runtime.Object runtime.Object
metav1.Object metav1.Object
@ -155,186 +87,17 @@ func parseTemplate(fqdnTemplate string) (tmpl *template.Template, err error) {
return template.New("endpoint").Funcs(funcs).Parse(fqdnTemplate) return template.New("endpoint").Funcs(funcs).Parse(fqdnTemplate)
} }
func getHostnamesFromAnnotations(annotations map[string]string) []string { func getAccessFromAnnotations(input map[string]string) string {
hostnameAnnotation, exists := annotations[hostnameAnnotationKey] return input[accessAnnotationKey]
if !exists {
return nil
}
return splitHostnameAnnotation(hostnameAnnotation)
} }
func getAccessFromAnnotations(annotations map[string]string) string { func getEndpointsTypeFromAnnotations(input map[string]string) string {
return annotations[accessAnnotationKey] return input[endpointsTypeAnnotationKey]
}
func getEndpointsTypeFromAnnotations(annotations map[string]string) string {
return annotations[endpointsTypeAnnotationKey]
}
func getInternalHostnamesFromAnnotations(annotations map[string]string) []string {
internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey]
if !exists {
return nil
}
return splitHostnameAnnotation(internalHostnameAnnotation)
}
func splitHostnameAnnotation(annotation string) []string {
return strings.Split(strings.ReplaceAll(annotation, " ", ""), ",")
}
func getAliasFromAnnotations(annotations map[string]string) bool {
aliasAnnotation, exists := annotations[aliasAnnotationKey]
return exists && aliasAnnotation == "true"
}
func getProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) {
providerSpecificAnnotations := endpoint.ProviderSpecific{}
if v, exists := annotations[CloudflareProxiedKey]; exists {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: CloudflareProxiedKey,
Value: v,
})
}
if v, exists := annotations[CloudflareCustomHostnameKey]; exists {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: CloudflareCustomHostnameKey,
Value: v,
})
}
if v, exists := annotations[CloudflareRegionKey]; exists {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: CloudflareRegionKey,
Value: v,
})
}
if getAliasFromAnnotations(annotations) {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: "alias",
Value: "true",
})
}
setIdentifier := ""
for k, v := range annotations {
if k == SetIdentifierKey {
setIdentifier = v
} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/aws-") {
attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/aws-")
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("aws/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/scw-") {
attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/scw-")
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("scw/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-") {
attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/ibmcloud-")
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("ibmcloud-%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, "external-dns.alpha.kubernetes.io/webhook-") {
// Support for wildcard annotations for webhook providers
attr := strings.TrimPrefix(k, "external-dns.alpha.kubernetes.io/webhook-")
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("webhook/%s", attr),
Value: v,
})
}
}
return providerSpecificAnnotations, setIdentifier
}
// getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
// Returns empty endpoints array if none are found.
func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets {
var targets endpoint.Targets
// Get the desired hostname of the ingress from the annotation.
targetAnnotation, exists := annotations[targetAnnotationKey]
if exists && targetAnnotation != "" {
// splits the hostname annotation and removes the trailing periods
targetsList := strings.Split(strings.ReplaceAll(targetAnnotation, " ", ""), ",")
for _, targetHostname := range targetsList {
targetHostname = strings.TrimSuffix(targetHostname, ".")
targets = append(targets, targetHostname)
}
}
return targets
}
// suitableType returns the DNS resource record type suitable for the target.
// In this case type A/AAAA for IPs and type CNAME for everything else.
func suitableType(target string) string {
netIP, err := netip.ParseAddr(target)
if err == nil && netIP.Is4() {
return endpoint.RecordTypeA
} else if err == nil && netIP.Is6() {
return endpoint.RecordTypeAAAA
}
return endpoint.RecordTypeCNAME
} }
// endpointsForHostname returns the endpoint objects for each host-target combination. // endpointsForHostname returns the endpoint objects for each host-target combination.
func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint { func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint return EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)
var aTargets endpoint.Targets
var aaaaTargets endpoint.Targets
var cnameTargets endpoint.Targets
for _, t := range targets {
switch suitableType(t) {
case endpoint.RecordTypeA:
aTargets = append(aTargets, t)
case endpoint.RecordTypeAAAA:
aaaaTargets = append(aaaaTargets, t)
default:
cnameTargets = append(cnameTargets, t)
}
}
if len(aTargets) > 0 {
epA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeA, ttl, aTargets...)
if epA != nil {
epA.ProviderSpecific = providerSpecific
epA.SetIdentifier = setIdentifier
if resource != "" {
epA.Labels[endpoint.ResourceLabelKey] = resource
}
endpoints = append(endpoints, epA)
}
}
if len(aaaaTargets) > 0 {
epAAAA := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeAAAA, ttl, aaaaTargets...)
if epAAAA != nil {
epAAAA.ProviderSpecific = providerSpecific
epAAAA.SetIdentifier = setIdentifier
if resource != "" {
epAAAA.Labels[endpoint.ResourceLabelKey] = resource
}
endpoints = append(endpoints, epAAAA)
}
}
if len(cnameTargets) > 0 {
epCNAME := endpoint.NewEndpointWithTTL(hostname, endpoint.RecordTypeCNAME, ttl, cnameTargets...)
if epCNAME != nil {
epCNAME.ProviderSpecific = providerSpecific
epCNAME.SetIdentifier = setIdentifier
if resource != "" {
epCNAME.Labels[endpoint.ResourceLabelKey] = resource
}
endpoints = append(endpoints, epCNAME)
}
}
return endpoints
} }
func getLabelSelector(annotationFilter string) (labels.Selector, error) { func getLabelSelector(annotationFilter string) (labels.Selector, error) {
@ -346,8 +109,7 @@ func getLabelSelector(annotationFilter string) (labels.Selector, error) {
} }
func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool { func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]string) bool {
annotations := labels.Set(srcAnnotations) return selector.Matches(labels.Set(srcAnnotations))
return selector.Matches(annotations)
} }
type eventHandlerFunc func() type eventHandlerFunc func()

View File

@ -17,340 +17,144 @@ limitations under the License.
package source package source
import ( import (
"fmt" "context"
"strconv" "reflect"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
) )
func TestGetTTLFromAnnotations(t *testing.T) { func TestParseTemplate(t *testing.T) {
for _, tc := range []struct { for _, tt := range []struct {
title string name string
annotations map[string]string annotationFilter string
expectedTTL endpoint.TTL fqdnTemplate string
combineFQDNAndAnnotation bool
expectError bool
}{ }{
{ {
title: "TTL annotation not present", name: "invalid template",
annotations: map[string]string{"foo": "bar"}, expectError: true,
expectedTTL: endpoint.TTL(0), fqdnTemplate: "{{.Name",
}, },
{ {
title: "TTL annotation value is not a number", name: "valid empty template",
annotations: map[string]string{ttlAnnotationKey: "foo"}, expectError: false,
expectedTTL: endpoint.TTL(0),
}, },
{ {
title: "TTL annotation value is empty", name: "valid template",
annotations: map[string]string{ttlAnnotationKey: ""}, expectError: false,
expectedTTL: endpoint.TTL(0), fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
}, },
{ {
title: "TTL annotation value is negative number", name: "valid template",
annotations: map[string]string{ttlAnnotationKey: "-1"}, expectError: false,
expectedTTL: endpoint.TTL(0), fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
}, },
{ {
title: "TTL annotation value is too high", name: "valid template",
annotations: map[string]string{ttlAnnotationKey: fmt.Sprintf("%d", 1<<32)}, expectError: false,
expectedTTL: endpoint.TTL(0), fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
combineFQDNAndAnnotation: true,
}, },
{ {
title: "TTL annotation value is set correctly using integer", name: "non-empty annotation filter label",
annotations: map[string]string{ttlAnnotationKey: "60"}, expectError: false,
expectedTTL: endpoint.TTL(60), annotationFilter: "kubernetes.io/ingress.class=nginx",
},
{
title: "TTL annotation value is set correctly using duration (whole)",
annotations: map[string]string{ttlAnnotationKey: "10m"},
expectedTTL: endpoint.TTL(600),
},
{
title: "TTL annotation value is set correctly using duration (fractional)",
annotations: map[string]string{ttlAnnotationKey: "20.5s"},
expectedTTL: endpoint.TTL(20),
}, },
} { } {
t.Run(tc.title, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ttl := getTTLFromAnnotations(tc.annotations, "resource/test") _, err := parseTemplate(tt.fqdnTemplate)
assert.Equal(t, tc.expectedTTL, ttl) if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
}) })
} }
} }
func TestSuitableType(t *testing.T) { func TestFqdnTemplate(t *testing.T) {
for _, tc := range []struct { tests := []struct {
target, recordType, expected string name string
}{ fqdnTemplate string
{"8.8.8.8", "", "A"}, expectedError bool
{"2001:db8::1", "", "AAAA"},
{"::ffff:c0a8:101", "", "AAAA"},
{"foo.example.org", "", "CNAME"},
{"bar.eu-central-1.elb.amazonaws.com", "", "CNAME"},
} {
recordType := suitableType(tc.target)
if recordType != tc.expected {
t.Errorf("expected %s, got %s", tc.expected, recordType)
}
}
}
func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedKey string
expectedValue bool
}{ }{
{ {
title: "Cloudflare proxied annotation is set correctly to true", name: "empty template",
annotations: map[string]string{CloudflareProxiedKey: "true"}, fqdnTemplate: "",
expectedKey: CloudflareProxiedKey, expectedError: false,
expectedValue: true,
}, },
{ {
title: "Cloudflare proxied annotation is set correctly to false", name: "valid template",
annotations: map[string]string{CloudflareProxiedKey: "false"}, fqdnTemplate: "{{ .Name }}.example.com",
expectedKey: CloudflareProxiedKey, expectedError: false,
expectedValue: false,
}, },
{
title: "Cloudflare proxied annotation among another annotations is set correctly to true",
annotations: map[string]string{
"random annotation 1": "random value 1",
CloudflareProxiedKey: "false",
"random annotation 2": "random value 2",
},
expectedKey: CloudflareProxiedKey,
expectedValue: false,
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == tc.expectedKey {
assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
return
}
}
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
})
} }
for _, tc := range []struct { for _, tt := range tests {
title string t.Run(tt.name, func(t *testing.T) {
annotations map[string]string tmpl, err := parseTemplate(tt.fqdnTemplate)
expectedKey string if tt.expectedError {
expectedValue string assert.Error(t, err)
}{ assert.Nil(t, tmpl)
{ } else {
title: "Cloudflare custom hostname annotation is set correctly", assert.NoError(t, err)
annotations: map[string]string{CloudflareCustomHostnameKey: "a.foo.fancybar.com"}, if tt.fqdnTemplate == "" {
expectedKey: CloudflareCustomHostnameKey, assert.Nil(t, tmpl)
expectedValue: "a.foo.fancybar.com", } else {
}, assert.NotNil(t, tmpl)
{
title: "Cloudflare custom hostname annotation among another annotations is set correctly",
annotations: map[string]string{
"random annotation 1": "random value 1",
CloudflareCustomHostnameKey: "a.foo.fancybar.com",
"random annotation 2": "random value 2"},
expectedKey: CloudflareCustomHostnameKey,
expectedValue: "a.foo.fancybar.com",
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == tc.expectedKey {
assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
return
}
}
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue)
})
}
for _, tc := range []struct {
title string
annotations map[string]string
expectedKey string
expectedValue string
}{
{
title: "Cloudflare region key annotation is set correctly",
annotations: map[string]string{CloudflareRegionKey: "us"},
expectedKey: CloudflareRegionKey,
expectedValue: "us",
},
{
title: "Cloudflare region key annotation among another annotations is set correctly",
annotations: map[string]string{
"random annotation 1": "random value 1",
CloudflareRegionKey: "us",
"random annotation 2": "random value 2",
},
expectedKey: CloudflareRegionKey,
expectedValue: "us",
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == tc.expectedKey {
assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value)
return
}
}
t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue)
})
}
}
func TestGetProviderSpecificAliasAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedKey string
expectedValue bool
}{
{
title: "alias annotation is set correctly to true",
annotations: map[string]string{aliasAnnotationKey: "true"},
expectedKey: aliasAnnotationKey,
expectedValue: true,
},
{
title: "alias annotation among another annotations is set correctly to true",
annotations: map[string]string{
"random annotation 1": "random value 1",
aliasAnnotationKey: "true",
"random annotation 2": "random value 2",
},
expectedKey: aliasAnnotationKey,
expectedValue: true,
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == "alias" {
assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value)
return
}
}
t.Errorf("provider specific annotation alias is not set correctly to %v", tc.expectedValue)
})
}
for _, tc := range []struct {
title string
annotations map[string]string
}{
{
title: "alias annotation is set to false",
annotations: map[string]string{aliasAnnotationKey: "false"},
},
{
title: "alias annotation is not set",
annotations: map[string]string{
"random annotation 1": "random value 1",
"random annotation 2": "random value 2",
},
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, _ := getProviderSpecificAnnotations(tc.annotations)
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == "alias" {
t.Error("provider specific annotation alias is not expected to be set")
}
}
})
}
}
func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedResult map[string]string
expectedIdentifier string
}{
{
title: "aws- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/aws-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/aws-annotation-2": "value 2",
},
expectedResult: map[string]string{
"aws/annotation-1": "value 1",
"aws/annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
{
title: "scw- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/scw-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/scw-annotation-2": "value 2",
},
expectedResult: map[string]string{
"scw/annotation-1": "value 1",
"scw/annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
{
title: "ibmcloud- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/ibmcloud-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/ibmcloud-annotation-2": "value 2",
},
expectedResult: map[string]string{
"ibmcloud-annotation-1": "value 1",
"ibmcloud-annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
{
title: "webhook- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/webhook-annotation-1": "value 1",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/webhook-annotation-2": "value 2",
},
expectedResult: map[string]string{
"webhook/annotation-1": "value 1",
"webhook/annotation-2": "value 2",
},
expectedIdentifier: "id1",
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, identifier := getProviderSpecificAnnotations(tc.annotations)
assert.Equal(t, tc.expectedIdentifier, identifier)
for expectedAnnotationKey, expectedAnnotationValue := range tc.expectedResult {
expectedResultFound := false
for _, providerSpecificAnnotation := range providerSpecificAnnotations {
if providerSpecificAnnotation.Name == expectedAnnotationKey {
assert.Equal(t, expectedAnnotationValue, providerSpecificAnnotation.Value)
expectedResultFound = true
break
}
}
if !expectedResultFound {
t.Errorf("provider specific annotation %s has not been set", expectedAnnotationKey)
} }
} }
}) })
} }
} }
type mockInformerFactory struct {
syncResults map[reflect.Type]bool
}
func (m *mockInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
return m.syncResults
}
func TestWaitForCacheSync(t *testing.T) {
tests := []struct {
name string
syncResults map[reflect.Type]bool
expectError bool
}{
{
name: "all caches synced",
syncResults: map[reflect.Type]bool{reflect.TypeOf(""): true},
expectError: false,
},
{
name: "some caches not synced",
syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false},
expectError: true,
},
{
name: "context timeout",
syncResults: map[reflect.Type]bool{reflect.TypeOf(""): false},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
factory := &mockInformerFactory{syncResults: tt.syncResults}
err := waitForCacheSync(ctx, factory)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -38,6 +38,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
) )
var ( var (
@ -255,7 +256,7 @@ func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) {
for _, ingressRoute := range ingressRoutes { for _, ingressRoute := range ingressRoutes {
var targets endpoint.Targets var targets endpoint.Targets
targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRoute.Annotations)...)
fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name)
@ -307,7 +308,7 @@ func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error
for _, ingressRouteTCP := range ingressRouteTCPs { for _, ingressRouteTCP := range ingressRouteTCPs {
var targets endpoint.Targets var targets endpoint.Targets
targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...) targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...)
fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name) fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name)
@ -359,7 +360,7 @@ func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error
for _, ingressRouteUDP := range ingressRouteUDPs { for _, ingressRouteUDP := range ingressRouteUDPs {
var targets endpoint.Targets var targets endpoint.Targets
targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...) targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...)
fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name) fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name)
@ -411,7 +412,7 @@ func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error
for _, ingressRoute := range ingressRoutes { for _, ingressRoute := range ingressRoutes {
var targets endpoint.Targets var targets endpoint.Targets
targets = append(targets, getTargetsFromTargetAnnotation(ingressRoute.Annotations)...) targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRoute.Annotations)...)
fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name) fullname := fmt.Sprintf("%s/%s", ingressRoute.Namespace, ingressRoute.Name)
@ -463,7 +464,7 @@ func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, er
for _, ingressRouteTCP := range ingressRouteTCPs { for _, ingressRouteTCP := range ingressRouteTCPs {
var targets endpoint.Targets var targets endpoint.Targets
targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...) targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRouteTCP.Annotations)...)
fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name) fullname := fmt.Sprintf("%s/%s", ingressRouteTCP.Namespace, ingressRouteTCP.Name)
@ -515,7 +516,7 @@ func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, er
for _, ingressRouteUDP := range ingressRouteUDPs { for _, ingressRouteUDP := range ingressRouteUDPs {
var targets endpoint.Targets var targets endpoint.Targets
targets = append(targets, getTargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...) targets = append(targets, annotations.TargetsFromTargetAnnotation(ingressRouteUDP.Annotations)...)
fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name) fullname := fmt.Sprintf("%s/%s", ingressRouteUDP.Namespace, ingressRouteUDP.Name)
@ -537,11 +538,7 @@ func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, er
// filterIngressRouteByAnnotation filters a list of IngressRoute by a given annotation selector. // filterIngressRouteByAnnotation filters a list of IngressRoute by a given annotation selector.
func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*IngressRoute) ([]*IngressRoute, error) { func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*IngressRoute) ([]*IngressRoute, error) {
labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) selector, err := annotations.ParseFilter(ts.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -554,11 +551,8 @@ func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*Ingress
filteredList := []*IngressRoute{} filteredList := []*IngressRoute{}
for _, ingressRoute := range ingressRoutes { for _, ingressRoute := range ingressRoutes {
// convert the IngressRoute's annotations to an equivalent label selector
annotations := labels.Set(ingressRoute.Annotations)
// include IngressRoute if its annotations match the selector // include IngressRoute if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(ingressRoute.Annotations)) {
filteredList = append(filteredList, ingressRoute) filteredList = append(filteredList, ingressRoute)
} }
} }
@ -568,11 +562,7 @@ func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*Ingress
// filterIngressRouteTcpByAnnotations filters a list of IngressRouteTCP by a given annotation selector. // filterIngressRouteTcpByAnnotations filters a list of IngressRouteTCP by a given annotation selector.
func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*IngressRouteTCP) ([]*IngressRouteTCP, error) { func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*IngressRouteTCP) ([]*IngressRouteTCP, error) {
labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) selector, err := annotations.ParseFilter(ts.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -585,11 +575,8 @@ func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*Ing
filteredList := []*IngressRouteTCP{} filteredList := []*IngressRouteTCP{}
for _, ingressRoute := range ingressRoutes { for _, ingressRoute := range ingressRoutes {
// convert the IngressRoute's annotations to an equivalent label selector
annotations := labels.Set(ingressRoute.Annotations)
// include IngressRoute if its annotations match the selector // include IngressRoute if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(ingressRoute.Annotations)) {
filteredList = append(filteredList, ingressRoute) filteredList = append(filteredList, ingressRoute)
} }
} }
@ -599,11 +586,7 @@ func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*Ing
// filterIngressRouteUdpByAnnotations filters a list of IngressRoute by a given annotation selector. // filterIngressRouteUdpByAnnotations filters a list of IngressRoute by a given annotation selector.
func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*IngressRouteUDP) ([]*IngressRouteUDP, error) { func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*IngressRouteUDP) ([]*IngressRouteUDP, error) {
labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) selector, err := annotations.ParseFilter(ts.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -616,11 +599,8 @@ func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*Ing
filteredList := []*IngressRouteUDP{} filteredList := []*IngressRouteUDP{}
for _, ingressRoute := range ingressRoutes { for _, ingressRoute := range ingressRoutes {
// convert the IngressRoute's annotations to an equivalent label selector
annotations := labels.Set(ingressRoute.Annotations)
// include IngressRoute if its annotations match the selector // include IngressRoute if its annotations match the selector
if selector.Matches(annotations) { if selector.Matches(labels.Set(ingressRoute.Annotations)) {
filteredList = append(filteredList, ingressRoute) filteredList = append(filteredList, ingressRoute)
} }
} }
@ -634,12 +614,12 @@ func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, t
resource := fmt.Sprintf("ingressroute/%s/%s", ingressRoute.Namespace, ingressRoute.Name) resource := fmt.Sprintf("ingressroute/%s/%s", ingressRoute.Namespace, ingressRoute.Name)
ttl := getTTLFromAnnotations(ingressRoute.Annotations, resource) ttl := annotations.TTLFromAnnotations(ingressRoute.Annotations, resource)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ingressRoute.Annotations)
if !ts.ignoreHostnameAnnotation { if !ts.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }
@ -670,12 +650,12 @@ func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRoute
resource := fmt.Sprintf("ingressroutetcp/%s/%s", ingressRoute.Namespace, ingressRoute.Name) resource := fmt.Sprintf("ingressroutetcp/%s/%s", ingressRoute.Namespace, ingressRoute.Name)
ttl := getTTLFromAnnotations(ingressRoute.Annotations, resource) ttl := annotations.TTLFromAnnotations(ingressRoute.Annotations, resource)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ingressRoute.Annotations)
if !ts.ignoreHostnameAnnotation { if !ts.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }
@ -707,12 +687,12 @@ func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *IngressRoute
resource := fmt.Sprintf("ingressrouteudp/%s/%s", ingressRoute.Namespace, ingressRoute.Name) resource := fmt.Sprintf("ingressrouteudp/%s/%s", ingressRoute.Namespace, ingressRoute.Name)
ttl := getTTLFromAnnotations(ingressRoute.Annotations, resource) ttl := annotations.TTLFromAnnotations(ingressRoute.Annotations, resource)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ingressRoute.Annotations) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ingressRoute.Annotations)
if !ts.ignoreHostnameAnnotation { if !ts.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
} }

67
source/utils.go Normal file
View File

@ -0,0 +1,67 @@
/*
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 (
"fmt"
"net/netip"
"strings"
"sigs.k8s.io/external-dns/endpoint"
)
// suitableType returns the DNS resource record type suitable for the target.
// In this case type A/AAAA for IPs and type CNAME for everything else.
func suitableType(target string) string {
netIP, err := netip.ParseAddr(target)
if err != nil {
return endpoint.RecordTypeCNAME
}
switch {
case netIP.Is4():
return endpoint.RecordTypeA
case netIP.Is6():
return endpoint.RecordTypeAAAA
default:
return endpoint.RecordTypeCNAME
}
}
// ParseIngress parses an ingress string in the format "namespace/name" or "name".
// It returns the namespace and name extracted from the string, or an error if the format is invalid.
// If the namespace is not provided, it defaults to an empty string.
func ParseIngress(ingress string) (namespace, name string, err error) {
parts := strings.Split(ingress, "/")
if len(parts) == 2 {
namespace, name = parts[0], parts[1]
} else if len(parts) == 1 {
name = parts[0]
} else {
err = fmt.Errorf("invalid ingress name (name or namespace/name) found %q", ingress)
}
return
}
// MatchesServiceSelector checks if all key-value pairs in the selector map
// are present and match the corresponding key-value pairs in the svcSelector map.
// It returns true if all pairs match, otherwise it returns false.
func MatchesServiceSelector(selector, svcSelector map[string]string) bool {
for k, v := range selector {
if lbl, ok := svcSelector[k]; !ok || lbl != v {
return false
}
}
return true
}

151
source/utils_test.go Normal file
View File

@ -0,0 +1,151 @@
/*
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"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
)
func TestSuitableType(t *testing.T) {
tests := []struct {
name string
target string
expected string
}{
{
name: "valid IPv4 address",
target: "192.168.1.1",
expected: endpoint.RecordTypeA,
},
{
name: "valid IPv6 address",
target: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
expected: endpoint.RecordTypeAAAA,
},
{
name: "invalid IP address, should return CNAME",
target: "example.com",
expected: endpoint.RecordTypeCNAME,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := suitableType(tt.target)
assert.Equal(t, tt.expected, result)
})
}
}
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 TestSelectorMatchesService(t *testing.T) {
tests := []struct {
name string
selector map[string]string
svcSelector map[string]string
expected bool
}{
{
name: "all key-value pairs match",
selector: map[string]string{"app": "nginx", "env": "prod"},
svcSelector: map[string]string{"app": "nginx", "env": "prod"},
expected: true,
},
{
name: "one key-value pair does not match",
selector: map[string]string{"app": "nginx", "env": "prod"},
svcSelector: map[string]string{"app": "nginx", "env": "dev"},
expected: false,
},
{
name: "key not present in svcSelector",
selector: map[string]string{"app": "nginx", "env": "prod"},
svcSelector: map[string]string{"app": "nginx"},
expected: false,
},
{
name: "empty selector",
selector: map[string]string{},
svcSelector: map[string]string{"app": "nginx", "env": "prod"},
expected: true,
},
{
name: "empty svcSelector",
selector: map[string]string{"app": "nginx", "env": "prod"},
svcSelector: map[string]string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := MatchesServiceSelector(tt.selector, tt.svcSelector)
assert.Equal(t, tt.expected, result)
})
}
}