istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/extensions.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 model
    16  
    17  import (
    18  	"net/url"
    19  	"strings"
    20  	"time"
    21  
    22  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    23  	httpwasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
    24  	networkwasm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
    25  	wasmextensions "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
    26  	anypb "google.golang.org/protobuf/types/known/anypb"
    27  	"google.golang.org/protobuf/types/known/durationpb"
    28  	"google.golang.org/protobuf/types/known/wrapperspb"
    29  	"k8s.io/apimachinery/pkg/types"
    30  
    31  	extensions "istio.io/api/extensions/v1alpha1"
    32  	typeapi "istio.io/api/type/v1beta1"
    33  	"istio.io/istio/pilot/pkg/model/credentials"
    34  	istionetworking "istio.io/istio/pilot/pkg/networking"
    35  	"istio.io/istio/pilot/pkg/util/protoconv"
    36  	"istio.io/istio/pkg/config"
    37  	"istio.io/istio/pkg/config/schema/gvk"
    38  	pm "istio.io/istio/pkg/model"
    39  	"istio.io/istio/pkg/util/protomarshal"
    40  )
    41  
    42  const (
    43  	defaultRuntime = "envoy.wasm.runtime.v8"
    44  	fileScheme     = "file"
    45  	ociScheme      = "oci"
    46  
    47  	WasmSecretEnv          = pm.WasmSecretEnv
    48  	WasmPolicyEnv          = pm.WasmPolicyEnv
    49  	WasmResourceVersionEnv = pm.WasmResourceVersionEnv
    50  
    51  	// WasmPluginResourceNamePrefix is the prefix of the resource name of WasmPlugin,
    52  	// preventing the name collision with other resources.
    53  	WasmPluginResourceNamePrefix = "extenstions.istio.io/wasmplugin/"
    54  )
    55  
    56  // WasmPluginType defines the type of wasm plugin
    57  type WasmPluginType int
    58  
    59  const (
    60  	WasmPluginTypeHTTP WasmPluginType = iota
    61  	WasmPluginTypeNetwork
    62  	WasmPluginTypeAny
    63  )
    64  
    65  func fromPluginType(pluginType extensions.PluginType) WasmPluginType {
    66  	switch pluginType {
    67  	case extensions.PluginType_HTTP:
    68  		return WasmPluginTypeHTTP
    69  	case extensions.PluginType_NETWORK:
    70  		return WasmPluginTypeNetwork
    71  	case extensions.PluginType_UNSPECIFIED_PLUGIN_TYPE:
    72  		return WasmPluginTypeHTTP // Return HTTP as default for backward compatibility.
    73  	}
    74  	return WasmPluginTypeHTTP
    75  }
    76  
    77  func workloadModeForListenerClass(class istionetworking.ListenerClass) typeapi.WorkloadMode {
    78  	switch class {
    79  	case istionetworking.ListenerClassGateway:
    80  		return typeapi.WorkloadMode_CLIENT
    81  	case istionetworking.ListenerClassSidecarInbound:
    82  		return typeapi.WorkloadMode_SERVER
    83  	case istionetworking.ListenerClassSidecarOutbound:
    84  		return typeapi.WorkloadMode_CLIENT
    85  	case istionetworking.ListenerClassUndefined:
    86  		// this should not happen, just in case
    87  		return typeapi.WorkloadMode_CLIENT
    88  	}
    89  	return typeapi.WorkloadMode_CLIENT
    90  }
    91  
    92  type WasmPluginWrapper struct {
    93  	*extensions.WasmPlugin
    94  
    95  	Name            string
    96  	Namespace       string
    97  	ResourceName    string
    98  	ResourceVersion string
    99  }
   100  
   101  func (p *WasmPluginWrapper) MatchListener(matcher WorkloadPolicyMatcher, li WasmPluginListenerInfo) bool {
   102  	if matcher.ShouldAttachPolicy(gvk.WasmPlugin, p.NamespacedName(), p) {
   103  		return matchTrafficSelectors(p.Match, li)
   104  	}
   105  
   106  	// If it doesn't match one of the above cases, the plugin is not bound to this workload
   107  	return false
   108  }
   109  
   110  func (p *WasmPluginWrapper) NamespacedName() types.NamespacedName {
   111  	return types.NamespacedName{Name: p.Name, Namespace: p.Namespace}
   112  }
   113  
   114  func (p *WasmPluginWrapper) MatchType(pluginType WasmPluginType) bool {
   115  	return pluginType == WasmPluginTypeAny || pluginType == fromPluginType(p.WasmPlugin.Type)
   116  }
   117  
   118  func (p *WasmPluginWrapper) BuildHTTPWasmFilter() *httpwasm.Wasm {
   119  	if !(p.Type == extensions.PluginType_HTTP || p.Type == extensions.PluginType_UNSPECIFIED_PLUGIN_TYPE) {
   120  		return nil
   121  	}
   122  	return &httpwasm.Wasm{
   123  		Config: p.buildPluginConfig(),
   124  	}
   125  }
   126  
   127  func (p *WasmPluginWrapper) BuildNetworkWasmFilter() *networkwasm.Wasm {
   128  	if p.Type != extensions.PluginType_NETWORK {
   129  		return nil
   130  	}
   131  	return &networkwasm.Wasm{
   132  		Config: p.buildPluginConfig(),
   133  	}
   134  }
   135  
   136  func (p *WasmPluginWrapper) buildPluginConfig() *wasmextensions.PluginConfig {
   137  	cfg := &anypb.Any{}
   138  	plugin := p.WasmPlugin
   139  	if plugin.PluginConfig != nil && len(plugin.PluginConfig.Fields) > 0 {
   140  		cfgJSON, err := protomarshal.ToJSON(plugin.PluginConfig)
   141  		if err != nil {
   142  			log.Warnf("wasmplugin %v/%v discarded due to json marshaling error: %s", p.Namespace, p.Name, err)
   143  			return nil
   144  		}
   145  		cfg = protoconv.MessageToAny(&wrapperspb.StringValue{
   146  			Value: cfgJSON,
   147  		})
   148  	}
   149  
   150  	u, err := url.Parse(plugin.Url)
   151  	if err != nil {
   152  		log.Warnf("wasmplugin %v/%v discarded due to failure to parse URL: %s", p.Namespace, p.Name, err)
   153  		return nil
   154  	}
   155  	// when no scheme is given, default to oci://
   156  	if u.Scheme == "" {
   157  		u.Scheme = ociScheme
   158  	}
   159  
   160  	datasource := buildDataSource(u, plugin)
   161  	resourceName := p.Namespace + "." + p.Name
   162  	return &wasmextensions.PluginConfig{
   163  		Name:          resourceName,
   164  		RootId:        plugin.PluginName,
   165  		Configuration: cfg,
   166  		Vm:            buildVMConfig(datasource, p.ResourceVersion, plugin),
   167  		FailOpen:      plugin.FailStrategy == extensions.FailStrategy_FAIL_OPEN,
   168  	}
   169  }
   170  
   171  type WasmPluginListenerInfo struct {
   172  	Port  int
   173  	Class istionetworking.ListenerClass
   174  
   175  	// Service that WasmPlugins can attach to via targetRefs (optional)
   176  	Service *Service
   177  }
   178  
   179  // If anyListener is used as a listener info,
   180  // the listener is matched with any TrafficSelector.
   181  var anyListener = WasmPluginListenerInfo{
   182  	Port:  0,
   183  	Class: istionetworking.ListenerClassUndefined,
   184  }
   185  
   186  func matchTrafficSelectors(ts []*extensions.WasmPlugin_TrafficSelector, li WasmPluginListenerInfo) bool {
   187  	if (li.Class == istionetworking.ListenerClassUndefined && li.Port == 0) || len(ts) == 0 {
   188  		return true
   189  	}
   190  
   191  	for _, match := range ts {
   192  		if matchMode(match.Mode, li.Class) && matchPorts(match.Ports, li.Port) {
   193  			return true
   194  		}
   195  	}
   196  	return false
   197  }
   198  
   199  func matchMode(workloadMode typeapi.WorkloadMode, class istionetworking.ListenerClass) bool {
   200  	switch workloadMode {
   201  	case typeapi.WorkloadMode_CLIENT_AND_SERVER, typeapi.WorkloadMode_UNDEFINED:
   202  		return true
   203  	default:
   204  		return workloadMode == workloadModeForListenerClass(class)
   205  	}
   206  }
   207  
   208  func matchPorts(portSelectors []*typeapi.PortSelector, port int) bool {
   209  	if len(portSelectors) == 0 {
   210  		// If there is no specified port, match with all the ports.
   211  		return true
   212  	}
   213  	for _, ps := range portSelectors {
   214  		if ps.GetNumber() != 0 && ps.GetNumber() == uint32(port) {
   215  			return true
   216  		}
   217  	}
   218  	return false
   219  }
   220  
   221  func convertToWasmPluginWrapper(originPlugin config.Config) *WasmPluginWrapper {
   222  	var ok bool
   223  	// Make a deep copy since we are going to mutate the resource later for secret env variable.
   224  	// We do not want to mutate the underlying resource at informer cache.
   225  	plugin := originPlugin.DeepCopy()
   226  	var wasmPlugin *extensions.WasmPlugin
   227  	if wasmPlugin, ok = plugin.Spec.(*extensions.WasmPlugin); !ok {
   228  		return nil
   229  	}
   230  
   231  	if wasmPlugin.PluginConfig != nil && len(wasmPlugin.PluginConfig.Fields) > 0 {
   232  		_, err := protomarshal.ToJSON(wasmPlugin.PluginConfig)
   233  		if err != nil {
   234  			log.Warnf("wasmplugin %v/%v discarded due to json marshaling error: %s", plugin.Namespace, plugin.Name, err)
   235  			return nil
   236  		}
   237  	}
   238  
   239  	u, err := url.Parse(wasmPlugin.Url)
   240  	if err != nil {
   241  		log.Warnf("wasmplugin %v/%v discarded due to failure to parse URL: %s", plugin.Namespace, plugin.Name, err)
   242  		return nil
   243  	}
   244  	// when no scheme is given, default to oci://
   245  	if u.Scheme == "" {
   246  		u.Scheme = ociScheme
   247  	}
   248  	// Normalize the image pull secret to the full resource name.
   249  	wasmPlugin.ImagePullSecret = toSecretResourceName(wasmPlugin.ImagePullSecret, plugin.Namespace)
   250  	return &WasmPluginWrapper{
   251  		Name:            plugin.Name,
   252  		Namespace:       plugin.Namespace,
   253  		ResourceName:    WasmPluginResourceNamePrefix + plugin.Namespace + "." + plugin.Name,
   254  		WasmPlugin:      wasmPlugin,
   255  		ResourceVersion: plugin.ResourceVersion,
   256  	}
   257  }
   258  
   259  // toSecretResourceName converts a imagePullSecret to a resource name referenced at Wasm SDS.
   260  // NOTE: the secret referenced by WasmPlugin has to be in the same namespace as the WasmPlugin,
   261  // so this function makes sure that the secret resource name, which will be used to retrieve secret at
   262  // xds generation time, has the same namespace as the WasmPlugin.
   263  func toSecretResourceName(name, pluginNamespace string) string {
   264  	if name == "" {
   265  		return ""
   266  	}
   267  	// Convert user provided secret name to secret resource name.
   268  	rn := credentials.ToResourceName(name)
   269  	// Parse the secret resource name.
   270  	sr, err := credentials.ParseResourceName(rn, pluginNamespace, "", "")
   271  	if err != nil {
   272  		log.Debugf("Failed to parse wasm secret resource name %v", err)
   273  		return ""
   274  	}
   275  	// Forcely rewrite secret namespace to plugin namespace, since we require secret resource
   276  	// referenced by WasmPlugin co-located with WasmPlugin in the same namespace.
   277  	sr.Namespace = pluginNamespace
   278  	return sr.KubernetesResourceName()
   279  }
   280  
   281  func buildDataSource(u *url.URL, wasmPlugin *extensions.WasmPlugin) *core.AsyncDataSource {
   282  	if u.Scheme == fileScheme {
   283  		return &core.AsyncDataSource{
   284  			Specifier: &core.AsyncDataSource_Local{
   285  				Local: &core.DataSource{
   286  					Specifier: &core.DataSource_Filename{
   287  						Filename: strings.TrimPrefix(wasmPlugin.Url, "file://"),
   288  					},
   289  				},
   290  			},
   291  		}
   292  	}
   293  
   294  	return &core.AsyncDataSource{
   295  		Specifier: &core.AsyncDataSource_Remote{
   296  			Remote: &core.RemoteDataSource{
   297  				HttpUri: &core.HttpUri{
   298  					Uri:     u.String(),
   299  					Timeout: durationpb.New(30 * time.Second), // TODO: make this configurable?
   300  					HttpUpstreamType: &core.HttpUri_Cluster{
   301  						// the agent will fetch this anyway, so no need for a cluster
   302  						Cluster: "_",
   303  					},
   304  				},
   305  				Sha256: wasmPlugin.Sha256,
   306  			},
   307  		},
   308  	}
   309  }
   310  
   311  func buildVMConfig(
   312  	datasource *core.AsyncDataSource,
   313  	resourceVersion string,
   314  	wasmPlugin *extensions.WasmPlugin,
   315  ) *wasmextensions.PluginConfig_VmConfig {
   316  	cfg := &wasmextensions.PluginConfig_VmConfig{
   317  		VmConfig: &wasmextensions.VmConfig{
   318  			Runtime: defaultRuntime,
   319  			Code:    datasource,
   320  			EnvironmentVariables: &wasmextensions.EnvironmentVariables{
   321  				KeyValues: map[string]string{},
   322  			},
   323  		},
   324  	}
   325  
   326  	if wasmPlugin.ImagePullSecret != "" {
   327  		cfg.VmConfig.EnvironmentVariables.KeyValues[WasmSecretEnv] = wasmPlugin.ImagePullSecret
   328  	}
   329  
   330  	if wasmPlugin.ImagePullPolicy != extensions.PullPolicy_UNSPECIFIED_POLICY {
   331  		cfg.VmConfig.EnvironmentVariables.KeyValues[WasmPolicyEnv] = wasmPlugin.ImagePullPolicy.String()
   332  	}
   333  
   334  	cfg.VmConfig.EnvironmentVariables.KeyValues[WasmResourceVersionEnv] = resourceVersion
   335  
   336  	vm := wasmPlugin.VmConfig
   337  	if vm != nil && len(vm.Env) != 0 {
   338  		hostEnvKeys := make([]string, 0, len(vm.Env))
   339  		for _, e := range vm.Env {
   340  			switch e.ValueFrom {
   341  			case extensions.EnvValueSource_INLINE:
   342  				cfg.VmConfig.EnvironmentVariables.KeyValues[e.Name] = e.Value
   343  			case extensions.EnvValueSource_HOST:
   344  				hostEnvKeys = append(hostEnvKeys, e.Name)
   345  			}
   346  		}
   347  		cfg.VmConfig.EnvironmentVariables.HostEnvKeys = hostEnvKeys
   348  	}
   349  
   350  	return cfg
   351  }