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 }