1
// Copyright Istio Authors
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
7
// http://www.apache.org/licenses/LICENSE-2.0
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.
23
udpa "github.com/cncf/xds/go/udpa/type/v1"
24
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
25
httprbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
26
httpwasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
27
networkrbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3"
28
networkwasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
29
wasmextensions "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
30
"github.com/envoyproxy/go-control-plane/pkg/conversion"
31
"github.com/hashicorp/go-multierror"
32
anypb "google.golang.org/protobuf/types/known/anypb"
34
extensions "istio.io/api/extensions/v1alpha1"
35
"istio.io/istio/pilot/pkg/model"
36
"istio.io/istio/pilot/pkg/util/protoconv"
37
"istio.io/istio/pkg/bootstrap"
38
"istio.io/istio/pkg/config/xds"
39
"istio.io/istio/pkg/util/istiomultierror"
43
allowHTTPTypedConfig = protoconv.MessageToAny(&httprbac.RBAC{})
44
allowNetworkTypedConfig = protoconv.MessageToAny(&networkrbac.RBAC{})
47
func createHTTPAllowAllFilter(name string) (*anypb.Any, error) {
48
ec := &core.TypedExtensionConfig{
50
TypedConfig: allowHTTPTypedConfig,
55
func createNetworkAllowAllFilter(name string) (*anypb.Any, error) {
56
ec := &core.TypedExtensionConfig{
58
TypedConfig: allowNetworkTypedConfig,
63
// MaybeConvertWasmExtensionConfig converts any presence of module remote download to local file.
64
// It downloads the Wasm module and stores the module locally in the file system.
65
func MaybeConvertWasmExtensionConfig(resources []*anypb.Any, cache Cache) error {
68
numResources := len(resources)
69
convertErrs := make([]error, numResources)
72
startTime := time.Now()
74
wasmConfigConversionDuration.Record(float64(time.Since(startTime).Milliseconds()))
77
for i := 0; i < numResources; i++ {
80
extConfig, wasmHTTPConfig, wasmNetworkConfig, err := tryUnmarshal(resources[i])
82
wasmConfigConversionCount.
83
With(resultTag.Value(unmarshalFailure)).
89
if extConfig == nil || (wasmHTTPConfig == nil && wasmNetworkConfig == nil) {
90
// If there is no config, it is not wasm config.
91
// Let's bypass the ECDS resource.
92
wasmConfigConversionCount.
93
With(resultTag.Value(noRemoteLoad)).
97
if wasmHTTPConfig != nil {
98
newExtensionConfig, err := convertHTTPWasmConfigFromRemoteToLocal(extConfig, wasmHTTPConfig, cache)
100
if !wasmHTTPConfig.GetConfig().GetFailOpen() {
104
// Use NOOP filter because the download failed.
105
newExtensionConfig, err = createHTTPAllowAllFilter(extConfig.GetName())
107
// If the fallback is failing, send the Nack regardless of fail_open.
108
err = fmt.Errorf("failed to create allow-all filter as a fallback of %s Wasm Module: %w", extConfig.GetName(), err)
113
resources[i] = newExtensionConfig
115
newExtensionConfig, err := convertNetworkWasmConfigFromRemoteToLocal(extConfig, wasmNetworkConfig, cache)
117
if !wasmNetworkConfig.GetConfig().GetFailOpen() {
121
// Use NOOP filter because the download failed.
122
newExtensionConfig, err = createNetworkAllowAllFilter(extConfig.GetName())
124
// If the fallback is failing, send the Nack regardless of fail_open.
125
err = fmt.Errorf("failed to create allow-all filter as a fallback of %s Wasm Module: %w", extConfig.GetName(), err)
130
resources[i] = newExtensionConfig
136
err := multierror.Append(istiomultierror.New(), convertErrs...).ErrorOrNil()
138
wasmLog.Errorf("convert the wasm config: %v", err)
143
// tryUnmarshal returns the typed extension config and wasm config by unmarsharling `resource`,
144
// if `resource` is a wasm config loading a wasm module from the remote site.
145
// It returns `nil` for both the typed extension config and wasm config if it is not for the remote wasm or has an error.
146
func tryUnmarshal(resource *anypb.Any) (*core.TypedExtensionConfig, *httpwasm.Wasm, *networkwasm.Wasm, error) {
147
ec := &core.TypedExtensionConfig{}
148
wasmHTTPFilterConfig := &httpwasm.Wasm{}
149
wasmNetworkFilterConfig := &networkwasm.Wasm{}
151
if err := resource.UnmarshalTo(ec); err != nil {
152
return nil, nil, nil, fmt.Errorf("failed to unmarshal extension config resource: %w", err)
155
// Wasm filter can be configured using typed struct and Wasm filter type
157
case ec.GetTypedConfig() == nil:
158
return nil, nil, nil, fmt.Errorf("typed extension config %+v does not contain any typed config", ec)
159
case ec.GetTypedConfig().TypeUrl == xds.WasmHTTPFilterType:
160
if err := ec.GetTypedConfig().UnmarshalTo(wasmHTTPFilterConfig); err != nil {
161
return nil, nil, nil, fmt.Errorf("failed to unmarshal extension config resource into Wasm HTTP filter: %w", err)
163
case ec.GetTypedConfig().TypeUrl == xds.WasmNetworkFilterType:
165
if err := ec.GetTypedConfig().UnmarshalTo(wasmNetworkFilterConfig); err != nil {
166
return nil, nil, nil, fmt.Errorf("failed to unmarshal extension config resource into Wasm Network filter: %w", err)
168
case ec.GetTypedConfig().TypeUrl == xds.TypedStructType:
169
typedStruct := &udpa.TypedStruct{}
170
wasmTypedConfig := ec.GetTypedConfig()
171
if err := wasmTypedConfig.UnmarshalTo(typedStruct); err != nil {
172
return nil, nil, nil, fmt.Errorf("failed to unmarshal typed config for wasm filter: %w", err)
175
if typedStruct.TypeUrl == xds.WasmHTTPFilterType {
176
if err := conversion.StructToMessage(typedStruct.Value, wasmHTTPFilterConfig); err != nil {
177
return nil, nil, nil, fmt.Errorf("failed to convert extension config struct %+v to Wasm Network filter", typedStruct)
179
} else if typedStruct.TypeUrl == xds.WasmNetworkFilterType {
181
if err := conversion.StructToMessage(typedStruct.Value, wasmNetworkFilterConfig); err != nil {
182
return nil, nil, nil, fmt.Errorf("failed to convert extension config struct %+v to Wasm HTTP filter", typedStruct)
185
// This is not a Wasm filter.
186
wasmLog.Debugf("typed extension config %+v does not contain wasm http filter", typedStruct)
187
return nil, nil, nil, nil
190
// This is not a Wasm filter.
191
wasmLog.Debugf("cannot find typed config or typed struct in %+v", ec)
192
return nil, nil, nil, nil
195
// At this point, we should have wasmNetworkFilterConfig or wasmHTTPFilterConfig should be unmarshalled.
197
if wasmNetworkFilterConfig.Config.GetVmConfig().GetCode().GetRemote() == nil {
198
if wasmNetworkFilterConfig.Config.GetVmConfig().GetCode().GetLocal() == nil {
199
return nil, nil, nil, fmt.Errorf("no remote and local load found in Wasm Network filter %+v", wasmNetworkFilterConfig)
201
// This has a local Wasm. Let's bypass it.
202
wasmLog.Debugf("no remote load found in Wasm Network filter %+v", wasmNetworkFilterConfig)
203
return nil, nil, nil, nil
205
return ec, nil, wasmNetworkFilterConfig, nil
207
if wasmHTTPFilterConfig.Config.GetVmConfig().GetCode().GetRemote() == nil {
208
if wasmHTTPFilterConfig.Config.GetVmConfig().GetCode().GetLocal() == nil {
209
return nil, nil, nil, fmt.Errorf("no remote and local load found in Wasm HTTP filter %+v", wasmHTTPFilterConfig)
211
// This has a local Wasm. Let's bypass it.
212
wasmLog.Debugf("no remote load found in Wasm HTTP filter %+v", wasmHTTPFilterConfig)
213
return nil, nil, nil, nil
216
return ec, wasmHTTPFilterConfig, nil, nil
219
func convertHTTPWasmConfigFromRemoteToLocal(ec *core.TypedExtensionConfig, wasmHTTPFilterConfig *httpwasm.Wasm, cache Cache) (*anypb.Any, error) {
220
status := conversionSuccess
222
wasmConfigConversionCount.
223
With(resultTag.Value(status)).
227
// ec.Name is resourceName.
228
// https://github.com/istio/istio/blob/9ea7ad532a9cc58a3564143d41ac89a61aaa8058/pilot/pkg/networking/core/v1alpha3/extension/wasmplugin.go#L103
229
err := rewriteVMConfig(ec.Name, wasmHTTPFilterConfig.Config.GetVmConfig(), &status, cache, wasmHTTPFilterConfig.Config.Name)
234
wasmTypedConfig, err := anypb.New(wasmHTTPFilterConfig)
236
status = marshalFailure
237
return nil, fmt.Errorf("failed to marshal new wasm HTTP filter %+v to protobuf Any: %w", wasmHTTPFilterConfig, err)
239
ec.TypedConfig = wasmTypedConfig
240
wasmLog.Debugf("new extension config resource %+v", ec)
242
nec, err := anypb.New(ec)
244
status = marshalFailure
245
return nil, fmt.Errorf("failed to marshal new extension config resource: %w", err)
248
// At this point, we are certain that wasm module has been downloaded and config is rewritten.
249
// ECDS will be rewritten successfully.
253
func convertNetworkWasmConfigFromRemoteToLocal(ec *core.TypedExtensionConfig, wasmNetworkFilterConfig *networkwasm.Wasm, cache Cache) (*anypb.Any, error) {
254
status := conversionSuccess
256
wasmConfigConversionCount.
257
With(resultTag.Value(status)).
261
// ec.Name is resourceName.
262
// https://github.com/istio/istio/blob/9ea7ad532a9cc58a3564143d41ac89a61aaa8058/pilot/pkg/networking/core/v1alpha3/extension/wasmplugin.go#L103
263
err := rewriteVMConfig(ec.Name, wasmNetworkFilterConfig.Config.GetVmConfig(), &status, cache, wasmNetworkFilterConfig.Config.Name)
267
wasmTypedConfig, err := anypb.New(wasmNetworkFilterConfig)
269
status = marshalFailure
270
return nil, fmt.Errorf("failed to marshal new wasm Network filter %+v to protobuf Any: %w", wasmNetworkFilterConfig, err)
272
ec.TypedConfig = wasmTypedConfig
273
wasmLog.Debugf("new extension config resource %+v", ec)
275
nec, err := anypb.New(ec)
277
status = marshalFailure
278
return nil, fmt.Errorf("failed to marshal new extension config resource: %w", err)
281
// At this point, we are certain that wasm module has been downloaded and config is rewritten.
282
// ECDS will be rewritten successfully.
286
func rewriteVMConfig(resourceName string, vm *wasmextensions.VmConfig, status *string, cache Cache, configName string) error {
287
envs := vm.GetEnvironmentVariables()
288
var pullSecret []byte
289
pullPolicy := extensions.PullPolicy_UNSPECIFIED_POLICY
290
resourceVersion := ""
292
if sec, found := envs.KeyValues[model.WasmSecretEnv]; found {
294
*status = fetchFailure
295
return fmt.Errorf("cannot fetch Wasm module %v: missing image pulling secret", configName)
297
pullSecret = []byte(sec)
300
if ps, found := envs.KeyValues[model.WasmPolicyEnv]; found {
301
if p, found := extensions.PullPolicy_value[ps]; found {
302
pullPolicy = extensions.PullPolicy(p)
305
resourceVersion = envs.KeyValues[model.WasmResourceVersionEnv]
307
// Strip all internal env variables(with ISTIO_META) from VM env variable.
308
// These env variables are added by Istio control plane and meant to be consumed by the
309
// agent for image pulling control should not be leaked to Envoy or the Wasm extension runtime.
310
for k := range envs.KeyValues {
311
if strings.HasPrefix(k, bootstrap.IstioMetaPrefix) {
312
delete(envs.KeyValues, k)
315
if len(envs.KeyValues) == 0 {
316
if len(envs.HostEnvKeys) == 0 {
317
vm.EnvironmentVariables = nil
323
remote := vm.GetCode().GetRemote()
324
httpURI := remote.GetHttpUri()
326
*status = missRemoteFetchHint
327
return fmt.Errorf("wasm remote fetch %+v does not have httpUri specified for config %s", remote, configName)
329
// checksum sent by istiod can be "nil" if not set by user - magic value used to avoid unmarshaling errors
330
if remote.Sha256 == "nil" {
334
// Default timeout, without this, if a user does not specify a timeout in the config, it fails with deadline exceeded
335
// while building transport in go container.
336
timeout := time.Second * 5
337
if remote.GetHttpUri().Timeout != nil {
338
// This is always 30s, because the timeout is set by the control plane when converted to WasmPluginWrapper.
339
// see buildDataSource() in pilot/pkg/model/extensions.go
340
timeout = remote.GetHttpUri().Timeout.AsDuration()
342
f, err := cache.Get(httpURI.GetUri(), GetOptions{
343
Checksum: remote.Sha256,
344
ResourceName: resourceName,
345
ResourceVersion: resourceVersion,
346
RequestTimeout: timeout,
347
PullSecret: pullSecret,
348
PullPolicy: pullPolicy,
351
*status = fetchFailure
352
return fmt.Errorf("cannot fetch Wasm module %v: %w", remote.GetHttpUri().GetUri(), err)
355
// Rewrite remote fetch to local file.
356
vm.Code = &core.AsyncDataSource{
357
Specifier: &core.AsyncDataSource_Local{
358
Local: &core.DataSource{
359
Specifier: &core.DataSource_Filename{