istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/xds_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package xds_test
    16  
    17  import (
    18  	"fmt"
    19  	"testing"
    20  
    21  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    22  
    23  	"istio.io/api/mesh/v1alpha1"
    24  	"istio.io/istio/pilot/pkg/model"
    25  	"istio.io/istio/pilot/test/xds"
    26  	"istio.io/istio/pilot/test/xdstest"
    27  	"istio.io/istio/pkg/cluster"
    28  	"istio.io/istio/pkg/config/constants"
    29  	"istio.io/istio/pkg/config/mesh"
    30  	"istio.io/istio/pkg/slices"
    31  	"istio.io/istio/pkg/test"
    32  	"istio.io/istio/pkg/test/util/structpath"
    33  	"istio.io/istio/pkg/wellknown"
    34  )
    35  
    36  type SidecarTestConfig struct {
    37  	ImportedNamespaces []string
    38  	Resolution         string
    39  	IngressListener    bool
    40  }
    41  
    42  var scopeConfig = `
    43  apiVersion: networking.istio.io/v1alpha3
    44  kind: Sidecar
    45  metadata:
    46    name: sidecar
    47    namespace:  app
    48  spec:
    49  {{- if .IngressListener }}
    50    ingress:
    51      - port:
    52          number: 9080
    53          protocol: HTTP
    54          name: custom-http
    55        defaultEndpoint: unix:///var/run/someuds.sock
    56  {{- end }}
    57    egress:
    58      - hosts:
    59  {{ range $i, $ns := .ImportedNamespaces }}
    60        - {{$ns}}
    61  {{ end }}
    62  ---
    63  apiVersion: networking.istio.io/v1alpha3
    64  kind: ServiceEntry
    65  metadata:
    66    name: app
    67    namespace: app
    68  spec:
    69    hosts:
    70    - app.com
    71    ports:
    72    - number: 80
    73      name: http
    74      protocol: HTTP
    75    resolution: {{.Resolution}}
    76    endpoints:
    77  {{- if eq .Resolution "DNS" }}
    78    - address: app.com
    79  {{- else }}
    80    - address: 1.1.1.1
    81  {{- end }}
    82  ---
    83  apiVersion: networking.istio.io/v1alpha3
    84  kind: ServiceEntry
    85  metadata:
    86    name: excluded
    87    namespace: excluded
    88  spec:
    89    hosts:
    90    - app.com
    91    ports:
    92    - number: 80
    93      name: http
    94      protocol: HTTP
    95    resolution: {{.Resolution}}
    96    endpoints:
    97  {{- if eq .Resolution "DNS" }}
    98    - address: excluded.com
    99  {{- else }}
   100    - address: 9.9.9.9
   101  {{- end }}
   102  ---
   103  apiVersion: networking.istio.io/v1alpha3
   104  kind: ServiceEntry
   105  metadata:
   106    name: included
   107    namespace: included
   108  spec:
   109    hosts:
   110    - app.com
   111    ports:
   112    - number: 80
   113      name: http
   114      protocol: HTTP
   115    resolution: {{.Resolution}}
   116    endpoints:
   117  {{- if eq .Resolution "DNS" }}
   118    - address: included.com
   119  {{- else }}
   120    - address: 2.2.2.2
   121  {{- end }}
   122  ---
   123  apiVersion: networking.istio.io/v1alpha3
   124  kind: ServiceEntry
   125  metadata:
   126    name: app-https
   127    namespace: app
   128  spec:
   129    hosts:
   130    - app.cluster.local
   131    addresses:
   132    - 5.5.5.5
   133    ports:
   134    - number: 443
   135      name: https
   136      protocol: HTTPS
   137    resolution: {{.Resolution}}
   138    endpoints:
   139  {{- if eq .Resolution "DNS" }}
   140    - address: app.com
   141  {{- else }}
   142    - address: 10.10.10.10
   143  {{- end }}
   144  ---
   145  apiVersion: networking.istio.io/v1alpha3
   146  kind: ServiceEntry
   147  metadata:
   148    name: excluded-https
   149    namespace: excluded
   150  spec:
   151    hosts:
   152    - app.cluster.local
   153    addresses:
   154    - 5.5.5.5
   155    ports:
   156    - number: 4431
   157      name: https
   158      protocol: HTTPS
   159    resolution: {{.Resolution}}
   160    endpoints:
   161  {{- if eq .Resolution "DNS" }}
   162    - address: app.com
   163  {{- else }}
   164    - address: 10.10.10.10
   165  {{- end }}
   166  `
   167  
   168  // TestServiceScoping is a high level test ensuring the Sidecar scoping works correctly, especially when
   169  // there are multiple hostnames that are in different namespaces.
   170  func TestServiceScoping(t *testing.T) {
   171  	baseProxy := func() *model.Proxy {
   172  		return &model.Proxy{
   173  			Metadata:        &model.NodeMetadata{},
   174  			ID:              "app.app",
   175  			Type:            model.SidecarProxy,
   176  			IPAddresses:     []string{"1.1.1.1"},
   177  			ConfigNamespace: "app",
   178  		}
   179  	}
   180  
   181  	t.Run("STATIC", func(t *testing.T) {
   182  		s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   183  			ConfigString: scopeConfig,
   184  			ConfigTemplateInput: SidecarTestConfig{
   185  				ImportedNamespaces: []string{"./*", "included/*"},
   186  				Resolution:         "STATIC",
   187  			},
   188  		})
   189  		proxy := s.SetupProxy(baseProxy())
   190  
   191  		endpoints := xdstest.ExtractLoadAssignments(s.Endpoints(proxy))
   192  		if !slices.EqualUnordered(endpoints["outbound|80||app.com"], []string{"1.1.1.1:80"}) {
   193  			t.Fatalf("expected 1.1.1.1, got %v", endpoints["outbound|80||app.com"])
   194  		}
   195  
   196  		assertListEqual(t, xdstest.ExtractListenerNames(s.Listeners(proxy)), []string{
   197  			"0.0.0.0_80",
   198  			"5.5.5.5_443",
   199  			"virtualInbound",
   200  			"virtualOutbound",
   201  		})
   202  	})
   203  
   204  	t.Run("Ingress Listener", func(t *testing.T) {
   205  		s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   206  			ConfigString: scopeConfig,
   207  			ConfigTemplateInput: SidecarTestConfig{
   208  				ImportedNamespaces: []string{"./*", "included/*"},
   209  				Resolution:         "STATIC",
   210  				IngressListener:    true,
   211  			},
   212  		})
   213  		p := baseProxy()
   214  		// Change the node's IP so that it does not match with any service entry
   215  		p.IPAddresses = []string{"100.100.100.100"}
   216  		proxy := s.SetupProxy(p)
   217  
   218  		endpoints := xdstest.ExtractClusterEndpoints(s.Clusters(proxy))
   219  		eps := endpoints["inbound|9080||"]
   220  		if !slices.EqualUnordered(eps, []string{"/var/run/someuds.sock"}) {
   221  			t.Fatalf("expected /var/run/someuds.sock, got %v", eps)
   222  		}
   223  
   224  		assertListEqual(t, xdstest.ExtractListenerNames(s.Listeners(proxy)), []string{
   225  			"0.0.0.0_80",
   226  			"5.5.5.5_443",
   227  			"virtualInbound",
   228  			"virtualOutbound",
   229  		})
   230  	})
   231  
   232  	t.Run("DNS", func(t *testing.T) {
   233  		s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   234  			ConfigString: scopeConfig,
   235  			ConfigTemplateInput: SidecarTestConfig{
   236  				ImportedNamespaces: []string{"./*", "included/*"},
   237  				Resolution:         "DNS",
   238  			},
   239  		})
   240  		proxy := s.SetupProxy(baseProxy())
   241  
   242  		assertListEqual(t, xdstest.ExtractClusterEndpoints(s.Clusters(proxy))["outbound|80||app.com"], []string{"app.com:80"})
   243  	})
   244  
   245  	t.Run("DNS no self import", func(t *testing.T) {
   246  		s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   247  			ConfigString: scopeConfig,
   248  			ConfigTemplateInput: SidecarTestConfig{
   249  				ImportedNamespaces: []string{"included/*"},
   250  				Resolution:         "DNS",
   251  			},
   252  		})
   253  		proxy := s.SetupProxy(baseProxy())
   254  
   255  		assertListEqual(t, xdstest.ExtractClusterEndpoints(s.Clusters(proxy))["outbound|80||app.com"], []string{"included.com:80"})
   256  	})
   257  }
   258  
   259  func TestSidecarListeners(t *testing.T) {
   260  	t.Run("empty", func(t *testing.T) {
   261  		s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
   262  		proxy := s.SetupProxy(&model.Proxy{
   263  			IPAddresses: []string{"10.2.0.1"},
   264  			ID:          "app3.testns",
   265  		})
   266  		structpath.ForProto(xdstest.ToDiscoveryResponse(s.Listeners(proxy))).
   267  			Exists("{.resources[?(@.address.socketAddress.portValue==15001)]}").
   268  			Select("{.resources[?(@.address.socketAddress.portValue==15001)]}").
   269  			Equals("virtualOutbound", "{.name}").
   270  			Equals("0.0.0.0", "{.address.socketAddress.address}").
   271  			Equals(wellknown.TCPProxy, "{.filterChains[1].filters[0].name}").
   272  			Equals("PassthroughCluster", "{.filterChains[1].filters[0].typedConfig.cluster}").
   273  			Equals("PassthroughCluster", "{.filterChains[1].filters[0].typedConfig.statPrefix}").
   274  			Equals(true, "{.useOriginalDst}").
   275  			CheckOrFail(t)
   276  	})
   277  
   278  	t.Run("mongo", func(t *testing.T) {
   279  		s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   280  			ConfigString: mustReadFile(t, "tests/testdata/config/se-example.yaml"),
   281  		})
   282  		proxy := s.SetupProxy(&model.Proxy{
   283  			IPAddresses: []string{"10.2.0.1"},
   284  			ID:          "app3.testns",
   285  		})
   286  		structpath.ForProto(xdstest.ToDiscoveryResponse(s.Listeners(proxy))).
   287  			Exists("{.resources[?(@.address.socketAddress.portValue==27018)]}").
   288  			Select("{.resources[?(@.address.socketAddress.portValue==27018)]}").
   289  			Equals("0.0.0.0", "{.address.socketAddress.address}").
   290  			// Example doing a struct comparison, note the pain with oneofs....
   291  			Equals(&core.SocketAddress{
   292  				Address: "0.0.0.0",
   293  				PortSpecifier: &core.SocketAddress_PortValue{
   294  					PortValue: uint32(27018),
   295  				},
   296  			}, "{.address.socketAddress}").
   297  			Select("{.filterChains[0].filters[0]}").
   298  			Equals("envoy.mongo_proxy", "{.name}").
   299  			Select("{.typedConfig}").
   300  			Exists("{.statPrefix}").
   301  			CheckOrFail(t)
   302  	})
   303  }
   304  
   305  func TestEgressProxy(t *testing.T) {
   306  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   307  		ConfigString: `
   308  # Add a random endpoint, otherwise there will be no routes to check
   309  apiVersion: networking.istio.io/v1alpha3
   310  kind: ServiceEntry
   311  metadata:
   312    name: pod
   313  spec:
   314    hosts:
   315    - pod.pod.svc.cluster.local
   316    ports:
   317    - number: 80
   318      name: http
   319      protocol: HTTP
   320    resolution: STATIC
   321    location: MESH_INTERNAL
   322    endpoints:
   323    - address: 10.10.10.20
   324  ---
   325  apiVersion: networking.istio.io/v1alpha3
   326  kind: Sidecar
   327  metadata:
   328    name: sidecar-with-egressproxy
   329    namespace: app
   330  spec:
   331    outboundTrafficPolicy:
   332      mode: ALLOW_ANY
   333      egressProxy:
   334        host: foo.bar
   335        subset: shiny
   336        port:
   337          number: 5000
   338    egress:
   339    - hosts:
   340      - "*/*"
   341  `,
   342  	})
   343  	proxy := s.SetupProxy(&model.Proxy{
   344  		ConfigNamespace: "app",
   345  	})
   346  
   347  	listeners := s.Listeners(proxy)
   348  	assertListEqual(t, xdstest.ExtractListenerNames(listeners), []string{
   349  		"0.0.0.0_80",
   350  		"virtualInbound",
   351  		"virtualOutbound",
   352  	})
   353  
   354  	expectedEgressCluster := "outbound|5000|shiny|foo.bar"
   355  
   356  	found := false
   357  	for _, f := range xdstest.ExtractListener("virtualOutbound", listeners).FilterChains {
   358  		// We want to check the match all filter chain, as this is testing the fallback logic
   359  		if f.FilterChainMatch != nil {
   360  			continue
   361  		}
   362  		tcp := xdstest.ExtractTCPProxy(t, f)
   363  		if tcp.GetCluster() != expectedEgressCluster {
   364  			t.Fatalf("got unexpected fallback destination: %v, want %v", tcp.GetCluster(), expectedEgressCluster)
   365  		}
   366  		found = true
   367  	}
   368  	if !found {
   369  		t.Fatalf("failed to find tcp proxy")
   370  	}
   371  
   372  	found = false
   373  	routes := s.Routes(proxy)
   374  	for _, rc := range routes {
   375  		for _, vh := range rc.GetVirtualHosts() {
   376  			if vh.GetName() == "allow_any" {
   377  				for _, r := range vh.GetRoutes() {
   378  					if expectedEgressCluster == r.GetRoute().GetCluster() {
   379  						found = true
   380  						break
   381  					}
   382  				}
   383  				break
   384  			}
   385  		}
   386  	}
   387  	if !found {
   388  		t.Fatalf("failed to find expected fallthrough route")
   389  	}
   390  }
   391  
   392  func assertListEqual(t test.Failer, a, b []string) {
   393  	t.Helper()
   394  	if !slices.EqualUnordered(a, b) {
   395  		t.Fatalf("Expected list %v to be equal to %v", a, b)
   396  	}
   397  }
   398  
   399  func TestClusterLocal(t *testing.T) {
   400  	tests := map[string]struct {
   401  		fakeOpts            xds.FakeOptions
   402  		serviceCluster      string
   403  		wantClusterLocal    map[cluster.ID][]string
   404  		wantNonClusterLocal map[cluster.ID][]string
   405  	}{
   406  		// set up a k8s service in each cluster, with a pod in each cluster and a workloadentry in cluster-1
   407  		"k8s service with pod and workloadentry": {
   408  			fakeOpts: func() xds.FakeOptions {
   409  				k8sObjects := map[cluster.ID]string{
   410  					"cluster-1": "",
   411  					"cluster-2": "",
   412  				}
   413  				i := 1
   414  				for range k8sObjects {
   415  					clusterID := fmt.Sprintf("cluster-%d", i)
   416  					k8sObjects[cluster.ID(clusterID)] = fmt.Sprintf(`
   417  apiVersion: v1
   418  kind: Service
   419  metadata:
   420    labels:
   421      app: echo-app
   422    name: echo-app
   423    namespace: default
   424  spec:
   425    clusterIP: 1.2.3.4
   426    selector:
   427      app: echo-app
   428    ports:
   429    - name: grpc
   430      port: 7070
   431  ---
   432  apiVersion: v1
   433  kind: Pod
   434  metadata:
   435    labels:
   436      app: echo-app
   437    name: echo-app-%s
   438    namespace: default
   439  ---
   440  apiVersion: discovery.k8s.io/v1
   441  kind: EndpointSlice
   442  metadata:
   443    name: echo-app
   444    namespace: default
   445    labels:
   446      app: echo-app
   447      kubernetes.io/service-name: echo-app
   448  endpoints:
   449  - addresses:
   450    - 10.0.0.%d
   451  ports:
   452  - name: grpc
   453    port: 7070
   454  `, clusterID, i)
   455  					i++
   456  				}
   457  				return xds.FakeOptions{
   458  					DefaultClusterName:              "cluster-1",
   459  					KubernetesObjectStringByCluster: k8sObjects,
   460  					ConfigString: `
   461  apiVersion: networking.istio.io/v1alpha3
   462  kind: WorkloadEntry
   463  metadata:
   464    name: echo-app
   465    namespace: default
   466  spec:
   467    address: 10.1.1.1
   468    labels:
   469      app: echo-app
   470  `,
   471  				}
   472  			}(),
   473  			serviceCluster: "outbound|7070||echo-app.default.svc.cluster.local",
   474  			wantClusterLocal: map[cluster.ID][]string{
   475  				"cluster-1": {"10.0.0.1:7070", "10.1.1.1:7070"},
   476  				"cluster-2": {"10.0.0.2:7070"},
   477  			},
   478  			wantNonClusterLocal: map[cluster.ID][]string{
   479  				"cluster-1": {"10.0.0.1:7070", "10.1.1.1:7070", "10.0.0.2:7070"},
   480  				"cluster-2": {"10.0.0.1:7070", "10.1.1.1:7070", "10.0.0.2:7070"},
   481  			},
   482  		},
   483  		"serviceentry": {
   484  			fakeOpts: xds.FakeOptions{
   485  				ConfigString: `
   486  apiVersion: networking.istio.io/v1alpha3
   487  kind: ServiceEntry
   488  metadata:
   489    name: external-svc-mongocluster
   490  spec:
   491    hosts:
   492    - mymongodb.somedomain 
   493    addresses:
   494    - 192.192.192.192/24 # VIPs
   495    ports:
   496    - number: 27018
   497      name: mongodb
   498      protocol: MONGO
   499    location: MESH_INTERNAL
   500    resolution: STATIC
   501    endpoints:
   502    - address: 2.2.2.2
   503    - address: 3.3.3.3
   504  `,
   505  			},
   506  			serviceCluster: "outbound|27018||mymongodb.somedomain",
   507  			wantClusterLocal: map[cluster.ID][]string{
   508  				constants.DefaultClusterName: {"2.2.2.2:27018", "3.3.3.3:27018"},
   509  				"other":                      {},
   510  			},
   511  			wantNonClusterLocal: map[cluster.ID][]string{
   512  				constants.DefaultClusterName: {"2.2.2.2:27018", "3.3.3.3:27018"},
   513  				"other":                      {"2.2.2.2:27018", "3.3.3.3:27018"},
   514  			},
   515  		},
   516  	}
   517  
   518  	for name, tt := range tests {
   519  		t.Run(name, func(t *testing.T) {
   520  			for _, local := range []bool{true, false} {
   521  				name := "cluster-local"
   522  				want := tt.wantClusterLocal
   523  				if !local {
   524  					name = "non-cluster-local"
   525  					want = tt.wantNonClusterLocal
   526  				}
   527  				t.Run(name, func(t *testing.T) {
   528  					meshConfig := mesh.DefaultMeshConfig()
   529  					meshConfig.ServiceSettings = []*v1alpha1.MeshConfig_ServiceSettings{
   530  						{Hosts: []string{"*"}, Settings: &v1alpha1.MeshConfig_ServiceSettings_Settings{
   531  							ClusterLocal: local,
   532  						}},
   533  					}
   534  					fakeOpts := tt.fakeOpts
   535  					fakeOpts.MeshConfig = meshConfig
   536  					s := xds.NewFakeDiscoveryServer(t, fakeOpts)
   537  					for clusterID := range want {
   538  						p := s.SetupProxy(&model.Proxy{Metadata: &model.NodeMetadata{ClusterID: clusterID}})
   539  						eps := xdstest.ExtractLoadAssignments(s.Endpoints(p))[tt.serviceCluster]
   540  						if want := want[clusterID]; !slices.EqualUnordered(eps, want) {
   541  							t.Errorf("got %v but want %v for %s", eps, want, clusterID)
   542  						}
   543  					}
   544  				})
   545  			}
   546  		})
   547  	}
   548  }