istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/mesh_network_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  	"context"
    19  	"fmt"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	corev1 "k8s.io/api/core/v1"
    25  	discoveryv1 "k8s.io/api/discovery/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/util/rand"
    29  
    30  	"istio.io/api/annotation"
    31  	"istio.io/api/label"
    32  	meshconfig "istio.io/api/mesh/v1alpha1"
    33  	networking "istio.io/api/networking/v1alpha3"
    34  	"istio.io/api/security/v1beta1"
    35  	"istio.io/istio/pilot/pkg/features"
    36  	"istio.io/istio/pilot/pkg/model"
    37  	"istio.io/istio/pilot/test/xds"
    38  	"istio.io/istio/pilot/test/xdstest"
    39  	"istio.io/istio/pkg/cluster"
    40  	"istio.io/istio/pkg/config"
    41  	"istio.io/istio/pkg/config/constants"
    42  	"istio.io/istio/pkg/config/labels"
    43  	"istio.io/istio/pkg/config/mesh"
    44  	"istio.io/istio/pkg/config/schema/gvk"
    45  	"istio.io/istio/pkg/network"
    46  	"istio.io/istio/pkg/ptr"
    47  	"istio.io/istio/pkg/slices"
    48  	"istio.io/istio/pkg/test"
    49  	"istio.io/istio/pkg/test/util/retry"
    50  )
    51  
    52  func TestNetworkGatewayUpdates(t *testing.T) {
    53  	test.SetForTest(t, &features.MultiNetworkGatewayAPI, true)
    54  	pod := &workload{
    55  		kind: Pod,
    56  		name: "app", namespace: "pod",
    57  		ip: "10.10.10.10", port: 8080,
    58  		metaNetwork: "network-1",
    59  		labels:      map[string]string{label.TopologyNetwork.Name: "network-1"},
    60  	}
    61  	vm := &workload{
    62  		kind: VirtualMachine,
    63  		name: "vm", namespace: "default",
    64  		ip: "10.10.10.30", port: 9090,
    65  		metaNetwork: "vm",
    66  	}
    67  	// VM always sees itself directly
    68  	vm.Expect(vm, "10.10.10.30:9090")
    69  
    70  	workloads := []*workload{pod, vm}
    71  
    72  	var kubeObjects []runtime.Object
    73  	var configObjects []config.Config
    74  	for _, w := range workloads {
    75  		_, objs := w.kubeObjects()
    76  		kubeObjects = append(kubeObjects, objs...)
    77  		configObjects = append(configObjects, w.configs()...)
    78  	}
    79  	meshNetworks := mesh.NewFixedNetworksWatcher(nil)
    80  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
    81  		KubernetesObjects: kubeObjects,
    82  		Configs:           configObjects,
    83  		NetworksWatcher:   meshNetworks,
    84  	})
    85  	for _, w := range workloads {
    86  		w.setupProxy(s)
    87  	}
    88  
    89  	t.Run("no gateway", func(t *testing.T) {
    90  		vm.Expect(pod, "10.10.10.10:8080")
    91  		vm.Test(t, s)
    92  	})
    93  	t.Run("gateway added via label", func(t *testing.T) {
    94  		_, err := s.KubeClient().Kube().CoreV1().Services("istio-system").Create(context.TODO(), &corev1.Service{
    95  			ObjectMeta: metav1.ObjectMeta{
    96  				Name:      "istio-ingressgateway",
    97  				Namespace: "istio-system",
    98  				Labels: map[string]string{
    99  					label.TopologyNetwork.Name: "network-1",
   100  				},
   101  			},
   102  			Spec: corev1.ServiceSpec{
   103  				Type:  corev1.ServiceTypeLoadBalancer,
   104  				Ports: []corev1.ServicePort{{Port: 15443, Protocol: corev1.ProtocolTCP}},
   105  			},
   106  			Status: corev1.ServiceStatus{
   107  				LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "3.3.3.3"}}},
   108  			},
   109  		}, metav1.CreateOptions{})
   110  		if err != nil {
   111  			t.Fatal(err)
   112  		}
   113  		if err := retry.Until(func() bool {
   114  			return len(s.PushContext().NetworkManager().GatewaysForNetwork("network-1")) == 1
   115  		}); err != nil {
   116  			t.Fatal("push context did not reinitialize with gateways; xds event may not have been triggered")
   117  		}
   118  		vm.Expect(pod, "3.3.3.3:15443")
   119  		vm.Test(t, s)
   120  	})
   121  
   122  	t.Run("gateway added via meshconfig", func(t *testing.T) {
   123  		_, err := s.KubeClient().Kube().CoreV1().Services("istio-system").Create(context.TODO(), &corev1.Service{
   124  			ObjectMeta: metav1.ObjectMeta{
   125  				Name:      "istio-meshnetworks-gateway",
   126  				Namespace: "istio-system",
   127  			},
   128  			Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer},
   129  			Status: corev1.ServiceStatus{
   130  				LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "4.4.4.4"}}},
   131  			},
   132  		}, metav1.CreateOptions{})
   133  		meshNetworks.SetNetworks(&meshconfig.MeshNetworks{Networks: map[string]*meshconfig.Network{
   134  			"network-1": {
   135  				Endpoints: []*meshconfig.Network_NetworkEndpoints{
   136  					{
   137  						Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{FromRegistry: "Kubernetes"},
   138  					},
   139  				},
   140  				Gateways: []*meshconfig.Network_IstioNetworkGateway{{
   141  					Gw: &meshconfig.Network_IstioNetworkGateway_RegistryServiceName{
   142  						RegistryServiceName: "istio-meshnetworks-gateway.istio-system.svc.cluster.local",
   143  					},
   144  					Port: 15443,
   145  				}},
   146  			},
   147  		}})
   148  		if err != nil {
   149  			t.Fatal(err)
   150  		}
   151  		if err := retry.Until(func() bool {
   152  			return len(s.PushContext().NetworkManager().GatewaysForNetwork("network-1")) == 2
   153  		}); err != nil {
   154  			t.Fatal("push context did not reinitialize with gateways; xds event may not have been triggered")
   155  		}
   156  		vm.Expect(pod, "3.3.3.3:15443", "4.4.4.4:15443")
   157  		vm.Test(t, s)
   158  	})
   159  }
   160  
   161  func TestMeshNetworking(t *testing.T) {
   162  	test.SetForTest(t, &features.MultiNetworkGatewayAPI, true)
   163  	ingressServiceScenarios := map[corev1.ServiceType]map[cluster.ID][]runtime.Object{
   164  		corev1.ServiceTypeLoadBalancer: {
   165  			// cluster/network 1's ingress can be found up by registry service name in meshNetworks (no label)
   166  			"cluster-1": {gatewaySvc("istio-ingressgateway", "2.2.2.2", "")},
   167  			// cluster/network 2's ingress can be found by it's network label
   168  			"cluster-2": {gatewaySvc("istio-eastwestgateway", "3.3.3.3", "network-2")},
   169  		},
   170  		corev1.ServiceTypeClusterIP: {
   171  			// cluster/network 1's ingress can be found up by registry service name in meshNetworks
   172  			"cluster-1": {&corev1.Service{
   173  				ObjectMeta: metav1.ObjectMeta{
   174  					Name:      "istio-ingressgateway",
   175  					Namespace: "istio-system",
   176  				},
   177  				Spec: corev1.ServiceSpec{
   178  					Type:        corev1.ServiceTypeClusterIP,
   179  					ExternalIPs: []string{"2.2.2.2"},
   180  				},
   181  			}},
   182  			// cluster/network 2's ingress can be found by it's network label
   183  			"cluster-2": {&corev1.Service{
   184  				ObjectMeta: metav1.ObjectMeta{
   185  					Name:      "istio-ingressgateway",
   186  					Namespace: "istio-system",
   187  					Labels: map[string]string{
   188  						label.TopologyNetwork.Name: "network-2",
   189  					},
   190  				},
   191  				Spec: corev1.ServiceSpec{
   192  					Type:        corev1.ServiceTypeClusterIP,
   193  					ExternalIPs: []string{"3.3.3.3"},
   194  				},
   195  			}},
   196  		},
   197  		corev1.ServiceTypeNodePort: {
   198  			"cluster-1": {
   199  				&corev1.Node{Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Type: corev1.NodeExternalIP, Address: "2.2.2.2"}}}},
   200  				&corev1.Service{
   201  					ObjectMeta: metav1.ObjectMeta{
   202  						Name:        "istio-ingressgateway",
   203  						Namespace:   "istio-system",
   204  						Annotations: map[string]string{annotation.TrafficNodeSelector.Name: "{}"},
   205  					},
   206  					Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeNodePort, Ports: []corev1.ServicePort{{Port: 15443, NodePort: 25443}}},
   207  				},
   208  			},
   209  			"cluster-2": {
   210  				&corev1.Node{Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Type: corev1.NodeExternalIP, Address: "3.3.3.3"}}}},
   211  				&corev1.Service{
   212  					ObjectMeta: metav1.ObjectMeta{
   213  						Name:        "istio-ingressgateway",
   214  						Namespace:   "istio-system",
   215  						Annotations: map[string]string{annotation.TrafficNodeSelector.Name: "{}"},
   216  						Labels: map[string]string{
   217  							label.TopologyNetwork.Name: "network-2",
   218  							// set the label here to test it = expectation doesn't change since we map back to that via NodePort
   219  							label.NetworkingGatewayPort.Name: "443",
   220  						},
   221  					},
   222  					Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeNodePort, Ports: []corev1.ServicePort{{Port: 443, NodePort: 25443}}},
   223  				},
   224  			},
   225  		},
   226  	}
   227  
   228  	// network-2 does not need to be specified, gateways and endpoints are found by labels
   229  	meshNetworkConfigs := map[string]*meshconfig.MeshNetworks{
   230  		"gateway Address": {Networks: map[string]*meshconfig.Network{
   231  			"network-1": {
   232  				Endpoints: []*meshconfig.Network_NetworkEndpoints{{
   233  					Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{FromRegistry: "cluster-1"},
   234  				}},
   235  				Gateways: []*meshconfig.Network_IstioNetworkGateway{{
   236  					Gw: &meshconfig.Network_IstioNetworkGateway_Address{Address: "2.2.2.2"}, Port: 15443,
   237  				}},
   238  			},
   239  		}},
   240  		"gateway fromRegistry": {Networks: map[string]*meshconfig.Network{
   241  			"network-1": {
   242  				Endpoints: []*meshconfig.Network_NetworkEndpoints{{
   243  					Ne: &meshconfig.Network_NetworkEndpoints_FromRegistry{FromRegistry: "cluster-1"},
   244  				}},
   245  				Gateways: []*meshconfig.Network_IstioNetworkGateway{{
   246  					Gw: &meshconfig.Network_IstioNetworkGateway_RegistryServiceName{
   247  						RegistryServiceName: "istio-ingressgateway.istio-system.svc.cluster.local",
   248  					},
   249  					Port: 15443,
   250  				}},
   251  			},
   252  		}},
   253  	}
   254  
   255  	type trafficConfig struct {
   256  		config.Config
   257  		allowCrossNetwork bool
   258  	}
   259  	var trafficConfigs []trafficConfig
   260  	for _, c := range []struct {
   261  		name string
   262  		mode v1beta1.PeerAuthentication_MutualTLS_Mode
   263  	}{
   264  		{"strict", v1beta1.PeerAuthentication_MutualTLS_STRICT},
   265  		{"permissive", v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE},
   266  		{"disable", v1beta1.PeerAuthentication_MutualTLS_DISABLE},
   267  	} {
   268  		name, mode := c.name, c.mode
   269  		trafficConfigs = append(trafficConfigs, trafficConfig{
   270  			Config: config.Config{
   271  				Meta: config.Meta{
   272  					GroupVersionKind: gvk.PeerAuthentication,
   273  					Namespace:        "istio-system",
   274  					Name:             "peer-authn-mtls-" + name,
   275  				},
   276  				Spec: &v1beta1.PeerAuthentication{
   277  					Mtls: &v1beta1.PeerAuthentication_MutualTLS{Mode: mode},
   278  				},
   279  			},
   280  			allowCrossNetwork: mode != v1beta1.PeerAuthentication_MutualTLS_DISABLE,
   281  		})
   282  	}
   283  
   284  	for ingrType, ingressObjects := range ingressServiceScenarios {
   285  		ingrType, ingressObjects := ingrType, ingressObjects
   286  		t.Run(string(ingrType), func(t *testing.T) {
   287  			for name, networkConfig := range meshNetworkConfigs {
   288  				name, networkConfig := name, networkConfig
   289  				t.Run(name, func(t *testing.T) {
   290  					for _, cfg := range trafficConfigs {
   291  						cfg := cfg
   292  						t.Run(cfg.Meta.Name, func(t *testing.T) {
   293  							pod := &workload{
   294  								kind: Pod,
   295  								name: "unlabeled", namespace: "pod",
   296  								ip: "10.10.10.10", port: 8080,
   297  								metaNetwork: "network-1", clusterID: "cluster-1",
   298  							}
   299  							labeledPod := &workload{
   300  								kind: Pod,
   301  								name: "labeled", namespace: "pod",
   302  								ip: "10.10.10.20", port: 9090,
   303  								metaNetwork: "network-2", clusterID: "cluster-2",
   304  								labels: map[string]string{label.TopologyNetwork.Name: "network-2"},
   305  							}
   306  							vm := &workload{
   307  								kind: VirtualMachine,
   308  								name: "vm", namespace: "default",
   309  								ip: "10.10.10.30", port: 9090,
   310  								metaNetwork: "vm",
   311  							}
   312  
   313  							// a workload entry with no endpoints in the local network should be ignored
   314  							// in a remote network it should use gateway IP
   315  							emptyAddress := &workload{
   316  								kind: VirtualMachine,
   317  								name: "empty-Address-net-2", namespace: "default",
   318  								ip: "", port: 8080,
   319  								metaNetwork: "network-2",
   320  								labels:      map[string]string{label.TopologyNetwork.Name: "network-2"},
   321  							}
   322  
   323  							// gw does not have endpoints, it's just some proxy used to test REQUESTED_NETWORK_VIEW
   324  							gw := &workload{
   325  								kind: Other,
   326  								name: "gw", ip: "2.2.2.2",
   327  								networkView: []string{"vm"},
   328  							}
   329  
   330  							net1gw, net1GwPort := "2.2.2.2", "15443"
   331  							net2gw, net2GwPort := "3.3.3.3", "15443"
   332  							if ingrType == corev1.ServiceTypeNodePort {
   333  								if name == "gateway fromRegistry" {
   334  									net1GwPort = "25443"
   335  								}
   336  								// network 2 gateway uses the labels approach - always does nodeport mapping
   337  								net2GwPort = "25443"
   338  							}
   339  							net1gw += ":" + net1GwPort
   340  							net2gw += ":" + net2GwPort
   341  
   342  							// local ip for self
   343  							pod.Expect(pod, "10.10.10.10:8080")
   344  							labeledPod.Expect(labeledPod, "10.10.10.20:9090")
   345  
   346  							// vm has no gateway, use the original IP
   347  							pod.Expect(vm, "10.10.10.30:9090")
   348  							labeledPod.Expect(vm, "10.10.10.30:9090")
   349  
   350  							vm.Expect(vm, "10.10.10.30:9090")
   351  
   352  							if cfg.allowCrossNetwork {
   353  								// pod in network-1 uses gateway to reach pod labeled with network-2
   354  								pod.Expect(labeledPod, net2gw)
   355  								pod.Expect(emptyAddress, net2gw)
   356  
   357  								// pod labeled as network-2 should use gateway for network-1
   358  								labeledPod.Expect(pod, net1gw)
   359  								// vm uses gateway to get to pods
   360  								vm.Expect(pod, net1gw)
   361  								vm.Expect(labeledPod, net2gw)
   362  								vm.Expect(emptyAddress, net2gw)
   363  							}
   364  
   365  							runMeshNetworkingTest(t, meshNetworkingTest{
   366  								workloads:         []*workload{pod, labeledPod, vm, gw, emptyAddress},
   367  								meshNetworkConfig: networkConfig,
   368  								kubeObjects:       ingressObjects,
   369  							}, cfg.Config)
   370  						})
   371  					}
   372  				})
   373  			}
   374  		})
   375  	}
   376  }
   377  
   378  func TestEmptyAddressWorkloadEntry(t *testing.T) {
   379  	test.SetForTest(t, &features.MultiNetworkGatewayAPI, true)
   380  	type entry struct{ address, sa, network, version string }
   381  	const name, port = "remote-we-svc", 80
   382  	serviceCases := []struct {
   383  		name, k8s, cfg string
   384  		expectKind     workloadKind
   385  	}{
   386  		{
   387  			expectKind: Pod,
   388  			name:       "Service",
   389  			k8s: `
   390  ---
   391  apiVersion: v1
   392  kind: Service
   393  metadata:
   394    name: remote-we-svc
   395    namespace: test
   396  spec:
   397    ports:
   398    - port: 80
   399      protocol: TCP
   400    selector:
   401      app: remote-we-svc
   402  `,
   403  		},
   404  		{
   405  			name: "ServiceEntry",
   406  			cfg: `
   407  ---
   408  apiVersion: networking.istio.io/v1alpha3
   409  kind: ServiceEntry
   410  metadata:
   411    name: remote-we-svc
   412    namespace: test
   413  spec:
   414    hosts:
   415    - remote-we-svc
   416    ports:
   417      - number: 80
   418        name: http
   419        protocol: HTTP
   420    resolution: STATIC
   421    location: MESH_INTERNAL
   422    workloadSelector:
   423      labels:
   424        app: remote-we-svc
   425  `,
   426  		},
   427  	}
   428  	workloadCases := []struct {
   429  		name         string
   430  		entries      []entry
   431  		expectations map[string][]xdstest.LocLbEpInfo
   432  	}{
   433  		{
   434  			name: "single subset",
   435  			entries: []entry{
   436  				// the only local endpoint giving a weight of 1
   437  				{sa: "foo", network: "network-1", address: "1.2.3.4", version: "v1"},
   438  				// same network, no address is ignored and doesn't affect weight
   439  				{sa: "foo", network: "network-1", address: "", version: "vj"},
   440  				// these will me merged giving the remote gateway a weight of 2
   441  				{sa: "foo", network: "network-2", address: "", version: "v1"},
   442  				{sa: "foo", network: "network-2", address: "", version: "v1"},
   443  				// this should not be included in the weight since it doesn't have an address OR a gateway
   444  				{sa: "foo", network: "no-gateway-address", address: "", version: "v1"},
   445  			},
   446  			expectations: map[string][]xdstest.LocLbEpInfo{
   447  				"": {xdstest.LocLbEpInfo{
   448  					LbEps: []xdstest.LbEpInfo{
   449  						{Address: "1.2.3.4", Weight: 1},
   450  						{Address: "2.2.2.2", Weight: 2},
   451  					},
   452  					Weight: 3,
   453  				}},
   454  				"v1": {xdstest.LocLbEpInfo{
   455  					LbEps: []xdstest.LbEpInfo{
   456  						{Address: "1.2.3.4", Weight: 1},
   457  						{Address: "2.2.2.2", Weight: 2},
   458  					},
   459  					Weight: 3,
   460  				}},
   461  			},
   462  		},
   463  		{
   464  			name: "multiple subsets",
   465  			entries: []entry{
   466  				{sa: "foo", network: "network-1", address: "1.2.3.4", version: "v1"},
   467  				{sa: "foo", network: "network-1", address: "", version: "v2"}, // ignored (does not contribute to weight)
   468  				{sa: "foo", network: "network-2", address: "", version: "v1"},
   469  				{sa: "foo", network: "network-2", address: "", version: "v2"},
   470  			},
   471  			expectations: map[string][]xdstest.LocLbEpInfo{
   472  				"": {xdstest.LocLbEpInfo{
   473  					LbEps: []xdstest.LbEpInfo{
   474  						{Address: "1.2.3.4", Weight: 1},
   475  						{Address: "2.2.2.2", Weight: 2},
   476  					},
   477  					Weight: 3,
   478  				}},
   479  				"v1": {xdstest.LocLbEpInfo{
   480  					LbEps: []xdstest.LbEpInfo{
   481  						{Address: "1.2.3.4", Weight: 1},
   482  						{Address: "2.2.2.2", Weight: 1},
   483  					},
   484  					Weight: 2,
   485  				}},
   486  				"v2": {xdstest.LocLbEpInfo{
   487  					LbEps: []xdstest.LbEpInfo{
   488  						{Address: "2.2.2.2", Weight: 1},
   489  					},
   490  					Weight: 1,
   491  				}},
   492  			},
   493  		},
   494  	}
   495  
   496  	for _, sc := range serviceCases {
   497  		t.Run(sc.name, func(t *testing.T) {
   498  			for _, tc := range workloadCases {
   499  				t.Run(tc.name, func(t *testing.T) {
   500  					client := &workload{
   501  						kind:        Pod,
   502  						name:        "client-pod",
   503  						namespace:   "test",
   504  						ip:          "10.0.0.1",
   505  						port:        80,
   506  						clusterID:   "cluster-1",
   507  						metaNetwork: "network-1",
   508  					}
   509  					// expect self
   510  					client.ExpectWithWeight(client, "", xdstest.LocLbEpInfo{
   511  						Weight: 1,
   512  						LbEps: []xdstest.LbEpInfo{
   513  							{Address: "10.0.0.1", Weight: 1},
   514  						},
   515  					})
   516  					for subset, eps := range tc.expectations {
   517  						client.ExpectWithWeight(&workload{kind: sc.expectKind, name: name, namespace: "test", port: port}, subset, eps...)
   518  					}
   519  					configObjects := `
   520  ---
   521  apiVersion: networking.istio.io/v1alpha3
   522  kind: DestinationRule
   523  metadata:
   524    name: subset-se
   525    namespace: test
   526  spec:
   527    host: "*"
   528    subsets:
   529    - name: v1
   530      labels:
   531        version: v1
   532    - name: v2
   533      labels:
   534        version: v2
   535    - name: v3
   536      labels:
   537        version: v3
   538  `
   539  					configObjects += sc.cfg
   540  					for i, entry := range tc.entries {
   541  						configObjects += fmt.Sprintf(`
   542  ---
   543  apiVersion: networking.istio.io/v1alpha3
   544  kind: WorkloadEntry
   545  metadata:
   546    name: we-%d
   547    namespace: test
   548  spec:
   549    address: %q
   550    serviceAccount: %q
   551    network: %q
   552    labels:
   553      app: remote-we-svc
   554      version: %q
   555  `, i, entry.address, entry.sa, entry.network, entry.version)
   556  					}
   557  
   558  					runMeshNetworkingTest(t, meshNetworkingTest{
   559  						workloads:       []*workload{client},
   560  						configYAML:      configObjects,
   561  						kubeObjectsYAML: map[cluster.ID]string{constants.DefaultClusterName: sc.k8s},
   562  						kubeObjects: map[cluster.ID][]runtime.Object{constants.DefaultClusterName: {
   563  							gatewaySvc("gateway-1", "1.1.1.1", "network-1"),
   564  							gatewaySvc("gateway-2", "2.2.2.2", "network-2"),
   565  						}},
   566  					})
   567  				})
   568  			}
   569  		})
   570  	}
   571  }
   572  
   573  func gatewaySvc(name, ip, network string) *corev1.Service {
   574  	return &corev1.Service{
   575  		ObjectMeta: metav1.ObjectMeta{
   576  			Name:      name,
   577  			Namespace: "istio-system",
   578  			Labels:    map[string]string{label.TopologyNetwork.Name: network},
   579  		},
   580  		Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer},
   581  		Status: corev1.ServiceStatus{
   582  			LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: ip}}},
   583  		},
   584  	}
   585  }
   586  
   587  type meshNetworkingTest struct {
   588  	workloads         []*workload
   589  	meshNetworkConfig *meshconfig.MeshNetworks
   590  	kubeObjects       map[cluster.ID][]runtime.Object
   591  	kubeObjectsYAML   map[cluster.ID]string
   592  	configYAML        string
   593  }
   594  
   595  func runMeshNetworkingTest(t *testing.T, tt meshNetworkingTest, configs ...config.Config) {
   596  	kubeObjects := map[cluster.ID][]runtime.Object{}
   597  	for k, v := range tt.kubeObjects {
   598  		kubeObjects[k] = v
   599  	}
   600  	configObjects := configs
   601  	for _, w := range tt.workloads {
   602  		k8sCluster, objs := w.kubeObjects()
   603  		if k8sCluster != "" {
   604  			kubeObjects[k8sCluster] = append(kubeObjects[k8sCluster], objs...)
   605  		}
   606  		configObjects = append(configObjects, w.configs()...)
   607  	}
   608  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   609  		KubernetesObjectsByCluster:      kubeObjects,
   610  		KubernetesObjectStringByCluster: tt.kubeObjectsYAML,
   611  		ConfigString:                    tt.configYAML,
   612  		Configs:                         configObjects,
   613  		NetworksWatcher:                 mesh.NewFixedNetworksWatcher(tt.meshNetworkConfig),
   614  	})
   615  	for _, w := range tt.workloads {
   616  		w.setupProxy(s)
   617  	}
   618  	for _, w := range tt.workloads {
   619  		w.Test(t, s)
   620  	}
   621  }
   622  
   623  type workloadKind int
   624  
   625  const (
   626  	Other workloadKind = iota
   627  	Pod
   628  	VirtualMachine
   629  )
   630  
   631  type workload struct {
   632  	kind workloadKind
   633  
   634  	name      string
   635  	namespace string
   636  
   637  	ip   string
   638  	port int32
   639  
   640  	clusterID   cluster.ID
   641  	metaNetwork network.ID
   642  	networkView []string
   643  
   644  	labels map[string]string
   645  
   646  	proxy *model.Proxy
   647  
   648  	expectations         map[string][]string
   649  	weightedExpectations map[string][]xdstest.LocLbEpInfo
   650  }
   651  
   652  func (w *workload) Expect(target *workload, ips ...string) {
   653  	if w.expectations == nil {
   654  		w.expectations = map[string][]string{}
   655  	}
   656  	w.expectations[target.clusterName("")] = ips
   657  }
   658  
   659  func (w *workload) ExpectWithWeight(target *workload, subset string, eps ...xdstest.LocLbEpInfo) {
   660  	if w.weightedExpectations == nil {
   661  		w.weightedExpectations = make(map[string][]xdstest.LocLbEpInfo)
   662  	}
   663  	w.weightedExpectations[target.clusterName(subset)] = eps
   664  }
   665  
   666  func (w *workload) Test(t *testing.T, s *xds.FakeDiscoveryServer) {
   667  	if w.expectations == nil && w.weightedExpectations == nil {
   668  		return
   669  	}
   670  	t.Run(fmt.Sprintf("from %s", w.proxy.ID), func(t *testing.T) {
   671  		w.testUnweighted(t, s)
   672  		w.testWeighted(t, s)
   673  	})
   674  }
   675  
   676  func (w *workload) testUnweighted(t *testing.T, s *xds.FakeDiscoveryServer) {
   677  	if w.expectations == nil {
   678  		return
   679  	}
   680  	t.Run("unweighted", func(t *testing.T) {
   681  		// wait for eds cache update
   682  		retry.UntilSuccessOrFail(t, func() error {
   683  			eps := xdstest.ExtractLoadAssignments(s.Endpoints(w.proxy))
   684  
   685  			for c, want := range w.expectations {
   686  				got := eps[c]
   687  				if !slices.EqualUnordered(got, want) {
   688  					err := fmt.Errorf("cluster %s, expected %v, but got %v", c, want, got)
   689  					fmt.Println(err)
   690  					return err
   691  				}
   692  			}
   693  			for c, got := range eps {
   694  				want := w.expectations[c]
   695  				if !slices.EqualUnordered(got, want) {
   696  					err := fmt.Errorf("cluster %s, expected %v, but got %v", c, want, got)
   697  					fmt.Println(err)
   698  					return err
   699  				}
   700  			}
   701  			return nil
   702  		}, retry.Timeout(3*time.Second))
   703  	})
   704  }
   705  
   706  func (w *workload) testWeighted(t *testing.T, s *xds.FakeDiscoveryServer) {
   707  	if w.weightedExpectations == nil {
   708  		return
   709  	}
   710  	t.Run("weighted", func(t *testing.T) {
   711  		// wait for eds cache update
   712  		retry.UntilSuccessOrFail(t, func() error {
   713  			eps := xdstest.ExtractLocalityLbEndpoints(s.Endpoints(w.proxy))
   714  			for c, want := range w.weightedExpectations {
   715  				got := eps[c]
   716  				if err := xdstest.CompareEndpoints(c, got, want); err != nil {
   717  					return err
   718  				}
   719  			}
   720  			for c, got := range eps {
   721  				want := w.weightedExpectations[c]
   722  				if err := xdstest.CompareEndpoints(c, got, want); err != nil {
   723  					return err
   724  				}
   725  			}
   726  			return nil
   727  		}, retry.Timeout(3*time.Second))
   728  	})
   729  }
   730  
   731  func (w *workload) clusterName(subset string) string {
   732  	name := w.name
   733  	if w.kind == Pod {
   734  		name = fmt.Sprintf("%s.%s.svc.cluster.local", w.name, w.namespace)
   735  	}
   736  	return fmt.Sprintf("outbound|%d|%s|%s", w.port, subset, name)
   737  }
   738  
   739  func (w *workload) kubeObjects() (cluster.ID, []runtime.Object) {
   740  	if w.kind == Pod {
   741  		return w.clusterID, w.buildPodService()
   742  	}
   743  	return "", nil
   744  }
   745  
   746  func (w *workload) configs() []config.Config {
   747  	if w.kind == VirtualMachine {
   748  		return []config.Config{{
   749  			Meta: config.Meta{
   750  				GroupVersionKind:  gvk.ServiceEntry,
   751  				Name:              w.name,
   752  				Namespace:         w.namespace,
   753  				CreationTimestamp: time.Now(),
   754  			},
   755  			Spec: &networking.ServiceEntry{
   756  				Hosts: []string{w.name},
   757  				Ports: []*networking.ServicePort{
   758  					{Number: uint32(w.port), Name: "http", Protocol: "HTTP"},
   759  				},
   760  				Endpoints: []*networking.WorkloadEntry{{
   761  					Address:        w.ip,
   762  					Labels:         w.labels,
   763  					Network:        string(w.metaNetwork),
   764  					ServiceAccount: w.name,
   765  				}},
   766  				Resolution: networking.ServiceEntry_STATIC,
   767  				Location:   networking.ServiceEntry_MESH_INTERNAL,
   768  			},
   769  		}}
   770  	}
   771  	return nil
   772  }
   773  
   774  func (w *workload) setupProxy(s *xds.FakeDiscoveryServer) {
   775  	p := &model.Proxy{
   776  		ID:     strings.Join([]string{w.name, w.namespace}, "."),
   777  		Labels: w.labels,
   778  		Metadata: &model.NodeMetadata{
   779  			Namespace:            w.namespace,
   780  			Network:              w.metaNetwork,
   781  			Labels:               w.labels,
   782  			RequestedNetworkView: w.networkView,
   783  		},
   784  		ConfigNamespace: w.namespace,
   785  	}
   786  	if w.kind == Pod {
   787  		p.Metadata.ClusterID = w.clusterID
   788  	} else {
   789  		p.Metadata.InterceptionMode = "NONE"
   790  	}
   791  	w.proxy = s.SetupProxy(p)
   792  }
   793  
   794  func (w *workload) buildPodService() []runtime.Object {
   795  	baseMeta := metav1.ObjectMeta{
   796  		Name: w.name,
   797  		Labels: labels.Instance{
   798  			"app":                        w.name,
   799  			label.SecurityTlsMode.Name:   model.IstioMutualTLSModeLabel,
   800  			discoveryv1.LabelServiceName: w.name,
   801  		},
   802  		Namespace: w.namespace,
   803  	}
   804  	podMeta := baseMeta
   805  	podMeta.Name = w.name + "-" + rand.String(4)
   806  	for k, v := range w.labels {
   807  		podMeta.Labels[k] = v
   808  	}
   809  
   810  	return []runtime.Object{
   811  		&corev1.Pod{
   812  			ObjectMeta: podMeta,
   813  		},
   814  		&corev1.Service{
   815  			ObjectMeta: baseMeta,
   816  			Spec: corev1.ServiceSpec{
   817  				ClusterIP: "1.2.3.4", // just can't be 0.0.0.0/ClusterIPNone, not used in eds
   818  				Selector:  baseMeta.Labels,
   819  				Ports: []corev1.ServicePort{{
   820  					Port:     w.port,
   821  					Name:     "http",
   822  					Protocol: corev1.ProtocolTCP,
   823  				}},
   824  			},
   825  		},
   826  		&discoveryv1.EndpointSlice{
   827  			ObjectMeta: baseMeta,
   828  			Endpoints: []discoveryv1.Endpoint{{
   829  				Addresses:  []string{w.ip},
   830  				Conditions: discoveryv1.EndpointConditions{},
   831  				Hostname:   nil,
   832  				TargetRef: &corev1.ObjectReference{
   833  					APIVersion: "v1",
   834  					Kind:       "Pod",
   835  					Name:       podMeta.Name,
   836  					Namespace:  podMeta.Namespace,
   837  				},
   838  				DeprecatedTopology: nil,
   839  				NodeName:           nil,
   840  				Zone:               nil,
   841  				Hints:              nil,
   842  			}},
   843  			Ports: []discoveryv1.EndpointPort{{
   844  				Name:     ptr.Of("http"),
   845  				Port:     ptr.Of(w.port),
   846  				Protocol: ptr.Of(corev1.ProtocolTCP),
   847  			}},
   848  		},
   849  	}
   850  }