mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
chore(source): code cleanup
This commit is contained in:
parent
7ae0405537
commit
9f427e5622
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
54
source/annotations/annotations.go
Normal file
54
source/annotations/annotations.go
Normal 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"
|
||||
)
|
125
source/annotations/processors.go
Normal file
125
source/annotations/processors.go
Normal 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)
|
||||
}
|
350
source/annotations/processors_test.go
Normal file
350
source/annotations/processors_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
76
source/annotations/provider_specific.go
Normal file
76
source/annotations/provider_specific.go
Normal 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
|
||||
}
|
305
source/annotations/provider_specific_test.go
Normal file
305
source/annotations/provider_specific_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
110
source/endpoints.go
Normal 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
255
source/endpoints_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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{
|
||||
|
@ -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, "")...)
|
||||
}
|
||||
|
@ -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)...)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)...)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)...)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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)...)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
272
source/source.go
272
source/source.go
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
67
source/utils.go
Normal 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
151
source/utils_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user