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  }