istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/common/traffic.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 common
    19  
    20  import (
    21  	"fmt"
    22  
    23  	"istio.io/istio/pkg/test"
    24  	"istio.io/istio/pkg/test/framework"
    25  	"istio.io/istio/pkg/test/framework/components/echo"
    26  	"istio.io/istio/pkg/test/framework/components/echo/check"
    27  	"istio.io/istio/pkg/test/framework/components/echo/common/deployment"
    28  	"istio.io/istio/pkg/test/framework/components/echo/echotest"
    29  	"istio.io/istio/pkg/test/framework/components/echo/match"
    30  	"istio.io/istio/pkg/test/framework/components/istio"
    31  	"istio.io/istio/pkg/test/framework/components/istio/ingress"
    32  	"istio.io/istio/pkg/test/framework/resource"
    33  	"istio.io/istio/pkg/test/util/tmpl"
    34  	"istio.io/istio/pkg/test/util/yml"
    35  )
    36  
    37  type TrafficCall struct {
    38  	name string
    39  	call func(t test.Failer, options echo.CallOptions) echo.CallResult
    40  	opts echo.CallOptions
    41  }
    42  
    43  type skip struct {
    44  	skip   bool
    45  	reason string
    46  }
    47  
    48  type TrafficTestCase struct {
    49  	name string
    50  	// config can optionally be templated using the params src, dst (each are []echo.Instance)
    51  	config string
    52  
    53  	// Multiple calls. Cannot be used with call/opts
    54  	children []TrafficCall
    55  
    56  	// Single call. Cannot be used with children or workloadAgnostic tests.
    57  	call func(t test.Failer, options echo.CallOptions) echo.CallResult
    58  	// opts specifies the echo call options. When using RunForApps, the To will be set dynamically.
    59  	opts echo.CallOptions
    60  	// setupOpts allows modifying options based on sources/destinations
    61  	setupOpts func(src echo.Caller, opts *echo.CallOptions)
    62  	// check is used to build validators dynamically when using RunForApps based on the active/src dest pair
    63  	check     func(src echo.Caller, opts *echo.CallOptions) echo.Checker
    64  	checkForN func(src echo.Caller, dst echo.Services, opts *echo.CallOptions) echo.Checker
    65  
    66  	// setting cases to skipped is better than not adding them - gives visibility to what needs to be fixed
    67  	skip skip
    68  
    69  	// workloadAgnostic is a temporary setting to trigger using RunForApps
    70  	// TODO remove this and force everything to be workoad agnostic
    71  	workloadAgnostic bool
    72  
    73  	// toN causes the test to be run for N destinations. The call will be made from instances of the first deployment
    74  	// in each subset in each cluster. See echotes.T's RunToN for more details.
    75  	toN int
    76  	// viaIngress makes the ingress gateway the caller for tests
    77  	viaIngress bool
    78  	// sourceMatchers allows adding additional filtering for workload agnostic cases to test using fewer clients
    79  	sourceMatchers []match.Matcher
    80  	// targetMatchers allows adding additional filtering for workload agnostic cases to test using fewer targets
    81  	targetMatchers []match.Matcher
    82  	// comboFilters allows conditionally filtering based on pairs of apps
    83  	comboFilters []echotest.CombinationFilter
    84  	// vars given to the config template
    85  	templateVars func(src echo.Callers, dest echo.Instances) map[string]any
    86  
    87  	// minIstioVersion allows conditionally skipping based on required version
    88  	minIstioVersion string
    89  
    90  	// If set, a datapath with no L7 proxies can run this test
    91  	RequiresL4 bool
    92  	// If set, will apply config to all clusters, not just the local one
    93  	globalConfig bool
    94  }
    95  
    96  func (c TrafficTestCase) RunForApps(t framework.TestContext, apps echo.Instances, namespace string) {
    97  	if c.skip.skip {
    98  		t.Skip(c.skip.reason)
    99  	}
   100  	if c.minIstioVersion != "" {
   101  		skipMV := !t.Settings().Revisions.AtLeast(resource.IstioVersion(c.minIstioVersion))
   102  		if skipMV {
   103  			t.SkipNow()
   104  		}
   105  	}
   106  	if c.opts.To != nil {
   107  		t.Fatal("TrafficTestCase.RunForApps: opts.To must not be specified")
   108  	}
   109  	if c.call != nil {
   110  		t.Fatal("TrafficTestCase.RunForApps: call must not be specified")
   111  	}
   112  	// just check if any of the required fields are set
   113  	optsSpecified := c.opts.Port.Name != "" || c.opts.Port.Protocol != "" || c.opts.Scheme != ""
   114  	if optsSpecified && len(c.children) > 0 {
   115  		t.Fatal("TrafficTestCase: must not specify both opts and children")
   116  	}
   117  	if !optsSpecified && len(c.children) == 0 {
   118  		t.Fatal("TrafficTestCase: must specify either opts or children")
   119  	}
   120  
   121  	if !c.RequiresL4 {
   122  		c.comboFilters = append(c.comboFilters, echotest.HasL7)
   123  	}
   124  
   125  	job := func(t framework.TestContext) {
   126  		echoT := echotest.New(t, apps).
   127  			SetupForServicePair(func(t framework.TestContext, src echo.Callers, dsts echo.Services) error {
   128  				tmplData := map[string]any{
   129  					// tests that use simple Run only need the first
   130  					"dst":    dsts[0],
   131  					"dstSvc": dsts[0][0].Config().Service,
   132  					// tests that use RunForN need all destination deployments
   133  					"dsts":    dsts,
   134  					"dstSvcs": dsts.NamespacedNames().Names(),
   135  				}
   136  				if len(src) > 0 {
   137  					tmplData["src"] = src
   138  					if src, ok := src[0].(echo.Instance); ok {
   139  						tmplData["srcSvc"] = src.Config().Service
   140  					}
   141  				}
   142  				if c.templateVars != nil {
   143  					for k, v := range c.templateVars(src, dsts[0]) {
   144  						tmplData[k] = v
   145  					}
   146  				}
   147  				cfg := yml.MustApplyNamespace(t, tmpl.MustEvaluate(c.config, tmplData), namespace)
   148  				// we only apply to config clusters
   149  				scope := t.ConfigIstio()
   150  				if c.globalConfig {
   151  					scope = t.ConfigKube()
   152  				}
   153  				return scope.YAML("", cfg).Apply()
   154  			}).
   155  			FromMatch(match.And(c.sourceMatchers...)).
   156  			// TODO mainly testing proxyless features as a client for now
   157  			ToMatch(match.And(append(c.targetMatchers, match.NotProxylessGRPC)...)).
   158  			WithDefaultFilters(1, c.toN).
   159  			ConditionallyTo(c.comboFilters...)
   160  
   161  		doTest := func(t framework.TestContext, from echo.Caller, to echo.Services) {
   162  			buildOpts := func(options echo.CallOptions) echo.CallOptions {
   163  				opts := options
   164  				opts.To = to[0]
   165  				if c.check != nil {
   166  					opts.Check = c.check(from, &opts)
   167  				}
   168  				if c.checkForN != nil {
   169  					opts.Check = c.checkForN(from, to, &opts)
   170  				}
   171  				if c.setupOpts != nil {
   172  					c.setupOpts(from, &opts)
   173  				}
   174  				// If unset, assume they want to just check the request succeeds
   175  				if opts.Check == nil {
   176  					opts.Check = check.OK()
   177  				}
   178  				return opts
   179  			}
   180  			if optsSpecified {
   181  				from.CallOrFail(t, buildOpts(c.opts))
   182  			}
   183  			for _, child := range c.children {
   184  				t.NewSubTest(child.name).Run(func(t framework.TestContext) {
   185  					from.CallOrFail(t, buildOpts(child.opts))
   186  				})
   187  			}
   188  		}
   189  
   190  		if c.toN > 0 {
   191  			echoT.RunToN(c.toN, func(t framework.TestContext, src echo.Instance, dsts echo.Services) {
   192  				doTest(t, src, dsts)
   193  			})
   194  		} else if c.viaIngress {
   195  			echoT.RunViaIngress(func(t framework.TestContext, from ingress.Instance, to echo.Target) {
   196  				doTest(t, from, echo.Services{to.Instances()})
   197  			})
   198  		} else {
   199  			echoT.Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
   200  				doTest(t, from, echo.Services{to.Instances()})
   201  			})
   202  		}
   203  	}
   204  
   205  	if c.name != "" {
   206  		t.NewSubTest(c.name).Run(job)
   207  	} else {
   208  		job(t)
   209  	}
   210  }
   211  
   212  func (c TrafficTestCase) Run(t framework.TestContext, namespace string) {
   213  	job := func(t framework.TestContext) {
   214  		if c.skip.skip {
   215  			t.Skip(c.skip.reason)
   216  		}
   217  		if c.minIstioVersion != "" {
   218  			skipMV := !t.Settings().Revisions.AtLeast(resource.IstioVersion(c.minIstioVersion))
   219  			if skipMV {
   220  				t.SkipNow()
   221  			}
   222  		}
   223  		// we only apply to config clusters
   224  		if len(c.config) > 0 {
   225  			tmplData := map[string]any{}
   226  			if c.templateVars != nil {
   227  				// we don't have echo instances so just pass nil
   228  				for k, v := range c.templateVars(nil, nil) {
   229  					tmplData[k] = v
   230  				}
   231  			}
   232  			cfg := yml.MustApplyNamespace(t, tmpl.MustEvaluate(c.config, tmplData), namespace)
   233  			scope := t.ConfigIstio()
   234  			if c.globalConfig {
   235  				scope = t.ConfigKube()
   236  			}
   237  			scope.YAML("", cfg).ApplyOrFail(t)
   238  		}
   239  
   240  		if c.call != nil && len(c.children) > 0 {
   241  			t.Fatal("TrafficTestCase: must not specify both call and children")
   242  		}
   243  
   244  		if c.call != nil {
   245  			c.call(t, c.opts)
   246  		}
   247  
   248  		for _, child := range c.children {
   249  			t.NewSubTest(child.name).Run(func(t framework.TestContext) {
   250  				child.call(t, child.opts)
   251  			})
   252  		}
   253  	}
   254  	if c.name != "" {
   255  		t.NewSubTest(c.name).Run(job)
   256  	} else {
   257  		job(t)
   258  	}
   259  }
   260  
   261  func skipAmbient(t framework.TestContext, reason string) skip {
   262  	return skip{skip: t.Settings().Ambient, reason: reason}
   263  }
   264  
   265  func RunAllTrafficTests(t framework.TestContext, i istio.Instance, apps deployment.SingleNamespaceView) {
   266  	RunCase := func(name string, f func(t TrafficContext)) {
   267  		t.NewSubTest(name).Run(func(t framework.TestContext) {
   268  			f(TrafficContext{TestContext: t, Apps: apps, Istio: i})
   269  		})
   270  	}
   271  	RunSkipAmbient := func(name string, f func(t TrafficContext), reason string) {
   272  		t.NewSubTest(name).Run(func(t framework.TestContext) {
   273  			if t.Settings().Ambient {
   274  				t.Skipf("ambient skipped: %v", reason)
   275  			} else {
   276  				f(TrafficContext{TestContext: t, Apps: apps, Istio: i})
   277  			}
   278  		})
   279  	}
   280  	RunCase("jwt-claim-route", jwtClaimRoute)
   281  	RunCase("virtualservice", virtualServiceCases)
   282  	RunCase("sniffing", protocolSniffingCases)
   283  	RunCase("selfcall", selfCallsCases)
   284  	RunSkipAmbient("serverfirst", serverFirstTestCases, "Expected success cases time out")
   285  	RunCase("gateway", gatewayCases)
   286  	RunCase("autopassthrough", autoPassthroughCases)
   287  	RunSkipAmbient("loop", trafficLoopCases, "does not error (waypoint -> waypoint)")
   288  	RunSkipAmbient("tls-origination", tlsOriginationCases, "not workload agnostic")
   289  	RunSkipAmbient("instanceip", instanceIPTests, "not supported")
   290  	RunCase("services", serviceCases)
   291  	RunSkipAmbient("externalname", externalNameCases, "Relies on X-Forwarded-Client-Cert in checker")
   292  	RunSkipAmbient("host", hostCases, "Relies on X-Forwarded-Client-Cert in checker")
   293  	RunSkipAmbient("envoyfilter", envoyFilterCases, "not supported")
   294  	RunCase("consistent-hash", consistentHashCases)
   295  	RunCase("use-client-protocol", useClientProtocolCases)
   296  	RunCase("destinationrule", destinationRuleCases)
   297  	RunCase("vm", VMTestCases(apps.VM))
   298  	RunSkipAmbient("dns", DNSTestCases, "https://github.com/istio/istio/issues/48614")
   299  	RunCase("externalservice", TestExternalService)
   300  }
   301  
   302  func ExpectString(got, expected, help string) error {
   303  	if got != expected {
   304  		return fmt.Errorf("got unexpected %v: got %q, wanted %q", help, got, expected)
   305  	}
   306  	return nil
   307  }
   308  
   309  func AlmostEquals(a, b, precision int) bool {
   310  	upper := a + precision
   311  	lower := a - precision
   312  	if b < lower || b > upper {
   313  		return false
   314  	}
   315  	return true
   316  }
   317  
   318  type TrafficContext struct {
   319  	framework.TestContext
   320  	Apps  deployment.SingleNamespaceView
   321  	Istio istio.Instance
   322  
   323  	// sourceFilters defines default filters for all cases
   324  	sourceMatchers []match.Matcher
   325  	// targetFilters defines default filters for all cases
   326  	targetMatchers []match.Matcher
   327  	// comboFilters defines default filters for all cases
   328  	comboFilters []echotest.CombinationFilter
   329  }
   330  
   331  func (t *TrafficContext) SetDefaultSourceMatchers(f ...match.Matcher) {
   332  	t.sourceMatchers = f
   333  }
   334  
   335  func (t *TrafficContext) SetDefaultTargetMatchers(f ...match.Matcher) {
   336  	t.targetMatchers = f
   337  }
   338  
   339  func (t *TrafficContext) SetDefaultComboFilter(f ...echotest.CombinationFilter) {
   340  	t.comboFilters = f
   341  }
   342  
   343  func (t TrafficContext) RunTraffic(tt TrafficTestCase) {
   344  	if tt.sourceMatchers == nil {
   345  		tt.sourceMatchers = t.sourceMatchers
   346  	}
   347  	if tt.targetMatchers == nil {
   348  		tt.targetMatchers = t.targetMatchers
   349  	}
   350  	if tt.comboFilters == nil {
   351  		tt.comboFilters = t.comboFilters
   352  	}
   353  	if tt.workloadAgnostic {
   354  		tt.RunForApps(t, t.Apps.All.Instances(), t.Apps.Namespace.Name())
   355  	} else {
   356  		tt.Run(t, t.Apps.Namespace.Name())
   357  	}
   358  }