mirror of
				https://github.com/kubernetes-sigs/external-dns.git
				synced 2025-10-31 18:50:59 +01:00 
			
		
		
		
	* fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * fix(deduplicate): deduplicate targets Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> --------- Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
		
			
				
	
	
		
			347 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2025 The Kubernetes Authors.
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package source
 | |
| 
 | |
| import (
 | |
| 	"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{},
 | |
| 		},
 | |
| 		{
 | |
| 			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{"158.123.32.23", "192.0.2.1"},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "matching service with duplicate 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", "192.0.2.1", "158.123.32.23"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			namespace: "default",
 | |
| 			selector:  map[string]string{"app": "nginx"},
 | |
| 			expected:  endpoint.Targets{"158.123.32.23", "192.0.2.1"},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "no matching service as service without selector",
 | |
| 			services: []*corev1.Service{
 | |
| 				{
 | |
| 					ObjectMeta: metav1.ObjectMeta{
 | |
| 						Name:      "svc1",
 | |
| 						Namespace: "default",
 | |
| 					},
 | |
| 					Spec: corev1.ServiceSpec{
 | |
| 						ExternalIPs: []string{"192.0.2.1"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			namespace: "default",
 | |
| 			selector:  map[string]string{"app": "nginx"},
 | |
| 			expected:  endpoint.Targets{},
 | |
| 		},
 | |
| 		{
 | |
| 			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"},
 | |
| 		},
 | |
| 		{
 | |
| 			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"},
 | |
| 		},
 | |
| 		{
 | |
| 			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{},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple selectors",
 | |
| 			services: []*corev1.Service{
 | |
| 				{
 | |
| 					ObjectMeta: metav1.ObjectMeta{
 | |
| 						Name:      "fake",
 | |
| 						Namespace: "default",
 | |
| 					},
 | |
| 					Spec: corev1.ServiceSpec{
 | |
| 						Selector:    map[string]string{"app": "apache", "version": "v1"},
 | |
| 						ExternalIPs: []string{"158.123.32.23"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			namespace: "default",
 | |
| 			selector:  map[string]string{"version": "v1"},
 | |
| 			expected:  endpoint.Targets{"158.123.32.23"},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "complex selectors",
 | |
| 			services: []*corev1.Service{
 | |
| 				{
 | |
| 					ObjectMeta: metav1.ObjectMeta{
 | |
| 						Name:      "fake",
 | |
| 						Namespace: "default",
 | |
| 					},
 | |
| 					Spec: corev1.ServiceSpec{
 | |
| 						Selector: map[string]string{
 | |
| 							"app":     "demo",
 | |
| 							"env":     "prod",
 | |
| 							"team":    "devops",
 | |
| 							"version": "v1",
 | |
| 							"release": "stable",
 | |
| 							"track":   "daily",
 | |
| 							"tier":    "backend",
 | |
| 						},
 | |
| 						ExternalIPs: []string{"158.123.32.23"},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			namespace: "default",
 | |
| 			selector: map[string]string{
 | |
| 				"version": "v1",
 | |
| 				"release": "stable",
 | |
| 				"tier":    "backend",
 | |
| 				"app":     "demo",
 | |
| 			},
 | |
| 			expected: endpoint.Targets{"158.123.32.23"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			client := fake.NewClientset()
 | |
| 			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)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestEndpointTargetsFromServicesWithFixtures(t *testing.T) {
 | |
| 	svcInformer, err := svcInformerWithServices(2, 9)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	sel := map[string]string{"app": "nginx", "env": "prod"}
 | |
| 
 | |
| 	targets, err := EndpointTargetsFromServices(svcInformer, "default", sel)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.Equal(t, 2, targets.Len())
 | |
| }
 |