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 }