istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/telemetry/api/wasmplugin_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 api 19 20 import ( 21 "fmt" 22 "testing" 23 "time" 24 25 "k8s.io/apiserver/pkg/storage/names" 26 27 "istio.io/istio/pkg/config/protocol" 28 "istio.io/istio/pkg/http/headers" 29 "istio.io/istio/pkg/test/framework" 30 "istio.io/istio/pkg/test/framework/components/crd" 31 "istio.io/istio/pkg/test/framework/components/echo" 32 "istio.io/istio/pkg/test/framework/components/echo/check" 33 "istio.io/istio/pkg/test/framework/components/echo/match" 34 "istio.io/istio/pkg/test/framework/components/prometheus" 35 "istio.io/istio/pkg/test/util/retry" 36 util "istio.io/istio/tests/integration/telemetry" 37 ) 38 39 const ( 40 imageName = "istio-testing/wasm/header-injector" 41 injectedHeader = "x-resp-injection" 42 wasmConfigFile = "testdata/wasm-filter.yaml" 43 ) 44 45 type wasmTestConfigs struct { 46 desc string 47 name string 48 policy string 49 tag string 50 upstreamVersion string 51 expectedVersion string 52 testHostname string 53 } 54 55 var generation = 0 56 57 func mapTagToVersionOrFail(t framework.TestContext, tag, version string) { 58 t.Helper() 59 if err := registry.SetupTagMap(map[string]string{ 60 imageName + ":" + tag: version, 61 }); err != nil { 62 t.Fatalf("failed to setup the tag map: %v", err) 63 } 64 } 65 66 func applyAndTestWasmWithOCI(ctx framework.TestContext, c wasmTestConfigs) { 67 applyAndTestCustomWasmConfigWithOCI(ctx, c, wasmConfigFile) 68 } 69 70 func applyAndTestCustomWasmConfigWithOCI(ctx framework.TestContext, c wasmTestConfigs, path string) { 71 ctx.NewSubTest("OCI_" + c.desc).Run(func(t framework.TestContext) { 72 defer func() { 73 generation++ 74 }() 75 mapTagToVersionOrFail(t, c.tag, c.upstreamVersion) 76 wasmModuleURL := fmt.Sprintf("oci://%v/%v:%v", registry.Address(), imageName, c.tag) 77 if err := installWasmExtension(t, c.name, wasmModuleURL, c.policy, fmt.Sprintf("g-%d", generation), path); err != nil { 78 t.Fatalf("failed to install WasmPlugin: %v", err) 79 } 80 if c.testHostname != "" { 81 sendTrafficToHostname(t, check.ResponseHeader(injectedHeader, c.expectedVersion), c.testHostname) 82 } else { 83 sendTraffic(t, check.ResponseHeader(injectedHeader, c.expectedVersion)) 84 } 85 }) 86 } 87 88 func resetWasm(ctx framework.TestContext, pluginName string) { 89 ctx.NewSubTest("Delete WasmPlugin " + pluginName).Run(func(t framework.TestContext) { 90 if err := uninstallWasmExtension(t, pluginName, wasmConfigFile); err != nil { 91 t.Fatal(err) 92 } 93 sendTraffic(t, check.ResponseHeader(injectedHeader, ""), retry.Converge(2)) 94 }) 95 } 96 97 func resetCustomWasmConfig(ctx framework.TestContext, pluginName, path string) { 98 ctx.NewSubTest("Delete WasmPlugin " + pluginName).Run(func(t framework.TestContext) { 99 if err := uninstallWasmExtension(t, pluginName, path); err != nil { 100 t.Fatal(err) 101 } 102 sendTraffic(t, check.ResponseHeader(injectedHeader, ""), retry.Converge(2)) 103 }) 104 } 105 106 func TestImagePullPolicy(t *testing.T) { 107 framework.NewTest(t). 108 Run(func(t framework.TestContext) { 109 tag := names.SimpleNameGenerator.GenerateName("test-tag-") 110 applyAndTestWasmWithOCI(t, wasmTestConfigs{ 111 desc: "initial creation with 0.0.1", 112 name: "wasm-test-module", 113 tag: tag, 114 policy: "", 115 upstreamVersion: "0.0.1", 116 expectedVersion: "0.0.1", 117 }) 118 119 resetWasm(t, "wasm-test-module") 120 applyAndTestWasmWithOCI(t, wasmTestConfigs{ 121 desc: "upstream is upgraded to 0.0.2, but 0.0.1 is already present and policy is IfNotPresent", 122 name: "wasm-test-module", 123 tag: tag, 124 policy: "IfNotPresent", 125 upstreamVersion: "0.0.2", 126 expectedVersion: "0.0.1", 127 }) 128 129 resetWasm(t, "wasm-test-module") 130 applyAndTestWasmWithOCI(t, wasmTestConfigs{ 131 desc: "upstream is upgraded to 0.0.2, but 0.0.1 is already present and policy is default", 132 name: "wasm-test-module", 133 tag: tag, 134 policy: "", 135 upstreamVersion: "0.0.2", 136 expectedVersion: "0.0.1", 137 }) 138 139 // Intentionally, do not reset here to see the upgrade from 0.0.1. 140 applyAndTestWasmWithOCI(t, wasmTestConfigs{ 141 desc: "upstream is upgraded to 0.0.2. 0.0.1 is already present but policy is Always, so pull 0.0.2", 142 name: "wasm-test-module", 143 tag: tag, 144 policy: "Always", 145 upstreamVersion: "0.0.2", 146 expectedVersion: "0.0.2", 147 }) 148 }) 149 } 150 151 func applyWasmConfig(ctx framework.TestContext, ns string, args map[string]any, path string) error { 152 return ctx.ConfigIstio().EvalFile(ns, args, path).Apply() 153 } 154 155 func installWasmExtension(ctx framework.TestContext, pluginName, wasmModuleURL, imagePullPolicy, pluginVersion, path string) error { 156 args := map[string]any{ 157 "WasmPluginName": pluginName, 158 "TestWasmModuleURL": wasmModuleURL, 159 "WasmPluginVersion": pluginVersion, 160 "TargetAppName": GetTarget().(echo.Instances).NamespacedName().Name, 161 "TargetGatewayName": GetTarget().(echo.Instances).ServiceName() + "-gateway", 162 } 163 164 if len(imagePullPolicy) != 0 { 165 args["ImagePullPolicy"] = imagePullPolicy 166 } 167 168 if err := applyWasmConfig(ctx, apps.Namespace.Name(), args, path); err != nil { 169 return err 170 } 171 172 return nil 173 } 174 175 func uninstallWasmExtension(ctx framework.TestContext, pluginName, path string) error { 176 args := map[string]any{ 177 "WasmPluginName": pluginName, 178 } 179 if err := ctx.ConfigIstio().EvalFile(apps.Namespace.Name(), args, path).Delete(); err != nil { 180 return err 181 } 182 return nil 183 } 184 185 func sendTraffic(ctx framework.TestContext, checker echo.Checker, options ...retry.Option) { 186 ctx.Helper() 187 if len(GetClientInstances()) == 0 { 188 ctx.Fatal("there is no client") 189 } 190 cltInstance := GetClientInstances()[0] 191 192 defaultOptions := []retry.Option{retry.Delay(100 * time.Millisecond), retry.Timeout(200 * time.Second)} 193 httpOpts := echo.CallOptions{ 194 To: GetTarget(), 195 Port: echo.Port{ 196 Name: "http", 197 }, 198 HTTP: echo.HTTP{ 199 Path: "/path", 200 Method: "GET", 201 }, 202 Count: 1, 203 Retry: echo.Retry{ 204 Options: append(defaultOptions, options...), 205 }, 206 Check: checker, 207 } 208 209 _ = cltInstance.CallOrFail(ctx, httpOpts) 210 } 211 212 func sendTrafficToHostname(ctx framework.TestContext, checker echo.Checker, hostname string, options ...retry.Option) { 213 ctx.Helper() 214 if len(GetClientInstances()) == 0 { 215 ctx.Fatal("there is no client") 216 } 217 cltInstance := GetClientInstances()[0] 218 219 defaultOptions := []retry.Option{retry.Delay(100 * time.Millisecond), retry.Timeout(200 * time.Second)} 220 httpOpts := echo.CallOptions{ 221 Address: hostname, 222 Port: echo.Port{ 223 Name: "http", 224 ServicePort: 80, 225 Protocol: protocol.HTTP, 226 }, 227 HTTP: echo.HTTP{ 228 Path: "/path", 229 Method: "GET", 230 Headers: headers.New().WithHost(fmt.Sprintf("%s.com", GetTarget().ServiceName())).Build(), 231 }, 232 Count: 1, 233 Retry: echo.Retry{ 234 Options: append(defaultOptions, options...), 235 }, 236 Check: checker, 237 } 238 239 _ = cltInstance.CallOrFail(ctx, httpOpts) 240 } 241 242 func applyAndTestWasmWithHTTP(ctx framework.TestContext, c wasmTestConfigs) { 243 applyAndTestCustomWasmConfigWithHTTP(ctx, c, wasmConfigFile) 244 } 245 246 func applyAndTestCustomWasmConfigWithHTTP(ctx framework.TestContext, c wasmTestConfigs, path string) { 247 ctx.NewSubTest("HTTP_" + c.desc).Run(func(t framework.TestContext) { 248 defer func() { 249 generation++ 250 }() 251 mapTagToVersionOrFail(t, c.tag, c.upstreamVersion) 252 // registry-redirector will redirect to the gzipped tarball of the first layer with this request. 253 // The gzipped tarball should have a wasm module. 254 wasmModuleURL := fmt.Sprintf("http://%v/layer/v1/%v:%v", registry.Address(), imageName, c.tag) 255 t.Logf("Trying to get a wasm file from %v", wasmModuleURL) 256 if err := installWasmExtension(t, c.name, wasmModuleURL, c.policy, fmt.Sprintf("g-%d", generation), path); err != nil { 257 t.Fatalf("failed to install WasmPlugin: %v", err) 258 } 259 sendTraffic(t, check.ResponseHeader(injectedHeader, c.expectedVersion)) 260 }) 261 } 262 263 // TestTargetRef vs workloadSelector for gateways 264 func TestGatewaySelection(t *testing.T) { 265 framework.NewTest(t). 266 Run(func(t framework.TestContext) { 267 crd.DeployGatewayAPIOrSkip(t) 268 args := map[string]any{ 269 "To": GetTarget().(echo.Instances), 270 } 271 t.ConfigIstio().EvalFile(apps.Namespace.Name(), args, "testdata/gateway-api.yaml").ApplyOrFail(t) 272 applyAndTestCustomWasmConfigWithOCI(t, wasmTestConfigs{ 273 desc: "initial creation with latest for a gateway", 274 name: "wasm-test-module", 275 tag: "latest", 276 policy: "", 277 upstreamVersion: "0.0.1", 278 expectedVersion: "0.0.1", 279 testHostname: fmt.Sprintf("%s-gateway-istio.%s.svc.cluster.local", GetTarget().ServiceName(), apps.Namespace.Name()), 280 }, "testdata/gateway-wasm-filter.yaml") 281 282 resetCustomWasmConfig(t, "wasm-test-module", "testdata/gateway-wasm-filter.yaml") 283 }) 284 } 285 286 // TestImagePullPolicyWithHTTP tests pulling Wasm Binary via HTTP and ImagePullPolicy. 287 func TestImagePullPolicyWithHTTP(t *testing.T) { 288 framework.NewTest(t). 289 Run(func(t framework.TestContext) { 290 tag := names.SimpleNameGenerator.GenerateName("test-tag-") 291 applyAndTestWasmWithHTTP(t, wasmTestConfigs{ 292 desc: "initial creation with 0.0.1", 293 name: "wasm-test-module-http", 294 tag: tag, 295 policy: "", 296 upstreamVersion: "0.0.1", 297 expectedVersion: "0.0.1", 298 }) 299 300 resetWasm(t, "wasm-test-module-http") 301 applyAndTestWasmWithHTTP(t, wasmTestConfigs{ 302 desc: "upstream is upgraded to 0.0.2, but 0.0.1 is already present and policy is IfNotPresent", 303 name: "wasm-test-module-http", 304 tag: tag, 305 policy: "IfNotPresent", 306 upstreamVersion: "0.0.2", 307 expectedVersion: "0.0.1", 308 }) 309 310 resetWasm(t, "wasm-test-module-http") 311 applyAndTestWasmWithHTTP(t, wasmTestConfigs{ 312 desc: "upstream is upgraded to 0.0.2, but 0.0.1 is already present and policy is default", 313 name: "wasm-test-module-http", 314 tag: tag, 315 policy: "", 316 upstreamVersion: "0.0.2", 317 expectedVersion: "0.0.1", 318 }) 319 320 // Intentionally, do not reset here to see the upgrade from 0.0.1. 321 applyAndTestWasmWithHTTP(t, wasmTestConfigs{ 322 desc: "upstream is upgraded to 0.0.2. 0.0.1 is already present but policy is Always, so pull 0.0.2", 323 name: "wasm-test-module-http", 324 tag: tag, 325 policy: "Always", 326 upstreamVersion: "0.0.2", 327 expectedVersion: "0.0.2", 328 }) 329 }) 330 } 331 332 // TestBadWasmRemoteLoad tests that bad Wasm remote load configuration won't affect service. 333 // The test will set up an echo client and server, test echo ping works fine. Then apply a 334 // Wasm filter which has a bad URL link, which will result as module download failure. After that, 335 // verifies that echo ping could still work. The test also verifies that metrics are properly 336 // recorded for module downloading failure and nack on ECDS update. 337 func TestBadWasmRemoteLoad(t *testing.T) { 338 framework.NewTest(t). 339 Run(func(t framework.TestContext) { 340 // Enable logging for debugging 341 applyTelemetryResource(t, true) 342 badWasmTestHelper(t, "testdata/bad-filter.yaml", false, true) 343 }) 344 } 345 346 // TestBadWasmWithFailOpen is basically the same with TestBadWasmRemoteLoad except 347 // it tests with "fail_open = true". To test the fail_open, the target pod is restarted 348 // after applying the Wasm filter. 349 // At this moment, there is no "fail_open" option in WasmPlugin API. So, we test it using 350 // EnvoyFilter. When WasmPlugin has a "fail_open" option in the API plane, we need to change 351 // this test to use the WasmPlugin API 352 func TestBadWasmWithFailOpen(t *testing.T) { 353 framework.NewTest(t). 354 Run(func(t framework.TestContext) { 355 // Enable logging for debugging 356 applyTelemetryResource(t, true) 357 // since this case is for "fail_open=true", ecds is not rejected. 358 badWasmTestHelper(t, "testdata/bad-wasm-envoy-filter-fail-open.yaml", true, false) 359 }) 360 } 361 362 func badWasmTestHelper(t framework.TestContext, filterConfigPath string, restartTarget bool, ecdsShouldReject bool) { 363 t.Helper() 364 // Test bad wasm remote load in only one cluster. 365 // There is no need to repeat the same testing logic in multiple clusters. 366 to := match.Cluster(t.Clusters().Default()).FirstOrFail(t, GetClientInstances()) 367 // Verify that echo server could return 200 368 SendTrafficOrFail(t, to) 369 t.Log("echo server returns OK, apply bad wasm remote load filter.") 370 371 // Apply bad filter config 372 t.Logf("use config in %s.", filterConfigPath) 373 t.ConfigIstio().File(apps.Namespace.Name(), filterConfigPath).ApplyOrFail(t) 374 if restartTarget { 375 target := match.Cluster(t.Clusters().Default()).FirstOrFail(t, GetTarget().Instances()) 376 if err := target.Restart(); err != nil { 377 t.Fatalf("failed to restart the target pod: %v", err) 378 } 379 } 380 381 // Wait until there is agent metrics for wasm download failure 382 retry.UntilSuccessOrFail(t, func() error { 383 q := prometheus.Query{Metric: "istio_agent_wasm_remote_fetch_count", Labels: map[string]string{"result": "download_failure"}} 384 c := to.Config().Cluster 385 if _, err := util.QueryPrometheus(t, c, q, promInst); err != nil { 386 util.PromDiff(t, promInst, c, q) 387 return err 388 } 389 return nil 390 }, retry.Delay(1*time.Second), retry.Timeout(80*time.Second)) 391 392 if ecdsShouldReject && t.Clusters().Default().IsPrimary() { // Only check istiod if running locally (i.e., not an external control plane) 393 // Verify that istiod has a stats about rejected ECDS update 394 // pilot_total_xds_rejects{type="type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig"} 395 retry.UntilSuccessOrFail(t, func() error { 396 q := prometheus.Query{Metric: "pilot_total_xds_rejects", Labels: map[string]string{"type": "ecds"}} 397 c := to.Config().Cluster 398 if _, err := util.QueryPrometheus(t, c, q, promInst); err != nil { 399 util.PromDiff(t, promInst, c, q) 400 return err 401 } 402 return nil 403 }, retry.Delay(1*time.Second), retry.Timeout(80*time.Second)) 404 } 405 406 t.Log("got istio_agent_wasm_remote_fetch_count metric in prometheus, bad wasm filter is applied, send request to echo server again.") 407 408 // Verify that echo server could still return 200 409 SendTrafficOrFail(t, to) 410 411 t.Log("echo server still returns ok after bad wasm filter is applied.") 412 }