/* Copyright 2020 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" "errors" "fmt" "testing" "sigs.k8s.io/external-dns/internal/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "istio.io/api/meta/v1alpha1" istionetworking "istio.io/api/networking/v1beta1" networkingv1 "istio.io/client-go/pkg/apis/networking/v1" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" templatetest "sigs.k8s.io/external-dns/source/template/testutil" ) // This is a compile-time validation that istioVirtualServiceSource is a Source. var _ Source = &virtualServiceSource{} type VirtualServiceSuite struct { suite.Suite source Source lbServices []*v1.Service ingresses []*networkv1.Ingress gwconfig *networkingv1.Gateway vsconfig *networkingv1.VirtualService } func (suite *VirtualServiceSuite) SetupTest() { fakeKubernetesClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() var err error suite.lbServices = []*v1.Service{ (fakeIngressGatewayService{ ips: []string{"8.8.8.8"}, hostnames: []string{"v1"}, namespace: "istio-system", name: "istio-gateway1", }).Service(), (fakeIngressGatewayService{ ips: []string{"1.1.1.1"}, hostnames: []string{"v42"}, namespace: "istio-system", name: "istio-gateway2", }).Service(), } for _, service := range suite.lbServices { _, err = fakeKubernetesClient.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{}) suite.NoError(err, "should succeed") } suite.ingresses = []*networkv1.Ingress{ (fakeIngress{ ips: []string{"2.2.2.2"}, hostnames: []string{"v2"}, namespace: "istio-system", name: "istio-ingress", }).Ingress(), (fakeIngress{ ips: []string{"3.3.3.3"}, hostnames: []string{"v62"}, namespace: "istio-system", name: "istio-ingress2", }).Ingress(), } for _, ingress := range suite.ingresses { _, err = fakeKubernetesClient.NetworkingV1().Ingresses(ingress.Namespace).Create(context.Background(), ingress, metav1.CreateOptions{}) suite.NoError(err, "should succeed") } suite.gwconfig = (fakeGatewayConfig{ name: "foo-gateway-with-targets", namespace: "istio-system", dnsnames: [][]string{{"*"}}, }).Config() _, err = fakeIstioClient.NetworkingV1().Gateways(suite.gwconfig.Namespace).Create(context.Background(), suite.gwconfig, metav1.CreateOptions{}) suite.NoError(err, "should succeed") suite.vsconfig = (fakeVirtualServiceConfig{ name: "foo-virtualservice", namespace: "istio-other", gateways: []string{"istio-system/foo-gateway-with-targets"}, dnsnames: []string{"foo"}, }).Config() _, err = fakeIstioClient.NetworkingV1().VirtualServices(suite.vsconfig.Namespace).Create(context.Background(), suite.vsconfig, metav1.CreateOptions{}) suite.NoError(err, "should succeed") suite.source, err = NewIstioVirtualServiceSource( context.TODO(), fakeKubernetesClient, fakeIstioClient, &Config{ TemplateEngine: templatetest.MustEngine(suite.T(), "{{.Name}}", "", "", false), }, ) suite.NoError(err, "should initialize virtualservice source") } func (suite *VirtualServiceSuite) TestResourceLabelIsSet() { endpoints, err := suite.source.Endpoints(context.Background()) suite.NoError(err, "should succeed") suite.Len(endpoints, 2, "should return the correct number of endpoints") for _, ep := range endpoints { suite.Equal("virtualservice/istio-other/foo-virtualservice", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label") } } func TestVirtualService(t *testing.T) { t.Parallel() suite.Run(t, new(VirtualServiceSuite)) t.Run("virtualServiceBindsToGateway", testVirtualServiceBindsToGateway) t.Run("endpointsFromVirtualServiceConfig", testEndpointsFromVirtualServiceConfig) t.Run("Endpoints", testVirtualServiceEndpoints) } func testVirtualServiceBindsToGateway(t *testing.T) { for _, ti := range []struct { title string gwconfig fakeGatewayConfig vsconfig fakeVirtualServiceConfig vsHost string expected bool }{ { title: "matching host *", gwconfig: fakeGatewayConfig{ dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{}, vsHost: "foo.bar", expected: true, }, { title: "matching host *.", gwconfig: fakeGatewayConfig{ dnsnames: [][]string{{"*.foo.bar"}}, }, vsconfig: fakeVirtualServiceConfig{}, vsHost: "baz.foo.bar", expected: true, }, { title: "not matching host *.", gwconfig: fakeGatewayConfig{ dnsnames: [][]string{{"*.foo.bar"}}, }, vsconfig: fakeVirtualServiceConfig{}, vsHost: "foo.bar", expected: false, }, { title: "not matching host *.", gwconfig: fakeGatewayConfig{ dnsnames: [][]string{{"*.foo.bar"}}, }, vsconfig: fakeVirtualServiceConfig{}, vsHost: "bazfoo.bar", expected: false, }, { title: "not matching host *.", gwconfig: fakeGatewayConfig{ dnsnames: [][]string{{"*.foo.bar"}}, }, vsconfig: fakeVirtualServiceConfig{}, vsHost: "*foo.bar", expected: false, }, { title: "matching host */*", gwconfig: fakeGatewayConfig{ dnsnames: [][]string{{"*/*"}}, }, vsconfig: fakeVirtualServiceConfig{}, vsHost: "foo.bar", expected: true, }, { title: "matching host /*", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"myns/*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "myns", }, vsHost: "foo.bar", expected: true, }, { title: "matching host ./*", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"./*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "istio-system", }, vsHost: "foo.bar", expected: true, }, { title: "not matching host ./*", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"./*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "myns", }, vsHost: "foo.bar", expected: false, }, { title: "not matching host /*", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"myns/*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "otherns", }, vsHost: "foo.bar", expected: false, }, { title: "not matching host /*", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"myns/*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "otherns", }, vsHost: "foo.bar", expected: false, }, { title: "matching exportTo *", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "otherns", exportTo: "*", }, vsHost: "foo.bar", expected: true, }, { title: "matching exportTo ", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "otherns", exportTo: "istio-system", }, vsHost: "foo.bar", expected: true, }, { title: "not matching exportTo ", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "otherns", exportTo: "myns", }, vsHost: "foo.bar", expected: false, }, { title: "not matching exportTo .", gwconfig: fakeGatewayConfig{ namespace: "istio-system", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ namespace: "otherns", exportTo: ".", }, vsHost: "foo.bar", expected: false, }, } { t.Run(ti.title, func(t *testing.T) { vsconfig := ti.vsconfig.Config() gwconfig := ti.gwconfig.Config() require.Equal(t, ti.expected, virtualServiceBindsToGateway(vsconfig, gwconfig, ti.vsHost)) }) } } func testEndpointsFromVirtualServiceConfig(t *testing.T) { t.Parallel() for _, ti := range []struct { title string lbServices []fakeIngressGatewayService ingresses []fakeIngress gwconfig fakeGatewayConfig vsconfig fakeVirtualServiceConfig expected []*endpoint.Endpoint }{ { title: "one rule.host one lb.hostname", lbServices: []fakeIngressGatewayService{ { hostnames: []string{"lb.com"}, // Kubernetes omits the trailing dot }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, }, { title: "one rule.host one lb.IP", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "one rule.host one lb.externalIPs", lbServices: []fakeIngressGatewayService{ { externalIPs: []string{"8.8.8.8"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "one rule.host two lb.IP and two lb.Hostname", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8", "127.0.0.1"}, hostnames: []string{"elb.com", "alb.com"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, }, { DNSName: "foo.bar", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, { title: "one rule.host two lb.IP and two lb.Hostname and two lb.externalIPs", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8", "127.0.0.1"}, hostnames: []string{"elb.com", "alb.com"}, externalIPs: []string{"1.1.1.1", "2.2.2.2"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1", "2.2.2.2"}, }, }, }, { title: "no rule.host", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8", "127.0.0.1"}, hostnames: []string{"elb.com", "alb.com"}, externalIPs: []string{"1.1.1.1", "2.2.2.2"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{}, }, expected: []*endpoint.Endpoint{}, }, { title: "no rule.gateway", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8", "127.0.0.1"}, hostnames: []string{"elb.com", "alb.com"}, externalIPs: []string{"1.1.1.1", "2.2.2.2"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{}, }, { title: "one empty rule.host", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8", "127.0.0.1"}, hostnames: []string{"elb.com", "alb.com"}, externalIPs: []string{"1.1.1.1", "2.2.2.2"}, }, }, gwconfig: fakeGatewayConfig{ dnsnames: [][]string{ {""}, }, }, expected: []*endpoint.Endpoint{}, }, { title: "no targets", lbServices: []fakeIngressGatewayService{{}}, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{}, }, { title: "matching selectors for service and gateway", lbServices: []fakeIngressGatewayService{ { name: "service1", selector: map[string]string{ "app": "myservice", }, hostnames: []string{"elb.com", "alb.com"}, }, { name: "service2", selector: map[string]string{ "app": "otherservice", }, ips: []string{"8.8.8.8", "127.0.0.1"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, selector: map[string]string{ "app": "myservice", }, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com", "alb.com"}, }, }, }, { title: "ingress gateway annotation same namespace", ingresses: []fakeIngress{ { name: "ingress1", hostnames: []string{"alb.com", "elb.com"}, }, { name: "ingress2", ips: []string{"127.0.0.1", "8.8.8.8"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, annotations: map[string]string{ IstioGatewayIngressSource: "ingress1", }, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"alb.com", "elb.com"}, }, }, }, { title: "ingress gateway annotation separate namespace", ingresses: []fakeIngress{ { name: "ingress1", namespace: "ingress", hostnames: []string{"alb.com", "elb.com"}, }, { name: "ingress2", namespace: "ingress", ips: []string{"127.0.0.1", "8.8.8.8"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, annotations: map[string]string{ IstioGatewayIngressSource: "ingress/ingress2", }, }, vsconfig: fakeVirtualServiceConfig{ gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"127.0.0.1", "8.8.8.8"}, }, }, }, { title: "provider-specific annotation is converted to endpoint property", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, }, }, gwconfig: fakeGatewayConfig{ name: "mygw", dnsnames: [][]string{{"*"}}, }, vsconfig: fakeVirtualServiceConfig{ annotations: map[string]string{ annotations.AWSPrefix + "weight": "10", }, gateways: []string{"mygw"}, dnsnames: []string{"foo.bar"}, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, ProviderSpecific: endpoint.ProviderSpecific{ {Name: "aws/weight", Value: "10"}, }, }, }, }, } { t.Run(ti.title, func(t *testing.T) { t.Parallel() for i := range ti.ingresses { if ti.ingresses[i].namespace == "" { ti.ingresses[i].namespace = "test" } } if ti.gwconfig.namespace == "" { ti.gwconfig.namespace = "test" } if ti.vsconfig.namespace == "" { ti.vsconfig.namespace = "test" } if source, err := newTestVirtualServiceSource(t, ti.lbServices, ti.ingresses, []fakeGatewayConfig{ti.gwconfig}); err != nil { require.NoError(t, err) } else if endpoints, err := source.endpointsFromVirtualService(t.Context(), ti.vsconfig.Config()); err != nil { require.NoError(t, err) } else { testutils.ValidateEndpoints(t, endpoints, ti.expected) } }) } } func testVirtualServiceEndpoints(t *testing.T) { t.Parallel() namespace := "testing" for _, ti := range []struct { title string targetNamespace string annotationFilter string labelFilter labels.Selector lbServices []fakeIngressGatewayService ingresses []fakeIngress gwConfigs []fakeGatewayConfig vsConfigs []fakeVirtualServiceConfig expected []*endpoint.Endpoint expectError bool fqdnTemplate string combineFQDNAndAnnotation bool ignoreHostnameAnnotation bool }{ { title: "two simple virtualservices with one gateway each, one ingressgateway loadbalancer service", lbServices: []fakeIngressGatewayService{ { namespace: namespace, ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"example.org"}}, }, { name: "fake2", namespace: namespace, dnsnames: [][]string{{"new.org"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake2"}, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, }, { title: "two simple virtualservices with one gateway each, one ingress", lbServices: []fakeIngressGatewayService{ { namespace: namespace, ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, }, }, ingresses: []fakeIngress{ { name: "ingress1", namespace: namespace, ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"example.org"}}, annotations: map[string]string{ IstioGatewayIngressSource: "ingress1", }, }, { name: "fake2", namespace: namespace, dnsnames: [][]string{{"new.org"}}, annotations: map[string]string{ IstioGatewayIngressSource: "ingress1", }, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake2"}, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, }, { title: "one virtualservice with two gateways, one ingressgateway loadbalancer service", lbServices: []fakeIngressGatewayService{ { namespace: namespace, ips: []string{"8.8.8.8"}, }, }, gwConfigs: []fakeGatewayConfig{ { name: "gw1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, { name: "gw2", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs", namespace: namespace, gateways: []string{"gw1", "gw2"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "one virtualservice with two gateways, one ingressgateway loadbalancer service with externalIPs", lbServices: []fakeIngressGatewayService{ { namespace: namespace, externalIPs: []string{"8.8.8.8"}, }, }, gwConfigs: []fakeGatewayConfig{ { name: "gw1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, { name: "gw2", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs", namespace: namespace, gateways: []string{"gw1", "gw2"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "two simple virtualservices on different namespaces with the same target gateway, one ingressgateway loadbalancer service", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, namespace: "istio-system", }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: "istio-system", dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: "testing1", gateways: []string{"istio-system/fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: "testing2", gateways: []string{"istio-system/fake1"}, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, }, { title: "two simple virtualservices with one gateway on different namespaces and a target namespace, one ingressgateway loadbalancer service", targetNamespace: "testing1", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, namespace: "testing1", }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: "testing1", dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: "testing1", gateways: []string{"testing1/fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: "testing2", gateways: []string{"testing1/fake1"}, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, }, { title: "two simple virtualservices with one gateway on different namespaces and a target namespace, one ingressgateway loadbalancer service with externalIPs", targetNamespace: "testing1", lbServices: []fakeIngressGatewayService{ { externalIPs: []string{"8.8.8.8"}, namespace: "testing1", }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: "testing1", dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: "testing1", gateways: []string{"testing1/fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: "testing2", gateways: []string{"testing1/fake1"}, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "two simple virtualservices with one gateway on different namespaces and a target namespace, one ingress", targetNamespace: "testing1", ingresses: []fakeIngress{ { name: "ingress1", ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, namespace: "testing1", }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: "testing1", dnsnames: [][]string{{"*"}}, annotations: map[string]string{ IstioGatewayIngressSource: "ingress1", }, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: "testing1", gateways: []string{"testing1/fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: "testing2", gateways: []string{"testing1/fake1"}, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, }, { title: "valid matching annotation filter expression", annotationFilter: "kubernetes.io/virtualservice.class in (alb, nginx)", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, annotations: map[string]string{ "kubernetes.io/virtualservice.class": "nginx", }, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "valid non-matching annotation filter expression", annotationFilter: "kubernetes.io/gateway.class in (alb, nginx)", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, annotations: map[string]string{ "kubernetes.io/virtualservice.class": "tectonic", }, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{}, }, { title: "valid matching annotation filter label", annotationFilter: "kubernetes.io/virtualservice.class=nginx", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, annotations: map[string]string{ "kubernetes.io/virtualservice.class": "nginx", }, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "valid non-matching annotation filter label", annotationFilter: "kubernetes.io/virtualservice.class=nginx", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, annotations: map[string]string{ "kubernetes.io/virtualservice.class": "alb", }, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{}, }, { title: "valid matching label filter", labelFilter: labels.SelectorFromSet(labels.Set{"app": "istio-vs"}), lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, labels: map[string]string{"app": "istio-vs"}, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "valid non-matching label filter", labelFilter: labels.SelectorFromSet(labels.Set{"app": "istio-vs"}), lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, labels: map[string]string{"app": "other"}, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{}, }, { title: "gateway ingress annotation; ingress not found", ingresses: []fakeIngress{ { name: "ingress1", namespace: namespace, ips: []string{"8.8.8.8"}, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, annotations: map[string]string{ IstioGatewayIngressSource: "ingress2", }, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{}, expectError: true, }, { title: "our controller type is dns-controller", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, annotations: map[string]string{ annotations.ControllerKey: annotations.ControllerValue, }, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, }, }, { title: "different controller types are ignored", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, annotations: map[string]string{ annotations.ControllerKey: "some-other-tool", }, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{}, }, { title: "template for virtualservice if host is missing", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, hostnames: []string{"elb.com"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{""}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "vs1.ext-dns.test.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "vs1.ext-dns.test.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"elb.com"}, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", }, { title: "multiple FQDN template hostnames", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{""}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "vs1.ext-dns.test.com", Targets: endpoint.Targets{"8.8.8.8"}, RecordType: endpoint.RecordTypeA, }, { DNSName: "vs1.ext-dna.test.com", Targets: endpoint.Targets{"8.8.8.8"}, RecordType: endpoint.RecordTypeA, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com", }, { title: "multiple FQDN template hostnames with restricted gw.hosts", gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, annotations: map[string]string{ annotations.TargetKey: "gateway-target.com", }, dnsnames: [][]string{{"*.org", "*.ext-dns.test.com"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "vs1.ext-dns.test.com", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "vs2.ext-dns.test.com", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "example.org", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com", combineFQDNAndAnnotation: true, }, { title: "virtualservice with target annotation", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.TargetKey: "virtualservice-target.com", }, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.TargetKey: "virtualservice-target.com", }, dnsnames: []string{"example2.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"virtualservice-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "example2.org", Targets: endpoint.Targets{"virtualservice-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, }, }, { title: "virtualservice; gateway with target annotation", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, annotations: map[string]string{ annotations.TargetKey: "gateway-target.com", }, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example2.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "example2.org", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, }, }, { title: "virtualservice; gateway with target and ingress annotation", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, ingresses: []fakeIngress{ { name: "ingress1", ips: []string{"1.1.1.1"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, annotations: map[string]string{ annotations.TargetKey: "gateway-target.com", IstioGatewayIngressSource: "ingress1", }, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{"example2.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "example2.org", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, }, }, { title: "virtualservice with hostname annotation", lbServices: []fakeIngressGatewayService{ { ips: []string{"1.2.3.4"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.HostnameKey: "dns-through-hostname.com", }, dnsnames: []string{"example.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, }, { DNSName: "dns-through-hostname.com", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, }, }, }, { title: "virtualservice with hostname annotation having multiple hostnames, restricted by gw.hosts", lbServices: []fakeIngressGatewayService{ { ips: []string{"1.2.3.4"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*.bar.com"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.HostnameKey: "foo.bar.com, another-dns-through-hostname.com", }, dnsnames: []string{"baz.bar.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "foo.bar.com", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, }, }, }, { title: "virtualservices with annotation and custom TTL", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.TtlKey: "6", }, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.TtlKey: "1", }, dnsnames: []string{"example2.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, RecordTTL: endpoint.TTL(6), }, { DNSName: "example2.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, RecordTTL: endpoint.TTL(1), }, }, }, { title: "template for virtualservice; gateway with target annotation", gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, annotations: map[string]string{ annotations.TargetKey: "gateway-target.com", }, dnsnames: [][]string{{"*"}}, }, { name: "fake2", namespace: namespace, annotations: map[string]string{ annotations.TargetKey: "gateway-target.com", }, dnsnames: [][]string{{"*"}}, }, { name: "fake3", namespace: namespace, annotations: map[string]string{ annotations.TargetKey: "1.2.3.4", }, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, dnsnames: []string{}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake2"}, dnsnames: []string{}, }, { name: "vs3", namespace: namespace, gateways: []string{"fake3"}, dnsnames: []string{}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "vs1.ext-dns.test.com", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "vs2.ext-dns.test.com", Targets: endpoint.Targets{"gateway-target.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "vs3.ext-dns.test.com", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", }, { title: "ignore hostname annotations", lbServices: []fakeIngressGatewayService{ { ips: []string{"8.8.8.8"}, hostnames: []string{"lb.com"}, namespace: namespace, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: namespace, dnsnames: [][]string{{"*"}}, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.HostnameKey: "ignore.me", }, dnsnames: []string{"example.org"}, }, { name: "vs2", namespace: namespace, gateways: []string{"fake1"}, annotations: map[string]string{ annotations.HostnameKey: "ignore.me.too", }, dnsnames: []string{"new.org"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}, }, { DNSName: "new.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.com"}, }, }, ignoreHostnameAnnotation: true, }, { title: "complex setup with multiple gateways and multiple vs.hosts only matching some of the gateway", lbServices: []fakeIngressGatewayService{ { name: "svc1", selector: map[string]string{ "app": "igw1", }, hostnames: []string{"target1.com"}, namespace: "istio-system", }, { name: "svc2", selector: map[string]string{ "app": "igw2", }, hostnames: []string{"target2.com"}, namespace: "testing1", }, { name: "svc3", selector: map[string]string{ "app": "igw3", }, hostnames: []string{"target3.com"}, namespace: "testing2", }, }, ingresses: []fakeIngress{ { name: "ingress1", namespace: "testing1", hostnames: []string{"target4.com"}, }, { name: "ingress3", namespace: "testing3", hostnames: []string{"target5.com"}, }, }, gwConfigs: []fakeGatewayConfig{ { name: "fake1", namespace: "istio-system", dnsnames: [][]string{{"*"}}, selector: map[string]string{ "app": "igw1", }, }, { name: "fake2", namespace: "testing1", dnsnames: [][]string{{"*.baz.com"}, {"*.bar.com"}}, selector: map[string]string{ "app": "igw2", }, }, { name: "fake3", namespace: "testing2", dnsnames: [][]string{{"*.bax.com", "*.bar.com"}}, selector: map[string]string{ "app": "igw3", }, }, { name: "fake4", namespace: "testing3", dnsnames: [][]string{{"*.bax.com", "*.bar.com"}}, selector: map[string]string{ "app": "igw4", }, annotations: map[string]string{ IstioGatewayIngressSource: "testing1/ingress1", }, }, }, vsConfigs: []fakeVirtualServiceConfig{ { name: "vs1", namespace: "testing3", gateways: []string{"istio-system/fake1", "testing1/fake2"}, dnsnames: []string{"somedomain.com", "foo.bar.com"}, }, { name: "vs2", namespace: "testing2", gateways: []string{"testing1/fake2", "fake3"}, dnsnames: []string{"hello.bar.com", "hello.bax.com", "hello.bak.com"}, }, { name: "vs3", namespace: "testing1", gateways: []string{"istio-system/fake1", "testing2/fake3"}, dnsnames: []string{"world.bax.com", "world.bak.com"}, }, { name: "vs4", namespace: "testing1", gateways: []string{"istio-system/fake1", "testing3/fake4"}, dnsnames: []string{"foo.bax.com", "foo.bak.com"}, }, }, expected: []*endpoint.Endpoint{ { DNSName: "somedomain.com", Targets: endpoint.Targets{"target1.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "foo.bar.com", Targets: endpoint.Targets{"target1.com", "target2.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "hello.bar.com", Targets: endpoint.Targets{"target2.com", "target3.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "hello.bax.com", Targets: endpoint.Targets{"target3.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "world.bak.com", Targets: endpoint.Targets{"target1.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "world.bax.com", Targets: endpoint.Targets{"target1.com", "target3.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "foo.bak.com", Targets: endpoint.Targets{"target1.com"}, RecordType: endpoint.RecordTypeCNAME, }, { DNSName: "foo.bax.com", Targets: endpoint.Targets{"target1.com", "target4.com"}, RecordType: endpoint.RecordTypeCNAME, }, }, fqdnTemplate: "{{.Name}}.ext-dns.test.com", }, } { t.Run(ti.title, func(t *testing.T) { t.Parallel() var gateways []*networkingv1.Gateway var virtualservices []*networkingv1.VirtualService for _, gwItem := range ti.gwConfigs { gateways = append(gateways, gwItem.Config()) } for _, vsItem := range ti.vsConfigs { virtualservices = append(virtualservices, vsItem.Config()) } fakeKubernetesClient := fake.NewClientset() for _, lb := range ti.lbServices { service := lb.Service() _, err := fakeKubernetesClient.CoreV1().Services(service.Namespace).Create(t.Context(), service, metav1.CreateOptions{}) require.NoError(t, err) } for _, ing := range ti.ingresses { ingress := ing.Ingress() _, err := fakeKubernetesClient.NetworkingV1().Ingresses(ingress.Namespace).Create(t.Context(), ingress, metav1.CreateOptions{}) require.NoError(t, err) } fakeIstioClient := istiofake.NewSimpleClientset() for _, gateway := range gateways { _, err := fakeIstioClient.NetworkingV1().Gateways(gateway.Namespace).Create(t.Context(), gateway, metav1.CreateOptions{}) require.NoError(t, err) } for _, vService := range virtualservices { _, err := fakeIstioClient.NetworkingV1().VirtualServices(vService.Namespace).Create(t.Context(), vService, metav1.CreateOptions{}) require.NoError(t, err) } virtualServiceSource, err := NewIstioVirtualServiceSource( t.Context(), fakeKubernetesClient, fakeIstioClient, &Config{ Namespace: ti.targetNamespace, AnnotationFilter: ti.annotationFilter, LabelFilter: ti.labelFilter, TemplateEngine: templatetest.MustEngine(t, ti.fqdnTemplate, "", "", ti.combineFQDNAndAnnotation), IgnoreHostnameAnnotation: ti.ignoreHostnameAnnotation, }, ) require.NoError(t, err) res, err := virtualServiceSource.Endpoints(t.Context()) if ti.expectError { assert.Error(t, err) } else { assert.NoError(t, err) } testutils.ValidateEndpoints(t, res, ti.expected) }) } } func newTestVirtualServiceSource(t *testing.T, loadBalancerList []fakeIngressGatewayService, ingressList []fakeIngress, gwList []fakeGatewayConfig) (*virtualServiceSource, error) { fakeKubernetesClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() for _, lb := range loadBalancerList { service := lb.Service() _, err := fakeKubernetesClient.CoreV1().Services(service.Namespace).Create(t.Context(), service, metav1.CreateOptions{}) if err != nil { return nil, err } } for _, ing := range ingressList { ingress := ing.Ingress() _, err := fakeKubernetesClient.NetworkingV1().Ingresses(ingress.Namespace).Create(t.Context(), ingress, metav1.CreateOptions{}) if err != nil { return nil, err } } for _, gw := range gwList { gwObj := gw.Config() // use create instead of add // https://github.com/kubernetes/client-go/blob/92512ee2b8cf6696e9909245624175b7f0c971d9/testing/fixture.go#LL336C3-L336C52 _, err := fakeIstioClient.NetworkingV1().Gateways(gw.namespace).Create(t.Context(), gwObj, metav1.CreateOptions{}) if err != nil { return nil, err } } src, err := NewIstioVirtualServiceSource( t.Context(), fakeKubernetesClient, fakeIstioClient, &Config{ TemplateEngine: templatetest.MustEngine(t, "{{ .Name }}", "", "", false), }, ) if err != nil { return nil, err } vssrc, ok := src.(*virtualServiceSource) if !ok { return nil, errors.New("underlying source type was not virtualservice") } return vssrc, nil } type fakeVirtualServiceConfig struct { namespace string name string gateways []string annotations map[string]string labels map[string]string dnsnames []string exportTo string } func (c fakeVirtualServiceConfig) Config() *networkingv1.VirtualService { vs := istionetworking.VirtualService{ Gateways: c.gateways, Hosts: c.dnsnames, } if c.exportTo != "" { vs.ExportTo = []string{c.exportTo} } return &networkingv1.VirtualService{ ObjectMeta: metav1.ObjectMeta{ Name: c.name, Namespace: c.namespace, Annotations: c.annotations, Labels: c.labels, }, Spec: *vs.DeepCopy(), } } func TestVirtualServiceSourceGetGateway(t *testing.T) { type fields struct { virtualServiceSource *virtualServiceSource } type args struct { ctx context.Context gatewayStr string virtualService *networkingv1.VirtualService } tests := []struct { name string fields fields args args want *networkingv1.Gateway expectedErrStr string }{ {name: "EmptyGateway", fields: fields{ virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(t, nil, nil, nil); return vs }(), }, args: args{ ctx: t.Context(), gatewayStr: "", virtualService: nil, }, want: nil, expectedErrStr: ""}, {name: "MeshGateway", fields: fields{ virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(t, nil, nil, nil); return vs }(), }, args: args{ ctx: t.Context(), gatewayStr: IstioMeshGateway, virtualService: nil, }, want: nil, expectedErrStr: ""}, {name: "MissingGateway", fields: fields{ virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(t, nil, nil, nil); return vs }(), }, args: args{ ctx: t.Context(), gatewayStr: "doesnt/exist", virtualService: &networkingv1.VirtualService{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Name: "exist", Namespace: "doesnt"}, Spec: istionetworking.VirtualService{}, Status: v1alpha1.IstioStatus{}, }, }, want: nil, expectedErrStr: ""}, {name: "InvalidGatewayStr", fields: fields{ virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(t, nil, nil, nil); return vs }(), }, args: args{ ctx: t.Context(), gatewayStr: "1/2/3/", virtualService: &networkingv1.VirtualService{}, }, want: nil, expectedErrStr: "invalid ingress name (name or namespace/name) found \"1/2/3/\""}, {name: "ExistingGateway", fields: fields{ virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(t, nil, nil, []fakeGatewayConfig{{ namespace: "bar", name: "foo", }}) return vs }(), }, args: args{ ctx: t.Context(), gatewayStr: "bar/foo", virtualService: &networkingv1.VirtualService{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, Spec: istionetworking.VirtualService{}, Status: v1alpha1.IstioStatus{}, }, }, want: &networkingv1.Gateway{ TypeMeta: metav1.TypeMeta{Kind: "Gateway"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, Spec: istionetworking.Gateway{}, Status: v1alpha1.IstioStatus{}, }, expectedErrStr: ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.fields.virtualServiceSource.getGateway(tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) if tt.expectedErrStr != "" { assert.EqualError(t, err, tt.expectedErrStr, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) return } else { require.NoError(t, err) } if tt.want != nil && got != nil { tt.want.Spec.ProtoReflect() tt.want.Status.ProtoReflect() assert.Equalf(t, tt.want, got, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) } else { assert.Equalf(t, tt.want, got, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService) } }) } } func TestIstioVirtualServiceSource_GWServiceSelectorMatchServiceSelector(t *testing.T) { tests := []struct { name string selectors map[string]string expected []*endpoint.Endpoint }{ { name: "gw single selector match with single service selector", selectors: map[string]string{ "version": "v1", }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, { name: "gw selector match all service selectors", selectors: map[string]string{ "app": "demo", "env": "prod", "team": "devops", "version": "v1", "release": "stable", "track": "daily", "tier": "backend", }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("example.org", endpoint.RecordTypeA, "10.10.10.255").WithLabel("resource", "virtualservice/default/fake-vservice"), }, }, { name: "gw selector has subset of service selectors", selectors: map[string]string{ "version": "v1", "release": "stable", "tier": "backend", "app": "demo", }, expected: []*endpoint.Endpoint{ 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) { fakeKubeClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() 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 := &networkingv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ Name: "fake-gateway", Namespace: "default", }, Spec: istionetworking.Gateway{ Servers: []*istionetworking.Server{ { Hosts: []string{"example.org"}, }, }, Selector: tt.selectors, }, } _, err = fakeIstioClient.NetworkingV1().Gateways(gw.Namespace).Create(t.Context(), gw, metav1.CreateOptions{}) require.NoError(t, err) gwService := &networkingv1.VirtualService{ 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.NetworkingV1().VirtualServices(gwService.Namespace).Create(t.Context(), gwService, metav1.CreateOptions{}) require.NoError(t, err) src, err := NewIstioVirtualServiceSource( t.Context(), fakeKubeClient, fakeIstioClient, &Config{}, ) require.NoError(t, err) require.NotNil(t, src) res, err := src.Endpoints(t.Context()) require.NoError(t, err) testutils.ValidateEndpoints(t, res, tt.expected) }) } } func TestTransformerInIstioGatewayVirtualServiceSource(t *testing.T) { newSource := func(t *testing.T, kClient *fake.Clientset, istioClient *istiofake.Clientset) *virtualServiceSource { t.Helper() src, err := NewIstioVirtualServiceSource(t.Context(), kClient, istioClient, &Config{}) require.NoError(t, err) vs, ok := src.(*virtualServiceSource) require.True(t, ok) return vs } t.Run("service strips managed fields and status conditions", func(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", v1.LastAppliedConfigAnnotation: `{"apiVersion":"v1"}`, }, UID: "someuid", ManagedFields: []metav1.ManagedFieldsEntry{ {Manager: "kubectl", Operation: metav1.ManagedFieldsOperationApply}, }, }, Spec: v1.ServiceSpec{ Selector: map[string]string{"selector": "one"}, ExternalIPs: []string{"1.2.3.4"}, Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{{IP: "5.6.7.8"}}, }, Conditions: []metav1.Condition{ {Type: "Available", Status: metav1.ConditionTrue, Reason: "Ready"}, }, }, } vs := newSource(t, fake.NewClientset(svc), istiofake.NewSimpleClientset()) retrieved, err := vs.serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) require.NoError(t, err) assert.Equal(t, svc.Name, retrieved.Name) assert.Equal(t, svc.Labels, retrieved.Labels) assert.Equal(t, svc.UID, retrieved.UID) assert.Empty(t, retrieved.ManagedFields) assert.NotContains(t, retrieved.Annotations, v1.LastAppliedConfigAnnotation) assert.Contains(t, retrieved.Annotations, "user-annotation") // Status.LoadBalancer preserved — used for endpoint generation assert.Equal(t, svc.Status.LoadBalancer, retrieved.Status.LoadBalancer) // Status.Conditions stripped assert.Empty(t, retrieved.Status.Conditions) }) t.Run("ingress strips managed fields", func(t *testing.T) { ingress := &networkv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ingress", Namespace: "default", Labels: map[string]string{"label1": "value1"}, Annotations: map[string]string{ "user-annotation": "value", v1.LastAppliedConfigAnnotation: `{"apiVersion":"networking.k8s.io/v1"}`, }, UID: "ingressuid", ManagedFields: []metav1.ManagedFieldsEntry{ {Manager: "kubectl", Operation: metav1.ManagedFieldsOperationApply}, }, }, Status: networkv1.IngressStatus{ LoadBalancer: networkv1.IngressLoadBalancerStatus{ Ingress: []networkv1.IngressLoadBalancerIngress{{IP: "1.2.3.4"}}, }, }, } vs := newSource(t, fake.NewClientset(ingress), istiofake.NewSimpleClientset()) retrieved, err := vs.ingressInformer.Lister().Ingresses(ingress.Namespace).Get(ingress.Name) require.NoError(t, err) assert.Equal(t, ingress.Name, retrieved.Name) assert.Equal(t, ingress.Labels, retrieved.Labels) assert.Equal(t, ingress.UID, retrieved.UID) assert.Empty(t, retrieved.ManagedFields) assert.NotContains(t, retrieved.Annotations, v1.LastAppliedConfigAnnotation) assert.Contains(t, retrieved.Annotations, "user-annotation") // Status.LoadBalancer preserved — used for endpoint generation assert.Equal(t, ingress.Status.LoadBalancer, retrieved.Status.LoadBalancer) }) t.Run("virtualservice strips managed fields", func(t *testing.T) { vsObj := &networkingv1.VirtualService{ ObjectMeta: metav1.ObjectMeta{ Name: "test-vs", Namespace: "default", Labels: map[string]string{"label1": "value1"}, Annotations: map[string]string{ "user-annotation": "value", v1.LastAppliedConfigAnnotation: `{"apiVersion":"networking.istio.io/v1beta1"}`, }, UID: "vsuid", ManagedFields: []metav1.ManagedFieldsEntry{ {Manager: "kubectl", Operation: metav1.ManagedFieldsOperationApply}, }, }, } istioClient := istiofake.NewSimpleClientset() _, err := istioClient.NetworkingV1().VirtualServices(vsObj.Namespace).Create(t.Context(), vsObj, metav1.CreateOptions{}) require.NoError(t, err) vs := newSource(t, fake.NewClientset(), istioClient) retrieved, err := vs.vServiceInformer.Lister().VirtualServices(vsObj.Namespace).Get(vsObj.Name) require.NoError(t, err) assert.Equal(t, vsObj.Name, retrieved.Name) assert.Equal(t, vsObj.Labels, retrieved.Labels) assert.Equal(t, vsObj.UID, retrieved.UID) assert.Empty(t, retrieved.ManagedFields) assert.NotContains(t, retrieved.Annotations, v1.LastAppliedConfigAnnotation) assert.Contains(t, retrieved.Annotations, "user-annotation") }) t.Run("gateway strips managed fields", func(t *testing.T) { gw := &networkingv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ Name: "test-gateway", Namespace: "default", Labels: map[string]string{"label1": "value1"}, Annotations: map[string]string{ "user-annotation": "value", v1.LastAppliedConfigAnnotation: `{"apiVersion":"networking.istio.io/v1beta1"}`, }, UID: "gatewayuid", ManagedFields: []metav1.ManagedFieldsEntry{ {Manager: "kubectl", Operation: metav1.ManagedFieldsOperationApply}, }, }, } istioClient := istiofake.NewSimpleClientset() _, err := istioClient.NetworkingV1().Gateways(gw.Namespace).Create(t.Context(), gw, metav1.CreateOptions{}) require.NoError(t, err) vs := newSource(t, fake.NewClientset(), istioClient) retrieved, err := vs.gatewayInformer.Lister().Gateways(gw.Namespace).Get(gw.Name) require.NoError(t, err) assert.Equal(t, gw.Name, retrieved.Name) assert.Equal(t, gw.Labels, retrieved.Labels) assert.Equal(t, gw.UID, retrieved.UID) assert.Empty(t, retrieved.ManagedFields) assert.NotContains(t, retrieved.Annotations, v1.LastAppliedConfigAnnotation) assert.Contains(t, retrieved.Annotations, "user-annotation") }) }