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 }