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 }