istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/mirror_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  	"fmt"
    22  	"math"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/hashicorp/go-multierror"
    27  	"k8s.io/apimachinery/pkg/util/rand"
    28  
    29  	"istio.io/istio/pkg/config/protocol"
    30  	"istio.io/istio/pkg/log"
    31  	"istio.io/istio/pkg/test/framework"
    32  	"istio.io/istio/pkg/test/framework/components/echo"
    33  	"istio.io/istio/pkg/test/framework/components/echo/common/deployment"
    34  	"istio.io/istio/pkg/test/util/retry"
    35  )
    36  
    37  //	Virtual service topology
    38  //
    39  //	    a                      b                     c
    40  //	|-------|             |-------|    mirror   |-------|
    41  //	| Host0 | ----------> | Host1 | ----------> | Host2 |
    42  //	|-------|             |-------|             |-------|
    43  //
    44  
    45  type VirtualServiceMirrorConfig struct {
    46  	Name       string
    47  	Absent     bool
    48  	Percent    float64
    49  	MirrorHost string
    50  }
    51  
    52  type testCaseMirror struct {
    53  	name                string
    54  	absent              bool
    55  	percentage          float64
    56  	threshold           float64
    57  	expectedDestination echo.Instances
    58  }
    59  
    60  type mirrorTestOptions struct {
    61  	cases      []testCaseMirror
    62  	mirrorHost string
    63  }
    64  
    65  var mirrorProtocols = []protocol.Instance{protocol.HTTP, protocol.GRPC}
    66  
    67  func TestMirroring(t *testing.T) {
    68  	runMirrorTest(t, mirrorTestOptions{
    69  		cases: []testCaseMirror{
    70  			{
    71  				name:       "mirror-percent-absent",
    72  				absent:     true,
    73  				percentage: 100.0,
    74  				threshold:  0.0,
    75  			},
    76  			{
    77  				name:       "mirror-50",
    78  				percentage: 50.0,
    79  				threshold:  10.0,
    80  			},
    81  			{
    82  				name:       "mirror-10",
    83  				percentage: 10.0,
    84  				threshold:  5.0,
    85  			},
    86  			{
    87  				name:       "mirror-0",
    88  				percentage: 0.0,
    89  				threshold:  0.0,
    90  			},
    91  		},
    92  	})
    93  }
    94  
    95  // Tests mirroring to an external service. Uses same topology as the test above, a -> b -> external, with "external" being external.
    96  //
    97  // Since we don't want to rely on actual external websites, we simulate that by using a Sidecar to limit connectivity
    98  // from "a" so that it cannot reach "external" directly, and we use a ServiceEntry to define our "external" website, which
    99  // is static and points to the service "external" ip.
   100  
   101  // Thus when "a" tries to mirror to the external service, it is actually connecting to "external" (which is not part of the
   102  // mesh because of the Sidecar), then we can inspect "external" logs to verify the requests were properly mirrored.
   103  func TestMirroringExternalService(t *testing.T) {
   104  	header := ""
   105  	if len(apps.External.All) > 0 {
   106  		header = apps.External.All.Config().HostHeader()
   107  	}
   108  	runMirrorTest(t, mirrorTestOptions{
   109  		mirrorHost: header,
   110  		cases: []testCaseMirror{
   111  			{
   112  				name:                "mirror-external",
   113  				absent:              true,
   114  				percentage:          100.0,
   115  				threshold:           0.0,
   116  				expectedDestination: apps.External.All,
   117  			},
   118  		},
   119  	})
   120  }
   121  
   122  func runMirrorTest(t *testing.T, options mirrorTestOptions) {
   123  	framework.
   124  		NewTest(t).
   125  		Run(func(t framework.TestContext) {
   126  			for _, c := range options.cases {
   127  				t.NewSubTest(c.name).Run(func(t framework.TestContext) {
   128  					mirrorHost := options.mirrorHost
   129  					if len(mirrorHost) == 0 {
   130  						mirrorHost = deployment.CSvc
   131  					}
   132  					vsc := VirtualServiceMirrorConfig{
   133  						c.name,
   134  						c.absent,
   135  						c.percentage,
   136  						mirrorHost,
   137  					}
   138  
   139  					// we only apply to config clusters
   140  					t.ConfigIstio().EvalFile(apps.Namespace.Name(), vsc, "testdata/traffic-mirroring-template.yaml").
   141  						ApplyOrFail(t)
   142  
   143  					for _, podA := range apps.A {
   144  						podA := podA
   145  						t.NewSubTest(fmt.Sprintf("from %s", podA.Config().Cluster.StableName())).Run(func(t framework.TestContext) {
   146  							for _, proto := range mirrorProtocols {
   147  								t.NewSubTest(string(proto)).Run(func(t framework.TestContext) {
   148  									retry.UntilSuccessOrFail(t, func() error {
   149  										testID := rand.String(16)
   150  										if err := sendTrafficMirror(podA, apps.B, proto, testID); err != nil {
   151  											return err
   152  										}
   153  										expected := c.expectedDestination
   154  										if expected == nil {
   155  											expected = apps.C
   156  										}
   157  
   158  										return verifyTrafficMirror(apps.B, expected, c, testID)
   159  									}, echo.DefaultCallRetryOptions()...)
   160  								})
   161  							}
   162  						})
   163  					}
   164  				})
   165  			}
   166  		})
   167  }
   168  
   169  func sendTrafficMirror(from echo.Instance, to echo.Target, proto protocol.Instance, testID string) error {
   170  	options := echo.CallOptions{
   171  		To:    to,
   172  		Count: 100,
   173  		Port: echo.Port{
   174  			Name: strings.ToLower(proto.String()),
   175  		},
   176  		Retry: echo.Retry{
   177  			NoRetry: true,
   178  		},
   179  	}
   180  	switch proto {
   181  	case protocol.HTTP:
   182  		options.HTTP.Path = "/" + testID
   183  	case protocol.GRPC:
   184  		options.Message = testID
   185  	default:
   186  		return fmt.Errorf("protocol not supported in mirror testing: %s", proto)
   187  	}
   188  
   189  	_, err := from.Call(options)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  func verifyTrafficMirror(dest, mirror echo.Instances, tc testCaseMirror, testID string) error {
   198  	countB, err := logCount(dest, testID)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	countC, err := logCount(mirror, testID)
   204  	if err != nil {
   205  		return err
   206  	}
   207  
   208  	actualPercent := (countC / countB) * 100
   209  	deltaFromExpected := math.Abs(actualPercent - tc.percentage)
   210  
   211  	var merr *multierror.Error
   212  	if tc.threshold-deltaFromExpected < 0 {
   213  		err := fmt.Errorf("unexpected mirror traffic. Expected %g%%, got %.1f%% (threshold: %g%%, testID: %s)",
   214  			tc.percentage, actualPercent, tc.threshold, testID)
   215  		log.Infof("%v", err)
   216  		merr = multierror.Append(merr, err)
   217  	} else {
   218  		log.Infof("Got expected mirror traffic. Expected %g%%, got %.1f%% (threshold: %g%%, , testID: %s)",
   219  			tc.percentage, actualPercent, tc.threshold, testID)
   220  	}
   221  
   222  	return merr.ErrorOrNil()
   223  }
   224  
   225  func logCount(instances echo.Instances, testID string) (float64, error) {
   226  	counts := map[string]float64{}
   227  	for _, instance := range instances {
   228  		workloads, err := instance.Workloads()
   229  		if err != nil {
   230  			return -1, fmt.Errorf("failed to get Subsets: %v", err)
   231  		}
   232  		var logs string
   233  		for _, w := range workloads {
   234  			l, err := w.Logs()
   235  			if err != nil {
   236  				return -1, fmt.Errorf("failed getting logs: %v", err)
   237  			}
   238  			logs += l
   239  		}
   240  		if c := float64(strings.Count(logs, testID)); c > 0 {
   241  			counts[instance.Config().Cluster.Name()] = c
   242  		}
   243  	}
   244  	var total float64
   245  	for _, c := range counts {
   246  		total += c
   247  	}
   248  	// TODO(landow) mirorr split does not always hit all clusters
   249  	return total, nil
   250  }