istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/telemetry/policy/helper_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors. All Rights Reserved.
     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 policy
    19  
    20  import (
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"strconv"
    25  	"testing"
    26  
    27  	"istio.io/api/annotation"
    28  	"istio.io/istio/pkg/config/protocol"
    29  	"istio.io/istio/pkg/http/headers"
    30  	"istio.io/istio/pkg/test/echo/common"
    31  	"istio.io/istio/pkg/test/env"
    32  	"istio.io/istio/pkg/test/framework"
    33  	"istio.io/istio/pkg/test/framework/components/echo"
    34  	"istio.io/istio/pkg/test/framework/components/echo/deployment"
    35  	"istio.io/istio/pkg/test/framework/components/environment/kube"
    36  	"istio.io/istio/pkg/test/framework/components/namespace"
    37  	"istio.io/istio/pkg/test/framework/components/prometheus"
    38  	"istio.io/istio/pkg/test/util/tmpl"
    39  	util "istio.io/istio/tests/integration/telemetry"
    40  )
    41  
    42  const (
    43  	// ServiceEntry is used to create conflicts on various ports
    44  	// As defined below, the tcp-conflict and https-conflict ports are 9443 and 9091
    45  	ServiceEntry = `
    46  apiVersion: networking.istio.io/v1alpha3
    47  kind: ServiceEntry
    48  metadata:
    49    name: http
    50  spec:
    51    hosts:
    52    - istio.io
    53    location: MESH_EXTERNAL
    54    ports:
    55    - name: http-for-https
    56      number: 9443
    57      protocol: HTTP
    58    - name: http-for-tcp
    59      number: 9091
    60      protocol: HTTP
    61    resolution: DNS
    62  `
    63  	SidecarScope = `
    64  apiVersion: networking.istio.io/v1alpha3
    65  kind: Sidecar
    66  metadata:
    67    name: restrict-to-service-entry-namespace
    68  spec:
    69    egress:
    70    - hosts:
    71      - "{{.ImportNamespace}}/*"
    72      - "istio-system/*"
    73    outboundTrafficPolicy:
    74      mode: "{{.TrafficPolicyMode}}"
    75  `
    76  
    77  	Gateway = `
    78  apiVersion: networking.istio.io/v1alpha3
    79  kind: Gateway
    80  metadata:
    81    name: istio-egressgateway
    82  spec:
    83    selector:
    84      istio: egressgateway
    85    servers:
    86    - port:
    87        number: 80
    88        name: http
    89        protocol: HTTP
    90      hosts:
    91      - "some-external-site.com"
    92  ---
    93  apiVersion: networking.istio.io/v1alpha3
    94  kind: VirtualService
    95  metadata:
    96    name: route-via-egressgateway
    97  spec:
    98    hosts:
    99      - "some-external-site.com"
   100    gateways:
   101    - istio-egressgateway
   102    - mesh
   103    http:
   104      - match:
   105        - gateways:
   106          - mesh # from sidecars, route to egress gateway service
   107          port: 80
   108        route:
   109        - destination:
   110            host: istio-egressgateway.istio-system.svc.cluster.local
   111            port:
   112              number: 80
   113          weight: 100
   114      - match:
   115        - gateways:
   116          - istio-egressgateway
   117          port: 80
   118        route:
   119        - destination:
   120            host: some-external-site.com
   121        headers:
   122          request:
   123            add:
   124              handled-by-egress-gateway: "true"
   125  ---
   126  apiVersion: networking.istio.io/v1alpha3
   127  kind: ServiceEntry
   128  metadata:
   129    name: ext-service-entry
   130  spec:
   131    hosts:
   132    - "some-external-site.com"
   133    location: MESH_EXTERNAL
   134    endpoints:
   135    - address: destination.{{.AppNamespace}}.svc.cluster.local
   136      network: external
   137    ports:
   138    - number: 80
   139      name: http
   140    resolution: DNS
   141  `
   142  )
   143  
   144  // TestCase represents what is being tested
   145  type TestCase struct {
   146  	Name     string
   147  	PortName string
   148  	HTTP2    bool
   149  	Host     string
   150  	Expected Expected
   151  }
   152  
   153  // Expected contains the metric and query to run against
   154  // prometheus to validate that expected telemetry information was gathered;
   155  // as well as the http response code
   156  type Expected struct {
   157  	Query           prometheus.Query
   158  	StatusCode      int
   159  	Metric          string
   160  	PromQueryFormat string
   161  	Protocol        string
   162  	RequestHeaders  map[string]string
   163  }
   164  
   165  // TrafficPolicy is the mode of the outbound traffic policy to use
   166  // when configuring the sidecar for the client
   167  type TrafficPolicy string
   168  
   169  const (
   170  	AllowAny     TrafficPolicy = "ALLOW_ANY"
   171  	RegistryOnly TrafficPolicy = "REGISTRY_ONLY"
   172  )
   173  
   174  // String implements fmt.Stringer
   175  func (t TrafficPolicy) String() string {
   176  	return string(t)
   177  }
   178  
   179  // We want to test "external" traffic. To do this without actually hitting an external endpoint,
   180  // we can import only the service namespace, so the apps are not known
   181  func createSidecarScope(t framework.TestContext, tPolicy TrafficPolicy, appsNamespace namespace.Instance, serviceNamespace namespace.Instance) {
   182  	args := map[string]string{"ImportNamespace": serviceNamespace.Name(), "TrafficPolicyMode": tPolicy.String()}
   183  	if err := t.ConfigIstio().Eval(appsNamespace.Name(), args, SidecarScope).Apply(); err != nil {
   184  		t.Errorf("failed to apply service entries: %v", err)
   185  	}
   186  }
   187  
   188  func mustReadCert(t framework.TestContext, f string) string {
   189  	t.Helper()
   190  	b, err := os.ReadFile(path.Join(env.IstioSrc, "tests/testdata/certs", f))
   191  	if err != nil {
   192  		t.Fatalf("failed to read %v: %v", f, err)
   193  	}
   194  	return string(b)
   195  }
   196  
   197  // We want to test "external" traffic. To do this without actually hitting an external endpoint,
   198  // we can import only the service namespace, so the apps are not known
   199  func createGateway(t framework.TestContext, appsNamespace namespace.Instance, serviceNamespace namespace.Instance) {
   200  	t.Helper()
   201  	b := tmpl.EvaluateOrFail(t, Gateway, map[string]string{"AppNamespace": appsNamespace.Name()})
   202  	if err := t.ConfigIstio().YAML(serviceNamespace.Name(), b).Apply(); err != nil {
   203  		t.Fatalf("failed to apply gateway: %v. template: %v", err, b)
   204  	}
   205  }
   206  
   207  // TODO support native environment for registry only/gateway. Blocked by #13177 because the listeners for native use static
   208  // routes and this test relies on the dynamic routes sent through pilot to allow external traffic.
   209  
   210  func RunExternalRequest(t *testing.T, cases []*TestCase, prometheus prometheus.Instance, mode TrafficPolicy) {
   211  	t.Helper()
   212  	// Testing of Blackhole and Passthrough clusters:
   213  	// Setup of environment:
   214  	// 1. client and destination are deployed to app-1-XXXX namespace
   215  	// 2. client is restricted to talk to destination via Sidecar scope where outbound policy is set (ALLOW_ANY, REGISTRY_ONLY)
   216  	//    and clients' egress can only be to service-2-XXXX/* and istio-system/*
   217  	// 3. a namespace service-2-YYYY is created
   218  	// 4. A gateway is put in service-2-YYYY where its host is set for some-external-site.com on port 80 and 443
   219  	// 3. a VirtualService is also created in service-2-XXXX to:
   220  	//    a) route requests for some-external-site.com to the istio-egressgateway
   221  	//       * if the request on port 80, then it will add an http header `handled-by-egress-gateway`
   222  	//    b) from the egressgateway it will forward the request to the destination pod deployed in the app-1-XXX
   223  	//       namespace
   224  
   225  	// Test cases:
   226  	// 1. http case:
   227  	//    client -------> Hits listener 0.0.0.0_80 cluster
   228  	//    Metric is istio_requests_total i.e. HTTP
   229  	//
   230  	// 2. https case:
   231  	//    client ----> Hits no listener -> 0.0.0.0_150001 -> ALLOW_ANY/REGISTRY_ONLY
   232  	//    Metric is istio_tcp_connections_closed_total i.e. TCP
   233  	//
   234  	// 3. https conflict case:
   235  	//    client ----> Hits listener 0.0.0.0_9443
   236  	//    Metric is istio_tcp_connections_closed_total i.e. TCP
   237  	//
   238  	// 4. http_egress
   239  	//    client ) ---HTTP request (Host: some-external-site.com----> Hits listener 0.0.0.0_80 ->
   240  	//      VS Routing (add Egress Header) --> Egress Gateway --> destination
   241  	//    Metric is istio_requests_total i.e. HTTP with destination as destination
   242  	//
   243  	// 5. TCP
   244  	//    client ---TCP request at port 9090----> Matches no listener -> 0.0.0.0_150001 -> ALLOW_ANY/REGISTRY_ONLY
   245  	//    Metric is istio_tcp_connections_closed_total i.e. TCP
   246  	//
   247  	// 5. TCP conflict
   248  	//    client ---TCP request at port 9091 ----> Hits listener 0.0.0.0_9091 ->  ALLOW_ANY/REGISTRY_ONLY
   249  	//    Metric is istio_tcp_connections_closed_total i.e. TCP
   250  	//
   251  	framework.
   252  		NewTest(t).
   253  		Run(func(t framework.TestContext) {
   254  			client, to := setupEcho(t, mode)
   255  
   256  			for _, tc := range cases {
   257  				t.NewSubTest(tc.Name).Run(func(t framework.TestContext) {
   258  					client.CallOrFail(t, echo.CallOptions{
   259  						To:    to,
   260  						Count: 1,
   261  						Port: echo.Port{
   262  							Name: tc.PortName,
   263  						},
   264  						HTTP: echo.HTTP{
   265  							HTTP2:   tc.HTTP2,
   266  							Headers: headers.New().WithHost(tc.Host).Build(),
   267  						},
   268  						Check: func(result echo.CallResult, err error) error {
   269  							// the expected response from a blackhole test case will have err
   270  							// set; use the length of the expected code to ignore this condition
   271  							if err != nil && tc.Expected.StatusCode > 0 {
   272  								return fmt.Errorf("request failed: %v", err)
   273  							}
   274  							codeStr := strconv.Itoa(tc.Expected.StatusCode)
   275  							for i, r := range result.Responses {
   276  								if codeStr != r.Code {
   277  									return fmt.Errorf("response[%d] received status code %s, expected %d", i, r.Code, tc.Expected.StatusCode)
   278  								}
   279  								for k, v := range tc.Expected.RequestHeaders {
   280  									if got := r.RequestHeaders.Get(k); got != v {
   281  										return fmt.Errorf("expected metadata %v=%v, got %q", k, v, got)
   282  									}
   283  								}
   284  							}
   285  							return nil
   286  						},
   287  					})
   288  
   289  					if tc.Expected.Query.Metric != "" {
   290  						util.ValidateMetric(t, t.Clusters().Default(), prometheus, tc.Expected.Query, 1)
   291  					}
   292  				})
   293  			}
   294  		})
   295  }
   296  
   297  func setupEcho(t framework.TestContext, mode TrafficPolicy) (echo.Instance, echo.Target) {
   298  	t.Helper()
   299  	appsNamespace := namespace.NewOrFail(t, t, namespace.Config{
   300  		Prefix: "app",
   301  		Inject: true,
   302  	})
   303  	serviceNamespace := namespace.NewOrFail(t, t, namespace.Config{
   304  		Prefix: "service",
   305  		Inject: true,
   306  	})
   307  
   308  	// External traffic should work even if we have service entries on the same ports
   309  	createSidecarScope(t, mode, appsNamespace, serviceNamespace)
   310  
   311  	var client, dest echo.Instance
   312  	deployment.New(t).
   313  		With(&client, echo.Config{
   314  			Service:   "client",
   315  			Namespace: appsNamespace,
   316  			Subsets:   []echo.SubsetConfig{{}},
   317  		}).
   318  		With(&dest, echo.Config{
   319  			Service:   "destination",
   320  			Namespace: appsNamespace,
   321  			Subsets:   []echo.SubsetConfig{{Annotations: map[string]string{annotation.SidecarInject.Name: "false"}}},
   322  			Ports: []echo.Port{
   323  				{
   324  					// Plain HTTP port, will match no listeners and fall through
   325  					Name:         "http",
   326  					Protocol:     protocol.HTTP,
   327  					ServicePort:  80,
   328  					WorkloadPort: 8080,
   329  				},
   330  				{
   331  					// HTTPS port, will match no listeners and fall through
   332  					Name:         "https",
   333  					Protocol:     protocol.HTTPS,
   334  					ServicePort:  443,
   335  					WorkloadPort: 8443,
   336  					TLS:          true,
   337  				},
   338  				{
   339  					// HTTPS port, there will be an HTTP service defined on this port that will match
   340  					Name:        "https-conflict",
   341  					Protocol:    protocol.HTTPS,
   342  					ServicePort: 9443,
   343  					TLS:         true,
   344  				},
   345  				{
   346  					// TCP port, will match no listeners and fall through
   347  					Name:        "tcp",
   348  					Protocol:    protocol.TCP,
   349  					ServicePort: 9090,
   350  				},
   351  				{
   352  					// TCP port, there will be an HTTP service defined on this port that will match
   353  					Name:        "tcp-conflict",
   354  					Protocol:    protocol.TCP,
   355  					ServicePort: 9091,
   356  				},
   357  			},
   358  			TLSSettings: &common.TLSSettings{
   359  				// Echo has these test certs baked into the docker image
   360  				ClientCert: mustReadCert(t, "cert.crt"),
   361  				Key:        mustReadCert(t, "cert.key"),
   362  			},
   363  		}).BuildOrFail(t)
   364  
   365  	if err := t.ConfigIstio().YAML(serviceNamespace.Name(), ServiceEntry).Apply(); err != nil {
   366  		t.Errorf("failed to apply service entries: %v", err)
   367  	}
   368  
   369  	if _, isKube := t.Environment().(*kube.Environment); isKube {
   370  		createGateway(t, appsNamespace, serviceNamespace)
   371  	}
   372  	return client, dest
   373  }