diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index a4ef2ee57a..5dad319229 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -204,6 +204,20 @@ type EnvoyProxySpec struct { // +optional DynamicModules []DynamicModuleEntry `json:"dynamicModules,omitempty"` + // WasmModules defines the set of Wasm modules that are allowed to be used by + // EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each + // entry registers a module by a logical name and a source (currently Local path). + // + // The EnvoyProxy owner is responsible for ensuring Local modules are available + // on the proxy container's filesystem (e.g., via init containers, custom images, + // or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. + // + // +kubebuilder:validation:MaxItems=16 + // +listType=map + // +listMapKey=name + // +optional + WasmModules []WasmModuleEntry `json:"wasmModules,omitempty"` + // GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. // // +optional diff --git a/api/v1alpha1/wasm_types.go b/api/v1alpha1/wasm_types.go index d296ea1c41..13c2821cb7 100644 --- a/api/v1alpha1/wasm_types.go +++ b/api/v1alpha1/wasm_types.go @@ -74,11 +74,13 @@ type Wasm struct { // // +kubebuilder:validation:XValidation:rule="self.type == 'HTTP' ? has(self.http) : !has(self.http)",message="If type is HTTP, http field needs to be set." // +kubebuilder:validation:XValidation:rule="self.type == 'Image' ? has(self.image) : !has(self.image)",message="If type is Image, image field needs to be set." +// +kubebuilder:validation:XValidation:rule="self.type == 'EnvoyProxyModule' ? has(self.envoyProxyModule) : !has(self.envoyProxyModule)",message="If type is EnvoyProxyModule, envoyProxyModule field needs to be set." +// +kubebuilder:validation:XValidation:rule="self.type == 'EnvoyProxyModule' ? !has(self.pullPolicy) : true",message="PullPolicy is only valid for HTTP and Image code sources." type WasmCodeSource struct { // Type is the type of the source of the Wasm code. - // Valid WasmCodeSourceType values are "HTTP" or "Image". + // Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". // - // +kubebuilder:validation:Enum=HTTP;Image;ConfigMap + // +kubebuilder:validation:Enum=HTTP;Image;EnvoyProxyModule // +unionDiscriminator Type WasmCodeSourceType `json:"type"` @@ -94,6 +96,16 @@ type WasmCodeSource struct { // +optional Image *ImageWasmCodeSource `json:"image,omitempty"` + // EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy + // wasmModules allowlist. The policy references the module by name only; + // the module source is configured by the infrastructure operator on EnvoyProxy. + // + // For Local modules this skips the control-plane fetch/cache path used by + // HTTP and Image sources. The operator must ensure the file is present on + // the Envoy proxy (for example via a custom image or volume mount). + // +optional + EnvoyProxyModule *EnvoyProxyModuleWasmCodeSource `json:"envoyProxyModule,omitempty"` + // PullPolicy is the policy to use when pulling the Wasm module by either the HTTP or Image source. // This field is only applicable when the SHA256 field is not set. // @@ -102,12 +114,14 @@ type WasmCodeSource struct { // Note: EG does not update the Wasm module every time an Envoy proxy requests // the Wasm module even if the pull policy is set to Always. // It only updates the Wasm module when the EnvoyExtension resource version changes. + // + // PullPolicy must not be set when Type is EnvoyProxyModule. // +optional PullPolicy *ImagePullPolicy `json:"pullPolicy,omitempty"` } // WasmCodeSourceType specifies the types of sources for the Wasm code. -// +kubebuilder:validation:Enum=HTTP;Image +// +kubebuilder:validation:Enum=HTTP;Image;EnvoyProxyModule type WasmCodeSourceType string const ( @@ -116,8 +130,76 @@ const ( // ImageWasmCodeSourceType allows the user to specify the Wasm code in an OCI image. ImageWasmCodeSourceType WasmCodeSourceType = "Image" + + // EnvoyProxyModuleWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy. + EnvoyProxyModuleWasmCodeSourceType WasmCodeSourceType = "EnvoyProxyModule" ) +// EnvoyProxyModuleWasmCodeSource references a Wasm module registered in EnvoyProxy.spec.wasmModules. +type EnvoyProxyModuleWasmCodeSource struct { + // Name is the logical name of a module in the EnvoyProxy wasmModules allowlist. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$` + Name string `json:"name"` +} + +// WasmModuleSourceType specifies the types of sources for registered Wasm modules. +// +kubebuilder:validation:Enum=Local +type WasmModuleSourceType string + +const ( + // LocalWasmModuleSourceType loads the module from the Envoy proxy local filesystem. + LocalWasmModuleSourceType WasmModuleSourceType = "Local" +) + +// WasmModuleSource defines where a registered Wasm module is loaded from. +// Mirrors DynamicModuleSource so additional source types can be added later. +// +union +// +// +kubebuilder:validation:XValidation:rule="self.type != 'Local' || has(self.local)",message="If type is Local, local field needs to be set." +type WasmModuleSource struct { + // Type is the type of the source of the Wasm module. + // Defaults to Local. + // + // +kubebuilder:default=Local + // +unionDiscriminator + // +optional + Type *WasmModuleSourceType `json:"type,omitempty"` + + // Local specifies a module loaded from the proxy's local filesystem + // by absolute path. + // + // +optional + Local *LocalWasmModuleSource `json:"local,omitempty"` +} + +// LocalWasmModuleSource defines a Wasm module loaded from the local filesystem. +type LocalWasmModuleSource struct { + // Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=4096 + // +kubebuilder:validation:Pattern=`^/.*` + Path string `json:"path"` +} + +// WasmModuleEntry defines a Wasm module that is registered and allowed for use +// by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. +type WasmModuleEntry struct { + // Name is the logical name for this module. EnvoyExtensionPolicy resources + // reference modules by this name. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$` + Name string `json:"name"` + + // Source defines where the Wasm module code is loaded from. + Source WasmModuleSource `json:"source"` +} + // HTTPWasmCodeSource defines the HTTP URL containing the Wasm code. type HTTPWasmCodeSource struct { // URL is the URL containing the Wasm code. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7245a49145..9408806d39 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3193,6 +3193,21 @@ func (in *EnvoyProxyList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EnvoyProxyModuleWasmCodeSource) DeepCopyInto(out *EnvoyProxyModuleWasmCodeSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyProxyModuleWasmCodeSource. +func (in *EnvoyProxyModuleWasmCodeSource) DeepCopy() *EnvoyProxyModuleWasmCodeSource { + if in == nil { + return nil + } + out := new(EnvoyProxyModuleWasmCodeSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvoyProxyProvider) DeepCopyInto(out *EnvoyProxyProvider) { *out = *in @@ -3296,6 +3311,13 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.WasmModules != nil { + in, out := &in.WasmModules, &out.WasmModules + *out = make([]WasmModuleEntry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.GeoIP != nil { in, out := &in.GeoIP, &out.GeoIP *out = new(EnvoyProxyGeoIP) @@ -5971,6 +5993,21 @@ func (in *LocalRateLimit) DeepCopy() *LocalRateLimit { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalWasmModuleSource) DeepCopyInto(out *LocalWasmModuleSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalWasmModuleSource. +func (in *LocalWasmModuleSource) DeepCopy() *LocalWasmModuleSource { + if in == nil { + return nil + } + out := new(LocalWasmModuleSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Lua) DeepCopyInto(out *Lua) { *out = *in @@ -8729,6 +8766,11 @@ func (in *WasmCodeSource) DeepCopyInto(out *WasmCodeSource) { *out = new(ImageWasmCodeSource) (*in).DeepCopyInto(*out) } + if in.EnvoyProxyModule != nil { + in, out := &in.EnvoyProxyModule, &out.EnvoyProxyModule + *out = new(EnvoyProxyModuleWasmCodeSource) + **out = **in + } if in.PullPolicy != nil { in, out := &in.PullPolicy, &out.PullPolicy *out = new(ImagePullPolicy) @@ -8782,6 +8824,47 @@ func (in *WasmEnv) DeepCopy() *WasmEnv { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WasmModuleEntry) DeepCopyInto(out *WasmModuleEntry) { + *out = *in + in.Source.DeepCopyInto(&out.Source) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WasmModuleEntry. +func (in *WasmModuleEntry) DeepCopy() *WasmModuleEntry { + if in == nil { + return nil + } + out := new(WasmModuleEntry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WasmModuleSource) DeepCopyInto(out *WasmModuleSource) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(WasmModuleSourceType) + **out = **in + } + if in.Local != nil { + in, out := &in.Local, &out.Local + *out = new(LocalWasmModuleSource) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WasmModuleSource. +func (in *WasmModuleSource) DeepCopy() *WasmModuleSource { + if in == nil { + return nil + } + out := new(WasmModuleSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WeightedZoneConfig) DeepCopyInto(out *WeightedZoneConfig) { *out = *in diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index ced0356581..6fe3b9910c 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -2025,6 +2025,26 @@ spec: code: description: Code is the Wasm code for the extension. properties: + envoyProxyModule: + description: |- + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the module source is configured by the infrastructure operator on EnvoyProxy. + + For Local modules this skips the control-plane fetch/cache path used by + HTTP and Image sources. The operator must ensure the file is present on + the Envoy proxy (for example via a custom image or volume mount). + properties: + name: + description: Name is the logical name of a module in + the EnvoyProxy wasmModules allowlist. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - name + type: object http: description: |- HTTP is the HTTP URL containing the Wasm code. @@ -2234,6 +2254,8 @@ spec: Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module even if the pull policy is set to Always. It only updates the Wasm module when the EnvoyExtension resource version changes. + + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -2243,13 +2265,14 @@ spec: - enum: - HTTP - Image + - EnvoyProxyModule - enum: - HTTP - Image - - ConfigMap + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP" or "Image". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -2259,6 +2282,14 @@ spec: rule: 'self.type == ''HTTP'' ? has(self.http) : !has(self.http)' - message: If type is Image, image field needs to be set. rule: 'self.type == ''Image'' ? has(self.image) : !has(self.image)' + - message: If type is EnvoyProxyModule, envoyProxyModule field + needs to be set. + rule: 'self.type == ''EnvoyProxyModule'' ? has(self.envoyProxyModule) + : !has(self.envoyProxyModule)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index d8a048a90b..4d50d99c56 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -18111,6 +18111,68 @@ spec: specified rule: '!(has(self.samplingRate) && has(self.samplingFraction))' type: object + wasmModules: + description: |- + WasmModules defines the set of Wasm modules that are allowed to be used by + EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each + entry registers a module by a logical name and a source (currently Local path). + + The EnvoyProxy owner is responsible for ensuring Local modules are available + on the proxy container's filesystem (e.g., via init containers, custom images, + or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. + items: + description: |- + WasmModuleEntry defines a Wasm module that is registered and allowed for use + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. + properties: + name: + description: |- + Name is the logical name for this module. EnvoyExtensionPolicy resources + reference modules by this name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + source: + description: Source defines where the Wasm module code is loaded + from. + properties: + local: + description: |- + Local specifies a module loaded from the proxy's local filesystem + by absolute path. + properties: + path: + description: Path is the absolute filesystem path to + the Wasm module on the Envoy proxy. + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - path + type: object + type: + default: Local + description: |- + Type is the type of the source of the Wasm module. + Defaults to Local. + enum: + - Local + type: string + type: object + x-kubernetes-validations: + - message: If type is Local, local field needs to be set. + rule: self.type != 'Local' || has(self.local) + required: + - name + - source + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object status: description: EnvoyProxyStatus defines the actual state of EnvoyProxy. diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index af2b874a7b..6cc7397417 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -2024,6 +2024,26 @@ spec: code: description: Code is the Wasm code for the extension. properties: + envoyProxyModule: + description: |- + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the module source is configured by the infrastructure operator on EnvoyProxy. + + For Local modules this skips the control-plane fetch/cache path used by + HTTP and Image sources. The operator must ensure the file is present on + the Envoy proxy (for example via a custom image or volume mount). + properties: + name: + description: Name is the logical name of a module in + the EnvoyProxy wasmModules allowlist. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - name + type: object http: description: |- HTTP is the HTTP URL containing the Wasm code. @@ -2233,6 +2253,8 @@ spec: Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module even if the pull policy is set to Always. It only updates the Wasm module when the EnvoyExtension resource version changes. + + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -2242,13 +2264,14 @@ spec: - enum: - HTTP - Image + - EnvoyProxyModule - enum: - HTTP - Image - - ConfigMap + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP" or "Image". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -2258,6 +2281,14 @@ spec: rule: 'self.type == ''HTTP'' ? has(self.http) : !has(self.http)' - message: If type is Image, image field needs to be set. rule: 'self.type == ''Image'' ? has(self.image) : !has(self.image)' + - message: If type is EnvoyProxyModule, envoyProxyModule field + needs to be set. + rule: 'self.type == ''EnvoyProxyModule'' ? has(self.envoyProxyModule) + : !has(self.envoyProxyModule)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 5736db43cd..ddbade7428 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -18110,6 +18110,68 @@ spec: specified rule: '!(has(self.samplingRate) && has(self.samplingFraction))' type: object + wasmModules: + description: |- + WasmModules defines the set of Wasm modules that are allowed to be used by + EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each + entry registers a module by a logical name and a source (currently Local path). + + The EnvoyProxy owner is responsible for ensuring Local modules are available + on the proxy container's filesystem (e.g., via init containers, custom images, + or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. + items: + description: |- + WasmModuleEntry defines a Wasm module that is registered and allowed for use + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. + properties: + name: + description: |- + Name is the logical name for this module. EnvoyExtensionPolicy resources + reference modules by this name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + source: + description: Source defines where the Wasm module code is loaded + from. + properties: + local: + description: |- + Local specifies a module loaded from the proxy's local filesystem + by absolute path. + properties: + path: + description: Path is the absolute filesystem path to + the Wasm module on the Envoy proxy. + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - path + type: object + type: + default: Local + description: |- + Type is the type of the source of the Wasm module. + Defaults to Local. + enum: + - Local + type: string + type: object + x-kubernetes-validations: + - message: If type is Local, local field needs to be set. + rule: self.type != 'Local' || has(self.local) + required: + - name + - source + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object status: description: EnvoyProxyStatus defines the actual state of EnvoyProxy. diff --git a/examples/wasm-module-test/Dockerfile b/examples/wasm-module-test/Dockerfile new file mode 100644 index 0000000000..febf4d829a --- /dev/null +++ b/examples/wasm-module-test/Dockerfile @@ -0,0 +1,5 @@ +# Custom Envoy image with a Wasm module baked into the filesystem for e2e. +ARG ENVOY_VERSION=dev + +FROM docker.io/envoyproxy/envoy:distroless-${ENVOY_VERSION} +COPY envoy_filter_http_wasm_example.wasm /var/lib/envoy/envoy_filter_http_wasm_example.wasm diff --git a/examples/wasm-module-test/Makefile b/examples/wasm-module-test/Makefile new file mode 100644 index 0000000000..493cf66995 --- /dev/null +++ b/examples/wasm-module-test/Makefile @@ -0,0 +1,9 @@ + +IMAGE_PREFIX ?= envoyproxy/gateway- +APP_NAME ?= wasm-module-test +TAG ?= latest +ENVOY_VERSION ?= dev + +.PHONY: docker-buildx +docker-buildx: + docker buildx build . --build-arg ENVOY_VERSION=$(ENVOY_VERSION) -t $(IMAGE_PREFIX)$(APP_NAME):$(TAG) --load diff --git a/examples/wasm-module-test/envoy_filter_http_wasm_example.wasm b/examples/wasm-module-test/envoy_filter_http_wasm_example.wasm new file mode 100644 index 0000000000..df2554e971 Binary files /dev/null and b/examples/wasm-module-test/envoy_filter_http_wasm_example.wasm differ diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index b320adbbe3..1595e741c6 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -518,18 +518,12 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( resources *resource.Resources, ) error { var ( - wasms []ir.Wasm luas []ir.Lua - wasmFailOpen, extProcFailOpen bool - wasmError, luaError, extProcError, dynamicModuleError error + extProcFailOpen bool + luaError, extProcError, dynamicModuleError, wasmError error errs error ) - if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, resources); wasmError != nil { - wasmError = perr.WithMessage(wasmError, "Wasm") - errs = errors.Join(errs, wasmError) - } - // Apply IR to all relevant routes prefix := irRoutePrefix(route) parentRefs := GetParentReferences(route) @@ -546,6 +540,19 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( continue } + var ( + wasms []ir.Wasm + wasmFailOpen bool + ) + // Built per parent Gateway because EnvoyProxyModule Wasm resolves against + // that Gateway's EnvoyProxy.wasmModules. HTTP/Image call WasmCache.Get + // here; IfNotPresent is a cache hit on repeats, Always may re-fetch + // once per parentRef (same placement as Lua and DynamicModules). + if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, resources, gtwCtx.envoyProxy); wasmError != nil { + wasmError = perr.WithMessage(wasmError, "Wasm") + errs = errors.Join(errs, wasmError) + } + if luas, luaError = t.buildLuas(policy, gtwCtx.envoyProxy); luaError != nil { luaError = perr.WithMessage(luaError, "Lua") errs = errors.Join(errs, luaError) @@ -646,7 +653,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( extProcError = perr.WithMessage(extProcError, "ExtProc") errs = errors.Join(errs, extProcError) } - if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, resources); wasmError != nil { + if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, resources, gateway.envoyProxy); wasmError != nil { wasmError = perr.WithMessage(wasmError, "Wasm") errs = errors.Join(errs, wasmError) } @@ -957,30 +964,35 @@ func irConfigNameForLua(policy *egv1a1.EnvoyExtensionPolicy, index int) string { func (t *Translator) buildWasms( policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources, + envoyProxy *egv1a1.EnvoyProxy, ) ([]ir.Wasm, error, bool) { var ( failOpen bool errs error ) - if len(policy.Spec.Wasm) == 0 { + if policy == nil || len(policy.Spec.Wasm) == 0 { return nil, nil, failOpen } wasmIRList := make([]ir.Wasm, 0, len(policy.Spec.Wasm)) - if t.WasmCache == nil { - return nil, fmt.Errorf("wasm cache is not initialized"), failOpen + // EnvoyProxyModule sources resolve from EnvoyProxy and skip the control-plane cache. + needsCache := false + for _, wasm := range policy.Spec.Wasm { + if wasm.Code.Type == egv1a1.HTTPWasmCodeSourceType || wasm.Code.Type == egv1a1.ImageWasmCodeSourceType { + needsCache = true + break + } } - - if policy == nil { - return nil, nil, failOpen + if needsCache && t.WasmCache == nil { + return nil, fmt.Errorf("wasm cache is not initialized"), failOpen } hasFailClose := false for idx, wasm := range policy.Spec.Wasm { name := irConfigNameForWasm(policy, idx) - wasmIR, err := t.buildWasm(name, &wasm, policy, idx, resources) + wasmIR, err := t.buildWasm(name, &wasm, policy, idx, resources, envoyProxy) if err != nil { errs = errors.Join(errs, err) if wasm.FailOpen == nil || !*wasm.FailOpen { @@ -1005,10 +1017,12 @@ func (t *Translator) buildWasm( policy *egv1a1.EnvoyExtensionPolicy, idx int, resources *resource.Resources, + envoyProxy *egv1a1.EnvoyProxy, ) (*ir.Wasm, error) { var ( failOpen = false code *ir.HTTPWasmCode + localPath string pullPolicy wasm.PullPolicy // the checksum provided by the user, it's used to validate the wasm module // downloaded from the original HTTP server or the OCI registry @@ -1034,6 +1048,42 @@ func (t *Translator) buildWasm( } switch config.Code.Type { + case egv1a1.EnvoyProxyModuleWasmCodeSourceType: + // Sanity check; CEL validation should have caught this. + if config.Code.EnvoyProxyModule == nil { + return nil, fmt.Errorf("missing EnvoyProxyModule field in Wasm code source") + } + moduleName := config.Code.EnvoyProxyModule.Name + if moduleName == "" { + return nil, fmt.Errorf("EnvoyProxyModule name must not be empty") + } + + var entry *egv1a1.WasmModuleEntry + if envoyProxy != nil { + for i := range envoyProxy.Spec.WasmModules { + if envoyProxy.Spec.WasmModules[i].Name == moduleName { + entry = &envoyProxy.Spec.WasmModules[i] + break + } + } + } + if entry == nil { + return nil, fmt.Errorf("wasm module %q is not registered in the EnvoyProxy wasmModules allowlist", moduleName) + } + + switch sourceType := ptr.Deref(entry.Source.Type, egv1a1.LocalWasmModuleSourceType); sourceType { + case egv1a1.LocalWasmModuleSourceType: + if entry.Source.Local == nil { + return nil, fmt.Errorf("wasm module %q has no local source configured", moduleName) + } + if entry.Source.Local.Path == "" { + return nil, fmt.Errorf("wasm module %q has empty path", moduleName) + } + localPath = entry.Source.Local.Path + default: + return nil, fmt.Errorf("wasm module %q has unsupported source type %q", moduleName, sourceType) + } + case egv1a1.HTTPWasmCodeSourceType: var checksum string @@ -1171,6 +1221,7 @@ func (t *Translator) buildWasm( Config: config.Config, FailOpen: failOpen, Code: code, + Path: localPath, } if config.Env != nil && len(config.Env.HostKeys) > 0 { diff --git a/internal/gatewayapi/envoyextensionpolicy_ca_test.go b/internal/gatewayapi/envoyextensionpolicy_ca_test.go index d7db676c6f..ab146e9f50 100644 --- a/internal/gatewayapi/envoyextensionpolicy_ca_test.go +++ b/internal/gatewayapi/envoyextensionpolicy_ca_test.go @@ -168,7 +168,7 @@ func TestBuildWasmWithTLS(t *testing.T) { }, } - _, err := translator.buildWasm("test-wasm", tt.wasm, policy, 0, tt.resources) + _, err := translator.buildWasm("test-wasm", tt.wasm, policy, 0, tt.resources, nil) if tt.wantErr { require.Error(t, err) } else { diff --git a/internal/gatewayapi/globalresources.go b/internal/gatewayapi/globalresources.go index 1000890724..2d31c23038 100644 --- a/internal/gatewayapi/globalresources.go +++ b/internal/gatewayapi/globalresources.go @@ -114,12 +114,19 @@ func containsGlobalRateLimit(httpListeners []*ir.HTTPListener) bool { return false } +// containsWasm reports whether any route uses a remote Wasm code source +// (HTTP/Image). EnvoyProxyModule Local Wasm does not use the control-plane +// wasm HTTP service or its client certificate. func containsWasm(httpListeners []*ir.HTTPListener) bool { for _, httpListener := range httpListeners { for _, route := range httpListener.Routes { - if route.EnvoyExtensions != nil && - len(route.EnvoyExtensions.Wasms) > 0 { - return true + if route.EnvoyExtensions == nil { + continue + } + for _, w := range route.EnvoyExtensions.Wasms { + if w.Code != nil { + return true + } } } } diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml new file mode 100644 index 0000000000..b1a55cba36 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml @@ -0,0 +1,70 @@ +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway + name: proxy-config + spec: + wasmModules: + - name: security-filter + source: + type: Local + local: + path: /var/lib/envoy/security-filter.wasm +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: proxy-config + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + wasm: + - name: wasm-filter-missing + code: + type: EnvoyProxyModule + envoyProxyModule: + name: not-registered diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml new file mode 100644 index 0000000000..b5e6c6fe5a --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml @@ -0,0 +1,273 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + name: policy-for-http-route + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + wasm: + - code: + envoyProxyModule: + name: not-registered + type: EnvoyProxyModule + name: wasm-filter-missing + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: 'Wasm: wasm module "not-registered" is not registered in the EnvoyProxy + wasmModules allowlist.' + reason: Invalid + status: "False" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: proxy-config + namespace: envoy-gateway + spec: + logging: {} + wasmModules: + - name: security-filter + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: EnvoyProxy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: proxy-config + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: proxy-config + namespace: envoy-gateway + spec: + logging: {} + wasmModules: + - name: security-filter + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: EnvoyProxy has been accepted. + reason: Accepted + status: "True" + type: Accepted + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + directResponse: + statusCode: 500 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml new file mode 100644 index 0000000000..22692112f1 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml @@ -0,0 +1,114 @@ +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway + name: proxy-config + spec: + wasmModules: + - name: security-filter + source: + type: Local + local: + path: /var/lib/envoy/security-filter.wasm + - name: metrics-filter + source: + type: Local + local: + path: /opt/wasm/metrics.wasm +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: proxy-config + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/bar" + backendRefs: + - name: service-1 + port: 8080 +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + wasm: + - name: wasm-filter-1 + code: + type: EnvoyProxyModule + envoyProxyModule: + name: security-filter + config: + mode: enforce +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-http-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + wasm: + - name: wasm-filter-2 + rootID: my-root-id + code: + type: EnvoyProxyModule + envoyProxyModule: + name: metrics-filter + failOpen: true diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml new file mode 100644 index 0000000000..3c146b5965 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml @@ -0,0 +1,408 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + name: policy-for-http-route + namespace: default + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + wasm: + - code: + envoyProxyModule: + name: metrics-filter + type: EnvoyProxyModule + failOpen: true + name: wasm-filter-2 + rootID: my-root-id + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + name: policy-for-gateway + namespace: envoy-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + wasm: + - code: + envoyProxyModule: + name: security-filter + type: EnvoyProxyModule + config: + mode: enforce + name: wasm-filter-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being overridden by other envoyExtensionPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: proxy-config + namespace: envoy-gateway + spec: + logging: {} + wasmModules: + - name: security-filter + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local + - name: metrics-filter + source: + local: + path: /opt/wasm/metrics.wasm + type: Local + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: EnvoyProxy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: proxy-config + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 2 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-2 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /bar + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: proxy-config + namespace: envoy-gateway + spec: + logging: {} + wasmModules: + - name: security-filter + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local + - name: metrics-filter + source: + local: + path: /opt/wasm/metrics.wasm + type: Local + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: EnvoyProxy has been accepted. + reason: Accepted + status: "True" + type: Accepted + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + envoyExtensions: + wasms: + - config: null + failOpen: true + name: envoyextensionpolicy/default/policy-for-http-route/wasm/0 + path: /opt/wasm/metrics.wasm + rootID: my-root-id + wasmName: wasm-filter-2 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + - destination: + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-2/rule/0/backend/0 + protocol: HTTP + weight: 1 + envoyExtensions: + wasms: + - config: + mode: enforce + failOpen: false + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + path: /var/lib/envoy/security-filter.wasm + wasmName: wasm-filter-1 + hostname: www.example.com + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-2 + namespace: default + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index c8ec2b573d..8ea5deb205 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -3782,12 +3782,15 @@ type Wasm struct { // during the initialization or the execution of the Wasm extension. FailOpen bool `json:"failOpen"` - // Code is the HTTP Wasm code source. - // Envoy only supports HTTP Wasm code source. EG downloads the Wasm code from the - // original URL(either an HTTP URL or an OCI image) and serves it through the - // local HTTP server. + // Code is the HTTP Wasm code source when the module is served by the EG HTTP + // server (HTTP/Image sources downloaded by the control plane). Code *HTTPWasmCode `json:"httpWasmCode,omitempty"` + // Path is the absolute filesystem path to the Wasm module on the Envoy proxy + // when using an EnvoyProxyModule Local source resolved from EnvoyProxy.wasmModules. + // Mutually exclusive with Code. + Path string `json:"path,omitempty"` + // HostKeys is a list of keys for environment variables from the host envoy process // that should be passed into the Wasm VM. HostKeys []string `json:"hostKeys,omitempty"` diff --git a/internal/xds/translator/globalresources.go b/internal/xds/translator/globalresources.go index 32a67d5203..23e6ff38b7 100644 --- a/internal/xds/translator/globalresources.go +++ b/internal/xds/translator/globalresources.go @@ -139,12 +139,19 @@ func buildEnvoyClientTLSSocket(envoyClientCertificate *ir.TLSCertificate) (*core }, nil } +// containsWasm reports whether any route uses a remote Wasm code source +// (HTTP/Image, served by the control-plane wasm HTTP service). EnvoyProxyModule +// Local sources load a path on the proxy and do not need wasm_cluster. func containsWasm(httpListeners []*ir.HTTPListener) bool { for _, httpListener := range httpListeners { for _, route := range httpListener.Routes { - if route.EnvoyExtensions != nil && - len(route.EnvoyExtensions.Wasms) > 0 { - return true + if route.EnvoyExtensions == nil { + continue + } + for _, w := range route.EnvoyExtensions.Wasms { + if w.Code != nil { + return true + } } } } diff --git a/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml b/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml new file mode 100644 index 0000000000..d51f77efa4 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml @@ -0,0 +1,60 @@ +http: +- address: 0.0.0.0 + hostnames: + - '*' + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + name: httproute/default/httproute-1/rule/0/backend/0 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + envoyExtensions: + wasms: + - config: + mode: enforce + failOpen: false + path: /var/lib/envoy/security-filter.wasm + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + wasmName: wasm-filter-1 + - destination: + name: httproute/default/httproute-2/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + name: httproute/default/httproute-2/rule/0/backend/0 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /bar + envoyExtensions: + wasms: + - config: null + failOpen: true + path: /opt/wasm/metrics.wasm + name: envoyextensionpolicy/default/policy-for-http-route/wasm/0 + wasmName: wasm-filter-2 + rootID: my-root-id diff --git a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml new file mode 100644 index 0000000000..d1b786402b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml @@ -0,0 +1,46 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: httproute/default/httproute-1/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-2/rule/0 + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: httproute/default/httproute-2/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.endpoints.yaml new file mode 100644 index 0000000000..05442a9a15 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.endpoints.yaml @@ -0,0 +1,24 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 +- clusterName: httproute/default/httproute-2/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-2/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.listeners.yaml new file mode 100644 index 0000000000..527f9d939d --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.listeners.yaml @@ -0,0 +1,67 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - disabled: true + name: envoy.filters.http.wasm/envoyextensionpolicy/default/policy-for-http-route/wasm/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + config: + configuration: + '@type': type.googleapis.com/google.protobuf.StringValue + value: "" + failOpen: true + name: wasm-filter-2 + rootId: my-root-id + vmConfig: + code: + local: + filename: /opt/wasm/metrics.wasm + runtime: envoy.wasm.runtime.v8 + vmId: envoyextensionpolicy/default/policy-for-http-route/wasm/0 + - disabled: true + name: envoy.filters.http.wasm/envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + config: + configuration: + '@type': type.googleapis.com/google.protobuf.StringValue + value: '{"mode":"enforce"}' + name: wasm-filter-1 + vmConfig: + code: + local: + filename: /var/lib/envoy/security-filter.wasm + runtime: envoy.wasm.runtime.v8 + vmId: envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0 + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: envoy-gateway/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: envoy-gateway/gateway-1/http + maxConnectionsToAcceptPerSocketEvent: 1 + name: envoy-gateway/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.routes.yaml new file mode 100644 index 0000000000..2ff24c12c6 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.routes.yaml @@ -0,0 +1,29 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-1/http + virtualHosts: + - domains: + - www.example.com + name: envoy-gateway/gateway-1/http/www_example_com + routes: + - match: + pathSeparatedPrefix: /foo + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.wasm/envoyextensionpolicy/envoy-gateway/policy-for-gateway/wasm/0: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} + - match: + pathSeparatedPrefix: /bar + name: httproute/default/httproute-2/rule/0/match/0/www_example_com + route: + cluster: httproute/default/httproute-2/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.wasm/envoyextensionpolicy/default/policy-for-http-route/wasm/0: + '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig + config: {} diff --git a/internal/xds/translator/wasm.go b/internal/xds/translator/wasm.go index 16b3500ab6..fd3f2081da 100644 --- a/internal/xds/translator/wasm.go +++ b/internal/xds/translator/wasm.go @@ -108,6 +108,47 @@ func wasmFilterName(wasm *ir.Wasm) string { return perRouteFilterName(egv1a1.EnvoyFilterWasm, wasm.Name) } +// wasmCodeSource builds the AsyncDataSource for a Wasm filter. +// Filesystem sources use a local path on the Envoy proxy; HTTP/Image sources use +// the EG-served remote URL. +func wasmCodeSource(wasm *ir.Wasm) (*corev3.AsyncDataSource, error) { + if wasm.Path != "" { + return &corev3.AsyncDataSource{ + Specifier: &corev3.AsyncDataSource_Local{ + Local: &corev3.DataSource{ + Specifier: &corev3.DataSource_Filename{ + Filename: wasm.Path, + }, + }, + }, + }, nil + } + if wasm.Code == nil { + return nil, errors.New("wasm code source is missing") + } + return &corev3.AsyncDataSource{ + Specifier: &corev3.AsyncDataSource_Remote{ + Remote: &corev3.RemoteDataSource{ + HttpUri: &corev3.HttpUri{ + Uri: wasm.Code.ServingURL, + HttpUpstreamType: &corev3.HttpUri_Cluster{ + Cluster: wasmHTTPServiceClusterName, + }, + Timeout: durationpb.New(defaultExtServiceRequestTimeout), + }, + Sha256: wasm.Code.SHA256, + RetryPolicy: &corev3.RetryPolicy{ + NumRetries: wrapperspb.UInt32(wasmFetchNumRetries), + RetryBackOff: &corev3.BackoffStrategy{ + BaseInterval: durationpb.New(wasmFetchBaseInterval), + MaxInterval: durationpb.New(wasmFetchMaxInterval), + }, + }, + }, + }, + }, nil +} + func wasmConfig(wasm *ir.Wasm) (*wasmfilterv3.Wasm, error) { var ( pluginConfig = "" @@ -124,30 +165,15 @@ func wasmConfig(wasm *ir.Wasm) (*wasmfilterv3.Wasm, error) { return nil, err } + codeSource, err := wasmCodeSource(wasm) + if err != nil { + return nil, err + } + vmConfig := &wasmv3.VmConfig{ VmId: wasm.Name, // Do not share VMs across different filters Runtime: vmRuntimeV8, - Code: &corev3.AsyncDataSource{ - Specifier: &corev3.AsyncDataSource_Remote{ - Remote: &corev3.RemoteDataSource{ - HttpUri: &corev3.HttpUri{ - Uri: wasm.Code.ServingURL, - HttpUpstreamType: &corev3.HttpUri_Cluster{ - Cluster: wasmHTTPServiceClusterName, - }, - Timeout: durationpb.New(defaultExtServiceRequestTimeout), - }, - Sha256: wasm.Code.SHA256, - RetryPolicy: &corev3.RetryPolicy{ - NumRetries: wrapperspb.UInt32(wasmFetchNumRetries), - RetryBackOff: &corev3.BackoffStrategy{ - BaseInterval: durationpb.New(wasmFetchBaseInterval), - MaxInterval: durationpb.New(wasmFetchMaxInterval), - }, - }, - }, - }, - }, + Code: codeSource, } if wasm.HostKeys != nil { diff --git a/internal/xds/translator/wasm_test.go b/internal/xds/translator/wasm_test.go new file mode 100644 index 0000000000..ff2241b859 --- /dev/null +++ b/internal/xds/translator/wasm_test.go @@ -0,0 +1,79 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "testing" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + "github.com/stretchr/testify/require" + + "github.com/envoyproxy/gateway/internal/ir" +) + +func TestWasmCodeSource(t *testing.T) { + tests := []struct { + name string + wasm ir.Wasm + wantLocal bool + wantFilename string + wantURI string + wantSHA256 string + wantErr bool + }{ + { + name: "filesystem local path", + wasm: ir.Wasm{ + Path: "/var/lib/envoy/filter.wasm", + }, + wantLocal: true, + wantFilename: "/var/lib/envoy/filter.wasm", + }, + { + name: "http remote code", + wasm: ir.Wasm{ + Code: &ir.HTTPWasmCode{ + ServingURL: "https://envoy-gateway:18002/module.wasm", + SHA256: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + }, + wantLocal: false, + wantURI: "https://envoy-gateway:18002/module.wasm", + wantSHA256: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + }, + { + name: "missing source", + wasm: ir.Wasm{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := wasmCodeSource(&tt.wasm) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotNil(t, got) + + if tt.wantLocal { + local, ok := got.Specifier.(*corev3.AsyncDataSource_Local) + require.True(t, ok, "expected local specifier") + filename, ok := local.Local.Specifier.(*corev3.DataSource_Filename) + require.True(t, ok, "expected filename specifier") + require.Equal(t, tt.wantFilename, filename.Filename) + return + } + + remote, ok := got.Specifier.(*corev3.AsyncDataSource_Remote) + require.True(t, ok, "expected remote specifier") + require.Equal(t, tt.wantURI, remote.Remote.HttpUri.Uri) + require.Equal(t, tt.wantSHA256, remote.Remote.Sha256) + }) + } +} diff --git a/release-notes/current/new_features/9448-wasm-filesystem-source.md b/release-notes/current/new_features/9448-wasm-filesystem-source.md new file mode 100644 index 0000000000..a31e06ed21 --- /dev/null +++ b/release-notes/current/new_features/9448-wasm-filesystem-source.md @@ -0,0 +1 @@ +Added an EnvoyProxyModule Wasm code source for EnvoyExtensionPolicy. Register modules on EnvoyProxy.spec.wasmModules (source type Local with a path today; shape matches DynamicModule for future sources), then reference them by name from the policy so Envoy can load local files without the control-plane fetch and cache path. The Wasm code type enum is now HTTP, Image, and EnvoyProxyModule; the unused ConfigMap enum value (never implemented) was removed. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 66d6c6cd5a..3196404e94 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2197,6 +2197,20 @@ _Appears in:_ | `envoyServiceAccount` | _[KubernetesServiceAccountSpec](#kubernetesserviceaccountspec)_ | true | | EnvoyServiceAccount defines the desired state of the Envoy service account resource. | +#### EnvoyProxyModuleWasmCodeSource + + + +EnvoyProxyModuleWasmCodeSource references a Wasm module registered in EnvoyProxy.spec.wasmModules. + +_Appears in:_ +- [WasmCodeSource](#wasmcodesource) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `name` | _string_ | true | | Name is the logical name of a module in the EnvoyProxy wasmModules allowlist. | + + #### EnvoyProxyProvider @@ -2256,6 +2270,7 @@ _Appears in:_ | `preserveRouteOrder` | _boolean_ | false | | PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API
specification (https://gateway-api.sigs.k8s.io/reference/api-spec/main/spec/#httprouterule)
or preserves the order defined by users in the HTTPRoute's HTTPRouteRule list.
Default: False | | `luaValidation` | _[LuaValidation](#luavalidation)_ | false | | LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict | | `dynamicModules` | _[DynamicModuleEntry](#dynamicmoduleentry) array_ | false | | DynamicModules defines the set of dynamic modules that are allowed to be
used by EnvoyExtensionPolicy resources and dynamic module load balancer
policies. Each entry registers a module by a logical name and specifies
the shared library that Envoy will load.
The EnvoyProxy owner is responsible for ensuring the module .so files are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). | +| `wasmModules` | _[WasmModuleEntry](#wasmmoduleentry) array_ | false | | WasmModules defines the set of Wasm modules that are allowed to be used by
EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each
entry registers a module by a logical name and a source (currently Local path).
The EnvoyProxy owner is responsible for ensuring Local modules are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. | | `geoIP` | _[EnvoyProxyGeoIP](#envoyproxygeoip)_ | false | | GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType controls how this EnvoyProxy merges with less specific configurations
in the hierarchy (EnvoyGateway defaults < GatewayClass < Gateway).
If unset, this EnvoyProxy completely replaces less specific settings.
Note: this field has no effect when set in EnvoyGateway's default EnvoyProxySpec. | @@ -4122,6 +4137,20 @@ _Appears in:_ | `rules` | _[RateLimitRule](#ratelimitrule) array_ | false | | Rules are a list of RateLimit selectors and limits. If a request matches
multiple rules, the strictest limit is applied. For example, if a request
matches two rules, one with 10rps and one with 20rps, the final limit will
be based on the rule with 10rps. | +#### LocalWasmModuleSource + + + +LocalWasmModuleSource defines a Wasm module loaded from the local filesystem. + +_Appears in:_ +- [WasmModuleSource](#wasmmodulesource) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `path` | _string_ | true | | Path is the absolute filesystem path to the Wasm module on the Envoy proxy. | + + #### LogLevel _Underlying type:_ _string_ @@ -6504,10 +6533,11 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `type` | _[WasmCodeSourceType](#wasmcodesourcetype)_ | true | | Type is the type of the source of the Wasm code.
Valid WasmCodeSourceType values are "HTTP" or "Image". | +| `type` | _[WasmCodeSourceType](#wasmcodesourcetype)_ | true | | Type is the type of the source of the Wasm code.
Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". | | `http` | _[HTTPWasmCodeSource](#httpwasmcodesource)_ | false | | HTTP is the HTTP URL containing the Wasm code.
Note that the HTTP server must be accessible from the Envoy proxy. | | `image` | _[ImageWasmCodeSource](#imagewasmcodesource)_ | false | | Image is the OCI image containing the Wasm code.
Note that the image must be accessible from the Envoy Gateway. | -| `pullPolicy` | _[ImagePullPolicy](#imagepullpolicy)_ | false | | PullPolicy is the policy to use when pulling the Wasm module by either the HTTP or Image source.
This field is only applicable when the SHA256 field is not set.
If not specified, the default policy is IfNotPresent except for OCI images whose tag is latest.
Note: EG does not update the Wasm module every time an Envoy proxy requests
the Wasm module even if the pull policy is set to Always.
It only updates the Wasm module when the EnvoyExtension resource version changes. | +| `envoyProxyModule` | _[EnvoyProxyModuleWasmCodeSource](#envoyproxymodulewasmcodesource)_ | false | | EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy
wasmModules allowlist. The policy references the module by name only;
the module source is configured by the infrastructure operator on EnvoyProxy.
For Local modules this skips the control-plane fetch/cache path used by
HTTP and Image sources. The operator must ensure the file is present on
the Envoy proxy (for example via a custom image or volume mount). | +| `pullPolicy` | _[ImagePullPolicy](#imagepullpolicy)_ | false | | PullPolicy is the policy to use when pulling the Wasm module by either the HTTP or Image source.
This field is only applicable when the SHA256 field is not set.
If not specified, the default policy is IfNotPresent except for OCI images whose tag is latest.
Note: EG does not update the Wasm module every time an Envoy proxy requests
the Wasm module even if the pull policy is set to Always.
It only updates the Wasm module when the EnvoyExtension resource version changes.
PullPolicy must not be set when Type is EnvoyProxyModule. | #### WasmCodeSourceTLSConfig @@ -6538,6 +6568,7 @@ _Appears in:_ | ----- | ----------- | | `HTTP` | HTTPWasmCodeSourceType allows the user to specify the Wasm code in an HTTP URL.
| | `Image` | ImageWasmCodeSourceType allows the user to specify the Wasm code in an OCI image.
| +| `EnvoyProxyModule` | EnvoyProxyModuleWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy.
| #### WasmEnv @@ -6554,6 +6585,52 @@ _Appears in:_ | `hostKeys` | _string array_ | false | | HostKeys is a list of keys for environment variables from the host envoy process
that should be passed into the Wasm VM. This is useful for passing secrets to to Wasm extensions. | +#### WasmModuleEntry + + + +WasmModuleEntry defines a Wasm module that is registered and allowed for use +by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `name` | _string_ | true | | Name is the logical name for this module. EnvoyExtensionPolicy resources
reference modules by this name. | +| `source` | _[WasmModuleSource](#wasmmodulesource)_ | true | | Source defines where the Wasm module code is loaded from. | + + +#### WasmModuleSource + + + +WasmModuleSource defines where a registered Wasm module is loaded from. +Mirrors DynamicModuleSource so additional source types can be added later. + +_Appears in:_ +- [WasmModuleEntry](#wasmmoduleentry) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `type` | _[WasmModuleSourceType](#wasmmodulesourcetype)_ | false | Local | Type is the type of the source of the Wasm module.
Defaults to Local. | +| `local` | _[LocalWasmModuleSource](#localwasmmodulesource)_ | false | | Local specifies a module loaded from the proxy's local filesystem
by absolute path. | + + +#### WasmModuleSourceType + +_Underlying type:_ _string_ + +WasmModuleSourceType specifies the types of sources for registered Wasm modules. + +_Appears in:_ +- [WasmModuleSource](#wasmmodulesource) + +| Value | Description | +| ----- | ----------- | +| `Local` | LocalWasmModuleSourceType loads the module from the Envoy proxy local filesystem.
| + + #### WeightedZoneConfig diff --git a/site/content/en/latest/tasks/extensibility/wasm.md b/site/content/en/latest/tasks/extensibility/wasm.md index 6e4ba511a7..e1eeea907c 100644 --- a/site/content/en/latest/tasks/extensibility/wasm.md +++ b/site/content/en/latest/tasks/extensibility/wasm.md @@ -16,9 +16,10 @@ This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute] ## Configuration -Envoy Gateway supports two types of Wasm extensions: +Envoy Gateway supports three types of Wasm extensions: * HTTP Wasm Extension: The Wasm extension is fetched from a remote URL. * Image Wasm Extension: The Wasm extension is packaged as an OCI image and fetched from an image registry. +* EnvoyProxyModule Wasm Extension: The Wasm extension is loaded from a module registered on [EnvoyProxy][] (`spec.wasmModules`). Today only a Local filesystem path is supported; the policy references the module by name. The following example demonstrates how to configure an [EnvoyExtensionPolicy][] to attach a Wasm extension to an [EnvoyExtensionPolicy][] . This Wasm extension adds a custom header `x-wasm-custom: FOO` to the response. @@ -141,6 +142,80 @@ spec: {{% /tab %}} {{< /tabpane >}} +### EnvoyProxyModule Wasm Extension + +Register the module on the [EnvoyProxy][] attached to the Gateway, then reference it by name from the [EnvoyExtensionPolicy][]. Envoy Gateway does not place Local modules on the proxy; provision them with a custom Envoy image or a volume mount. Local modules skip the control-plane download path, which avoids a fail-closed load window when the file is already on the proxy. + +Update the EnvoyProxy used by the Gateway: + +```yaml +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: custom-proxy-config + namespace: envoy-gateway-system +spec: + wasmModules: + - name: example-filter + source: + type: Local + local: + path: /var/lib/envoy/example-filter.wasm +``` + +Then apply the EnvoyExtensionPolicy: + +{{< tabpane text=true >}} +{{% tab header="Apply from stdin" %}} + +```shell +cat <}} + Verify the EnvoyExtensionPolicy status: ```shell @@ -183,5 +258,6 @@ kubectl delete envoyextensionpolicy/wasm-test Checkout the [Developer Guide](/community/develop) to get involved in the project. [EnvoyExtensionPolicy]: ../../../api/extension_types#envoyextensionpolicy +[EnvoyProxy]: ../../../api/extension_types#envoyproxy [Gateway]: https://gateway-api.sigs.k8s.io/reference/api-types/gateway/ [HTTPRoute]: https://gateway-api.sigs.k8s.io/reference/api-types/httproute/ diff --git a/test/cel-validation/envoyextensionpolicy_test.go b/test/cel-validation/envoyextensionpolicy_test.go index d838c0c2d2..72e6fffe7c 100644 --- a/test/cel-validation/envoyextensionpolicy_test.go +++ b/test/cel-validation/envoyextensionpolicy_test.go @@ -964,6 +964,147 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { ": Exactly one of inline or valueRef must be set with correct type.", }, }, + // Wasm EnvoyProxyModule code source + { + desc: "valid Wasm EnvoyProxyModule code source", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Name: new("wasm-filter"), + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ + Name: "security-filter", + }, + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "Wasm EnvoyProxyModule without envoyProxyModule field", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{"If type is EnvoyProxyModule, envoyProxyModule field needs to be set"}, + }, + { + desc: "Wasm HTTP with envoyProxyModule field set", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.HTTPWasmCodeSourceType, + HTTP: &egv1a1.HTTPWasmCodeSource{ + URL: "https://example.com/filter.wasm", + }, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ + Name: "security-filter", + }, + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{"If type is EnvoyProxyModule, envoyProxyModule field needs to be set"}, + }, + { + desc: "Wasm EnvoyProxyModule with pullPolicy set", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ + Name: "security-filter", + }, + PullPolicy: new(egv1a1.ImagePullPolicyAlways), + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{"PullPolicy is only valid for HTTP and Image code sources"}, + }, + { + desc: "Wasm EnvoyProxyModule with empty module name", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ + Name: "", + }, + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{ + "spec.wasm[0].code.envoyProxyModule.name: Invalid value:", + "should be at least 1 chars long", + }, + }, // DynamicModules { desc: "valid DynamicModule with all fields", diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index 20e425157c..ec215347cb 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2485,6 +2485,139 @@ func TestEnvoyProxyProvider(t *testing.T) { }, wantErrors: []string{"If type is Remote, local field must not be set"}, }, + // WasmModules + { + desc: "valid: wasmModules with local source", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + Source: egv1a1.WasmModuleSource{ + Type: new(egv1a1.LocalWasmModuleSourceType), + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/security-filter.wasm", + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "valid: multiple wasmModules", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/security-filter.wasm", + }, + }, + }, + { + Name: "metrics-filter", + Source: egv1a1.WasmModuleSource{ + Type: new(egv1a1.LocalWasmModuleSourceType), + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/opt/wasm/metrics.wasm", + }, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "invalid: wasmModules with empty name", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/filter.wasm", + }, + }, + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].name in body should be at least 1 chars long"}, + }, + { + desc: "invalid: wasmModules name with uppercase", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "My-Filter", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/filter.wasm", + }, + }, + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].name in body should match"}, + }, + { + desc: "invalid: wasmModules Local type without local field", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + Source: egv1a1.WasmModuleSource{}, + }, + }, + } + }, + wantErrors: []string{"If type is Local, local field needs to be set"}, + }, + { + desc: "invalid: wasmModules relative path", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "relative/filter.wasm", + }, + }, + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].source.local.path in body should match"}, + }, + { + desc: "invalid: wasmModules empty path", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "", + }, + }, + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].source.local.path in body should be at least 1 chars long"}, + }, } for _, tc := range cases { diff --git a/test/e2e/testdata/wasm-envoyproxy-module.yaml b/test/e2e/testdata/wasm-envoyproxy-module.yaml new file mode 100644 index 0000000000..21c12d3067 --- /dev/null +++ b/test/e2e/testdata/wasm-envoyproxy-module.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: wasm-module-proxy + namespace: gateway-conformance-infra +spec: + ipFamily: IPv4 + provider: + type: Kubernetes + kubernetes: + envoyDeployment: + container: + image: envoyproxy/gateway-wasm-module-test:latest + wasmModules: + - name: example-filter + source: + type: Local + local: + path: /var/lib/envoy/envoy_filter_http_wasm_example.wasm +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: wasm-module-gateway + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: wasm-module-proxy + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-with-wasm-module + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: wasm-module-gateway + rules: + - matches: + - path: + type: PathPrefix + value: /wasm-module + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: wasm-module-test + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: http-with-wasm-module + wasm: + - name: wasm-filter + rootID: my_root_id + code: + type: EnvoyProxyModule + envoyProxyModule: + name: example-filter diff --git a/test/e2e/tests/wasm_envoyproxy_module.go b/test/e2e/tests/wasm_envoyproxy_module.go new file mode 100644 index 0000000000..d5b3abaf9e --- /dev/null +++ b/test/e2e/tests/wasm_envoyproxy_module.go @@ -0,0 +1,83 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e + +package tests + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, WasmEnvoyProxyModuleTest) +} + +// WasmEnvoyProxyModuleTest loads Wasm from a local path registered on EnvoyProxy. +// The Envoy image (envoyproxy/gateway-wasm-module-test) embeds the example .wasm. +var WasmEnvoyProxyModuleTest = suite.ConformanceTest{ + ShortName: "WasmEnvoyProxyModule", + Description: "Test EnvoyProxyModule Wasm source that loads a local module and adds response headers", + Manifests: []string{"testdata/wasm-envoyproxy-module.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("http route with envoy proxy module wasm", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-wasm-module", Namespace: ns} + gwNN := types.NamespacedName{Name: "wasm-module-gateway", Namespace: ns} + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN) + + ancestorRef := gwapiv1.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + } + EnvoyExtensionPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "wasm-module-test", Namespace: ns}, suite.ControllerName, ancestorRef) + + // Wait for the Envoy proxy pods to be running and ready. + gwPodNamespace := GetGatewayResourceNamespace() + WaitForPods(t, suite.Client, gwPodNamespace, map[string]string{ + "gateway.envoyproxy.io/owning-gateway-name": gwNN.Name, + "gateway.envoyproxy.io/owning-gateway-namespace": gwNN.Namespace, + }, corev1.PodRunning, &PodReady) + + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/wasm-module", + }, + // Empty ExpectedRequest: the example Wasm appends "Hello, world" to the + // response body, which invalidates the JSON format used to extract request + // properties (same workaround as the HTTP Wasm e2e). + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Host: "", + Method: "", + Path: "", + Headers: nil, + }, + }, + Namespace: "", + Response: http.Response{ + StatusCodes: []int{200}, + Headers: map[string]string{ + "x-wasm-custom": "FOO", + }, + }, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +} diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 31f9836eeb..2f662ba122 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -32607,6 +32607,26 @@ spec: code: description: Code is the Wasm code for the extension. properties: + envoyProxyModule: + description: |- + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the module source is configured by the infrastructure operator on EnvoyProxy. + + For Local modules this skips the control-plane fetch/cache path used by + HTTP and Image sources. The operator must ensure the file is present on + the Envoy proxy (for example via a custom image or volume mount). + properties: + name: + description: Name is the logical name of a module in + the EnvoyProxy wasmModules allowlist. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - name + type: object http: description: |- HTTP is the HTTP URL containing the Wasm code. @@ -32816,6 +32836,8 @@ spec: Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module even if the pull policy is set to Always. It only updates the Wasm module when the EnvoyExtension resource version changes. + + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -32825,13 +32847,14 @@ spec: - enum: - HTTP - Image + - EnvoyProxyModule - enum: - HTTP - Image - - ConfigMap + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP" or "Image". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -32841,6 +32864,14 @@ spec: rule: 'self.type == ''HTTP'' ? has(self.http) : !has(self.http)' - message: If type is Image, image field needs to be set. rule: 'self.type == ''Image'' ? has(self.image) : !has(self.image)' + - message: If type is EnvoyProxyModule, envoyProxyModule field + needs to be set. + rule: 'self.type == ''EnvoyProxyModule'' ? has(self.envoyProxyModule) + : !has(self.envoyProxyModule)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. @@ -51868,6 +51899,68 @@ spec: specified rule: '!(has(self.samplingRate) && has(self.samplingFraction))' type: object + wasmModules: + description: |- + WasmModules defines the set of Wasm modules that are allowed to be used by + EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each + entry registers a module by a logical name and a source (currently Local path). + + The EnvoyProxy owner is responsible for ensuring Local modules are available + on the proxy container's filesystem (e.g., via init containers, custom images, + or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. + items: + description: |- + WasmModuleEntry defines a Wasm module that is registered and allowed for use + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. + properties: + name: + description: |- + Name is the logical name for this module. EnvoyExtensionPolicy resources + reference modules by this name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + source: + description: Source defines where the Wasm module code is loaded + from. + properties: + local: + description: |- + Local specifies a module loaded from the proxy's local filesystem + by absolute path. + properties: + path: + description: Path is the absolute filesystem path to + the Wasm module on the Envoy proxy. + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - path + type: object + type: + default: Local + description: |- + Type is the type of the source of the Wasm module. + Defaults to Local. + enum: + - Local + type: string + type: object + x-kubernetes-validations: + - message: If type is Local, local field needs to be set. + rule: self.type != 'Local' || has(self.local) + required: + - name + - source + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object status: description: EnvoyProxyStatus defines the actual state of EnvoyProxy. diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 02b6065f79..4a9bfa8d4b 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -8545,6 +8545,26 @@ spec: code: description: Code is the Wasm code for the extension. properties: + envoyProxyModule: + description: |- + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the module source is configured by the infrastructure operator on EnvoyProxy. + + For Local modules this skips the control-plane fetch/cache path used by + HTTP and Image sources. The operator must ensure the file is present on + the Envoy proxy (for example via a custom image or volume mount). + properties: + name: + description: Name is the logical name of a module in + the EnvoyProxy wasmModules allowlist. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - name + type: object http: description: |- HTTP is the HTTP URL containing the Wasm code. @@ -8754,6 +8774,8 @@ spec: Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module even if the pull policy is set to Always. It only updates the Wasm module when the EnvoyExtension resource version changes. + + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -8763,13 +8785,14 @@ spec: - enum: - HTTP - Image + - EnvoyProxyModule - enum: - HTTP - Image - - ConfigMap + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP" or "Image". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -8779,6 +8802,14 @@ spec: rule: 'self.type == ''HTTP'' ? has(self.http) : !has(self.http)' - message: If type is Image, image field needs to be set. rule: 'self.type == ''Image'' ? has(self.image) : !has(self.image)' + - message: If type is EnvoyProxyModule, envoyProxyModule field + needs to be set. + rule: 'self.type == ''EnvoyProxyModule'' ? has(self.envoyProxyModule) + : !has(self.envoyProxyModule)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. @@ -27806,6 +27837,68 @@ spec: specified rule: '!(has(self.samplingRate) && has(self.samplingFraction))' type: object + wasmModules: + description: |- + WasmModules defines the set of Wasm modules that are allowed to be used by + EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each + entry registers a module by a logical name and a source (currently Local path). + + The EnvoyProxy owner is responsible for ensuring Local modules are available + on the proxy container's filesystem (e.g., via init containers, custom images, + or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. + items: + description: |- + WasmModuleEntry defines a Wasm module that is registered and allowed for use + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. + properties: + name: + description: |- + Name is the logical name for this module. EnvoyExtensionPolicy resources + reference modules by this name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + source: + description: Source defines where the Wasm module code is loaded + from. + properties: + local: + description: |- + Local specifies a module loaded from the proxy's local filesystem + by absolute path. + properties: + path: + description: Path is the absolute filesystem path to + the Wasm module on the Envoy proxy. + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - path + type: object + type: + default: Local + description: |- + Type is the type of the source of the Wasm module. + Defaults to Local. + enum: + - Local + type: string + type: object + x-kubernetes-validations: + - message: If type is Local, local field needs to be set. + rule: self.type != 'Local' || has(self.local) + required: + - name + - source + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object status: description: EnvoyProxyStatus defines the actual state of EnvoyProxy. diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 0b1d7764f0..f0992211c8 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -8545,6 +8545,26 @@ spec: code: description: Code is the Wasm code for the extension. properties: + envoyProxyModule: + description: |- + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the module source is configured by the infrastructure operator on EnvoyProxy. + + For Local modules this skips the control-plane fetch/cache path used by + HTTP and Image sources. The operator must ensure the file is present on + the Envoy proxy (for example via a custom image or volume mount). + properties: + name: + description: Name is the logical name of a module in + the EnvoyProxy wasmModules allowlist. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - name + type: object http: description: |- HTTP is the HTTP URL containing the Wasm code. @@ -8754,6 +8774,8 @@ spec: Note: EG does not update the Wasm module every time an Envoy proxy requests the Wasm module even if the pull policy is set to Always. It only updates the Wasm module when the EnvoyExtension resource version changes. + + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -8763,13 +8785,14 @@ spec: - enum: - HTTP - Image + - EnvoyProxyModule - enum: - HTTP - Image - - ConfigMap + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP" or "Image". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -8779,6 +8802,14 @@ spec: rule: 'self.type == ''HTTP'' ? has(self.http) : !has(self.http)' - message: If type is Image, image field needs to be set. rule: 'self.type == ''Image'' ? has(self.image) : !has(self.image)' + - message: If type is EnvoyProxyModule, envoyProxyModule field + needs to be set. + rule: 'self.type == ''EnvoyProxyModule'' ? has(self.envoyProxyModule) + : !has(self.envoyProxyModule)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. @@ -27806,6 +27837,68 @@ spec: specified rule: '!(has(self.samplingRate) && has(self.samplingFraction))' type: object + wasmModules: + description: |- + WasmModules defines the set of Wasm modules that are allowed to be used by + EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. Each + entry registers a module by a logical name and a source (currently Local path). + + The EnvoyProxy owner is responsible for ensuring Local modules are available + on the proxy container's filesystem (e.g., via init containers, custom images, + or shared volumes). EnvoyExtensionPolicy never carries a raw filesystem path. + items: + description: |- + WasmModuleEntry defines a Wasm module that is registered and allowed for use + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. + properties: + name: + description: |- + Name is the logical name for this module. EnvoyExtensionPolicy resources + reference modules by this name. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + source: + description: Source defines where the Wasm module code is loaded + from. + properties: + local: + description: |- + Local specifies a module loaded from the proxy's local filesystem + by absolute path. + properties: + path: + description: Path is the absolute filesystem path to + the Wasm module on the Envoy proxy. + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - path + type: object + type: + default: Local + description: |- + Type is the type of the source of the Wasm module. + Defaults to Local. + enum: + - Local + type: string + type: object + x-kubernetes-validations: + - message: If type is Local, local field needs to be set. + rule: self.type != 'Local' || has(self.local) + required: + - name + - source + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map type: object status: description: EnvoyProxyStatus defines the actual state of EnvoyProxy. diff --git a/tools/make/examples.mk b/tools/make/examples.mk index ca1f592b7a..4341077275 100644 --- a/tools/make/examples.mk +++ b/tools/make/examples.mk @@ -1,4 +1,4 @@ -EXAMPLE_APPS := simple-extension-server extension-server envoy-ext-auth grpc-ext-proc preserve-case-backend static-file-server dynamic-module-test backend-utilization +EXAMPLE_APPS := simple-extension-server extension-server envoy-ext-auth grpc-ext-proc preserve-case-backend static-file-server dynamic-module-test wasm-module-test backend-utilization EXAMPLE_IMAGE_PREFIX ?= envoyproxy/gateway- EXAMPLE_TAG ?= latest