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.
func (z zoneTags) append(id string, tags []route53types.Tag) {
zoneId := fmt.Sprintf("/hostedzone/%s", id)
if _, exists := z[zoneId]; !exists {
if _, ok := z[zoneId]; !ok {
z[zoneId] = make(map[string]string)
}
for _, tag := range tags {

View File

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

View File

@ -312,7 +312,7 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes)
}
mesh := sets.New[endpoint.EndpointKey]()
for _, newEndpoint := range changes.Create {
if _, exists := curZone[newEndpoint.Key()]; exists {
if _, ok := curZone[newEndpoint.Key()]; ok {
return ErrRecordAlreadyExists
}
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 {
if _, exists := curZone[updateEndpoint.Key()]; !exists {
if _, ok := curZone[updateEndpoint.Key()]; !ok {
return ErrRecordNotFound
}
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 {
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
}
}
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
}
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
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)
responses = append(responses, dynamodbtypes.BatchStatementResponse{
Error: &dynamodbtypes.BatchStatementError{

View File

@ -38,6 +38,7 @@ import (
"k8s.io/client-go/tools/cache"
"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
@ -119,7 +120,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
}
// Get a list of Ambassador Host resources
ambassadorHosts := []*ambassador.Host{}
var ambassadorHosts []*ambassador.Host
for _, hostObj := range hosts {
unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
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")
}
endpoints := []*endpoint.Endpoint{}
var endpoints []*endpoint.Endpoint
for _, host := range ambassadorHosts {
fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
@ -152,7 +153,7 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
continue
}
targets := getTargetsFromTargetAnnotation(host.Annotations)
targets := annotations.TargetsFromTargetAnnotation(host.Annotations)
if len(targets) == 0 {
targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service)
if err != nil {
@ -185,11 +186,10 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
// endpointsFromHost extracts the endpoints from a Host object
func (sc *ambassadorHostSource) endpointsFromHost(host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
annotations := host.Annotations
resource := fmt.Sprintf("host/%s/%s", host.Namespace, host.Name)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annotations)
ttl := getTTLFromAnnotations(annotations, resource)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(host.Annotations)
ttl := annotations.TTLFromAnnotations(host.Annotations, resource)
if host.Spec != nil {
hostname := host.Spec.Hostname

View File

@ -33,6 +33,7 @@ import (
fakeDynamic "k8s.io/client-go/dynamic/fake"
fakeKube "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
const defaultAmbassadorNamespace = "ambassador"
@ -246,8 +247,8 @@ func TestAmbassadorHostSource(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "basic-host",
Annotations: map[string]string{
ambHostAnnotation: hostAnnotation,
CloudflareProxiedKey: "true",
ambHostAnnotation: hostAnnotation,
annotations.CloudflareProxiedKey: "true",
},
},
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
// Get the desired hostname of the service from the annotation.
hostname, exists := svc.Annotations[mateAnnotationKey]
if !exists {
hostname, ok := svc.Annotations[mateAnnotationKey]
if !ok {
return nil
}
@ -84,8 +84,8 @@ func legacyEndpointsFromMoleculeService(svc *v1.Service) []*endpoint.Endpoint {
}
// Get the desired hostname of the service from the annotation.
hostnameAnnotation, exists := svc.Annotations[moleculeAnnotationKey]
if !exists {
hostnameAnnotation, ok := svc.Annotations[moleculeAnnotationKey]
if !ok {
return nil
}

View File

@ -34,6 +34,7 @@ import (
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
// 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)
ttl := getTTLFromAnnotations(httpProxy.Annotations, resource)
ttl := annotations.TTLFromAnnotations(httpProxy.Annotations, resource)
targets := getTargetsFromTargetAnnotation(httpProxy.Annotations)
targets := annotations.TargetsFromTargetAnnotation(httpProxy.Annotations)
if len(targets) == 0 {
for _, lb := range httpProxy.Status.LoadBalancer.Ingress {
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
for _, hostname := range hostnames {
@ -224,14 +225,11 @@ func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTT
return httpProxies, nil
}
filteredList := []*projectcontour.HTTPProxy{}
var filteredList []*projectcontour.HTTPProxy
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(httpProxy.Annotations)) {
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) {
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 {
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
@ -270,7 +268,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP
// Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(httpProxy.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(httpProxy.Annotations)
for _, hostname := range hostnameList {
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"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
var f5TransportServerGVR = schema.GroupVersionResource{
@ -150,9 +151,9 @@ func (ts *f5TransportServerSource) endpointsFromTransportServers(transportServer
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 != "" {
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"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
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)
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 != "" {
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"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
const (
@ -236,8 +237,8 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo
// Create endpoints from hostnames and targets.
var routeEndpoints []*endpoint.Endpoint
resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots)
ttl := getTTLFromAnnotations(annots, resource)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots)
ttl := annotations.TTLFromAnnotations(annots, resource)
for host, targets := range hostTargets {
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 {
continue
}
override := getTargetsFromTargetAnnotation(gw.gateway.Annotations)
override := annotations.TargetsFromTargetAnnotation(gw.gateway.Annotations)
hostTargets[host] = append(hostTargets[host], override...)
if len(override) == 0 {
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"
// but other sources don't check if fqdn-template is set. Which should it be?
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.
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"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/source/annotations"
v1 "sigs.k8s.io/gateway-api/apis/v1"
v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
gatewayfake "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned/fake"
@ -1034,8 +1035,8 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) {
Name: "provider-annotations",
Namespace: "default",
Annotations: map[string]string{
SetIdentifierKey: "test-set-identifier",
aliasAnnotationKey: "true",
annotations.SetIdentifierKey: "test-set-identifier",
aliasAnnotationKey: "true",
},
},
Spec: v1.HTTPRouteSpec{

View File

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

View File

@ -34,6 +34,7 @@ import (
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
const (
@ -184,14 +185,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpo
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 {
targets = targetsFromIngressStatus(ing.Status)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ing.Annotations)
var endpoints []*endpoint.Endpoint
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 {
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 {
targets = targetsFromIngressStatus(ing.Status)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ing.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ing.Annotations)
// Gather endpoints defined on hosts sections of the ingress
var definedHostsEndpoints []*endpoint.Endpoint
@ -309,7 +310,7 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool,
// Gather endpoints defined on annotations in the ingress
var annotationEndpoints []*endpoint.Endpoint
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)...)
}
}

View File

@ -36,6 +36,7 @@ import (
"k8s.io/client-go/tools/cache"
"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
@ -200,11 +201,7 @@ func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
// filterByAnnotations filters a list of configs by a given annotation selector.
func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
@ -217,11 +214,8 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate
var filteredList []*networkingv1alpha3.Gateway
for _, gw := range gateways {
// convert the annotations to an equivalent label selector
annotations := labels.Set(gw.Annotations)
// include if the annotations match the selector
if selector.Matches(annotations) {
if selector.Matches(labels.Set(gw.Annotations)) {
filteredList = append(filteredList, gw)
}
}
@ -229,21 +223,8 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate
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) {
namespace, name, err := parseIngress(ingressStr)
namespace, name, err := ParseIngress(ingressStr)
if err != nil {
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
}
func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
targets = getTargetsFromTargetAnnotation(gateway.Annotations)
func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) {
targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) > 0 {
return
return targets, nil
}
ingressStr, ok := gateway.Annotations[IstioGatewayIngressSource]
if ok && ingressStr != "" {
targets, err = sc.targetsFromIngress(ctx, ingressStr, gateway)
return
return sc.targetsFromIngress(ctx, ingressStr, gateway)
}
services, err := sc.serviceInformer.Lister().Services(sc.namespace).List(labels.Everything())
targets, err := EndpointTargetsFromServices(sc.serviceInformer, sc.namespace, gateway.Spec.Selector)
if err != nil {
log.Error(err)
return
return nil, err
}
for _, service := range services {
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
return targets, nil
}
// 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)
annotations := gateway.Annotations
ttl := getTTLFromAnnotations(annotations, resource)
ttl := annotations.TTLFromAnnotations(gateway.Annotations, resource)
targets := getTargetsFromTargetAnnotation(annotations)
targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) == 0 {
targets, err = sc.targetsFromGateway(ctx, gateway)
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 {
endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
@ -356,7 +316,7 @@ func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gatewa
}
if !sc.ignoreHostnameAnnotation {
hostnames = append(hostnames, getHostnamesFromAnnotations(gateway.Annotations)...)
hostnames = append(hostnames, annotations.HostnamesFromAnnotations(gateway.Annotations)...)
}
return hostnames, nil

View File

@ -37,6 +37,7 @@ import (
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
// 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)
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
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.
func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkingv1alpha3.VirtualService) ([]*networkingv1alpha3.VirtualService, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
@ -268,13 +265,10 @@ func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkin
var filteredList []*networkingv1alpha3.VirtualService
for _, virtualservice := range virtualservices {
// convert the annotations to an equivalent label selector
annotations := labels.Set(virtualservice.Annotations)
for _, vs := range virtualservices {
// include if the annotations match the selector
if selector.Matches(annotations) {
filteredList = append(filteredList, virtualservice)
if selector.Matches(labels.Set(vs.Annotations)) {
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)
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 {
if host == "" || host == "*" {
@ -356,7 +350,7 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context,
// Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(virtualservice.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(virtualservice.Annotations)
for _, hostname := range hostnameList {
targets := targetsFromAnnotation
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) {
namespace, name, err := parseIngress(ingressStr)
namespace, name, err := ParseIngress(ingressStr)
if err != nil {
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) {
targets = getTargetsFromTargetAnnotation(gateway.Annotations)
targets = annotations.TargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) > 0 {
return
}

View File

@ -37,6 +37,7 @@ import (
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
var kongGroupdVersionResource = schema.GroupVersionResource{
@ -126,7 +127,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
var endpoints []*endpoint.Endpoint
for _, tcpIngress := range tcpIngresses {
targets := getTargetsFromTargetAnnotation(tcpIngress.Annotations)
targets := annotations.TargetsFromTargetAnnotation(tcpIngress.Annotations)
if len(targets) == 0 {
for _, lb := range tcpIngress.Status.LoadBalancer.Ingress {
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.
func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(sc.annotationFilter)
if err != nil {
return nil, err
}
@ -176,14 +173,11 @@ func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress)
return tcpIngresses, nil
}
filteredList := []*TCPIngress{}
var filteredList []*TCPIngress
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(tcpIngress.Annotations)) {
filteredList = append(filteredList, tcpIngress)
}
}
@ -197,12 +191,12 @@ func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress,
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 {
hostnameList := getHostnamesFromAnnotations(tcpIngress.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(tcpIngress.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
}

View File

@ -23,7 +23,6 @@ import (
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
kubeinformers "k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
@ -31,6 +30,7 @@ import (
"k8s.io/client-go/tools/cache"
"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."
@ -115,7 +115,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
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
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)
}
addrs := getTargetsFromTargetAnnotation(node.Annotations)
addrs := annotations.TargetsFromTargetAnnotation(node.Annotations)
if len(addrs) == 0 {
addrs, err = ns.nodeAddresses(node)
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.
func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) {
labelSelector, err := metav1.ParseToLabelSelector(ns.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(ns.annotationFilter)
if err != nil {
return nil, err
}
@ -222,14 +218,11 @@ func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error)
return nodes, nil
}
filteredList := []*v1.Node{}
var filteredList []*v1.Node
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(node.Annotations)) {
filteredList = append(filteredList, node)
}
}

View File

@ -24,16 +24,16 @@ import (
"time"
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"
routeInformer "github.com/openshift/client-go/route/informers/externalversions/route/v1"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
// 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)
ttl := getTTLFromAnnotations(ocpRoute.Annotations, resource)
ttl := annotations.TTLFromAnnotations(ocpRoute.Annotations, resource)
targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
targets := annotations.TargetsFromTargetAnnotation(ocpRoute.Annotations)
if len(targets) == 0 {
targetsFromRoute, _ := ors.getTargetsFromRouteStatus(ocpRoute.Status)
targets = targetsFromRoute
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations)
var endpoints []*endpoint.Endpoint
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) {
labelSelector, err := metav1.ParseToLabelSelector(ors.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(ors.annotationFilter)
if err != nil {
return nil, err
}
@ -211,11 +207,8 @@ func (ors *ocpRouteSource) filterByAnnotations(ocpRoutes []*routev1.Route) ([]*r
filteredList := []*routev1.Route{}
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(ocpRoute.Annotations)) {
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)
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)
if len(targets) == 0 {
targets = targetsFromRoute
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations)
if host != "" {
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
if !ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ocpRoute.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(ocpRoute.Annotations)
for _, hostname := range hostnameList {
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"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/source/annotations"
)
type podSource struct {
@ -93,10 +95,10 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
continue
}
targets := getTargetsFromTargetAnnotation(pod.Annotations)
targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
if domainAnnotation, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation)
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList {
if len(targets) == 0 {
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 {
domainList := splitHostnameAnnotation(domainAnnotation)
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList {
if len(targets) == 0 {
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 domainAnnotation, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation)
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList {
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
}
}
if domainAnnotation, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation)
domainList := annotations.SplitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList {
node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
for _, address := range node.Status.Addresses {

View File

@ -33,6 +33,8 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/external-dns/source/annotations"
"sigs.k8s.io/external-dns/endpoint"
)
@ -307,7 +309,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
}
for _, headlessDomain := range headlessDomains {
targets := getTargetsFromTargetAnnotation(pod.Annotations)
targets := annotations.TargetsFromTargetAnnotation(pod.Annotations)
if len(targets) == 0 {
if endpointsType == EndpointsTypeNodeExternalIP {
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
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(svc.Annotations)
var endpoints []*endpoint.Endpoint
for _, hostname := range hostnames {
@ -401,16 +403,16 @@ func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
// Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation {
providerSpecific, setIdentifier := getProviderSpecificAnnotations(svc.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(svc.Annotations)
var hostnameList []string
var internalHostnameList []string
hostnameList = getHostnamesFromAnnotations(svc.Annotations)
hostnameList = annotations.HostnamesFromAnnotations(svc.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific, setIdentifier, false)...)
}
internalHostnameList = getInternalHostnamesFromAnnotations(svc.Annotations)
internalHostnameList = annotations.InternalHostnamesFromAnnotations(svc.Annotations)
for _, hostname := range internalHostnameList {
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
}
filteredList := []*v1.Service{}
var filteredList []*v1.Service
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(service.Annotations)) {
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)
ttl := getTTLFromAnnotations(svc.Annotations, resource)
ttl := annotations.TTLFromAnnotations(svc.Annotations, resource)
targets := getTargetsFromTargetAnnotation(svc.Annotations)
targets := annotations.TargetsFromTargetAnnotation(svc.Annotations)
if len(targets) == 0 {
switch svc.Spec.Type {

View File

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

View File

@ -36,6 +36,7 @@ import (
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
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)
// 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 {
targets = targetsFromRouteGroupStatus(rg.Status)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(rg.Metadata.Annotations)
providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(rg.Metadata.Annotations)
var endpoints []*endpoint.Endpoint
// 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)
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 {
for _, lb := range rg.Status.LoadBalancer.RouteGroup {
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 {
if src == "" {
@ -354,7 +355,7 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint.
// Skip endpoints if we do not want entries from annotations
if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(rg.Metadata.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(rg.Metadata.Annotations)
for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...)
}

View File

@ -21,7 +21,6 @@ import (
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"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"
"context"
"fmt"
"math"
"net/netip"
"reflect"
"strconv"
"strings"
"text/template"
"time"
"unicode"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
const (
// The annotation used for figuring out which controller is responsible
controllerAnnotationKey = "external-dns.alpha.kubernetes.io/controller"
// The annotation used for defining the desired hostname
hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname"
// The annotation used for specifying whether the public or private interface address is used
accessAnnotationKey = "external-dns.alpha.kubernetes.io/access"
// The annotation used for specifying the type of endpoints to use for headless services
endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type"
// The annotation used for defining the desired ingress/service target
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
// 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"
)
controllerAnnotationKey = annotations.ControllerKey
hostnameAnnotationKey = annotations.HostnameKey
accessAnnotationKey = annotations.AccessKey
endpointsTypeAnnotationKey = annotations.EndpointsTypeKey
targetAnnotationKey = annotations.TargetKey
ttlAnnotationKey = annotations.TtlKey
aliasAnnotationKey = annotations.AliasKey
ingressHostnameSourceKey = annotations.IngressHostnameSourceKey
controllerAnnotationValue = annotations.ControllerValue
internalHostnameAnnotationKey = annotations.InternalHostnameKey
const (
EndpointsTypeNodeExternalIP = "NodeExternalIP"
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.
type Source interface {
Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error)
@ -89,43 +58,6 @@ type Source interface {
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 {
runtime.Object
metav1.Object
@ -155,186 +87,17 @@ func parseTemplate(fqdnTemplate string) (tmpl *template.Template, err error) {
return template.New("endpoint").Funcs(funcs).Parse(fqdnTemplate)
}
func getHostnamesFromAnnotations(annotations map[string]string) []string {
hostnameAnnotation, exists := annotations[hostnameAnnotationKey]
if !exists {
return nil
}
return splitHostnameAnnotation(hostnameAnnotation)
func getAccessFromAnnotations(input map[string]string) string {
return input[accessAnnotationKey]
}
func getAccessFromAnnotations(annotations map[string]string) string {
return annotations[accessAnnotationKey]
}
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
func getEndpointsTypeFromAnnotations(input map[string]string) string {
return input[endpointsTypeAnnotationKey]
}
// 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 {
var endpoints []*endpoint.Endpoint
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
return EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)
}
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 {
annotations := labels.Set(srcAnnotations)
return selector.Matches(annotations)
return selector.Matches(labels.Set(srcAnnotations))
}
type eventHandlerFunc func()

View File

@ -17,340 +17,144 @@ limitations under the License.
package source
import (
"fmt"
"strconv"
"context"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
)
func TestGetTTLFromAnnotations(t *testing.T) {
for _, tc := range []struct {
title string
annotations map[string]string
expectedTTL endpoint.TTL
func TestParseTemplate(t *testing.T) {
for _, tt := range []struct {
name string
annotationFilter string
fqdnTemplate string
combineFQDNAndAnnotation bool
expectError bool
}{
{
title: "TTL annotation not present",
annotations: map[string]string{"foo": "bar"},
expectedTTL: endpoint.TTL(0),
name: "invalid template",
expectError: true,
fqdnTemplate: "{{.Name",
},
{
title: "TTL annotation value is not a number",
annotations: map[string]string{ttlAnnotationKey: "foo"},
expectedTTL: endpoint.TTL(0),
name: "valid empty template",
expectError: false,
},
{
title: "TTL annotation value is empty",
annotations: map[string]string{ttlAnnotationKey: ""},
expectedTTL: endpoint.TTL(0),
name: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
},
{
title: "TTL annotation value is negative number",
annotations: map[string]string{ttlAnnotationKey: "-1"},
expectedTTL: endpoint.TTL(0),
name: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
},
{
title: "TTL annotation value is too high",
annotations: map[string]string{ttlAnnotationKey: fmt.Sprintf("%d", 1<<32)},
expectedTTL: endpoint.TTL(0),
name: "valid template",
expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com",
combineFQDNAndAnnotation: true,
},
{
title: "TTL annotation value is set correctly using integer",
annotations: map[string]string{ttlAnnotationKey: "60"},
expectedTTL: endpoint.TTL(60),
},
{
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),
name: "non-empty annotation filter label",
expectError: false,
annotationFilter: "kubernetes.io/ingress.class=nginx",
},
} {
t.Run(tc.title, func(t *testing.T) {
ttl := getTTLFromAnnotations(tc.annotations, "resource/test")
assert.Equal(t, tc.expectedTTL, ttl)
t.Run(tt.name, func(t *testing.T) {
_, err := parseTemplate(tt.fqdnTemplate)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSuitableType(t *testing.T) {
for _, tc := range []struct {
target, recordType, expected string
}{
{"8.8.8.8", "", "A"},
{"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
func TestFqdnTemplate(t *testing.T) {
tests := []struct {
name string
fqdnTemplate string
expectedError bool
}{
{
title: "Cloudflare proxied annotation is set correctly to true",
annotations: map[string]string{CloudflareProxiedKey: "true"},
expectedKey: CloudflareProxiedKey,
expectedValue: true,
name: "empty template",
fqdnTemplate: "",
expectedError: false,
},
{
title: "Cloudflare proxied annotation is set correctly to false",
annotations: map[string]string{CloudflareProxiedKey: "false"},
expectedKey: CloudflareProxiedKey,
expectedValue: false,
name: "valid template",
fqdnTemplate: "{{ .Name }}.example.com",
expectedError: 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 {
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, _ := 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)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpl, err := parseTemplate(tt.fqdnTemplate)
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, tmpl)
} else {
assert.NoError(t, err)
if tt.fqdnTemplate == "" {
assert.Nil(t, tmpl)
} else {
assert.NotNil(t, tmpl)
}
}
})
}
}
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"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/source/annotations"
)
var (
@ -255,7 +256,7 @@ func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) {
for _, ingressRoute := range ingressRoutes {
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)
@ -307,7 +308,7 @@ func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error
for _, ingressRouteTCP := range ingressRouteTCPs {
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)
@ -359,7 +360,7 @@ func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error
for _, ingressRouteUDP := range ingressRouteUDPs {
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)
@ -411,7 +412,7 @@ func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error
for _, ingressRoute := range ingressRoutes {
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)
@ -463,7 +464,7 @@ func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, er
for _, ingressRouteTCP := range ingressRouteTCPs {
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)
@ -515,7 +516,7 @@ func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, er
for _, ingressRouteUDP := range ingressRouteUDPs {
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)
@ -537,11 +538,7 @@ func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, er
// filterIngressRouteByAnnotation filters a list of IngressRoute by a given annotation selector.
func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*IngressRoute) ([]*IngressRoute, error) {
labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(ts.annotationFilter)
if err != nil {
return nil, err
}
@ -554,11 +551,8 @@ func (ts *traefikSource) filterIngressRouteByAnnotation(ingressRoutes []*Ingress
filteredList := []*IngressRoute{}
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(ingressRoute.Annotations)) {
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.
func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*IngressRouteTCP) ([]*IngressRouteTCP, error) {
labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(ts.annotationFilter)
if err != nil {
return nil, err
}
@ -585,11 +575,8 @@ func (ts *traefikSource) filterIngressRouteTcpByAnnotations(ingressRoutes []*Ing
filteredList := []*IngressRouteTCP{}
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(ingressRoute.Annotations)) {
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.
func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*IngressRouteUDP) ([]*IngressRouteUDP, error) {
labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
selector, err := annotations.ParseFilter(ts.annotationFilter)
if err != nil {
return nil, err
}
@ -616,11 +599,8 @@ func (ts *traefikSource) filterIngressRouteUdpByAnnotations(ingressRoutes []*Ing
filteredList := []*IngressRouteUDP{}
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
if selector.Matches(annotations) {
if selector.Matches(labels.Set(ingressRoute.Annotations)) {
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)
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 {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList {
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)
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 {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList {
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)
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 {
hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations)
hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations)
for _, hostname := range hostnameList {
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)
})
}
}