istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/echotest/run.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package echotest
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"k8s.io/apimachinery/pkg/types"
    22  
    23  	"istio.io/istio/pkg/config/constants"
    24  	"istio.io/istio/pkg/slices"
    25  	"istio.io/istio/pkg/test/framework"
    26  	"istio.io/istio/pkg/test/framework/components/cluster"
    27  	"istio.io/istio/pkg/test/framework/components/echo"
    28  	"istio.io/istio/pkg/test/framework/components/istio"
    29  	"istio.io/istio/pkg/test/framework/components/istio/ingress"
    30  )
    31  
    32  type (
    33  	perDeploymentTest  func(t framework.TestContext, deployments echo.Instances)
    34  	perNDeploymentTest func(t framework.TestContext, deployments echo.Services)
    35  	perInstanceTest    func(t framework.TestContext, inst echo.Instance)
    36  	perClusterTest     func(t framework.TestContext, c cluster.Cluster)
    37  
    38  	oneToOneTest      func(t framework.TestContext, from echo.Instance, to echo.Target)
    39  	oneToNTest        func(t framework.TestContext, from echo.Instance, dsts echo.Services)
    40  	oneClusterOneTest func(t framework.TestContext, from cluster.Cluster, to echo.Target)
    41  	ingressTest       func(t framework.TestContext, from ingress.Instance, to echo.Target)
    42  )
    43  
    44  // Run will generate and run one subtest to send traffic between each combination
    45  // of source instance to target deployment.
    46  //
    47  // For example, in a test named `a/to_b/from_cluster-0`,
    48  // `a` is the source deployment, `b` is the destination deployment and
    49  // `cluster-0` marks which instance of the source deployment.
    50  //
    51  // We use a combination of deployment name and cluster name to identify
    52  // a particular source instance, as there should typically be one instance
    53  // per cluster for any deployment. However we do not identify a destination
    54  // cluster, as we expect most tests will cause load-balancing across all possible
    55  // clusters.
    56  func (t *T) Run(testFn oneToOneTest) {
    57  	t.rootCtx.Logf("Running tests with: sources %v -> destinations %v",
    58  		t.sources.Services().NamespacedNames().NamesWithNamespacePrefix(),
    59  		t.destinations.Services().NamespacedNames().NamesWithNamespacePrefix())
    60  
    61  	if t.sources.Len() == 0 {
    62  		t.rootCtx.Error("Sources are empty")
    63  	}
    64  	if t.destinations.Len() == 0 {
    65  		t.rootCtx.Error("Destinations are empty")
    66  	}
    67  
    68  	// Build and apply any completed configuration that does not require to/from params.
    69  	t.cfg.BuildCompleteSources().Apply()
    70  
    71  	t.fromEachDeployment(t.rootCtx, func(ctx framework.TestContext, from echo.Instances) {
    72  		// Build and apply per-source configuration.
    73  		// TODO(nmittler): Consider merging this with t.setup below.
    74  		callers := from.Callers()
    75  		firstCaller := echo.Callers{callers[0]}
    76  		t.cfg.Context(ctx).BuildFrom(firstCaller...).Apply()
    77  
    78  		// Run setup functions for the callers.
    79  		t.setup(ctx, from.Callers())
    80  
    81  		t.toEachDeployment(ctx, func(ctx framework.TestContext, to echo.Instances) {
    82  			// Build and apply per-destination config
    83  			t.cfg.Context(ctx).BuildFromAndTo(firstCaller, to.Services()).Apply()
    84  
    85  			t.setupPair(ctx, callers, echo.Services{to})
    86  			t.fromEachWorkloadCluster(ctx, from, func(ctx framework.TestContext, from echo.Instance) {
    87  				filteredDst := t.applyCombinationFilters(from, to)
    88  				if len(filteredDst) == 0 {
    89  					// this only happens due to conditional filters and when an entire deployment is filtered we should be noisy
    90  					ctx.Skipf("cases from %s in %s with %s as destination are removed by filters ",
    91  						from.Config().Service, from.Config().Cluster.StableName(), to[0].Config().Service)
    92  				}
    93  				testFn(ctx, from, filteredDst)
    94  			})
    95  		})
    96  	})
    97  }
    98  
    99  // RunFromClusters will generate and run one subtest to send traffic to
   100  // destination instance. This is for ingress gateway testing when source instance
   101  // destination instances. This can be used when we're not using echo workloads
   102  // as the source of traffic, such as from the ingress gateway. For example:
   103  //
   104  //	RunFromClusters(func(t framework.TestContext, src cluster.Cluster, dst echo.Instances)) {
   105  //	  ingr := ist.IngressFor(src)
   106  //	  ingr.CallWithRetryOrFail(...)
   107  //	})
   108  func (t *T) RunFromClusters(testFn oneClusterOneTest) {
   109  	t.toEachDeployment(t.rootCtx, func(ctx framework.TestContext, dstInstances echo.Instances) {
   110  		t.setupPair(ctx, nil, echo.Services{dstInstances})
   111  		if len(ctx.Clusters()) == 1 {
   112  			testFn(ctx, ctx.Clusters()[0], dstInstances)
   113  		} else {
   114  			t.fromEachCluster(ctx, func(ctx framework.TestContext, c cluster.Cluster) {
   115  				testFn(ctx, c, dstInstances)
   116  			})
   117  		}
   118  	})
   119  }
   120  
   121  // fromEachCluster runs test from each cluster without requiring source deployment.
   122  func (t *T) fromEachCluster(ctx framework.TestContext, testFn perClusterTest) {
   123  	for _, fromCluster := range t.sources.Clusters() {
   124  		fromCluster := fromCluster
   125  		ctx.NewSubTestf("from %s", fromCluster.StableName()).Run(func(ctx framework.TestContext) {
   126  			testFn(ctx, fromCluster)
   127  		})
   128  	}
   129  }
   130  
   131  // RunToN will generate nested subtests for every instance in every deployment subsets of the full set of deployments,
   132  // such that every deployment is a destination at least once. To create as few subtests as possible, the same deployment
   133  // may appear as a target in multiple subtests.
   134  //
   135  // Example: Given a as the only source, with a, b, c, d as destinationsand n = 3, we get the following subtests:
   136  //   - a/to_a_b_c/from_cluster_1:
   137  //   - a/to_a_b_c/from_cluster_2:
   138  //   - a/to_b_c_d/from_cluster_1:
   139  //   - a/to_b_c_d/from_cluster_2:
   140  func (t *T) RunToN(n int, testFn oneToNTest) {
   141  	t.fromEachDeployment(t.rootCtx, func(ctx framework.TestContext, from echo.Instances) {
   142  		t.setup(ctx, from.Callers())
   143  		t.toNDeployments(ctx, n, from, func(ctx framework.TestContext, toServices echo.Services) {
   144  			t.setupPair(ctx, from.Callers(), toServices)
   145  			t.fromEachWorkloadCluster(ctx, from, func(ctx framework.TestContext, fromInstance echo.Instance) {
   146  				// reapply destination filters to only get the reachable instances for this cluster
   147  				// this can be done safely since toNDeployments asserts the Services won't change
   148  				destDeployments := t.applyCombinationFilters(fromInstance, toServices.Instances()).Services()
   149  				testFn(ctx, fromInstance, destDeployments)
   150  			})
   151  		})
   152  	})
   153  }
   154  
   155  type gatewayInstance struct {
   156  	types.NamespacedName
   157  	GatewayClass string
   158  }
   159  
   160  func (gi gatewayInstance) ServiceName() types.NamespacedName {
   161  	return types.NamespacedName{
   162  		Namespace: gi.Namespace,
   163  		Name:      gi.Name + "-" + gi.GatewayClass,
   164  	}
   165  }
   166  
   167  func (t *T) RunViaGatewayIngress(gatewayClass string, testFn ingressTest) {
   168  	// Build and apply any completed configuration that does not require to/from params.
   169  	t.cfg.BuildCompleteSources().Apply()
   170  	istioInstance := istio.GetOrFail(t.rootCtx, t.rootCtx)
   171  	t.toEachDeployment(t.rootCtx, func(ctx framework.TestContext, dstInstances echo.Instances) {
   172  		gwInstance := gatewayInstance{
   173  			NamespacedName: types.NamespacedName{
   174  				Namespace: dstInstances.NamespaceName(),
   175  				Name:      dstInstances.ServiceName() + "-gateway",
   176  			},
   177  			GatewayClass: gatewayClass,
   178  		}
   179  		// Build and apply per-destination config
   180  		gwIngress := istioInstance.CustomIngressFor(ctx.Clusters()[0], gwInstance.ServiceName(), fmt.Sprintf("%s=%s", constants.GatewayNameLabel, gwInstance.Name))
   181  		callers := ingress.Instances{gwIngress}.Callers()
   182  		t.cfg.Context(ctx).BuildFromAndTo(callers, dstInstances.Services()).Apply()
   183  
   184  		t.setupPair(ctx, callers, echo.Services{dstInstances})
   185  		doTest := func(ctx framework.TestContext, fromCluster cluster.Cluster, dst echo.Instances) {
   186  			if gwIngress == nil {
   187  				ctx.Skipf("no gateway for %s", fromCluster.StableName())
   188  			}
   189  			testFn(ctx, gwIngress, dst)
   190  		}
   191  		if len(ctx.Clusters()) == 1 {
   192  			doTest(ctx, ctx.Clusters()[0], dstInstances)
   193  		} else {
   194  			t.fromEachCluster(ctx, func(ctx framework.TestContext, c cluster.Cluster) {
   195  				doTest(ctx, c, dstInstances)
   196  			})
   197  		}
   198  	})
   199  }
   200  
   201  func (t *T) RunViaIngress(testFn ingressTest) {
   202  	// Build and apply any completed configuration that does not require to/from params.
   203  	t.cfg.BuildCompleteSources().Apply()
   204  
   205  	istioInstance := istio.GetOrFail(t.rootCtx, t.rootCtx)
   206  	t.toEachDeployment(t.rootCtx, func(ctx framework.TestContext, dstInstances echo.Instances) {
   207  		// Build and apply per-destination config
   208  		callers := istioInstance.Ingresses().Callers()
   209  		t.cfg.Context(ctx).BuildFromAndTo(callers, dstInstances.Services()).Apply()
   210  
   211  		t.setupPair(ctx, callers, echo.Services{dstInstances})
   212  		doTest := func(ctx framework.TestContext, fromCluster cluster.Cluster, dst echo.Instances) {
   213  			ingr := istioInstance.IngressFor(fromCluster)
   214  			if ingr == nil {
   215  				ctx.Skipf("no ingress for %s", fromCluster.StableName())
   216  			}
   217  			testFn(ctx, ingr, dst)
   218  		}
   219  		if len(ctx.Clusters()) == 1 {
   220  			doTest(ctx, ctx.Clusters()[0], dstInstances)
   221  		} else {
   222  			t.fromEachCluster(ctx, func(ctx framework.TestContext, c cluster.Cluster) {
   223  				doTest(ctx, c, dstInstances)
   224  			})
   225  		}
   226  	})
   227  }
   228  
   229  func (t *T) isMultipleNamespaces() bool {
   230  	namespaces := map[string]struct{}{}
   231  	for _, instances := range []echo.Instances{t.sources, t.destinations} {
   232  		for _, i := range instances {
   233  			namespaces[i.Config().Namespace.Name()] = struct{}{}
   234  			if len(namespaces) > 1 {
   235  				return true
   236  			}
   237  		}
   238  	}
   239  	return false
   240  }
   241  
   242  // fromEachDeployment enumerates subtests for deployment with the structure <src>
   243  // Intended to be used in combination with other helpers to enumerate subtests for destinations.
   244  func (t *T) fromEachDeployment(ctx framework.TestContext, testFn perDeploymentTest) {
   245  	includeNS := t.isMultipleNamespaces()
   246  
   247  	for _, from := range t.sources.Services() {
   248  		from := from
   249  		subtestName := from.Config().Service
   250  		if includeNS {
   251  			subtestName += "." + from.Config().Namespace.Prefix()
   252  		}
   253  		ctx.NewSubTest(subtestName).Run(func(ctx framework.TestContext) {
   254  			testFn(ctx, from)
   255  		})
   256  	}
   257  }
   258  
   259  // toEachDeployment enumerates subtests for every deployment as a destination, adding /to_<dst> to the parent test.
   260  // Intended to be used in combination with other helpers which enumerates the subtests and chooses the source instances.
   261  func (t *T) toEachDeployment(ctx framework.TestContext, testFn perDeploymentTest) {
   262  	includeNS := t.isMultipleNamespaces()
   263  
   264  	for _, to := range t.destinations.Services() {
   265  		to := to
   266  		subtestName := to.Config().Service
   267  		if includeNS {
   268  			subtestName += "." + to.Config().Namespace.Prefix()
   269  		}
   270  		ctx.NewSubTestf("to %s", subtestName).Run(func(ctx framework.TestContext) {
   271  			testFn(ctx, to)
   272  		})
   273  	}
   274  }
   275  
   276  func (t *T) fromEachWorkloadCluster(ctx framework.TestContext, from echo.Instances, testFn perInstanceTest) {
   277  	for _, fromInstance := range from {
   278  		fromInstance := fromInstance
   279  		if len(ctx.Clusters()) == 1 && len(from) == 1 {
   280  			testFn(ctx, fromInstance)
   281  		} else {
   282  			ctx.NewSubTestf("from %s", fromInstance.Config().Cluster.StableName()).Run(func(ctx framework.TestContext) {
   283  				// assumes we don't change config from cluster to cluster
   284  				ctx.SkipDumping()
   285  				testFn(ctx, fromInstance)
   286  			})
   287  		}
   288  	}
   289  }
   290  
   291  func (t *T) toNDeployments(ctx framework.TestContext, n int, from echo.Instances, testFn perNDeploymentTest) {
   292  	includeNS := t.isMultipleNamespaces()
   293  
   294  	// every eligible target instance from any cluster (map to dedupe)
   295  	var commonTargets []string
   296  	for _, fromInstance := range from {
   297  		// eligible target instances from the source cluster
   298  		filteredForSource := t.applyCombinationFilters(fromInstance, t.destinations)
   299  		// make sure this targets the same deployments (not necessarily the same instances)
   300  		targetNames := filteredForSource.Services().FQDNs()
   301  		if len(commonTargets) == 0 {
   302  			commonTargets = targetNames
   303  		} else if !slices.Equal(targetNames, commonTargets) {
   304  			ctx.Fatalf("%s in each cluster each cluster would not target the same set of deploments", fromInstance.Config().Service)
   305  		}
   306  	}
   307  
   308  	// we take all instances that match the deployments
   309  	// combination filters should be run again for individual sources
   310  	filteredTargets := t.destinations.Services().MatchFQDNs(commonTargets...)
   311  	for _, svc := range nDestinations(ctx, n, filteredTargets) {
   312  		svc := svc
   313  
   314  		namespacedNames := svc.NamespacedNames()
   315  		var toNames []string
   316  		if includeNS {
   317  			toNames = namespacedNames.NamesWithNamespacePrefix()
   318  		} else {
   319  			toNames = namespacedNames.Names()
   320  		}
   321  
   322  		ctx.NewSubTestf("to %s", strings.Join(toNames, " ")).Run(func(ctx framework.TestContext) {
   323  			testFn(ctx, svc)
   324  		})
   325  	}
   326  }
   327  
   328  // nDestinations splits the given deployments into subsets of size n. A deployment may be present in multiple subsets to
   329  // ensure every deployment is included.
   330  func nDestinations(ctx framework.TestContext, n int, deployments echo.Services) (out []echo.Services) {
   331  	nDests := len(deployments)
   332  	if nDests < n {
   333  		ctx.Fatalf("want to run with %d destinations but there are only %d total", n, nDests)
   334  	}
   335  	for i := 0; i < nDests; i += n {
   336  		start := i
   337  		if start+n-1 >= nDests {
   338  			// re-use a few destinations to fit the entire slice in
   339  			start = nDests - n
   340  		}
   341  		out = append(out, deployments[start:start+n])
   342  	}
   343  	return
   344  }