istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/ingress_test.go (about) 1 //go:build integ 2 // +build integ 3 4 // Copyright Istio Authors 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 18 package pilot 19 20 import ( 21 "context" 22 "fmt" 23 "net" 24 "net/http" 25 "os" 26 "path/filepath" 27 "testing" 28 "time" 29 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 32 "istio.io/istio/pkg/config/protocol" 33 "istio.io/istio/pkg/http/headers" 34 "istio.io/istio/pkg/test/echo/common/scheme" 35 "istio.io/istio/pkg/test/env" 36 "istio.io/istio/pkg/test/framework" 37 kubecluster "istio.io/istio/pkg/test/framework/components/cluster/kube" 38 "istio.io/istio/pkg/test/framework/components/echo" 39 "istio.io/istio/pkg/test/framework/components/echo/check" 40 "istio.io/istio/pkg/test/framework/components/environment/kube" 41 "istio.io/istio/pkg/test/framework/components/istio" 42 "istio.io/istio/pkg/test/framework/components/namespace" 43 "istio.io/istio/pkg/test/framework/resource/config/apply" 44 "istio.io/istio/pkg/test/helm" 45 kubetest "istio.io/istio/pkg/test/kube" 46 "istio.io/istio/pkg/test/util/retry" 47 helmtest "istio.io/istio/tests/integration/helm" 48 ingressutil "istio.io/istio/tests/integration/security/sds_ingress/util" 49 ) 50 51 func skipIfIngressClassUnsupported(t framework.TestContext) { 52 if !t.Clusters().Default().MinKubeVersion(18) { 53 t.Skip("IngressClass not supported") 54 } 55 } 56 57 // TestIngress tests that we can route using standard Kubernetes Ingress objects. 58 func TestIngress(t *testing.T) { 59 framework. 60 NewTest(t). 61 Run(func(t framework.TestContext) { 62 skipIfIngressClassUnsupported(t) 63 // Set up secret contain some TLS certs for *.example.com 64 // we will define one for foo.example.com and one for bar.example.com, to ensure both can co-exist 65 ingressutil.CreateIngressKubeSecret(t, "k8s-ingress-secret-foo", ingressutil.TLS, ingressutil.IngressCredentialA, false, t.Clusters().Kube()...) 66 ingressutil.CreateIngressKubeSecret(t, "k8s-ingress-secret-bar", ingressutil.TLS, ingressutil.IngressCredentialB, false, t.Clusters().Kube()...) 67 68 ingressClassConfig := ` 69 apiVersion: networking.k8s.io/v1 70 kind: IngressClass 71 metadata: 72 name: istio-test 73 spec: 74 controller: istio.io/ingress-controller` 75 76 ingressConfigTemplate := ` 77 apiVersion: networking.k8s.io/v1 78 kind: Ingress 79 metadata: 80 name: %s 81 spec: 82 ingressClassName: %s 83 tls: 84 - hosts: ["foo.example.com"] 85 secretName: k8s-ingress-secret-foo 86 - hosts: ["bar.example.com"] 87 secretName: k8s-ingress-secret-bar 88 rules: 89 - http: 90 paths: 91 - backend: 92 service: 93 name: b 94 port: 95 name: http 96 path: %s/namedport 97 pathType: ImplementationSpecific 98 - backend: 99 service: 100 name: b 101 port: 102 number: 80 103 path: %s 104 pathType: ImplementationSpecific 105 - backend: 106 service: 107 name: b 108 port: 109 number: 80 110 path: %s 111 pathType: Prefix 112 ` 113 114 successChecker := check.And(check.OK(), check.ReachedClusters(t.AllClusters(), apps.B.Clusters())) 115 failureChecker := check.Status(http.StatusNotFound) 116 count := 2 * t.Clusters().Len() 117 118 // TODO check all clusters were hit 119 cases := []struct { 120 name string 121 path string 122 prefixPath string 123 call echo.CallOptions 124 }{ 125 { 126 // Basic HTTP call 127 name: "http", 128 call: echo.CallOptions{ 129 Port: echo.Port{ 130 Protocol: protocol.HTTP, 131 }, 132 HTTP: echo.HTTP{ 133 Path: "/test", 134 Headers: headers.New().WithHost("server").Build(), 135 }, 136 Check: successChecker, 137 Count: count, 138 }, 139 path: "/test", 140 prefixPath: "/prefix", 141 }, 142 { 143 // Prefix /prefix/should MATCHES prefix/should/match 144 name: "http-prefix-matches-subpath", 145 call: echo.CallOptions{ 146 Port: echo.Port{ 147 Protocol: protocol.HTTP, 148 }, 149 HTTP: echo.HTTP{ 150 Path: "/prefix/should/match", 151 Headers: headers.New().WithHost("server").Build(), 152 }, 153 Check: successChecker, 154 Count: count, 155 }, 156 path: "/test", 157 prefixPath: "/prefix/should", 158 }, 159 { 160 // Prefix /prefix/test/ should match path /prefix/test 161 name: "http-prefix-matches-without-trailing-backslash", 162 call: echo.CallOptions{ 163 Port: echo.Port{ 164 Protocol: protocol.HTTP, 165 }, 166 HTTP: echo.HTTP{ 167 Path: "/prefix/test", 168 Headers: headers.New().WithHost("server").Build(), 169 }, 170 Check: successChecker, 171 Count: count, 172 }, 173 path: "/test", 174 prefixPath: "/prefix/test/", 175 }, 176 { 177 // Prefix /prefix/test should match /prefix/test/ 178 name: "http-prefix-matches-trailing-blackslash", 179 call: echo.CallOptions{ 180 Port: echo.Port{ 181 Protocol: protocol.HTTP, 182 }, 183 HTTP: echo.HTTP{ 184 Path: "/prefix/test/", 185 Headers: headers.New().WithHost("server").Build(), 186 }, 187 Check: successChecker, 188 Count: count, 189 }, 190 path: "/test", 191 prefixPath: "/prefix/test", 192 }, 193 { 194 // Prefix /prefix/test should NOT match /prefix/testrandom 195 name: "http-prefix-should-not-match-path-continuation", 196 call: echo.CallOptions{ 197 Port: echo.Port{ 198 Protocol: protocol.HTTP, 199 }, 200 HTTP: echo.HTTP{ 201 Path: "/prefix/testrandom/", 202 Headers: headers.New().WithHost("server").Build(), 203 }, 204 Check: failureChecker, 205 Count: count, 206 }, 207 path: "/test", 208 prefixPath: "/prefix/test", 209 }, 210 { 211 // Prefix / should match any path 212 name: "http-root-prefix-should-match-random-path", 213 call: echo.CallOptions{ 214 Port: echo.Port{ 215 Protocol: protocol.HTTP, 216 }, 217 HTTP: echo.HTTP{ 218 Path: "/testrandom", 219 Headers: headers.New().WithHost("server").Build(), 220 }, 221 Check: successChecker, 222 Count: count, 223 }, 224 path: "/test", 225 prefixPath: "/", 226 }, 227 { 228 // Basic HTTPS call for foo. CaCert matches the secret 229 name: "https-foo", 230 call: echo.CallOptions{ 231 Port: echo.Port{ 232 Protocol: protocol.HTTPS, 233 }, 234 HTTP: echo.HTTP{ 235 Path: "/test", 236 Headers: headers.New().WithHost("foo.example.com").Build(), 237 }, 238 TLS: echo.TLS{ 239 CaCert: ingressutil.IngressCredentialA.CaCert, 240 }, 241 Check: successChecker, 242 Count: count, 243 }, 244 path: "/test", 245 prefixPath: "/prefix", 246 }, 247 { 248 // Basic HTTPS call for bar. CaCert matches the secret 249 name: "https-bar", 250 call: echo.CallOptions{ 251 Port: echo.Port{ 252 Protocol: protocol.HTTPS, 253 }, 254 HTTP: echo.HTTP{ 255 Path: "/test", 256 Headers: headers.New().WithHost("bar.example.com").Build(), 257 }, 258 TLS: echo.TLS{ 259 CaCert: ingressutil.IngressCredentialB.CaCert, 260 }, 261 Check: successChecker, 262 Count: count, 263 }, 264 path: "/test", 265 prefixPath: "/prefix", 266 }, 267 { 268 // HTTPS call for bar with namedport route. CaCert matches the secret 269 name: "https-namedport", 270 call: echo.CallOptions{ 271 Port: echo.Port{ 272 Protocol: protocol.HTTPS, 273 }, 274 HTTP: echo.HTTP{ 275 Path: "/test/namedport", 276 Headers: headers.New().WithHost("bar.example.com").Build(), 277 }, 278 TLS: echo.TLS{ 279 CaCert: ingressutil.IngressCredentialB.CaCert, 280 }, 281 Check: successChecker, 282 Count: count, 283 }, 284 path: "/test", 285 prefixPath: "/prefix", 286 }, 287 } 288 289 for _, ingr := range istio.IngressesOrFail(t, t) { 290 ingr := ingr 291 t.NewSubTestf("from %s", ingr.Cluster().StableName()).Run(func(t framework.TestContext) { 292 for _, c := range cases { 293 c := c 294 t.NewSubTest(c.name).Run(func(t framework.TestContext) { 295 if err := t.ConfigIstio().YAML(apps.Namespace.Name(), ingressClassConfig, 296 fmt.Sprintf(ingressConfigTemplate, "ingress", "istio-test", c.path, c.path, c.prefixPath)). 297 Apply(); err != nil { 298 t.Fatal(err) 299 } 300 c.call.Retry.Options = []retry.Option{ 301 retry.Delay(500 * time.Millisecond), 302 retry.Timeout(time.Minute * 2), 303 } 304 ingr.CallOrFail(t, c.call) 305 }) 306 } 307 }) 308 } 309 310 defaultIngress := istio.DefaultIngressOrFail(t, t) 311 t.NewSubTest("status").Run(func(t framework.TestContext) { 312 if !t.Environment().(*kube.Environment).Settings().LoadBalancerSupported { 313 t.Skip("ingress status not supported without load balancer") 314 } 315 if err := t.ConfigIstio().YAML(apps.Namespace.Name(), ingressClassConfig, 316 fmt.Sprintf(ingressConfigTemplate, "ingress", "istio-test", "/test", "/test", "/test")). 317 Apply(); err != nil { 318 t.Fatal(err) 319 } 320 321 hosts, _ := defaultIngress.HTTPAddresses() 322 for _, host := range hosts { 323 hostIsIP := net.ParseIP(host).String() != "<nil>" 324 ingressHostFound := false 325 actualHosts := []string{} 326 retry.UntilSuccessOrFail(t, func() error { 327 ing, err := t.Clusters().Default().Kube().NetworkingV1().Ingresses(apps.Namespace.Name()).Get(context.Background(), "ingress", metav1.GetOptions{}) 328 if err != nil { 329 return err 330 } 331 if len(ing.Status.LoadBalancer.Ingress) < 1 { 332 return fmt.Errorf("unexpected ingress status, ingress is empty") 333 } 334 for _, ingress := range ing.Status.LoadBalancer.Ingress { 335 got := ingress.Hostname 336 if hostIsIP { 337 got = ingress.IP 338 } 339 actualHosts = append(actualHosts, got) 340 if got == host { 341 ingressHostFound = true 342 break 343 } 344 } 345 if !ingressHostFound { 346 return fmt.Errorf("unexpected ingress status, got %+v want %v", actualHosts, host) 347 } 348 return nil 349 }, retry.Timeout(time.Second*90)) 350 } 351 }) 352 353 // setup another ingress pointing to a different route; the ingress will have an ingress class that should be targeted at first 354 const updateIngressName = "update-test-ingress" 355 if err := t.ConfigIstio().YAML(apps.Namespace.Name(), ingressClassConfig, 356 fmt.Sprintf(ingressConfigTemplate, updateIngressName, "istio-test", "/update-test", "/update-test", "/update-test")). 357 Apply(); err != nil { 358 t.Fatal(err) 359 } 360 // these cases make sure that when new Ingress configs are applied our controller picks up on them 361 // and updates the accessible ingress-gateway routes accordingly 362 ingressUpdateCases := []struct { 363 name string 364 ingressClass string 365 path string 366 call echo.CallOptions 367 }{ 368 // Ensure we get a 200 initially 369 { 370 name: "initial state", 371 ingressClass: "istio-test", 372 path: "/update-test", 373 call: echo.CallOptions{ 374 Port: echo.Port{ 375 Protocol: protocol.HTTP, 376 }, 377 HTTP: echo.HTTP{ 378 Path: "/update-test", 379 Headers: headers.New().WithHost("server").Build(), 380 }, 381 Check: check.OK(), 382 }, 383 }, 384 { 385 name: "update-class-not-istio", 386 ingressClass: "not-istio", 387 path: "/update-test", 388 call: echo.CallOptions{ 389 Port: echo.Port{ 390 Protocol: protocol.HTTP, 391 }, 392 HTTP: echo.HTTP{ 393 Path: "/update-test", 394 Headers: headers.New().WithHost("server").Build(), 395 }, 396 Check: func(result echo.CallResult, err error) error { 397 if err != nil { 398 return nil 399 } 400 401 return check.Status(http.StatusNotFound).Check(result, nil) 402 }, 403 }, 404 }, 405 { 406 name: "update-class-istio", 407 ingressClass: "istio-test", 408 path: "/update-test", 409 call: echo.CallOptions{ 410 Port: echo.Port{ 411 Protocol: protocol.HTTP, 412 }, 413 HTTP: echo.HTTP{ 414 Path: "/update-test", 415 Headers: headers.New().WithHost("server").Build(), 416 }, 417 Check: check.OK(), 418 }, 419 }, 420 { 421 name: "update-path", 422 ingressClass: "istio-test", 423 path: "/updated", 424 call: echo.CallOptions{ 425 Port: echo.Port{ 426 Protocol: protocol.HTTP, 427 }, 428 HTTP: echo.HTTP{ 429 Path: "/updated", 430 Headers: headers.New().WithHost("server").Build(), 431 }, 432 Check: check.OK(), 433 }, 434 }, 435 } 436 437 for _, c := range ingressUpdateCases { 438 c := c 439 updatedIngress := fmt.Sprintf(ingressConfigTemplate, updateIngressName, c.ingressClass, c.path, c.path, c.path) 440 t.ConfigIstio().YAML(apps.Namespace.Name(), updatedIngress).ApplyOrFail(t) 441 t.NewSubTest(c.name).Run(func(t framework.TestContext) { 442 c.call.Retry.Options = []retry.Option{retry.Timeout(time.Minute)} 443 defaultIngress.CallOrFail(t, c.call) 444 }) 445 } 446 }) 447 } 448 449 // TestCustomGateway deploys a simple gateway deployment, that is fully injected, and verifies it can startup and send traffic 450 func TestCustomGateway(t *testing.T) { 451 framework. 452 NewTest(t). 453 Run(func(t framework.TestContext) { 454 inject := false 455 if t.Settings().Compatibility { 456 inject = true 457 } 458 injectLabel := `sidecar.istio.io/inject: "true"` 459 if t.Settings().Revisions.Default() != "" { 460 injectLabel = fmt.Sprintf(`istio.io/rev: "%v"`, t.Settings().Revisions.Default()) 461 } 462 463 templateParams := map[string]string{ 464 "imagePullSecret": t.Settings().Image.PullSecretNameOrFail(t), 465 "injectLabel": injectLabel, 466 "host": apps.A.Config().ClusterLocalFQDN(), 467 "imagePullPolicy": t.Settings().Image.PullPolicy, 468 } 469 470 t.NewSubTest("minimal").Run(func(t framework.TestContext) { 471 gatewayNs := namespace.NewOrFail(t, t, namespace.Config{Prefix: "custom-gateway-minimal", Inject: inject}) 472 _ = t.ConfigIstio().Eval(gatewayNs.Name(), templateParams, `apiVersion: v1 473 kind: Service 474 metadata: 475 name: custom-gateway 476 labels: 477 istio: custom 478 spec: 479 ports: 480 - port: 80 481 targetPort: 8080 482 name: http 483 selector: 484 istio: custom 485 --- 486 apiVersion: apps/v1 487 kind: Deployment 488 metadata: 489 name: custom-gateway 490 spec: 491 selector: 492 matchLabels: 493 istio: custom 494 template: 495 metadata: 496 annotations: 497 inject.istio.io/templates: gateway 498 labels: 499 istio: custom 500 {{ .injectLabel }} 501 spec: 502 {{- if ne .imagePullSecret "" }} 503 imagePullSecrets: 504 - name: {{ .imagePullSecret }} 505 {{- end }} 506 containers: 507 - name: istio-proxy 508 image: auto 509 imagePullPolicy: {{ .imagePullPolicy }} 510 --- 511 apiVersion: networking.istio.io/v1alpha3 512 kind: Gateway 513 metadata: 514 name: app 515 spec: 516 selector: 517 istio: custom 518 servers: 519 - port: 520 number: 80 521 name: http 522 protocol: HTTP 523 hosts: 524 - "*" 525 --- 526 apiVersion: networking.istio.io/v1alpha3 527 kind: VirtualService 528 metadata: 529 name: app 530 spec: 531 hosts: 532 - "*" 533 gateways: 534 - app 535 http: 536 - route: 537 - destination: 538 host: {{ .host }} 539 port: 540 number: 80 541 `).Apply(apply.NoCleanup) 542 cs := t.Clusters().Default().(*kubecluster.Cluster) 543 retry.UntilSuccessOrFail(t, func() error { 544 _, err := kubetest.CheckPodsAreReady(kubetest.NewPodFetch(cs, gatewayNs.Name(), "istio=custom")) 545 return err 546 }, retry.Timeout(time.Minute*2)) 547 apps.B[0].CallOrFail(t, echo.CallOptions{ 548 Port: echo.Port{ServicePort: 80}, 549 Scheme: scheme.HTTP, 550 Address: fmt.Sprintf("custom-gateway.%s.svc.cluster.local", gatewayNs.Name()), 551 Check: check.OK(), 552 }) 553 }) 554 // TODO we could add istioctl as well, but the framework adds a bunch of stuff beyond just `istioctl install` 555 // that mess with certs, multicluster, etc 556 t.NewSubTest("helm").Run(func(t framework.TestContext) { 557 gatewayNs := namespace.NewOrFail(t, t, namespace.Config{Prefix: "custom-gateway-helm", Inject: inject}) 558 d := filepath.Join(t.TempDir(), "gateway-values.yaml") 559 rev := "" 560 if t.Settings().Revisions.Default() != "" { 561 rev = t.Settings().Revisions.Default() 562 } 563 os.WriteFile(d, []byte(fmt.Sprintf(` 564 revision: %v 565 gateways: 566 istio-ingressgateway: 567 name: custom-gateway-helm 568 injectionTemplate: gateway 569 type: ClusterIP # LoadBalancer is slow and not necessary for this tests 570 autoscaleMax: 1 571 resources: 572 requests: 573 cpu: 10m 574 memory: 40Mi 575 labels: 576 istio: custom-gateway-helm 577 `, rev)), 0o644) 578 cs := t.Clusters().Default().(*kubecluster.Cluster) 579 h := helm.New(cs.Filename()) 580 // Install ingress gateway chart 581 if err := h.InstallChart("ingress", filepath.Join(env.IstioSrc, "manifests/charts/gateways/istio-ingress"), gatewayNs.Name(), 582 d, helmtest.Timeout); err != nil { 583 t.Fatal(err) 584 } 585 retry.UntilSuccessOrFail(t, func() error { 586 _, err := kubetest.CheckPodsAreReady(kubetest.NewPodFetch(cs, gatewayNs.Name(), "istio=custom-gateway-helm")) 587 return err 588 }, retry.Timeout(time.Minute*2), retry.Delay(time.Millisecond*500)) 589 _ = t.ConfigIstio().YAML(gatewayNs.Name(), fmt.Sprintf(`apiVersion: networking.istio.io/v1alpha3 590 kind: Gateway 591 metadata: 592 name: app 593 spec: 594 selector: 595 istio: custom-gateway-helm 596 servers: 597 - port: 598 number: 80 599 name: http 600 protocol: HTTP 601 hosts: 602 - "*" 603 --- 604 apiVersion: networking.istio.io/v1alpha3 605 kind: VirtualService 606 metadata: 607 name: app 608 spec: 609 hosts: 610 - "*" 611 gateways: 612 - app 613 http: 614 - route: 615 - destination: 616 host: %s 617 port: 618 number: 80 619 `, apps.A.Config().ClusterLocalFQDN())).Apply(apply.NoCleanup) 620 apps.B[0].CallOrFail(t, echo.CallOptions{ 621 Port: echo.Port{ServicePort: 80}, 622 Scheme: scheme.HTTP, 623 Address: fmt.Sprintf("custom-gateway-helm.%s.svc.cluster.local", gatewayNs.Name()), 624 Check: check.OK(), 625 }) 626 }) 627 t.NewSubTest("helm-simple").Run(func(t framework.TestContext) { 628 gatewayNs := namespace.NewOrFail(t, t, namespace.Config{Prefix: "custom-gateway-helm", Inject: inject}) 629 d := filepath.Join(t.TempDir(), "gateway-values.yaml") 630 rev := "" 631 if t.Settings().Revisions.Default() != "" { 632 rev = t.Settings().Revisions.Default() 633 } 634 os.WriteFile(d, []byte(fmt.Sprintf(` 635 revision: %q 636 service: 637 type: ClusterIP # LoadBalancer is slow and not necessary for this tests 638 autoscaling: 639 enabled: false 640 resources: 641 requests: 642 cpu: 10m 643 memory: 40Mi 644 `, rev)), 0o644) 645 cs := t.Clusters().Default().(*kubecluster.Cluster) 646 h := helm.New(cs.Filename()) 647 // Install ingress gateway chart 648 if err := h.InstallChart("helm-simple", filepath.Join(env.IstioSrc, "manifests/charts/gateway"), gatewayNs.Name(), 649 d, helmtest.Timeout); err != nil { 650 t.Fatal(err) 651 } 652 retry.UntilSuccessOrFail(t, func() error { 653 _, err := kubetest.CheckPodsAreReady(kubetest.NewPodFetch(cs, gatewayNs.Name(), "istio=helm-simple")) 654 return err 655 }, retry.Timeout(time.Minute*2), retry.Delay(time.Millisecond*500)) 656 _ = t.ConfigIstio().YAML(gatewayNs.Name(), fmt.Sprintf(`apiVersion: networking.istio.io/v1alpha3 657 kind: Gateway 658 metadata: 659 name: app 660 spec: 661 selector: 662 istio: helm-simple 663 servers: 664 - port: 665 number: 80 666 name: http 667 protocol: HTTP 668 hosts: 669 - "*" 670 --- 671 apiVersion: networking.istio.io/v1alpha3 672 kind: VirtualService 673 metadata: 674 name: app 675 spec: 676 hosts: 677 - "*" 678 gateways: 679 - app 680 http: 681 - route: 682 - destination: 683 host: %s 684 port: 685 number: 80 686 `, apps.A.Config().ClusterLocalFQDN())).Apply(apply.NoCleanup) 687 apps.B[0].CallOrFail(t, echo.CallOptions{ 688 Port: echo.Port{ServicePort: 80}, 689 Scheme: scheme.HTTP, 690 Address: fmt.Sprintf("helm-simple.%s.svc.cluster.local", gatewayNs.Name()), 691 Check: check.OK(), 692 }) 693 }) 694 }) 695 }