From 6a0f06c8c1ca8551ceef5a2d2f97cb394702a94d Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Fri, 8 Aug 2025 10:38:28 +0100 Subject: [PATCH 1/4] chore(source/istio): added transfomrers Signed-off-by: ivan katliarchuk --- source/informers/fake.go | 49 +++++++ source/informers/transfomers.go | 84 ++++++++++++ source/informers/transformers_test.go | 176 +++++++++++++++++++++++++ source/istio_gateway.go | 32 ++--- source/istio_gateway_test.go | 170 ++++++++++++++++++------ source/istio_virtualservice.go | 42 +++--- source/istio_virtualservice_test.go | 180 ++++++++++++++++++++------ 7 files changed, 612 insertions(+), 121 deletions(-) create mode 100644 source/informers/transfomers.go create mode 100644 source/informers/transformers_test.go diff --git a/source/informers/fake.go b/source/informers/fake.go index aed7b3aee7..294206d7f3 100644 --- a/source/informers/fake.go +++ b/source/informers/fake.go @@ -15,6 +15,9 @@ package informers import ( "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" corev1lister "k8s.io/client-go/listers/core/v1" discoveryv1lister "k8s.io/client-go/listers/discovery/v1" "k8s.io/client-go/tools/cache" @@ -58,3 +61,49 @@ func (f *FakeNodeInformer) Informer() cache.SharedIndexInformer { func (f *FakeNodeInformer) Lister() corev1lister.NodeLister { return corev1lister.NewNodeLister(f.Informer().GetIndexer()) } + +func fakeService() *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "ns", + Labels: map[string]string{"env": "prod", "team": "devops"}, + Annotations: map[string]string{"description": "Enriched service object"}, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{"app": "demo"}, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt32(8080), + Protocol: corev1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + TargetPort: intstr.FromInt32(8443), + Protocol: corev1.ProtocolTCP, + }, + }, + Type: corev1.ServiceTypeLoadBalancer, + }, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + {IP: "5.6.7.8", Hostname: "lb.example.com"}, + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "MinimumReplicasAvailable", + Message: "Service is available", + LastTransitionTime: metav1.Now(), + }, + }, + }, + } +} diff --git a/source/informers/transfomers.go b/source/informers/transfomers.go new file mode 100644 index 0000000000..2d3daf5bce --- /dev/null +++ b/source/informers/transfomers.go @@ -0,0 +1,84 @@ +/* +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 informers + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +type TransformOptions struct { + specSelector bool + specExternalIps bool + statusLb bool +} + +func TransformerWithOptions[T metav1.Object](optFns ...func(options *TransformOptions)) cache.TransformFunc { + options := TransformOptions{} + for _, fn := range optFns { + fn(&options) + } + return func(obj any) (any, error) { + // only transform if the object is a Service at the moment + entity, ok := obj.(*corev1.Service) + if !ok { + return nil, nil + } + if entity.UID == "" { + // Pod was already transformed and we must be idempotent. + return entity, nil + } + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: entity.Name, + Namespace: entity.Namespace, + DeletionTimestamp: entity.DeletionTimestamp, + }, + Spec: corev1.ServiceSpec{}, + Status: corev1.ServiceStatus{}, + } + if options.specSelector { + svc.Spec.Selector = entity.Spec.Selector + } + if options.specExternalIps { + svc.Spec.ExternalIPs = entity.Spec.ExternalIPs + } + if options.statusLb { + svc.Status.LoadBalancer = entity.Status.LoadBalancer + } + return svc, nil + } +} + +// TransformWithSpecSelector enables copying the Service's .spec.selector field. +func TransformWithSpecSelector() func(options *TransformOptions) { + return func(options *TransformOptions) { + options.specSelector = true + } +} + +// TransformWithSpecExternalIPs enables copying the Service's .spec.externalIPs field. +func TransformWithSpecExternalIPs() func(options *TransformOptions) { + return func(options *TransformOptions) { + options.specExternalIps = true + } +} + +// TransformWithStatusLoadBalancer enables copying the Service's .status.loadBalancer field. +func TransformWithStatusLoadBalancer() func(options *TransformOptions) { + return func(options *TransformOptions) { + options.statusLb = true + } +} diff --git a/source/informers/transformers_test.go b/source/informers/transformers_test.go new file mode 100644 index 0000000000..7d96b48554 --- /dev/null +++ b/source/informers/transformers_test.go @@ -0,0 +1,176 @@ +/* +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 informers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + 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" +) + +func TestTransformerWithOptions_Service(t *testing.T) { + base := fakeService() + + tests := []struct { + name string + options []func(*TransformOptions) + asserts func(any) + }{ + { + name: "minimalistic object", + options: nil, + asserts: func(obj any) { + svc, ok := obj.(*corev1.Service) + assert.True(t, ok) + assert.Empty(t, svc.UID) + assert.NotEmpty(t, svc.Name) + assert.NotEmpty(t, svc.Namespace) + }, + }, + { + name: "with selector", + options: []func(*TransformOptions){TransformWithSpecSelector()}, + asserts: func(obj any) { + svc, ok := obj.(*corev1.Service) + assert.True(t, ok) + assert.NotEmpty(t, svc.Spec.Selector) + assert.Empty(t, svc.Spec.ExternalIPs) + assert.Empty(t, svc.Status.LoadBalancer.Ingress) + }, + }, + { + name: "with selector", + options: []func(*TransformOptions){TransformWithSpecSelector()}, + asserts: func(obj any) { + svc, ok := obj.(*corev1.Service) + assert.True(t, ok) + assert.NotEmpty(t, svc.Spec.Selector) + assert.Empty(t, svc.Spec.ExternalIPs) + assert.Empty(t, svc.Status.LoadBalancer.Ingress) + }, + }, + { + name: "with loadBalancer", + options: []func(*TransformOptions){TransformWithStatusLoadBalancer()}, + asserts: func(obj any) { + svc, ok := obj.(*corev1.Service) + assert.True(t, ok) + assert.Empty(t, svc.Spec.Selector) + assert.Empty(t, svc.Spec.ExternalIPs) + assert.NotEmpty(t, svc.Status.LoadBalancer.Ingress) + }, + }, + { + name: "all options", + options: []func(*TransformOptions){ + TransformWithSpecSelector(), + TransformWithSpecExternalIPs(), + TransformWithStatusLoadBalancer(), + }, + asserts: func(obj any) { + svc, ok := obj.(*corev1.Service) + assert.True(t, ok) + assert.NotEmpty(t, svc.Spec.Selector) + assert.NotEmpty(t, svc.Spec.ExternalIPs) + assert.NotEmpty(t, svc.Status.LoadBalancer.Ingress) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + transform := TransformerWithOptions[*corev1.Service](tt.options...) + got, err := transform(base) + require.NoError(t, err) + tt.asserts(got) + }) + } + + t.Run("non-service input", func(t *testing.T) { + transform := TransformerWithOptions[*corev1.Service]() + out, err := transform("not-a-service") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out != nil { + t.Errorf("expected nil output for non-service input, got %v", out) + } + }) +} + +func TestTransformer_Service_WithFakeClient(t *testing.T) { + t.Run("with transformer", func(t *testing.T) { + ctx := t.Context() + svc := fakeService() + fakeClient := fake.NewClientset() + + _, err := fakeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) + require.NoError(t, err) + + factory := kubeinformers.NewSharedInformerFactoryWithOptions(fakeClient, 0, kubeinformers.WithNamespace(svc.Namespace)) + serviceInformer := factory.Core().V1().Services() + err = serviceInformer.Informer().SetTransform(TransformerWithOptions[*corev1.Service]( + TransformWithSpecSelector(), + TransformWithSpecExternalIPs(), + TransformWithStatusLoadBalancer(), + )) + require.NoError(t, err) + + factory.Start(ctx.Done()) + err = WaitForCacheSync(ctx, factory) + require.NoError(t, err) + + got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) + require.NoError(t, err) + + assert.Equal(t, svc.Spec.Selector, got.Spec.Selector) + assert.Equal(t, svc.Spec.ExternalIPs, got.Spec.ExternalIPs) + assert.Equal(t, svc.Status.LoadBalancer.Ingress, got.Status.LoadBalancer.Ingress) + assert.NotEqual(t, svc.Annotations, got.Annotations) + assert.NotEqual(t, svc.Labels, got.Labels) + }) + + t.Run("without transformer", func(t *testing.T) { + ctx := t.Context() + svc := fakeService() + fakeClient := fake.NewClientset() + + _, err := fakeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) + require.NoError(t, err) + + factory := kubeinformers.NewSharedInformerFactoryWithOptions(fakeClient, 0, kubeinformers.WithNamespace(svc.Namespace)) + serviceInformer := factory.Core().V1().Services() + + err = serviceInformer.Informer().GetIndexer().Add(svc) + require.NoError(t, err) + + factory.Start(ctx.Done()) + err = WaitForCacheSync(ctx, factory) + require.NoError(t, err) + + got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) + require.NoError(t, err) + + // assert.Equal(t, svc.Spec.Selector, got.Spec.Selector) + // assert.Equal(t, svc.Spec.ExternalIPs, got.Spec.ExternalIPs) + assert.Equal(t, svc.Status.LoadBalancer.Ingress, got.Status.LoadBalancer.Ingress) + assert.Equal(t, svc.Annotations, got.Annotations) + assert.Equal(t, svc.Labels, got.Labels) + }) +} diff --git a/source/istio_gateway.go b/source/istio_gateway.go index 9043cdcfe7..ce3f177565 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -28,12 +28,12 @@ import ( istioclient "istio.io/client-go/pkg/clientset/versioned" istioinformers "istio.io/client-go/pkg/informers/externalversions" networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1" + corev1 "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" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" @@ -84,30 +84,26 @@ func NewIstioGatewaySource( gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways() // Add default resource event handlers to properly initialize informer. - _, _ = serviceInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - log.Debug("service added") - }, - }, - ) - - _, _ = gatewayInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - log.Debug("gateway added") - }, - }, - ) + _, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + err = serviceInformer.Informer().SetTransform(informers.TransformerWithOptions[*corev1.Service]( + informers.TransformWithSpecSelector(), + informers.TransformWithSpecExternalIPs(), + informers.TransformWithStatusLoadBalancer(), + )) + if err != nil { + return nil, err + } + + _, _ = gatewayInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) informerFactory.Start(ctx.Done()) istioInformerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { return nil, err } - if err := informers.WaitForCacheSync(context.Background(), istioInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, istioInformerFactory); err != nil { return nil, err } diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index 5d140e3656..5ee5a02c49 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -19,6 +19,7 @@ package source import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -30,6 +31,8 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -1523,25 +1526,6 @@ func testGatewayEndpoints(t *testing.T) { } func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { - svc := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-service", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Selector: map[string]string{ - "app": "demo", - "env": "prod", - "team": "devops", - "version": "v1", - "release": "stable", - "track": "daily", - "tier": "backend", - }, - ExternalIPs: []string{"10.10.10.255"}, - }, - } - tests := []struct { name string selectors map[string]string @@ -1585,25 +1569,33 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { }, } - for _, tt := range tests { + for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + UID: types.UID(fmt.Sprintf("fake-service-uid-%d", i)), + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": "demo", + "env": "prod", + "team": "devops", + "version": "v1", + "release": "stable", + "track": "daily", + "tier": "backend", + }, + ExternalIPs: []string{"10.10.10.255"}, + }, + } + + ctx := context.Background() fakeKubeClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() - src, err := NewIstioGatewaySource( - t.Context(), - fakeKubeClient, - fakeIstioClient, - "", - "", - "", - false, - false, - ) - require.NoError(t, err) - require.NotNil(t, src) - - _, err = fakeKubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) + _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) require.NoError(t, err) gw := &networkingv1beta1.Gateway{ @@ -1621,10 +1613,23 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { }, } - _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{}) require.NoError(t, err) - res, err := src.Endpoints(t.Context()) + src, err := NewIstioGatewaySource( + ctx, + fakeKubeClient, + fakeIstioClient, + "", + "", + "", + false, + false, + ) + require.NoError(t, err) + require.NotNil(t, src) + + res, err := src.Endpoints(ctx) require.NoError(t, err) validateEndpoints(t, res, tt.expected) @@ -1632,6 +1637,99 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { } } +func TestTransformerInIstioGatewaySource(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + Annotations: map[string]string{ + "user-annotation": "value", + "external-dns.alpha.kubernetes.io/hostname": "test-hostname", + "external-dns.alpha.kubernetes.io/random": "value", + "other/annotation": "value", + }, + UID: "someuid", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt32(8080), + Protocol: v1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + TargetPort: intstr.FromInt32(8443), + Protocol: v1.ProtocolTCP, + }, + }, + Type: v1.ServiceTypeLoadBalancer, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {IP: "5.6.7.8", Hostname: "lb.example.com"}, + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "MinimumReplicasAvailable", + Message: "Service is available", + LastTransitionTime: metav1.Now(), + }, + }, + }, + } + + fakeClient := fake.NewClientset() + + _, err := fakeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) + require.NoError(t, err) + + src, err := NewIstioGatewaySource( + t.Context(), + fakeClient, + istiofake.NewSimpleClientset(), + "", + "", + "", + false, + false) + require.NoError(t, err) + gwSource, ok := src.(*gatewaySource) + require.True(t, ok) + + rService, err := gwSource.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) + require.NoError(t, err) + + assert.Equal(t, "fake-service", rService.Name) + assert.Empty(t, rService.Labels) + assert.Empty(t, rService.Annotations) + assert.Empty(t, rService.UID) + assert.NotEmpty(t, rService.Status.LoadBalancer) + assert.Empty(t, rService.Status.Conditions) + assert.Equal(t, map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, rService.Spec.Selector) +} + // gateway specific helper functions func newTestGatewaySource(loadBalancerList []fakeIngressGatewayService, ingressList []fakeIngress) (*gatewaySource, error) { fakeKubernetesClient := fake.NewClientset() diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index d16c236b01..08b0ec4266 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -30,13 +30,13 @@ import ( istioclient "istio.io/client-go/pkg/clientset/versioned" istioinformers "istio.io/client-go/pkg/informers/externalversions" networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" 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" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" @@ -88,38 +88,28 @@ func NewIstioVirtualServiceSource( gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways() // Add default resource event handlers to properly initialize informer. - _, _ = serviceInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - log.Debug("service added") - }, - }, - ) - - _, _ = virtualServiceInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - log.Debug("virtual service added") - }, - }, - ) - - _, _ = gatewayInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - log.Debug("gateway added") - }, - }, - ) + _, _ = serviceInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + err = serviceInformer.Informer().SetTransform(informers.TransformerWithOptions[*corev1.Service]( + informers.TransformWithSpecSelector(), + informers.TransformWithSpecExternalIPs(), + informers.TransformWithStatusLoadBalancer(), + )) + if err != nil { + return nil, err + } + + _, _ = virtualServiceInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + + _, _ = gatewayInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) informerFactory.Start(ctx.Done()) istioInformerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(context.Background(), informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { return nil, err } - if err := informers.WaitForCacheSync(context.Background(), istioInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, istioInformerFactory); err != nil { return nil, err } diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index fe83dbb1a7..49beb78895 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -19,6 +19,7 @@ package source import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -31,6 +32,8 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -49,7 +52,7 @@ type VirtualServiceSuite struct { } func (suite *VirtualServiceSuite) SetupTest() { - fakeKubernetesClient := fake.NewSimpleClientset() + fakeKubernetesClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() var err error @@ -189,7 +192,7 @@ func TestNewIstioVirtualServiceSource(t *testing.T) { _, err := NewIstioVirtualServiceSource( context.TODO(), - fake.NewSimpleClientset(), + fake.NewClientset(), istiofake.NewSimpleClientset(), "", ti.annotationFilter, @@ -2214,25 +2217,6 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { } func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { - svc := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-service", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Selector: map[string]string{ - "app": "demo", - "env": "prod", - "team": "devops", - "version": "v1", - "release": "stable", - "track": "daily", - "tier": "backend", - }, - ExternalIPs: []string{"10.10.10.255"}, - }, - } - tests := []struct { name string selectors map[string]string @@ -2244,7 +2228,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "version": "v1", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-virtualservice"), }, }, { @@ -2259,7 +2243,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "tier": "backend", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-virtualservice"), }, }, { @@ -2271,29 +2255,37 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "app": "demo", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-virtualservice"), }, }, } - for _, tt := range tests { + for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() fakeKubeClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() - src, err := NewIstioGatewaySource( - t.Context(), - fakeKubeClient, - fakeIstioClient, - "", - "", - "", - false, - false, - ) - require.NoError(t, err) - require.NotNil(t, src) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + UID: types.UID(fmt.Sprintf("fake-service-uid-%d", i)), + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": "demo", + "env": "prod", + "team": "devops", + "version": "v1", + "release": "stable", + "track": "daily", + "tier": "backend", + }, + ExternalIPs: []string{"10.10.10.255"}, + }, + } - _, err = fakeKubeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) + _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) require.NoError(t, err) gw := &networkingv1beta1.Gateway{ @@ -2311,20 +2303,33 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { }, } - _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{}) require.NoError(t, err) gwService := &networkingv1beta1.VirtualService{ - ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: "fake-virtualservice", Namespace: "default"}, Spec: istionetworking.VirtualService{ Gateways: []string{gw.Namespace + "/" + gw.Name}, Hosts: []string{"example.org"}, ExportTo: []string{"*"}, }, } - _, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(gwService.Namespace).Create(t.Context(), gwService, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(gwService.Namespace).Create(ctx, gwService, metav1.CreateOptions{}) require.NoError(t, err) + src, err := NewIstioVirtualServiceSource( + ctx, + fakeKubeClient, + fakeIstioClient, + "", + "", + "", + false, + false, + ) + require.NoError(t, err) + require.NotNil(t, src) + res, err := src.Endpoints(t.Context()) require.NoError(t, err) @@ -2332,3 +2337,96 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { }) } } + +func TestTransformerInIstioGatewayVirtualServiceSource(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + Annotations: map[string]string{ + "user-annotation": "value", + "external-dns.alpha.kubernetes.io/hostname": "test-hostname", + "external-dns.alpha.kubernetes.io/random": "value", + "other/annotation": "value", + }, + UID: "someuid", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt32(8080), + Protocol: v1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + TargetPort: intstr.FromInt32(8443), + Protocol: v1.ProtocolTCP, + }, + }, + Type: v1.ServiceTypeLoadBalancer, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {IP: "5.6.7.8", Hostname: "lb.example.com"}, + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "MinimumReplicasAvailable", + Message: "Service is available", + LastTransitionTime: metav1.Now(), + }, + }, + }, + } + + fakeClient := fake.NewClientset() + + _, err := fakeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) + require.NoError(t, err) + + src, err := NewIstioVirtualServiceSource( + t.Context(), + fakeClient, + istiofake.NewSimpleClientset(), + "", + "", + "", + false, + false) + require.NoError(t, err) + gwSource, ok := src.(*virtualServiceSource) + require.True(t, ok) + + rService, err := gwSource.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) + require.NoError(t, err) + + assert.Equal(t, svc.Name, rService.Name) + assert.Empty(t, rService.Labels) + assert.Empty(t, rService.Annotations) + assert.Empty(t, rService.UID) + assert.NotEmpty(t, rService.Status.LoadBalancer) + assert.Empty(t, rService.Status.Conditions) + assert.Equal(t, map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, rService.Spec.Selector) +} From 7dc4632e8a6d42d27f4e835c0300deca5ccb2924 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Thu, 14 Aug 2025 10:33:31 +0100 Subject: [PATCH 2/4] chore(source/istio): added transfomrers Signed-off-by: ivan katliarchuk --- source/istio_gateway_test.go | 16 +++++++--------- source/istio_virtualservice_test.go | 19 +++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index 5ee5a02c49..79a151d2c5 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -1571,6 +1571,9 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { + fakeKubeClient := fake.NewClientset() + fakeIstioClient := istiofake.NewSimpleClientset() + svc := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "fake-service", @@ -1590,12 +1593,7 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { ExternalIPs: []string{"10.10.10.255"}, }, } - - ctx := context.Background() - fakeKubeClient := fake.NewClientset() - fakeIstioClient := istiofake.NewSimpleClientset() - - _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) + _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) require.NoError(t, err) gw := &networkingv1beta1.Gateway{ @@ -1613,11 +1611,11 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { }, } - _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{}) require.NoError(t, err) src, err := NewIstioGatewaySource( - ctx, + t.Context(), fakeKubeClient, fakeIstioClient, "", @@ -1629,7 +1627,7 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { require.NoError(t, err) require.NotNil(t, src) - res, err := src.Endpoints(ctx) + res, err := src.Endpoints(t.Context()) require.NoError(t, err) validateEndpoints(t, res, tt.expected) diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index 49beb78895..198b87cc30 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -2216,7 +2216,7 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { } } -func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { +func TestIstioVirtualServiceSource_GWServiceSelectorMatchServiceSelector(t *testing.T) { tests := []struct { name string selectors map[string]string @@ -2228,7 +2228,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "version": "v1", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-virtualservice"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, { @@ -2243,7 +2243,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "tier": "backend", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-virtualservice"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, { @@ -2255,13 +2255,12 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "app": "demo", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-virtualservice"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, } for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() fakeKubeClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() @@ -2285,7 +2284,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { }, } - _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(ctx, svc, metav1.CreateOptions{}) + _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) require.NoError(t, err) gw := &networkingv1beta1.Gateway{ @@ -2303,22 +2302,22 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { }, } - _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(ctx, gw, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{}) require.NoError(t, err) gwService := &networkingv1beta1.VirtualService{ - ObjectMeta: metav1.ObjectMeta{Name: "fake-virtualservice", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: "fake-vservice", Namespace: "default"}, Spec: istionetworking.VirtualService{ Gateways: []string{gw.Namespace + "/" + gw.Name}, Hosts: []string{"example.org"}, ExportTo: []string{"*"}, }, } - _, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(gwService.Namespace).Create(ctx, gwService, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(gwService.Namespace).Create(t.Context(), gwService, metav1.CreateOptions{}) require.NoError(t, err) src, err := NewIstioVirtualServiceSource( - ctx, + t.Context(), fakeKubeClient, fakeIstioClient, "", From 5c76e3902a4323c9d5824f4d4ecc8d9f843a9675 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Thu, 14 Aug 2025 11:05:01 +0100 Subject: [PATCH 3/4] chore(source/istio): added transfomrers Signed-off-by: ivan katliarchuk --- source/istio_gateway_test.go | 70 ++++++++++++------------- source/istio_virtualservice_test.go | 79 +++++++++++++++-------------- 2 files changed, 77 insertions(+), 72 deletions(-) diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index 5d140e3656..0352bf6897 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -19,6 +19,7 @@ package source import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -30,6 +31,7 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -1523,25 +1525,6 @@ func testGatewayEndpoints(t *testing.T) { } func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { - svc := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-service", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Selector: map[string]string{ - "app": "demo", - "env": "prod", - "team": "devops", - "version": "v1", - "release": "stable", - "track": "daily", - "tier": "backend", - }, - ExternalIPs: []string{"10.10.10.255"}, - }, - } - tests := []struct { name string selectors map[string]string @@ -1585,25 +1568,31 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { }, } - for _, tt := range tests { + for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { fakeKubeClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() - src, err := NewIstioGatewaySource( - t.Context(), - fakeKubeClient, - fakeIstioClient, - "", - "", - "", - false, - false, - ) - require.NoError(t, err) - require.NotNil(t, src) - - _, err = fakeKubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + UID: types.UID(fmt.Sprintf("fake-service-uid-%d", i)), + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": "demo", + "env": "prod", + "team": "devops", + "version": "v1", + "release": "stable", + "track": "daily", + "tier": "backend", + }, + ExternalIPs: []string{"10.10.10.255"}, + }, + } + _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) require.NoError(t, err) gw := &networkingv1beta1.Gateway{ @@ -1624,6 +1613,19 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { _, err = fakeIstioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(context.Background(), gw, metav1.CreateOptions{}) require.NoError(t, err) + src, err := NewIstioGatewaySource( + t.Context(), + fakeKubeClient, + fakeIstioClient, + "", + "", + "", + false, + false, + ) + require.NoError(t, err) + require.NotNil(t, src) + res, err := src.Endpoints(t.Context()) require.NoError(t, err) diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index e02180629e..e39f95d5cc 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -19,6 +19,7 @@ package source import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -31,6 +32,7 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -2213,26 +2215,7 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { } } -func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { - svc := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-service", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Selector: map[string]string{ - "app": "demo", - "env": "prod", - "team": "devops", - "version": "v1", - "release": "stable", - "track": "daily", - "tier": "backend", - }, - ExternalIPs: []string{"10.10.10.255"}, - }, - } - +func TestIstioVirtualServiceSource_GWServiceSelectorMatchServiceSelector(t *testing.T) { tests := []struct { name string selectors map[string]string @@ -2244,7 +2227,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "version": "v1", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, { @@ -2259,7 +2242,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "tier": "backend", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, { @@ -2271,29 +2254,36 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { "app": "demo", }, expected: []*endpoint.Endpoint{ - endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "gateway/default/fake-gateway"), + endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, } - for _, tt := range tests { + for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { fakeKubeClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() - src, err := NewIstioGatewaySource( - t.Context(), - fakeKubeClient, - fakeIstioClient, - "", - "", - "", - false, - false, - ) - require.NoError(t, err) - require.NotNil(t, src) + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + UID: types.UID(fmt.Sprintf("fake-service-uid-%d", i)), + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": "demo", + "env": "prod", + "team": "devops", + "version": "v1", + "release": "stable", + "track": "daily", + "tier": "backend", + }, + ExternalIPs: []string{"10.10.10.255"}, + }, + } - _, err = fakeKubeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) + _, err := fakeKubeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) require.NoError(t, err) gw := &networkingv1beta1.Gateway{ @@ -2315,7 +2305,7 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { require.NoError(t, err) gwService := &networkingv1beta1.VirtualService{ - ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}, + ObjectMeta: metav1.ObjectMeta{Name: "fake-vservice", Namespace: "default"}, Spec: istionetworking.VirtualService{ Gateways: []string{gw.Namespace + "/" + gw.Name}, Hosts: []string{"example.org"}, @@ -2325,6 +2315,19 @@ func TestGatewaySource_GWVServiceSelectorMatchServiceSelector(t *testing.T) { _, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(gwService.Namespace).Create(t.Context(), gwService, metav1.CreateOptions{}) require.NoError(t, err) + src, err := NewIstioVirtualServiceSource( + t.Context(), + fakeKubeClient, + fakeIstioClient, + "", + "", + "", + false, + false, + ) + require.NoError(t, err) + require.NotNil(t, src) + res, err := src.Endpoints(t.Context()) require.NoError(t, err) From ee510068ef3f7b21b2248ee778c74236790092d0 Mon Sep 17 00:00:00 2001 From: ivan katliarchuk Date: Thu, 14 Aug 2025 11:31:38 +0100 Subject: [PATCH 4/4] chore(source/istio): added transfomrers Signed-off-by: ivan katliarchuk --- source/informers/fake.go | 1 + source/informers/transformers_test.go | 4 +- source/istio_gateway_test.go | 94 +++++++++++++++++++++++++++ source/istio_virtualservice_test.go | 94 +++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 2 deletions(-) diff --git a/source/informers/fake.go b/source/informers/fake.go index 294206d7f3..2eb86e3265 100644 --- a/source/informers/fake.go +++ b/source/informers/fake.go @@ -69,6 +69,7 @@ func fakeService() *corev1.Service { Namespace: "ns", Labels: map[string]string{"env": "prod", "team": "devops"}, Annotations: map[string]string{"description": "Enriched service object"}, + UID: "1234", }, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": "demo"}, diff --git a/source/informers/transformers_test.go b/source/informers/transformers_test.go index 7d96b48554..b816649772 100644 --- a/source/informers/transformers_test.go +++ b/source/informers/transformers_test.go @@ -167,8 +167,8 @@ func TestTransformer_Service_WithFakeClient(t *testing.T) { got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) require.NoError(t, err) - // assert.Equal(t, svc.Spec.Selector, got.Spec.Selector) - // assert.Equal(t, svc.Spec.ExternalIPs, got.Spec.ExternalIPs) + assert.Equal(t, map[string]string{"app": "demo"}, got.Spec.Selector) + assert.Equal(t, []string{"1.2.3.4"}, got.Spec.ExternalIPs) assert.Equal(t, svc.Status.LoadBalancer.Ingress, got.Status.LoadBalancer.Ingress) assert.Equal(t, svc.Annotations, got.Annotations) assert.Equal(t, svc.Labels, got.Labels) diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index 0352bf6897..79a151d2c5 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -32,6 +32,7 @@ import ( networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -1634,6 +1635,99 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { } } +func TestTransformerInIstioGatewaySource(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + Annotations: map[string]string{ + "user-annotation": "value", + "external-dns.alpha.kubernetes.io/hostname": "test-hostname", + "external-dns.alpha.kubernetes.io/random": "value", + "other/annotation": "value", + }, + UID: "someuid", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt32(8080), + Protocol: v1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + TargetPort: intstr.FromInt32(8443), + Protocol: v1.ProtocolTCP, + }, + }, + Type: v1.ServiceTypeLoadBalancer, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {IP: "5.6.7.8", Hostname: "lb.example.com"}, + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "MinimumReplicasAvailable", + Message: "Service is available", + LastTransitionTime: metav1.Now(), + }, + }, + }, + } + + fakeClient := fake.NewClientset() + + _, err := fakeClient.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) + require.NoError(t, err) + + src, err := NewIstioGatewaySource( + t.Context(), + fakeClient, + istiofake.NewSimpleClientset(), + "", + "", + "", + false, + false) + require.NoError(t, err) + gwSource, ok := src.(*gatewaySource) + require.True(t, ok) + + rService, err := gwSource.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) + require.NoError(t, err) + + assert.Equal(t, "fake-service", rService.Name) + assert.Empty(t, rService.Labels) + assert.Empty(t, rService.Annotations) + assert.Empty(t, rService.UID) + assert.NotEmpty(t, rService.Status.LoadBalancer) + assert.Empty(t, rService.Status.Conditions) + assert.Equal(t, map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, rService.Spec.Selector) +} + // gateway specific helper functions func newTestGatewaySource(loadBalancerList []fakeIngressGatewayService, ingressList []fakeIngress) (*gatewaySource, error) { fakeKubernetesClient := fake.NewClientset() diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index e39f95d5cc..198b87cc30 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -33,6 +33,7 @@ import ( networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -2335,3 +2336,96 @@ func TestIstioVirtualServiceSource_GWServiceSelectorMatchServiceSelector(t *test }) } } + +func TestTransformerInIstioGatewayVirtualServiceSource(t *testing.T) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-service", + Namespace: "default", + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "label3": "value3", + }, + Annotations: map[string]string{ + "user-annotation": "value", + "external-dns.alpha.kubernetes.io/hostname": "test-hostname", + "external-dns.alpha.kubernetes.io/random": "value", + "other/annotation": "value", + }, + UID: "someuid", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, + ExternalIPs: []string{"1.2.3.4"}, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt32(8080), + Protocol: v1.ProtocolTCP, + }, + { + Name: "https", + Port: 443, + TargetPort: intstr.FromInt32(8443), + Protocol: v1.ProtocolTCP, + }, + }, + Type: v1.ServiceTypeLoadBalancer, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + {IP: "5.6.7.8", Hostname: "lb.example.com"}, + }, + }, + Conditions: []metav1.Condition{ + { + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "MinimumReplicasAvailable", + Message: "Service is available", + LastTransitionTime: metav1.Now(), + }, + }, + }, + } + + fakeClient := fake.NewClientset() + + _, err := fakeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) + require.NoError(t, err) + + src, err := NewIstioVirtualServiceSource( + t.Context(), + fakeClient, + istiofake.NewSimpleClientset(), + "", + "", + "", + false, + false) + require.NoError(t, err) + gwSource, ok := src.(*virtualServiceSource) + require.True(t, ok) + + rService, err := gwSource.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) + require.NoError(t, err) + + assert.Equal(t, svc.Name, rService.Name) + assert.Empty(t, rService.Labels) + assert.Empty(t, rService.Annotations) + assert.Empty(t, rService.UID) + assert.NotEmpty(t, rService.Status.LoadBalancer) + assert.Empty(t, rService.Status.Conditions) + assert.Equal(t, map[string]string{ + "selector": "one", + "selector2": "two", + "selector3": "three", + }, rService.Spec.Selector) +}