istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/gateway_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  	"testing"
    24  	"time"
    25  
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	k8sv1 "sigs.k8s.io/gateway-api/apis/v1"
    29  
    30  	"istio.io/istio/pilot/pkg/model/kstatus"
    31  	"istio.io/istio/pkg/config/constants"
    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/framework"
    36  	"istio.io/istio/pkg/test/framework/components/crd"
    37  	"istio.io/istio/pkg/test/framework/components/echo"
    38  	"istio.io/istio/pkg/test/framework/components/echo/check"
    39  	"istio.io/istio/pkg/test/framework/components/istio"
    40  	testKube "istio.io/istio/pkg/test/kube"
    41  	"istio.io/istio/pkg/test/util/assert"
    42  	"istio.io/istio/pkg/test/util/retry"
    43  	ingressutil "istio.io/istio/tests/integration/security/sds_ingress/util"
    44  )
    45  
    46  func TestGateway(t *testing.T) {
    47  	framework.
    48  		NewTest(t).
    49  		Run(func(t framework.TestContext) {
    50  			crd.DeployGatewayAPIOrSkip(t)
    51  
    52  			t.NewSubTest("unmanaged").Run(UnmanagedGatewayTest)
    53  			t.NewSubTest("managed").Run(ManagedGatewayTest)
    54  			t.NewSubTest("managed-owner").Run(ManagedOwnerGatewayTest)
    55  			t.NewSubTest("status").Run(StatusGatewayTest)
    56  			t.NewSubTest("managed-short-name").Run(ManagedGatewayShortNameTest)
    57  		})
    58  }
    59  
    60  func ManagedOwnerGatewayTest(t framework.TestContext) {
    61  	image := fmt.Sprintf("%s/app:%s", t.Settings().Image.Hub, t.Settings().Image.Tag)
    62  	t.ConfigIstio().YAML(apps.Namespace.Name(), fmt.Sprintf(`
    63  apiVersion: v1
    64  kind: Service
    65  metadata:
    66    name: managed-owner-istio
    67  spec:
    68    ports:
    69    - appProtocol: http
    70      name: default
    71      port: 80
    72    selector:
    73      gateway.networking.k8s.io/gateway-name: managed-owner
    74  ---
    75  apiVersion: apps/v1
    76  kind: Deployment
    77  metadata:
    78    name: managed-owner-istio
    79  spec:
    80    selector:
    81      matchLabels:
    82        gateway.networking.k8s.io/gateway-name: managed-owner
    83    replicas: 1
    84    template:
    85      metadata:
    86        labels:
    87          gateway.networking.k8s.io/gateway-name: managed-owner
    88          istio.io/gateway-name: managed-owner
    89      spec:
    90        containers:
    91        - name: fake
    92          image: %s
    93  `, image)).ApplyOrFail(t)
    94  	cls := t.Clusters().Kube().Default()
    95  	fetchFn := testKube.NewSinglePodFetch(cls, apps.Namespace.Name(), "gateway.networking.k8s.io/gateway-name=managed-owner")
    96  	if _, err := testKube.WaitUntilPodsAreReady(fetchFn); err != nil {
    97  		t.Fatal(err)
    98  	}
    99  
   100  	t.ConfigIstio().YAML(apps.Namespace.Name(), `
   101  apiVersion: gateway.networking.k8s.io/v1beta1
   102  kind: Gateway
   103  metadata:
   104    name: managed-owner
   105  spec:
   106    gatewayClassName: istio
   107    listeners:
   108    - name: default
   109      hostname: "*.example.com"
   110      port: 80
   111      protocol: HTTP
   112  `).ApplyOrFail(t)
   113  
   114  	// Make sure Gateway becomes programmed..
   115  	client := t.Clusters().Kube().Default().GatewayAPI().GatewayV1beta1().Gateways(apps.Namespace.Name())
   116  	check := func() error {
   117  		gw, _ := client.Get(context.Background(), "managed-owner", metav1.GetOptions{})
   118  		if gw == nil {
   119  			return fmt.Errorf("failed to find gateway")
   120  		}
   121  		cond := kstatus.GetCondition(gw.Status.Conditions, string(k8sv1.GatewayConditionProgrammed))
   122  		if cond.Status != metav1.ConditionTrue {
   123  			return fmt.Errorf("failed to find programmed condition: %+v", cond)
   124  		}
   125  		if cond.ObservedGeneration != gw.Generation {
   126  			return fmt.Errorf("stale GWC generation: %+v", cond)
   127  		}
   128  		return nil
   129  	}
   130  	retry.UntilSuccessOrFail(t, check)
   131  
   132  	// Make sure we did not overwrite our deployment or service
   133  	dep, err := t.Clusters().Kube().Default().Kube().AppsV1().Deployments(apps.Namespace.Name()).
   134  		Get(context.Background(), "managed-owner-istio", metav1.GetOptions{})
   135  	assert.NoError(t, err)
   136  	assert.Equal(t, dep.Labels[constants.ManagedGatewayLabel], "")
   137  	assert.Equal(t, dep.Spec.Template.Spec.Containers[0].Image, image)
   138  
   139  	svc, err := t.Clusters().Kube().Default().Kube().CoreV1().Services(apps.Namespace.Name()).
   140  		Get(context.Background(), "managed-owner-istio", metav1.GetOptions{})
   141  	assert.NoError(t, err)
   142  	assert.Equal(t, svc.Labels[constants.ManagedGatewayLabel], "")
   143  	assert.Equal(t, svc.Spec.Type, corev1.ServiceTypeClusterIP)
   144  }
   145  
   146  func ManagedGatewayTest(t framework.TestContext) {
   147  	t.ConfigIstio().YAML(apps.Namespace.Name(), `apiVersion: gateway.networking.k8s.io/v1beta1
   148  kind: Gateway
   149  metadata:
   150    name: gateway
   151  spec:
   152    gatewayClassName: istio
   153    listeners:
   154    - name: default
   155      hostname: "*.example.com"
   156      port: 80
   157      protocol: HTTP
   158  ---
   159  apiVersion: gateway.networking.k8s.io/v1beta1
   160  kind: HTTPRoute
   161  metadata:
   162    name: http-1
   163  spec:
   164    parentRefs:
   165    - name: gateway
   166    hostnames: ["bar.example.com"]
   167    rules:
   168    - backendRefs:
   169      - name: b
   170        port: 80
   171  ---
   172  apiVersion: gateway.networking.k8s.io/v1beta1
   173  kind: HTTPRoute
   174  metadata:
   175    name: http-2
   176  spec:
   177    parentRefs:
   178    - name: gateway
   179    hostnames: ["foo.example.com"]
   180    rules:
   181    - backendRefs:
   182      - name: d
   183        port: 80
   184  `).ApplyOrFail(t)
   185  	testCases := []struct {
   186  		check echo.Checker
   187  		from  echo.Instances
   188  		host  string
   189  	}{
   190  		{
   191  			check: check.OK(),
   192  			from:  apps.B,
   193  			host:  "bar.example.com",
   194  		},
   195  		{
   196  			check: check.NotOK(),
   197  			from:  apps.B,
   198  			host:  "bar",
   199  		},
   200  	}
   201  	if t.Settings().EnableDualStack {
   202  		additionalTestCases := []struct {
   203  			check echo.Checker
   204  			from  echo.Instances
   205  			host  string
   206  		}{
   207  			// apps.D hosts a dual-stack service,
   208  			// apps.E hosts an ipv6 only service and
   209  			// apps.B hosts an ipv4 only service
   210  			{
   211  				check: check.OK(),
   212  				from:  apps.D,
   213  				host:  "bar.example.com",
   214  			},
   215  			{
   216  				check: check.OK(),
   217  				from:  apps.E,
   218  				host:  "bar.example.com",
   219  			},
   220  			{
   221  				check: check.OK(),
   222  				from:  apps.E,
   223  				host:  "foo.example.com",
   224  			},
   225  			{
   226  				check: check.OK(),
   227  				from:  apps.D,
   228  				host:  "foo.example.com",
   229  			},
   230  			{
   231  				check: check.OK(),
   232  				from:  apps.B,
   233  				host:  "foo.example.com",
   234  			},
   235  		}
   236  		testCases = append(testCases, additionalTestCases...)
   237  	}
   238  	for _, tc := range testCases {
   239  		t.NewSubTest(fmt.Sprintf("gateway-connectivity-from-%s", tc.from[0].NamespacedName())).Run(func(t framework.TestContext) {
   240  			tc.from[0].CallOrFail(t, echo.CallOptions{
   241  				Port: echo.Port{
   242  					Protocol:    protocol.HTTP,
   243  					ServicePort: 80,
   244  				},
   245  				Scheme: scheme.HTTP,
   246  				HTTP: echo.HTTP{
   247  					Headers: headers.New().WithHost(tc.host).Build(),
   248  				},
   249  				Address: fmt.Sprintf("gateway-istio.%s.svc.cluster.local", apps.Namespace.Name()),
   250  				Check:   tc.check,
   251  			})
   252  		})
   253  	}
   254  }
   255  
   256  func ManagedGatewayShortNameTest(t framework.TestContext) {
   257  	t.ConfigIstio().YAML(apps.Namespace.Name(), `apiVersion: gateway.networking.k8s.io/v1beta1
   258  kind: Gateway
   259  metadata:
   260    name: gateway
   261  spec:
   262    gatewayClassName: istio
   263    listeners:
   264    - name: default
   265      hostname: "bar"
   266      port: 80
   267      protocol: HTTP
   268  ---
   269  apiVersion: gateway.networking.k8s.io/v1beta1
   270  kind: HTTPRoute
   271  metadata:
   272    name: http
   273  spec:
   274    parentRefs:
   275    - name: gateway
   276    rules:
   277    - backendRefs:
   278      - name: b
   279        port: 80
   280  `).ApplyOrFail(t)
   281  	apps.B[0].CallOrFail(t, echo.CallOptions{
   282  		Port:   echo.Port{ServicePort: 80},
   283  		Scheme: scheme.HTTP,
   284  		HTTP: echo.HTTP{
   285  			Headers: headers.New().WithHost("bar").Build(),
   286  		},
   287  		Address: fmt.Sprintf("gateway-istio.%s.svc.cluster.local", apps.Namespace.Name()),
   288  		Check:   check.OK(),
   289  		Retry: echo.Retry{
   290  			Options: []retry.Option{retry.Timeout(time.Minute)},
   291  		},
   292  	})
   293  	apps.B[0].CallOrFail(t, echo.CallOptions{
   294  		Port:   echo.Port{ServicePort: 80},
   295  		Scheme: scheme.HTTP,
   296  		HTTP: echo.HTTP{
   297  			Headers: headers.New().WithHost("bar.example.com").Build(),
   298  		},
   299  		Address: fmt.Sprintf("gateway-istio.%s.svc.cluster.local", apps.Namespace.Name()),
   300  		Check:   check.NotOK(),
   301  		Retry: echo.Retry{
   302  			Options: []retry.Option{retry.Timeout(time.Minute)},
   303  		},
   304  	})
   305  }
   306  
   307  func UnmanagedGatewayTest(t framework.TestContext) {
   308  	ingressutil.CreateIngressKubeSecret(t, "test-gateway-cert-same", ingressutil.TLS, ingressutil.IngressCredentialA,
   309  		false, t.Clusters().Configs()...)
   310  	ingressutil.CreateIngressKubeSecretInNamespace(t, "test-gateway-cert-cross", ingressutil.TLS, ingressutil.IngressCredentialB,
   311  		false, apps.Namespace.Name(), t.Clusters().Configs()...)
   312  
   313  	t.ConfigIstio().
   314  		YAML("", `
   315  apiVersion: gateway.networking.k8s.io/v1beta1
   316  kind: GatewayClass
   317  metadata:
   318    name: custom-istio
   319  spec:
   320    controllerName: istio.io/gateway-controller
   321  `).
   322  		YAML("", fmt.Sprintf(`
   323  apiVersion: gateway.networking.k8s.io/v1beta1
   324  kind: Gateway
   325  metadata:
   326    name: gateway
   327    namespace: istio-system
   328  spec:
   329    addresses:
   330    - value: istio-ingressgateway
   331      type: Hostname
   332    gatewayClassName: custom-istio
   333    listeners:
   334    - name: http
   335      hostname: "*.domain.example"
   336      port: 80
   337      protocol: HTTP
   338      allowedRoutes:
   339        namespaces:
   340          from: All
   341    - name: tcp
   342      port: 31400
   343      protocol: TCP
   344      allowedRoutes:
   345        namespaces:
   346          from: All
   347    - name: tls-cross
   348      hostname: cross-namespace.domain.example
   349      port: 443
   350      protocol: HTTPS
   351      allowedRoutes:
   352        namespaces:
   353          from: All
   354      tls:
   355        mode: Terminate
   356        certificateRefs:
   357        - kind: Secret
   358          name: test-gateway-cert-cross
   359          namespace: "%s"
   360    - name: tls-same
   361      hostname: same-namespace.domain.example
   362      port: 443
   363      protocol: HTTPS
   364      allowedRoutes:
   365        namespaces:
   366          from: All
   367      tls:
   368        mode: Terminate
   369        certificateRefs:
   370        - kind: Secret
   371          name: test-gateway-cert-same
   372  `, apps.Namespace.Name())).
   373  		YAML(apps.Namespace.Name(), `
   374  apiVersion: gateway.networking.k8s.io/v1beta1
   375  kind: HTTPRoute
   376  metadata:
   377    name: http
   378  spec:
   379    hostnames: ["my.domain.example"]
   380    parentRefs:
   381    - name: gateway
   382      namespace: istio-system
   383    rules:
   384    - matches:
   385      - path:
   386          type: PathPrefix
   387          value: /get/
   388      backendRefs:
   389      - name: b
   390        port: 80
   391  ---
   392  apiVersion: gateway.networking.k8s.io/v1alpha2
   393  kind: TCPRoute
   394  metadata:
   395    name: tcp
   396  spec:
   397    parentRefs:
   398    - name: gateway
   399      namespace: istio-system
   400    rules:
   401    - backendRefs:
   402      - name: b
   403        port: 80
   404  ---
   405  apiVersion: gateway.networking.k8s.io/v1beta1
   406  kind: HTTPRoute
   407  metadata:
   408    name: b
   409  spec:
   410    parentRefs:
   411    - group: ""
   412      kind: Service
   413      name: b
   414    - name: gateway
   415      namespace: istio-system
   416    hostnames: ["b"]
   417    rules:
   418    - matches:
   419      - path:
   420          type: PathPrefix
   421          value: /path
   422      filters:
   423      - type: RequestHeaderModifier
   424        requestHeaderModifier:
   425          add:
   426          - name: my-added-header
   427            value: added-value
   428      backendRefs:
   429      - name: b
   430        port: 80
   431  ---
   432  apiVersion: gateway.networking.k8s.io/v1alpha2
   433  kind: GRPCRoute
   434  metadata:
   435    name: grpc
   436  spec:
   437    parentRefs:
   438    - group: ""
   439      kind: Service
   440      name: c
   441    - name: gateway
   442      namespace: istio-system
   443    rules:
   444    - matches:
   445      - method:
   446          method: Echo
   447      filters:
   448      - type: RequestHeaderModifier
   449        requestHeaderModifier:
   450          add:
   451          - name: my-added-header
   452            value: added-grpc-value
   453      backendRefs:
   454      - name: c
   455        port: 7070
   456  ---
   457  apiVersion: gateway.networking.k8s.io/v1beta1
   458  kind: HTTPRoute
   459  metadata:
   460    name: tls-same
   461  spec:
   462    parentRefs:
   463    - name: gateway
   464      sectionName: tls-same
   465      namespace: istio-system
   466    rules:
   467    - backendRefs:
   468      - name: b
   469        port: 80
   470  ---
   471  apiVersion: gateway.networking.k8s.io/v1beta1
   472  kind: HTTPRoute
   473  metadata:
   474    name: tls-cross
   475  spec:
   476    parentRefs:
   477    - name: gateway
   478      sectionName: tls-cross
   479      namespace: istio-system
   480    rules:
   481    - backendRefs:
   482      - name: b
   483        port: 80
   484  `).YAML(apps.Namespace.Name(), fmt.Sprintf(`
   485  apiVersion: gateway.networking.k8s.io/v1beta1
   486  kind: ReferenceGrant
   487  metadata:
   488    name: allow-gateways-to-ref-secrets
   489    namespace: "%s"
   490  spec:
   491    from:
   492    - group: gateway.networking.k8s.io
   493      kind: Gateway
   494      namespace: istio-system
   495    to:
   496    - group: ""
   497      kind: Secret
   498  `, apps.Namespace.Name())).
   499  		ApplyOrFail(t)
   500  	for _, ingr := range istio.IngressesOrFail(t, t) {
   501  		t.NewSubTest(ingr.Cluster().StableName()).Run(func(t framework.TestContext) {
   502  			t.NewSubTest("http").Run(func(t framework.TestContext) {
   503  				paths := []string{"/get", "/get/", "/get/prefix"}
   504  				for _, path := range paths {
   505  					_ = ingr.CallOrFail(t, echo.CallOptions{
   506  						Port: echo.Port{
   507  							Protocol: protocol.HTTP,
   508  						},
   509  						HTTP: echo.HTTP{
   510  							Path:    path,
   511  							Headers: headers.New().WithHost("my.domain.example").Build(),
   512  						},
   513  						Check: check.OK(),
   514  					})
   515  				}
   516  			})
   517  			t.NewSubTest("tcp").Run(func(t framework.TestContext) {
   518  				_ = ingr.CallOrFail(t, echo.CallOptions{
   519  					Port: echo.Port{
   520  						Protocol:    protocol.HTTP,
   521  						ServicePort: 31400,
   522  					},
   523  					HTTP: echo.HTTP{
   524  						Path:    "/",
   525  						Headers: headers.New().WithHost("my.domain.example").Build(),
   526  					},
   527  					Check: check.OK(),
   528  				})
   529  			})
   530  			t.NewSubTest("mesh").Run(func(t framework.TestContext) {
   531  				_ = apps.A[0].CallOrFail(t, echo.CallOptions{
   532  					To:    apps.B,
   533  					Count: 1,
   534  					Port: echo.Port{
   535  						Name: "http",
   536  					},
   537  					HTTP: echo.HTTP{
   538  						Path: "/path",
   539  					},
   540  					Check: check.And(
   541  						check.OK(),
   542  						check.RequestHeader("My-Added-Header", "added-value")),
   543  				})
   544  			})
   545  			t.NewSubTest("mesh-grpc").Run(func(t framework.TestContext) {
   546  				_ = apps.A[0].CallOrFail(t, echo.CallOptions{
   547  					To:    apps.C,
   548  					Count: 1,
   549  					Port: echo.Port{
   550  						Name: "grpc",
   551  					},
   552  					Check: check.And(
   553  						check.OK(),
   554  						check.RequestHeader("My-Added-Header", "added-grpc-value")),
   555  				})
   556  			})
   557  			t.NewSubTest("status").Run(func(t framework.TestContext) {
   558  				retry.UntilSuccessOrFail(t, func() error {
   559  					gwc, err := t.Clusters().Kube().Default().GatewayAPI().GatewayV1beta1().GatewayClasses().Get(context.Background(), "istio", metav1.GetOptions{})
   560  					if err != nil {
   561  						return err
   562  					}
   563  					if s := kstatus.GetCondition(gwc.Status.Conditions, string(k8sv1.GatewayClassConditionStatusAccepted)).Status; s != metav1.ConditionTrue {
   564  						return fmt.Errorf("expected status %q, got %q", metav1.ConditionTrue, s)
   565  					}
   566  					return nil
   567  				})
   568  			})
   569  			t.NewSubTest("tls-same").Run(func(t framework.TestContext) {
   570  				_ = ingr.CallOrFail(t, echo.CallOptions{
   571  					Port: echo.Port{
   572  						Protocol:    protocol.HTTPS,
   573  						ServicePort: 443,
   574  					},
   575  					HTTP: echo.HTTP{
   576  						Path:    "/",
   577  						Headers: headers.New().WithHost("same-namespace.domain.example").Build(),
   578  					},
   579  					Check: check.OK(),
   580  				})
   581  			})
   582  			t.NewSubTest("tls-cross").Run(func(t framework.TestContext) {
   583  				_ = ingr.CallOrFail(t, echo.CallOptions{
   584  					Port: echo.Port{
   585  						Protocol:    protocol.HTTPS,
   586  						ServicePort: 443,
   587  					},
   588  					HTTP: echo.HTTP{
   589  						Path:    "/",
   590  						Headers: headers.New().WithHost("cross-namespace.domain.example").Build(),
   591  					},
   592  					Check: check.OK(),
   593  				})
   594  			})
   595  		})
   596  	}
   597  }
   598  
   599  func StatusGatewayTest(t framework.TestContext) {
   600  	client := t.Clusters().Kube().Default().GatewayAPI().GatewayV1beta1().GatewayClasses()
   601  
   602  	check := func() error {
   603  		gwc, _ := client.Get(context.Background(), "istio", metav1.GetOptions{})
   604  		if gwc == nil {
   605  			return fmt.Errorf("failed to find GatewayClass istio")
   606  		}
   607  		cond := kstatus.GetCondition(gwc.Status.Conditions, string(k8sv1.GatewayClassConditionStatusAccepted))
   608  		if cond.Status != metav1.ConditionTrue {
   609  			return fmt.Errorf("failed to find accepted condition: %+v", cond)
   610  		}
   611  		if cond.ObservedGeneration != gwc.Generation {
   612  			return fmt.Errorf("stale GWC generation: %+v", cond)
   613  		}
   614  		return nil
   615  	}
   616  	retry.UntilSuccessOrFail(t, check)
   617  
   618  	// Wipe out the status
   619  	gwc, _ := client.Get(context.Background(), "istio", metav1.GetOptions{})
   620  	gwc.Status.Conditions = nil
   621  	client.Update(context.Background(), gwc, metav1.UpdateOptions{})
   622  	// It should be added back
   623  	retry.UntilSuccessOrFail(t, check)
   624  }
   625  
   626  // Verify that the envoy readiness probes are reachable at
   627  // https://GatewaySvcIP:15021/healthz/ready . This is being explicitly done
   628  // to make sure, in dual-stack scenarios both v4 and v6 probes are reachable.
   629  func TestGatewayReadinessProbes(t *testing.T) {
   630  	// nolint: staticcheck
   631  	framework.NewTest(t).
   632  		RequiresSingleCluster().
   633  		RequiresLocalControlPlane().
   634  		Run(func(t framework.TestContext) {
   635  			c := t.Clusters().Default()
   636  			var svc *corev1.Service
   637  			svc, _, err := testKube.WaitUntilServiceEndpointsAreReady(c.Kube(), "istio-system", "istio-ingressgateway")
   638  			if err != nil {
   639  				t.Fatalf("error getting ingress gateway svc ips: %v", err)
   640  			}
   641  			for _, ip := range svc.Spec.ClusterIPs {
   642  				t.NewSubTest("gateway-readiness-probe-" + ip).Run(func(t framework.TestContext) {
   643  					apps.External.All[0].CallOrFail(t, echo.CallOptions{
   644  						Address: ip,
   645  						Port:    echo.Port{ServicePort: 15021},
   646  						Scheme:  scheme.HTTP,
   647  						HTTP: echo.HTTP{
   648  							Path: "/healthz/ready",
   649  						},
   650  						Check: check.And(
   651  							check.Status(200),
   652  						),
   653  					})
   654  				})
   655  			}
   656  		})
   657  }
   658  
   659  // Verify that the envoy metrics endpoints are reachable at
   660  // https://GatewayPodIP:15090/stats/prometheus . This is being explicitly done
   661  // to make sure, in dual-stack scenarios both v4 and v6 probes are reachable.
   662  func TestGatewayMetricsEndpoints(t *testing.T) {
   663  	// nolint: staticcheck
   664  	framework.NewTest(t).
   665  		RequiresSingleCluster().
   666  		RequiresLocalControlPlane().
   667  		Run(func(t framework.TestContext) {
   668  			c := t.Clusters().Default()
   669  			podIPs, err := i.PodIPsFor(c, i.Settings().SystemNamespace, "app=istio-ingressgateway")
   670  			if err != nil {
   671  				t.Fatalf("error getting ingress gateway pod ips: %v", err)
   672  			}
   673  			for _, ip := range podIPs {
   674  				t.NewSubTest("gateway-metrics-endpoints-" + ip.IP).Run(func(t framework.TestContext) {
   675  					apps.External.All[0].CallOrFail(t, echo.CallOptions{
   676  						Address: ip.IP,
   677  						Port:    echo.Port{ServicePort: 15090},
   678  						Scheme:  scheme.HTTP,
   679  						HTTP: echo.HTTP{
   680  							Path: "/stats/prometheus",
   681  						},
   682  						Check: check.And(
   683  							check.Status(200),
   684  						),
   685  					})
   686  				})
   687  			}
   688  		})
   689  }