From 3e206c592944c291b7e8f7d7d744292e4c8c1c52 Mon Sep 17 00:00:00 2001 From: daanvinken Date: Mon, 20 Jul 2026 16:56:51 +0200 Subject: [PATCH 1/7] feat: support Filesystem Wasm code source via EnvoyProxy allowlist Register Wasm modules on EnvoyProxy.spec.wasmModules and reference them by name from EnvoyExtensionPolicy with type Filesystem. Envoy loads the local file via AsyncDataSource.local and skips the control-plane fetch/cache path used by HTTP and Image sources. Includes generated CRD/helm fixtures, API docs, and the jwt DnsCluster fixture update needed for gen-check. Fixes #9448 Signed-off-by: daanvinken --- api/v1alpha1/envoyproxy_types.go | 14 + api/v1alpha1/wasm_types.go | 55 ++- api/v1alpha1/zz_generated.deepcopy.go | 40 ++ ....envoyproxy.io_envoyextensionpolicies.yaml | 35 +- .../gateway.envoyproxy.io_envoyproxies.yaml | 41 ++ ....envoyproxy.io_envoyextensionpolicies.yaml | 35 +- .../gateway.envoyproxy.io_envoyproxies.yaml | 41 ++ internal/gatewayapi/envoyextensionpolicy.go | 70 ++- .../envoyextensionpolicy_ca_test.go | 2 +- ...-with-wasm-filesystem-unregistered.in.yaml | 67 +++ ...with-wasm-filesystem-unregistered.out.yaml | 267 ++++++++++++ ...tensionpolicy-with-wasm-filesystem.in.yaml | 108 +++++ ...ensionpolicy-with-wasm-filesystem.out.yaml | 400 ++++++++++++++++++ internal/ir/xds.go | 11 +- .../testdata/in/xds-ir/wasm-filesystem.yaml | 65 +++ .../jwt-allow-missing-or-failed.clusters.yaml | 10 +- .../out/xds-ir/wasm-filesystem.clusters.yaml | 105 +++++ .../out/xds-ir/wasm-filesystem.endpoints.yaml | 24 ++ .../out/xds-ir/wasm-filesystem.listeners.yaml | 67 +++ .../out/xds-ir/wasm-filesystem.routes.yaml | 29 ++ .../out/xds-ir/wasm-filesystem.secrets.yaml | 6 + internal/xds/translator/wasm.go | 68 ++- internal/xds/translator/wasm_test.go | 79 ++++ .../9448-wasm-filesystem-source.md | 1 + site/content/en/latest/api/extension_types.md | 37 +- .../en/latest/tasks/extensibility/wasm.md | 75 +++- .../envoyextensionpolicy_test.go | 141 ++++++ test/cel-validation/envoyproxy_test.go | 89 ++++ test/helm/gateway-crds-helm/all.out.yaml | 76 +++- test/helm/gateway-crds-helm/e2e.out.yaml | 76 +++- .../envoy-gateway-crds.out.yaml | 76 +++- 31 files changed, 2149 insertions(+), 61 deletions(-) create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml create mode 100644 internal/xds/translator/wasm_test.go create mode 100644 release-notes/current/new_features/9448-wasm-filesystem-source.md diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index a4ef2ee57a..b993a36375 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 a Filesystem code source. Each entry + // registers a module by a logical name and the absolute path on the Envoy proxy. + // + // The EnvoyProxy owner is responsible for ensuring the Wasm files 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..49081c7cc8 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 == 'Filesystem' ? has(self.filesystem) : !has(self.filesystem)",message="If type is Filesystem, filesystem field needs to be set." +// +kubebuilder:validation:XValidation:rule="self.type == 'Filesystem' ? !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 "Filesystem". // - // +kubebuilder:validation:Enum=HTTP;Image;ConfigMap + // +kubebuilder:validation:Enum=HTTP;Image;Filesystem // +unionDiscriminator Type WasmCodeSourceType `json:"type"` @@ -94,6 +96,16 @@ type WasmCodeSource struct { // +optional Image *ImageWasmCodeSource `json:"image,omitempty"` + // Filesystem loads Wasm code from a module registered on the EnvoyProxy + // wasmModules allowlist. The policy references the module by name only; + // the filesystem path is configured by the infrastructure operator on EnvoyProxy. + // + // This source 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 + Filesystem *FilesystemWasmCodeSource `json:"filesystem,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 Filesystem. // +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;Filesystem type WasmCodeSourceType string const ( @@ -116,8 +130,43 @@ const ( // ImageWasmCodeSourceType allows the user to specify the Wasm code in an OCI image. ImageWasmCodeSourceType WasmCodeSourceType = "Image" + + // FilesystemWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy. + FilesystemWasmCodeSourceType WasmCodeSourceType = "Filesystem" ) +// FilesystemWasmCodeSource references a Wasm module registered in EnvoyProxy.spec.wasmModules. +type FilesystemWasmCodeSource 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"` +} + +// WasmModuleEntry defines a Wasm module that is registered and allowed for use +// by EnvoyExtensionPolicy resources with a Filesystem 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"` + + // Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + // + // The EnvoyProxy owner is responsible for ensuring the file is available on + // the proxy container's filesystem (for example via a custom image or volume mount). + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=4096 + // +kubebuilder:validation:Pattern=`^/.*` + Path string `json:"path"` +} + // 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..46a0b72964 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3296,6 +3296,11 @@ 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)) + copy(*out, *in) + } if in.GeoIP != nil { in, out := &in.GeoIP, &out.GeoIP *out = new(EnvoyProxyGeoIP) @@ -3819,6 +3824,21 @@ func (in *FileEnvoyProxyAccessLog) DeepCopy() *FileEnvoyProxyAccessLog { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FilesystemWasmCodeSource) DeepCopyInto(out *FilesystemWasmCodeSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FilesystemWasmCodeSource. +func (in *FilesystemWasmCodeSource) DeepCopy() *FilesystemWasmCodeSource { + if in == nil { + return nil + } + out := new(FilesystemWasmCodeSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FilterPosition) DeepCopyInto(out *FilterPosition) { *out = *in @@ -8729,6 +8749,11 @@ func (in *WasmCodeSource) DeepCopyInto(out *WasmCodeSource) { *out = new(ImageWasmCodeSource) (*in).DeepCopyInto(*out) } + if in.Filesystem != nil { + in, out := &in.Filesystem, &out.Filesystem + *out = new(FilesystemWasmCodeSource) + **out = **in + } if in.PullPolicy != nil { in, out := &in.PullPolicy, &out.PullPolicy *out = new(ImagePullPolicy) @@ -8782,6 +8807,21 @@ 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 +} + +// 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 *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..50e36d59d6 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: + filesystem: + description: |- + Filesystem loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the filesystem path is configured by the infrastructure operator on EnvoyProxy. + + This source 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 Filesystem. enum: - IfNotPresent - Always @@ -2243,13 +2265,14 @@ spec: - enum: - HTTP - Image + - Filesystem - enum: - HTTP - Image - - ConfigMap + - Filesystem 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 "Filesystem". 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 Filesystem, filesystem field needs to + be set. + rule: 'self.type == ''Filesystem'' ? has(self.filesystem) + : !has(self.filesystem)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''Filesystem'' ? !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..9cb85730d8 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,47 @@ 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 a Filesystem code source. Each entry + registers a module by a logical name and the absolute path on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the Wasm files 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 a Filesystem 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 + path: + description: |- + Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the file is available on + the proxy container's filesystem (for example via a custom image or volume mount). + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - name + - path + 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..f06619748c 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: + filesystem: + description: |- + Filesystem loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the filesystem path is configured by the infrastructure operator on EnvoyProxy. + + This source 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 Filesystem. enum: - IfNotPresent - Always @@ -2242,13 +2264,14 @@ spec: - enum: - HTTP - Image + - Filesystem - enum: - HTTP - Image - - ConfigMap + - Filesystem 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 "Filesystem". 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 Filesystem, filesystem field needs to + be set. + rule: 'self.type == ''Filesystem'' ? has(self.filesystem) + : !has(self.filesystem)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''Filesystem'' ? !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..63a6774627 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,47 @@ 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 a Filesystem code source. Each entry + registers a module by a logical name and the absolute path on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the Wasm files 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 a Filesystem 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 + path: + description: |- + Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the file is available on + the proxy container's filesystem (for example via a custom image or volume mount). + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - name + - path + 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/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index b320adbbe3..1e0cb3f81c 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,15 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( continue } + var ( + wasms []ir.Wasm + wasmFailOpen bool + ) + 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 +649,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 +960,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 + // Filesystem 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 +1013,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 +1044,33 @@ func (t *Translator) buildWasm( } switch config.Code.Type { + case egv1a1.FilesystemWasmCodeSourceType: + // Sanity check; CEL validation should have caught this. + if config.Code.Filesystem == nil { + return nil, fmt.Errorf("missing Filesystem field in Wasm code source") + } + moduleName := config.Code.Filesystem.Name + if moduleName == "" { + return nil, fmt.Errorf("Filesystem 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) + } + if entry.Path == "" { + return nil, fmt.Errorf("wasm module %q has empty path", moduleName) + } + localPath = entry.Path + case egv1a1.HTTPWasmCodeSourceType: var checksum string @@ -1171,6 +1208,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/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..16ba5a56b3 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml @@ -0,0 +1,67 @@ +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway + name: proxy-config + spec: + wasmModules: + - name: security-filter + 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: Filesystem + filesystem: + 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..c57652702a --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml @@ -0,0 +1,267 @@ +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: + filesystem: + name: not-registered + type: Filesystem + 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 + path: /var/lib/envoy/security-filter.wasm + 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 + path: /var/lib/envoy/security-filter.wasm + 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..97473b78d3 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml @@ -0,0 +1,108 @@ +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway + name: proxy-config + spec: + wasmModules: + - name: security-filter + path: /var/lib/envoy/security-filter.wasm + - name: metrics-filter + 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: Filesystem + filesystem: + 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: Filesystem + filesystem: + 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..9dd8c56f13 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml @@ -0,0 +1,400 @@ +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: + filesystem: + name: metrics-filter + type: Filesystem + 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: + filesystem: + name: security-filter + type: Filesystem + 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 + path: /var/lib/envoy/security-filter.wasm + - name: metrics-filter + path: /opt/wasm/metrics.wasm + 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 + path: /var/lib/envoy/security-filter.wasm + - name: metrics-filter + path: /opt/wasm/metrics.wasm + 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: + envoyClientCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + name: envoy-gateway-system/envoy + privateKey: '[redacted]' + 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..2ef44400e1 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 a Filesystem code 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/testdata/in/xds-ir/wasm-filesystem.yaml b/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml new file mode 100644 index 0000000000..566d467848 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml @@ -0,0 +1,65 @@ +globalResources: + envoyClientCertificate: + name: envoy-gateway-system/envoy + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] + certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] +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/jwt-allow-missing-or-failed.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-allow-missing-or-failed.clusters.yaml index 7bba0835f2..585647f1b8 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jwt-allow-missing-or-failed.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-allow-missing-or-failed.clusters.yaml @@ -47,10 +47,16 @@ - circuitBreakers: thresholds: - maxRetries: 1024 + clusterType: + name: envoy.cluster.dns + typedConfig: + '@type': type.googleapis.com/envoy.extensions.clusters.dns.v3.DnsCluster + dnsLookupFamily: V4_PREFERRED + dnsRefreshRate: 30s + respectDnsTtl: true commonLbConfig: {} connectTimeout: 10s dnsLookupFamily: V4_PREFERRED - dnsRefreshRate: 30s ignoreHealthOnHostRemoval: true loadAssignment: clusterName: localhost_443 @@ -75,7 +81,6 @@ localityWeightedLbConfig: {} name: localhost_443 perConnectionBufferLimitBytes: 32768 - respectDnsTtl: true transportSocket: name: envoy.transport_sockets.tls typedConfig: @@ -85,4 +90,3 @@ trustedCa: filename: /etc/ssl/certs/ca-certificates.crt sni: localhost - type: STRICT_DNS 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..0ca9135bd1 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml @@ -0,0 +1,105 @@ +- 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 +- circuitBreakers: + thresholds: + - maxRetries: 1024 + clusterType: + name: envoy.cluster.dns + typedConfig: + '@type': type.googleapis.com/envoy.extensions.clusters.dns.v3.DnsCluster + dnsLookupFamily: V4_PREFERRED + dnsRefreshRate: 30s + respectDnsTtl: true + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + ignoreHealthOnHostRemoval: true + loadAssignment: + clusterName: wasm_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: envoy-gateway.envoy-gateway-system.svc.cluster.local + portValue: 18002 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: wasm_cluster/backend/-1 + 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: wasm_cluster + perConnectionBufferLimitBytes: 32768 + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + tlsCertificateSdsSecretConfigs: + - name: envoy-gateway-system/envoy + sdsConfig: + ads: {} + resourceApiVersion: V3 + tlsParams: + tlsMaximumProtocolVersion: TLSv1_3 + validationContext: + trustedCa: + filename: /certs/ca.crt + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 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/testdata/out/xds-ir/wasm-filesystem.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml new file mode 100644 index 0000000000..fb08915118 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml @@ -0,0 +1,6 @@ +- name: envoy-gateway-system/envoy + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= 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..f59365f694 --- /dev/null +++ b/release-notes/current/new_features/9448-wasm-filesystem-source.md @@ -0,0 +1 @@ +Added a Filesystem Wasm code source for EnvoyExtensionPolicy. Register modules on EnvoyProxy.spec.wasmModules by name and path, then reference them by name from the policy so Envoy loads local files without the control-plane fetch and cache path. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 66d6c6cd5a..66cc92a3a6 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2256,6 +2256,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 a Filesystem code source. Each entry
registers a module by a logical name and the absolute path on the Envoy proxy.
The EnvoyProxy owner is responsible for ensuring the Wasm files 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. | @@ -2592,6 +2593,20 @@ _Appears in:_ | `path` | _string_ | true | | Path defines the file path used to expose envoy access log(e.g. /dev/stdout). | +#### FilesystemWasmCodeSource + + + +FilesystemWasmCodeSource 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. | + + #### FilterPosition @@ -6504,10 +6519,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 "Filesystem". | | `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. | +| `filesystem` | _[FilesystemWasmCodeSource](#filesystemwasmcodesource)_ | false | | Filesystem loads Wasm code from a module registered on the EnvoyProxy
wasmModules allowlist. The policy references the module by name only;
the filesystem path is configured by the infrastructure operator on EnvoyProxy.
This source 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 Filesystem. | #### WasmCodeSourceTLSConfig @@ -6538,6 +6554,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.
| +| `Filesystem` | FilesystemWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy.
| #### WasmEnv @@ -6554,6 +6571,22 @@ _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 a Filesystem 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. | +| `path` | _string_ | true | | Path is the absolute filesystem path to the Wasm module on the Envoy proxy.
The EnvoyProxy owner is responsible for ensuring the file is available on
the proxy container's filesystem (for example via a custom image or volume mount). | + + #### WeightedZoneConfig diff --git a/site/content/en/latest/tasks/extensibility/wasm.md b/site/content/en/latest/tasks/extensibility/wasm.md index 6e4ba511a7..036be39a92 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. +* Filesystem Wasm Extension: The Wasm extension is loaded from a path already present on the Envoy proxy. Modules are registered on [EnvoyProxy][] (`spec.wasmModules`) and referenced by name from [EnvoyExtensionPolicy][]. 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,77 @@ spec: {{% /tab %}} {{< /tabpane >}} +### Filesystem Wasm Extension + +Register the module path on the [EnvoyProxy][] attached to the Gateway, then reference it by name from the [EnvoyExtensionPolicy][]. Envoy Gateway does not place the file on the proxy; provision it with a custom Envoy image or a volume mount. This source skips the control-plane download path, which avoids a fail-closed load window when the module 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 + 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 +255,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..4e0d44c2bc 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 Filesystem code source + { + desc: "valid Wasm Filesystem code source", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Name: new("wasm-filter"), + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.FilesystemWasmCodeSourceType, + Filesystem: &egv1a1.FilesystemWasmCodeSource{ + 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 Filesystem without filesystem field", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.FilesystemWasmCodeSourceType, + }, + }, + }, + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + }, + } + }, + wantErrors: []string{"If type is Filesystem, filesystem field needs to be set"}, + }, + { + desc: "Wasm HTTP with filesystem 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", + }, + Filesystem: &egv1a1.FilesystemWasmCodeSource{ + 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 Filesystem, filesystem field needs to be set"}, + }, + { + desc: "Wasm Filesystem with pullPolicy set", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.FilesystemWasmCodeSourceType, + Filesystem: &egv1a1.FilesystemWasmCodeSource{ + 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 Filesystem with empty module name", + mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { + eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{ + { + Code: egv1a1.WasmCodeSource{ + Type: egv1a1.FilesystemWasmCodeSourceType, + Filesystem: &egv1a1.FilesystemWasmCodeSource{ + 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.filesystem.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..ac91a01f85 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2485,6 +2485,95 @@ func TestEnvoyProxyProvider(t *testing.T) { }, wantErrors: []string{"If type is Remote, local field must not be set"}, }, + // WasmModules + { + desc: "valid: wasmModules with name and path", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + 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", + Path: "/var/lib/envoy/security-filter.wasm", + }, + { + Name: "metrics-filter", + 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: "", + 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", + Path: "/var/lib/envoy/filter.wasm", + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].name in body should match"}, + }, + { + desc: "invalid: wasmModules relative path", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + WasmModules: []egv1a1.WasmModuleEntry{ + { + Name: "security-filter", + Path: "relative/filter.wasm", + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].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", + Path: "", + }, + }, + } + }, + wantErrors: []string{"spec.wasmModules[0].path in body should be at least 1 chars long"}, + }, } for _, tc := range cases { diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 31f9836eeb..8c6b5a4da4 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: + filesystem: + description: |- + Filesystem loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the filesystem path is configured by the infrastructure operator on EnvoyProxy. + + This source 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 Filesystem. enum: - IfNotPresent - Always @@ -32825,13 +32847,14 @@ spec: - enum: - HTTP - Image + - Filesystem - enum: - HTTP - Image - - ConfigMap + - Filesystem 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 "Filesystem". 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 Filesystem, filesystem field needs to + be set. + rule: 'self.type == ''Filesystem'' ? has(self.filesystem) + : !has(self.filesystem)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''Filesystem'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. @@ -51868,6 +51899,47 @@ 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 a Filesystem code source. Each entry + registers a module by a logical name and the absolute path on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the Wasm files 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 a Filesystem 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 + path: + description: |- + Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the file is available on + the proxy container's filesystem (for example via a custom image or volume mount). + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - name + - path + 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..3c9116273b 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: + filesystem: + description: |- + Filesystem loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the filesystem path is configured by the infrastructure operator on EnvoyProxy. + + This source 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 Filesystem. enum: - IfNotPresent - Always @@ -8763,13 +8785,14 @@ spec: - enum: - HTTP - Image + - Filesystem - enum: - HTTP - Image - - ConfigMap + - Filesystem 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 "Filesystem". 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 Filesystem, filesystem field needs to + be set. + rule: 'self.type == ''Filesystem'' ? has(self.filesystem) + : !has(self.filesystem)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''Filesystem'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. @@ -27806,6 +27837,47 @@ 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 a Filesystem code source. Each entry + registers a module by a logical name and the absolute path on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the Wasm files 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 a Filesystem 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 + path: + description: |- + Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the file is available on + the proxy container's filesystem (for example via a custom image or volume mount). + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - name + - path + 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..dd8cda307c 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: + filesystem: + description: |- + Filesystem loads Wasm code from a module registered on the EnvoyProxy + wasmModules allowlist. The policy references the module by name only; + the filesystem path is configured by the infrastructure operator on EnvoyProxy. + + This source 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 Filesystem. enum: - IfNotPresent - Always @@ -8763,13 +8785,14 @@ spec: - enum: - HTTP - Image + - Filesystem - enum: - HTTP - Image - - ConfigMap + - Filesystem 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 "Filesystem". 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 Filesystem, filesystem field needs to + be set. + rule: 'self.type == ''Filesystem'' ? has(self.filesystem) + : !has(self.filesystem)' + - message: PullPolicy is only valid for HTTP and Image code + sources. + rule: 'self.type == ''Filesystem'' ? !has(self.pullPolicy) + : true' config: description: |- Config is the configuration for the Wasm extension. @@ -27806,6 +27837,47 @@ 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 a Filesystem code source. Each entry + registers a module by a logical name and the absolute path on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the Wasm files 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 a Filesystem 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 + path: + description: |- + Path is the absolute filesystem path to the Wasm module on the Envoy proxy. + + The EnvoyProxy owner is responsible for ensuring the file is available on + the proxy container's filesystem (for example via a custom image or volume mount). + maxLength: 4096 + minLength: 1 + pattern: ^/.* + type: string + required: + - name + - path + 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. From 1b2b67459254ce2f898f6715d6a3dd905a43c50e Mon Sep 17 00:00:00 2001 From: daanvinken Date: Mon, 20 Jul 2026 17:17:50 +0200 Subject: [PATCH 2/7] fix(xds): omit wasm_cluster for filesystem-only Wasm Only create the control-plane wasm HTTP service cluster (and gate the client certificate) when a route uses a remote HTTP/Image code source. Filesystem modules load a local path and never use that cluster. Signed-off-by: daanvinken --- internal/gatewayapi/globalresources.go | 13 +++- internal/xds/translator/globalresources.go | 13 +++- .../testdata/in/xds-ir/wasm-filesystem.yaml | 5 -- .../out/xds-ir/wasm-filesystem.clusters.yaml | 59 ------------------- .../out/xds-ir/wasm-filesystem.secrets.yaml | 6 -- 5 files changed, 20 insertions(+), 76 deletions(-) delete mode 100644 internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml diff --git a/internal/gatewayapi/globalresources.go b/internal/gatewayapi/globalresources.go index 1000890724..cad35c0549 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). Filesystem 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/xds/translator/globalresources.go b/internal/xds/translator/globalresources.go index 32a67d5203..c228eedb75 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). Filesystem +// sources load a local 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 index 566d467848..d51f77efa4 100644 --- a/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/wasm-filesystem.yaml @@ -1,8 +1,3 @@ -globalResources: - envoyClientCertificate: - name: envoy-gateway-system/envoy - privateKey: [107, 101, 121, 45, 100, 97, 116, 97] - certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] http: - address: 0.0.0.0 hostnames: 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 index 0ca9135bd1..d1b786402b 100644 --- a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.clusters.yaml @@ -44,62 +44,3 @@ name: httproute/default/httproute-2/rule/0 perConnectionBufferLimitBytes: 32768 type: EDS -- circuitBreakers: - thresholds: - - maxRetries: 1024 - clusterType: - name: envoy.cluster.dns - typedConfig: - '@type': type.googleapis.com/envoy.extensions.clusters.dns.v3.DnsCluster - dnsLookupFamily: V4_PREFERRED - dnsRefreshRate: 30s - respectDnsTtl: true - commonLbConfig: {} - connectTimeout: 10s - dnsLookupFamily: V4_PREFERRED - ignoreHealthOnHostRemoval: true - loadAssignment: - clusterName: wasm_cluster - endpoints: - - lbEndpoints: - - endpoint: - address: - socketAddress: - address: envoy-gateway.envoy-gateway-system.svc.cluster.local - portValue: 18002 - loadBalancingWeight: 1 - loadBalancingWeight: 1 - locality: - region: wasm_cluster/backend/-1 - 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: wasm_cluster - perConnectionBufferLimitBytes: 32768 - transportSocket: - name: envoy.transport_sockets.tls - typedConfig: - '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext - commonTlsContext: - tlsCertificateSdsSecretConfigs: - - name: envoy-gateway-system/envoy - sdsConfig: - ads: {} - resourceApiVersion: V3 - tlsParams: - tlsMaximumProtocolVersion: TLSv1_3 - validationContext: - trustedCa: - filename: /certs/ca.crt - typedExtensionProtocolOptions: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicitHttpConfig: - http2ProtocolOptions: - initialConnectionWindowSize: 1048576 - initialStreamWindowSize: 65536 diff --git a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml deleted file mode 100644 index fb08915118..0000000000 --- a/internal/xds/translator/testdata/out/xds-ir/wasm-filesystem.secrets.yaml +++ /dev/null @@ -1,6 +0,0 @@ -- name: envoy-gateway-system/envoy - tlsCertificate: - certificateChain: - inlineBytes: Y2VydC1kYXRh - privateKey: - inlineBytes: a2V5LWRhdGE= From 21ef0a30f8206ba0225a5350b91d70eb015123f0 Mon Sep 17 00:00:00 2001 From: daanvinken Date: Mon, 20 Jul 2026 17:17:50 +0200 Subject: [PATCH 3/7] docs: note per-parentRef Wasm cache Get for multi-gateway routes Filesystem resolution is Gateway-scoped, so buildWasms runs per parentRef like Lua and DynamicModules. HTTP/Image may call WasmCache.Get once per parent; IfNotPresent hits the cache, Always may re-fetch. Signed-off-by: daanvinken --- internal/gatewayapi/envoyextensionpolicy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index 1e0cb3f81c..d33aee703a 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -544,6 +544,10 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( wasms []ir.Wasm wasmFailOpen bool ) + // Built per parent Gateway because Filesystem 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) From edb360c37610cae43ac516d1eb987ac8588fdac4 Mon Sep 17 00:00:00 2001 From: daanvinken Date: Mon, 20 Jul 2026 17:17:50 +0200 Subject: [PATCH 4/7] docs: note ConfigMap Wasm enum value removal in release note ConfigMap was never implemented; the code source enum is now HTTP, Image, and Filesystem only. Signed-off-by: daanvinken --- .../current/new_features/9448-wasm-filesystem-source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/current/new_features/9448-wasm-filesystem-source.md b/release-notes/current/new_features/9448-wasm-filesystem-source.md index f59365f694..872c3e7317 100644 --- a/release-notes/current/new_features/9448-wasm-filesystem-source.md +++ b/release-notes/current/new_features/9448-wasm-filesystem-source.md @@ -1 +1 @@ -Added a Filesystem Wasm code source for EnvoyExtensionPolicy. Register modules on EnvoyProxy.spec.wasmModules by name and path, then reference them by name from the policy so Envoy loads local files without the control-plane fetch and cache path. +Added a Filesystem Wasm code source for EnvoyExtensionPolicy. Register modules on EnvoyProxy.spec.wasmModules by name and path, then reference them by name from the policy so Envoy loads local files without the control-plane fetch and cache path. The Wasm code type enum is now HTTP, Image, and Filesystem; the unused ConfigMap enum value (never implemented) was removed. From ea94f446cf0ef90145c857314e5b16c1ef0d9ceb Mon Sep 17 00:00:00 2001 From: daanvinken Date: Tue, 21 Jul 2026 15:37:48 +0200 Subject: [PATCH 5/7] api: shape Wasm modules like DynamicModule Local source Register wasmModules with source.type Local and source.local.path, and reference them from EnvoyExtensionPolicy as type EnvoyProxyModule. Leaves room for additional EnvoyProxy module sources later without implementing HTTP/OCI registration yet. Signed-off-by: daanvinken --- api/v1alpha1/envoyproxy_types.go | 6 +- api/v1alpha1/wasm_types.go | 85 ++++++++++++------ api/v1alpha1/zz_generated.deepcopy.go | 81 +++++++++++++---- ....envoyproxy.io_envoyextensionpolicies.yaml | 30 +++---- .../gateway.envoyproxy.io_envoyproxies.yaml | 51 +++++++---- ....envoyproxy.io_envoyextensionpolicies.yaml | 30 +++---- .../gateway.envoyproxy.io_envoyproxies.yaml | 51 +++++++---- internal/gatewayapi/envoyextensionpolicy.go | 29 ++++--- internal/gatewayapi/globalresources.go | 4 +- ...-with-wasm-filesystem-unregistered.in.yaml | 9 +- ...with-wasm-filesystem-unregistered.out.yaml | 14 ++- ...tensionpolicy-with-wasm-filesystem.in.yaml | 18 ++-- ...ensionpolicy-with-wasm-filesystem.out.yaml | 32 ++++--- internal/ir/xds.go | 2 +- internal/xds/translator/globalresources.go | 4 +- .../9448-wasm-filesystem-source.md | 2 +- site/content/en/latest/api/extension_types.md | 86 ++++++++++++++----- .../en/latest/tasks/extensibility/wasm.md | 19 ++-- .../envoyextensionpolicy_test.go | 34 ++++---- test/cel-validation/envoyproxy_test.go | 64 +++++++++++--- test/helm/gateway-crds-helm/all.out.yaml | 81 ++++++++++------- test/helm/gateway-crds-helm/e2e.out.yaml | 81 ++++++++++------- .../envoy-gateway-crds.out.yaml | 81 ++++++++++------- 23 files changed, 599 insertions(+), 295 deletions(-) diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index b993a36375..5dad319229 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -205,10 +205,10 @@ type EnvoyProxySpec struct { DynamicModules []DynamicModuleEntry `json:"dynamicModules,omitempty"` // WasmModules defines the set of Wasm modules that are allowed to be used by - // EnvoyExtensionPolicy resources with a Filesystem code source. Each entry - // registers a module by a logical name and the absolute path on the Envoy proxy. + // 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 the Wasm files are available + // 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. // diff --git a/api/v1alpha1/wasm_types.go b/api/v1alpha1/wasm_types.go index 49081c7cc8..13c2821cb7 100644 --- a/api/v1alpha1/wasm_types.go +++ b/api/v1alpha1/wasm_types.go @@ -74,13 +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 == 'Filesystem' ? has(self.filesystem) : !has(self.filesystem)",message="If type is Filesystem, filesystem field needs to be set." -// +kubebuilder:validation:XValidation:rule="self.type == 'Filesystem' ? !has(self.pullPolicy) : true",message="PullPolicy is only valid for HTTP and Image code sources." +// +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", "Image", or "Filesystem". + // Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". // - // +kubebuilder:validation:Enum=HTTP;Image;Filesystem + // +kubebuilder:validation:Enum=HTTP;Image;EnvoyProxyModule // +unionDiscriminator Type WasmCodeSourceType `json:"type"` @@ -96,15 +96,15 @@ type WasmCodeSource struct { // +optional Image *ImageWasmCodeSource `json:"image,omitempty"` - // Filesystem loads Wasm code from a module registered on the EnvoyProxy + // EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy // wasmModules allowlist. The policy references the module by name only; - // the filesystem path is configured by the infrastructure operator on EnvoyProxy. + // the module source is configured by the infrastructure operator on EnvoyProxy. // - // This source 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). + // 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 - Filesystem *FilesystemWasmCodeSource `json:"filesystem,omitempty"` + 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. @@ -115,13 +115,13 @@ type WasmCodeSource struct { // 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 Filesystem. + // 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;Filesystem +// +kubebuilder:validation:Enum=HTTP;Image;EnvoyProxyModule type WasmCodeSourceType string const ( @@ -131,12 +131,12 @@ const ( // ImageWasmCodeSourceType allows the user to specify the Wasm code in an OCI image. ImageWasmCodeSourceType WasmCodeSourceType = "Image" - // FilesystemWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy. - FilesystemWasmCodeSourceType WasmCodeSourceType = "Filesystem" + // EnvoyProxyModuleWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy. + EnvoyProxyModuleWasmCodeSourceType WasmCodeSourceType = "EnvoyProxyModule" ) -// FilesystemWasmCodeSource references a Wasm module registered in EnvoyProxy.spec.wasmModules. -type FilesystemWasmCodeSource struct { +// 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 @@ -145,8 +145,48 @@ type FilesystemWasmCodeSource struct { 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 a Filesystem code source. +// 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. @@ -156,15 +196,8 @@ type WasmModuleEntry struct { // +kubebuilder:validation:Pattern=`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$` Name string `json:"name"` - // Path is the absolute filesystem path to the Wasm module on the Envoy proxy. - // - // The EnvoyProxy owner is responsible for ensuring the file is available on - // the proxy container's filesystem (for example via a custom image or volume mount). - // - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=4096 - // +kubebuilder:validation:Pattern=`^/.*` - Path string `json:"path"` + // Source defines where the Wasm module code is loaded from. + Source WasmModuleSource `json:"source"` } // HTTPWasmCodeSource defines the HTTP URL containing the Wasm code. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 46a0b72964..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 @@ -3299,7 +3314,9 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) { if in.WasmModules != nil { in, out := &in.WasmModules, &out.WasmModules *out = make([]WasmModuleEntry, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.GeoIP != nil { in, out := &in.GeoIP, &out.GeoIP @@ -3824,21 +3841,6 @@ func (in *FileEnvoyProxyAccessLog) DeepCopy() *FileEnvoyProxyAccessLog { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FilesystemWasmCodeSource) DeepCopyInto(out *FilesystemWasmCodeSource) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FilesystemWasmCodeSource. -func (in *FilesystemWasmCodeSource) DeepCopy() *FilesystemWasmCodeSource { - if in == nil { - return nil - } - out := new(FilesystemWasmCodeSource) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FilterPosition) DeepCopyInto(out *FilterPosition) { *out = *in @@ -5991,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 @@ -8749,9 +8766,9 @@ func (in *WasmCodeSource) DeepCopyInto(out *WasmCodeSource) { *out = new(ImageWasmCodeSource) (*in).DeepCopyInto(*out) } - if in.Filesystem != nil { - in, out := &in.Filesystem, &out.Filesystem - *out = new(FilesystemWasmCodeSource) + if in.EnvoyProxyModule != nil { + in, out := &in.EnvoyProxyModule, &out.EnvoyProxyModule + *out = new(EnvoyProxyModuleWasmCodeSource) **out = **in } if in.PullPolicy != nil { @@ -8810,6 +8827,7 @@ func (in *WasmEnv) DeepCopy() *WasmEnv { // 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. @@ -8822,6 +8840,31 @@ func (in *WasmModuleEntry) DeepCopy() *WasmModuleEntry { 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 50e36d59d6..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,15 +2025,15 @@ spec: code: description: Code is the Wasm code for the extension. properties: - filesystem: + envoyProxyModule: description: |- - Filesystem loads Wasm code from a module registered on the EnvoyProxy + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy wasmModules allowlist. The policy references the module by name only; - the filesystem path is configured by the infrastructure operator on EnvoyProxy. + the module source is configured by the infrastructure operator on EnvoyProxy. - This source 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). + 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 @@ -2255,7 +2255,7 @@ spec: 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 Filesystem. + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -2265,14 +2265,14 @@ spec: - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP", "Image", or "Filesystem". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -2282,13 +2282,13 @@ 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 Filesystem, filesystem field needs to - be set. - rule: 'self.type == ''Filesystem'' ? has(self.filesystem) - : !has(self.filesystem)' + - 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 == ''Filesystem'' ? !has(self.pullPolicy) + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) : true' config: description: |- 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 9cb85730d8..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 @@ -18114,16 +18114,16 @@ spec: wasmModules: description: |- WasmModules defines the set of Wasm modules that are allowed to be used by - EnvoyExtensionPolicy resources with a Filesystem code source. Each entry - registers a module by a logical name and the absolute path on the Envoy proxy. + 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 the Wasm files are available + 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 a Filesystem code source. + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. properties: name: description: |- @@ -18133,19 +18133,40 @@ spec: minLength: 1 pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ type: string - path: - description: |- - Path is the absolute filesystem path to the Wasm module on the Envoy proxy. - - The EnvoyProxy owner is responsible for ensuring the file is available on - the proxy container's filesystem (for example via a custom image or volume mount). - maxLength: 4096 - minLength: 1 - pattern: ^/.* - 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 - - path + - source type: object maxItems: 16 type: array 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 f06619748c..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,15 +2024,15 @@ spec: code: description: Code is the Wasm code for the extension. properties: - filesystem: + envoyProxyModule: description: |- - Filesystem loads Wasm code from a module registered on the EnvoyProxy + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy wasmModules allowlist. The policy references the module by name only; - the filesystem path is configured by the infrastructure operator on EnvoyProxy. + the module source is configured by the infrastructure operator on EnvoyProxy. - This source 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). + 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 @@ -2254,7 +2254,7 @@ spec: 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 Filesystem. + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -2264,14 +2264,14 @@ spec: - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP", "Image", or "Filesystem". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -2281,13 +2281,13 @@ 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 Filesystem, filesystem field needs to - be set. - rule: 'self.type == ''Filesystem'' ? has(self.filesystem) - : !has(self.filesystem)' + - 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 == ''Filesystem'' ? !has(self.pullPolicy) + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) : true' config: description: |- 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 63a6774627..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 @@ -18113,16 +18113,16 @@ spec: wasmModules: description: |- WasmModules defines the set of Wasm modules that are allowed to be used by - EnvoyExtensionPolicy resources with a Filesystem code source. Each entry - registers a module by a logical name and the absolute path on the Envoy proxy. + 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 the Wasm files are available + 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 a Filesystem code source. + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. properties: name: description: |- @@ -18132,19 +18132,40 @@ spec: minLength: 1 pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ type: string - path: - description: |- - Path is the absolute filesystem path to the Wasm module on the Envoy proxy. - - The EnvoyProxy owner is responsible for ensuring the file is available on - the proxy container's filesystem (for example via a custom image or volume mount). - maxLength: 4096 - minLength: 1 - pattern: ^/.* - 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 - - path + - source type: object maxItems: 16 type: array diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index d33aee703a..1595e741c6 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -544,7 +544,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( wasms []ir.Wasm wasmFailOpen bool ) - // Built per parent Gateway because Filesystem Wasm resolves against + // 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). @@ -977,7 +977,7 @@ func (t *Translator) buildWasms( wasmIRList := make([]ir.Wasm, 0, len(policy.Spec.Wasm)) - // Filesystem sources resolve from EnvoyProxy and skip the control-plane cache. + // 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 { @@ -1048,14 +1048,14 @@ func (t *Translator) buildWasm( } switch config.Code.Type { - case egv1a1.FilesystemWasmCodeSourceType: + case egv1a1.EnvoyProxyModuleWasmCodeSourceType: // Sanity check; CEL validation should have caught this. - if config.Code.Filesystem == nil { - return nil, fmt.Errorf("missing Filesystem field in Wasm code source") + if config.Code.EnvoyProxyModule == nil { + return nil, fmt.Errorf("missing EnvoyProxyModule field in Wasm code source") } - moduleName := config.Code.Filesystem.Name + moduleName := config.Code.EnvoyProxyModule.Name if moduleName == "" { - return nil, fmt.Errorf("Filesystem name must not be empty") + return nil, fmt.Errorf("EnvoyProxyModule name must not be empty") } var entry *egv1a1.WasmModuleEntry @@ -1070,10 +1070,19 @@ func (t *Translator) buildWasm( if entry == nil { return nil, fmt.Errorf("wasm module %q is not registered in the EnvoyProxy wasmModules allowlist", moduleName) } - if entry.Path == "" { - return nil, fmt.Errorf("wasm module %q has empty path", 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) } - localPath = entry.Path case egv1a1.HTTPWasmCodeSourceType: var checksum string diff --git a/internal/gatewayapi/globalresources.go b/internal/gatewayapi/globalresources.go index cad35c0549..2d31c23038 100644 --- a/internal/gatewayapi/globalresources.go +++ b/internal/gatewayapi/globalresources.go @@ -115,8 +115,8 @@ func containsGlobalRateLimit(httpListeners []*ir.HTTPListener) bool { } // containsWasm reports whether any route uses a remote Wasm code source -// (HTTP/Image). Filesystem Wasm does not use the control-plane wasm HTTP -// service or its client certificate. +// (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 { diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml index 16ba5a56b3..b1a55cba36 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.in.yaml @@ -7,7 +7,10 @@ envoyProxiesForGateways: spec: wasmModules: - name: security-filter - path: /var/lib/envoy/security-filter.wasm + source: + type: Local + local: + path: /var/lib/envoy/security-filter.wasm gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -62,6 +65,6 @@ envoyextensionpolicies: wasm: - name: wasm-filter-missing code: - type: Filesystem - filesystem: + 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 index c57652702a..b5e6c6fe5a 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem-unregistered.out.yaml @@ -11,9 +11,9 @@ envoyExtensionPolicies: name: httproute-1 wasm: - code: - filesystem: + envoyProxyModule: name: not-registered - type: Filesystem + type: EnvoyProxyModule name: wasm-filter-missing status: ancestors: @@ -46,7 +46,10 @@ envoyProxiesForGateways: logging: {} wasmModules: - name: security-filter - path: /var/lib/envoy/security-filter.wasm + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local status: ancestors: - ancestorRef: @@ -156,7 +159,10 @@ infraIR: logging: {} wasmModules: - name: security-filter - path: /var/lib/envoy/security-filter.wasm + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local status: ancestors: - ancestorRef: diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml index 97473b78d3..22692112f1 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.in.yaml @@ -7,9 +7,15 @@ envoyProxiesForGateways: spec: wasmModules: - name: security-filter - path: /var/lib/envoy/security-filter.wasm + source: + type: Local + local: + path: /var/lib/envoy/security-filter.wasm - name: metrics-filter - path: /opt/wasm/metrics.wasm + source: + type: Local + local: + path: /opt/wasm/metrics.wasm gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -83,8 +89,8 @@ envoyextensionpolicies: wasm: - name: wasm-filter-1 code: - type: Filesystem - filesystem: + type: EnvoyProxyModule + envoyProxyModule: name: security-filter config: mode: enforce @@ -102,7 +108,7 @@ envoyextensionpolicies: - name: wasm-filter-2 rootID: my-root-id code: - type: Filesystem - filesystem: + 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 index 9dd8c56f13..3c146b5965 100644 --- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-wasm-filesystem.out.yaml @@ -11,9 +11,9 @@ envoyExtensionPolicies: name: httproute-1 wasm: - code: - filesystem: + envoyProxyModule: name: metrics-filter - type: Filesystem + type: EnvoyProxyModule failOpen: true name: wasm-filter-2 rootID: my-root-id @@ -49,9 +49,9 @@ envoyExtensionPolicies: name: gateway-1 wasm: - code: - filesystem: + envoyProxyModule: name: security-filter - type: Filesystem + type: EnvoyProxyModule config: mode: enforce name: wasm-filter-1 @@ -90,9 +90,15 @@ envoyProxiesForGateways: logging: {} wasmModules: - name: security-filter - path: /var/lib/envoy/security-filter.wasm + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local - name: metrics-filter - path: /opt/wasm/metrics.wasm + source: + local: + path: /opt/wasm/metrics.wasm + type: Local status: ancestors: - ancestorRef: @@ -239,9 +245,15 @@ infraIR: logging: {} wasmModules: - name: security-filter - path: /var/lib/envoy/security-filter.wasm + source: + local: + path: /var/lib/envoy/security-filter.wasm + type: Local - name: metrics-filter - path: /opt/wasm/metrics.wasm + source: + local: + path: /opt/wasm/metrics.wasm + type: Local status: ancestors: - ancestorRef: @@ -277,10 +289,6 @@ xdsIR: json: - path: /dev/stdout globalResources: - envoyClientCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= - name: envoy-gateway-system/envoy - privateKey: '[redacted]' proxyServiceCluster: metadata: kind: Service diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 2ef44400e1..8ea5deb205 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -3787,7 +3787,7 @@ type Wasm struct { Code *HTTPWasmCode `json:"httpWasmCode,omitempty"` // Path is the absolute filesystem path to the Wasm module on the Envoy proxy - // when using a Filesystem code source resolved from EnvoyProxy.wasmModules. + // when using an EnvoyProxyModule Local source resolved from EnvoyProxy.wasmModules. // Mutually exclusive with Code. Path string `json:"path,omitempty"` diff --git a/internal/xds/translator/globalresources.go b/internal/xds/translator/globalresources.go index c228eedb75..23e6ff38b7 100644 --- a/internal/xds/translator/globalresources.go +++ b/internal/xds/translator/globalresources.go @@ -140,8 +140,8 @@ func buildEnvoyClientTLSSocket(envoyClientCertificate *ir.TLSCertificate) (*core } // containsWasm reports whether any route uses a remote Wasm code source -// (HTTP/Image, served by the control-plane wasm HTTP service). Filesystem -// sources load a local path on the proxy and do not need wasm_cluster. +// (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 { diff --git a/release-notes/current/new_features/9448-wasm-filesystem-source.md b/release-notes/current/new_features/9448-wasm-filesystem-source.md index 872c3e7317..a31e06ed21 100644 --- a/release-notes/current/new_features/9448-wasm-filesystem-source.md +++ b/release-notes/current/new_features/9448-wasm-filesystem-source.md @@ -1 +1 @@ -Added a Filesystem Wasm code source for EnvoyExtensionPolicy. Register modules on EnvoyProxy.spec.wasmModules by name and path, then reference them by name from the policy so Envoy loads local files without the control-plane fetch and cache path. The Wasm code type enum is now HTTP, Image, and Filesystem; the unused ConfigMap enum value (never implemented) was removed. +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 66cc92a3a6..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,7 +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 a Filesystem code source. Each entry
registers a module by a logical name and the absolute path on the Envoy proxy.
The EnvoyProxy owner is responsible for ensuring the Wasm files 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. | +| `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. | @@ -2593,20 +2607,6 @@ _Appears in:_ | `path` | _string_ | true | | Path defines the file path used to expose envoy access log(e.g. /dev/stdout). | -#### FilesystemWasmCodeSource - - - -FilesystemWasmCodeSource 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. | - - #### FilterPosition @@ -4137,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_ @@ -6519,11 +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", "Image", or "Filesystem". | +| `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. | -| `filesystem` | _[FilesystemWasmCodeSource](#filesystemwasmcodesource)_ | false | | Filesystem loads Wasm code from a module registered on the EnvoyProxy
wasmModules allowlist. The policy references the module by name only;
the filesystem path is configured by the infrastructure operator on EnvoyProxy.
This source 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 Filesystem. | +| `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 @@ -6554,7 +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.
| -| `Filesystem` | FilesystemWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy.
| +| `EnvoyProxyModule` | EnvoyProxyModuleWasmCodeSourceType loads Wasm code from a module registered on EnvoyProxy.
| #### WasmEnv @@ -6576,7 +6590,7 @@ _Appears in:_ WasmModuleEntry defines a Wasm module that is registered and allowed for use -by EnvoyExtensionPolicy resources with a Filesystem code source. +by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. _Appears in:_ - [EnvoyProxySpec](#envoyproxyspec) @@ -6584,7 +6598,37 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | _string_ | true | | Name is the logical name for this module. EnvoyExtensionPolicy resources
reference modules by this name. | -| `path` | _string_ | true | | Path is the absolute filesystem path to the Wasm module on the Envoy proxy.
The EnvoyProxy owner is responsible for ensuring the file is available on
the proxy container's filesystem (for example via a custom image or volume mount). | +| `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 036be39a92..e1eeea907c 100644 --- a/site/content/en/latest/tasks/extensibility/wasm.md +++ b/site/content/en/latest/tasks/extensibility/wasm.md @@ -19,7 +19,7 @@ This instantiated resource can be linked to a [Gateway][Gateway] and [HTTPRoute] 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. -* Filesystem Wasm Extension: The Wasm extension is loaded from a path already present on the Envoy proxy. Modules are registered on [EnvoyProxy][] (`spec.wasmModules`) and referenced by name from [EnvoyExtensionPolicy][]. +* 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. @@ -142,9 +142,9 @@ spec: {{% /tab %}} {{< /tabpane >}} -### Filesystem Wasm Extension +### EnvoyProxyModule Wasm Extension -Register the module path on the [EnvoyProxy][] attached to the Gateway, then reference it by name from the [EnvoyExtensionPolicy][]. Envoy Gateway does not place the file on the proxy; provision it with a custom Envoy image or a volume mount. This source skips the control-plane download path, which avoids a fail-closed load window when the module is already on the proxy. +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: @@ -157,7 +157,10 @@ metadata: spec: wasmModules: - name: example-filter - path: /var/lib/envoy/example-filter.wasm + source: + type: Local + local: + path: /var/lib/envoy/example-filter.wasm ``` Then apply the EnvoyExtensionPolicy: @@ -180,8 +183,8 @@ spec: - name: wasm-filter rootID: my_root_id code: - type: Filesystem - filesystem: + type: EnvoyProxyModule + envoyProxyModule: name: example-filter EOF ``` @@ -205,8 +208,8 @@ spec: - name: wasm-filter rootID: my_root_id code: - type: Filesystem - filesystem: + type: EnvoyProxyModule + envoyProxyModule: name: example-filter ``` diff --git a/test/cel-validation/envoyextensionpolicy_test.go b/test/cel-validation/envoyextensionpolicy_test.go index 4e0d44c2bc..72e6fffe7c 100644 --- a/test/cel-validation/envoyextensionpolicy_test.go +++ b/test/cel-validation/envoyextensionpolicy_test.go @@ -964,17 +964,17 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { ": Exactly one of inline or valueRef must be set with correct type.", }, }, - // Wasm Filesystem code source + // Wasm EnvoyProxyModule code source { - desc: "valid Wasm Filesystem 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.FilesystemWasmCodeSourceType, - Filesystem: &egv1a1.FilesystemWasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ Name: "security-filter", }, }, @@ -994,13 +994,13 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { wantErrors: []string{}, }, { - desc: "Wasm Filesystem without filesystem field", + desc: "Wasm EnvoyProxyModule without envoyProxyModule field", mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ Wasm: []egv1a1.Wasm{ { Code: egv1a1.WasmCodeSource{ - Type: egv1a1.FilesystemWasmCodeSourceType, + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, }, }, }, @@ -1015,10 +1015,10 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { }, } }, - wantErrors: []string{"If type is Filesystem, filesystem field needs to be set"}, + wantErrors: []string{"If type is EnvoyProxyModule, envoyProxyModule field needs to be set"}, }, { - desc: "Wasm HTTP with filesystem field set", + desc: "Wasm HTTP with envoyProxyModule field set", mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ Wasm: []egv1a1.Wasm{ @@ -1028,7 +1028,7 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { HTTP: &egv1a1.HTTPWasmCodeSource{ URL: "https://example.com/filter.wasm", }, - Filesystem: &egv1a1.FilesystemWasmCodeSource{ + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ Name: "security-filter", }, }, @@ -1045,17 +1045,17 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { }, } }, - wantErrors: []string{"If type is Filesystem, filesystem field needs to be set"}, + wantErrors: []string{"If type is EnvoyProxyModule, envoyProxyModule field needs to be set"}, }, { - desc: "Wasm Filesystem with pullPolicy set", + desc: "Wasm EnvoyProxyModule with pullPolicy set", mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ Wasm: []egv1a1.Wasm{ { Code: egv1a1.WasmCodeSource{ - Type: egv1a1.FilesystemWasmCodeSourceType, - Filesystem: &egv1a1.FilesystemWasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ Name: "security-filter", }, PullPolicy: new(egv1a1.ImagePullPolicyAlways), @@ -1076,14 +1076,14 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { wantErrors: []string{"PullPolicy is only valid for HTTP and Image code sources"}, }, { - desc: "Wasm Filesystem with empty module name", + desc: "Wasm EnvoyProxyModule with empty module name", mutate: func(eep *egv1a1.EnvoyExtensionPolicy) { eep.Spec = egv1a1.EnvoyExtensionPolicySpec{ Wasm: []egv1a1.Wasm{ { Code: egv1a1.WasmCodeSource{ - Type: egv1a1.FilesystemWasmCodeSourceType, - Filesystem: &egv1a1.FilesystemWasmCodeSource{ + Type: egv1a1.EnvoyProxyModuleWasmCodeSourceType, + EnvoyProxyModule: &egv1a1.EnvoyProxyModuleWasmCodeSource{ Name: "", }, }, @@ -1101,7 +1101,7 @@ func TestEnvoyExtensionPolicyTarget(t *testing.T) { } }, wantErrors: []string{ - "spec.wasm[0].code.filesystem.name: Invalid value:", + "spec.wasm[0].code.envoyProxyModule.name: Invalid value:", "should be at least 1 chars long", }, }, diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index ac91a01f85..ec215347cb 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2487,13 +2487,18 @@ func TestEnvoyProxyProvider(t *testing.T) { }, // WasmModules { - desc: "valid: wasmModules with name and path", + desc: "valid: wasmModules with local source", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ WasmModules: []egv1a1.WasmModuleEntry{ { Name: "security-filter", - Path: "/var/lib/envoy/security-filter.wasm", + Source: egv1a1.WasmModuleSource{ + Type: new(egv1a1.LocalWasmModuleSourceType), + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/security-filter.wasm", + }, + }, }, }, } @@ -2507,11 +2512,20 @@ func TestEnvoyProxyProvider(t *testing.T) { WasmModules: []egv1a1.WasmModuleEntry{ { Name: "security-filter", - Path: "/var/lib/envoy/security-filter.wasm", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/security-filter.wasm", + }, + }, }, { Name: "metrics-filter", - Path: "/opt/wasm/metrics.wasm", + Source: egv1a1.WasmModuleSource{ + Type: new(egv1a1.LocalWasmModuleSourceType), + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/opt/wasm/metrics.wasm", + }, + }, }, }, } @@ -2525,7 +2539,11 @@ func TestEnvoyProxyProvider(t *testing.T) { WasmModules: []egv1a1.WasmModuleEntry{ { Name: "", - Path: "/var/lib/envoy/filter.wasm", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "/var/lib/envoy/filter.wasm", + }, + }, }, }, } @@ -2539,13 +2557,31 @@ func TestEnvoyProxyProvider(t *testing.T) { WasmModules: []egv1a1.WasmModuleEntry{ { Name: "My-Filter", - Path: "/var/lib/envoy/filter.wasm", + 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) { @@ -2553,12 +2589,16 @@ func TestEnvoyProxyProvider(t *testing.T) { WasmModules: []egv1a1.WasmModuleEntry{ { Name: "security-filter", - Path: "relative/filter.wasm", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "relative/filter.wasm", + }, + }, }, }, } }, - wantErrors: []string{"spec.wasmModules[0].path in body should match"}, + wantErrors: []string{"spec.wasmModules[0].source.local.path in body should match"}, }, { desc: "invalid: wasmModules empty path", @@ -2567,12 +2607,16 @@ func TestEnvoyProxyProvider(t *testing.T) { WasmModules: []egv1a1.WasmModuleEntry{ { Name: "security-filter", - Path: "", + Source: egv1a1.WasmModuleSource{ + Local: &egv1a1.LocalWasmModuleSource{ + Path: "", + }, + }, }, }, } }, - wantErrors: []string{"spec.wasmModules[0].path in body should be at least 1 chars long"}, + wantErrors: []string{"spec.wasmModules[0].source.local.path in body should be at least 1 chars long"}, }, } diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 8c6b5a4da4..2f662ba122 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -32607,15 +32607,15 @@ spec: code: description: Code is the Wasm code for the extension. properties: - filesystem: + envoyProxyModule: description: |- - Filesystem loads Wasm code from a module registered on the EnvoyProxy + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy wasmModules allowlist. The policy references the module by name only; - the filesystem path is configured by the infrastructure operator on EnvoyProxy. + the module source is configured by the infrastructure operator on EnvoyProxy. - This source 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). + 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 @@ -32837,7 +32837,7 @@ spec: 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 Filesystem. + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -32847,14 +32847,14 @@ spec: - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP", "Image", or "Filesystem". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -32864,13 +32864,13 @@ 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 Filesystem, filesystem field needs to - be set. - rule: 'self.type == ''Filesystem'' ? has(self.filesystem) - : !has(self.filesystem)' + - 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 == ''Filesystem'' ? !has(self.pullPolicy) + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) : true' config: description: |- @@ -51902,16 +51902,16 @@ spec: wasmModules: description: |- WasmModules defines the set of Wasm modules that are allowed to be used by - EnvoyExtensionPolicy resources with a Filesystem code source. Each entry - registers a module by a logical name and the absolute path on the Envoy proxy. + 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 the Wasm files are available + 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 a Filesystem code source. + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. properties: name: description: |- @@ -51921,19 +51921,40 @@ spec: minLength: 1 pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ type: string - path: - description: |- - Path is the absolute filesystem path to the Wasm module on the Envoy proxy. - - The EnvoyProxy owner is responsible for ensuring the file is available on - the proxy container's filesystem (for example via a custom image or volume mount). - maxLength: 4096 - minLength: 1 - pattern: ^/.* - 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 - - path + - source type: object maxItems: 16 type: array diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 3c9116273b..4a9bfa8d4b 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -8545,15 +8545,15 @@ spec: code: description: Code is the Wasm code for the extension. properties: - filesystem: + envoyProxyModule: description: |- - Filesystem loads Wasm code from a module registered on the EnvoyProxy + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy wasmModules allowlist. The policy references the module by name only; - the filesystem path is configured by the infrastructure operator on EnvoyProxy. + the module source is configured by the infrastructure operator on EnvoyProxy. - This source 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). + 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 @@ -8775,7 +8775,7 @@ spec: 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 Filesystem. + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -8785,14 +8785,14 @@ spec: - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP", "Image", or "Filesystem". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -8802,13 +8802,13 @@ 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 Filesystem, filesystem field needs to - be set. - rule: 'self.type == ''Filesystem'' ? has(self.filesystem) - : !has(self.filesystem)' + - 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 == ''Filesystem'' ? !has(self.pullPolicy) + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) : true' config: description: |- @@ -27840,16 +27840,16 @@ spec: wasmModules: description: |- WasmModules defines the set of Wasm modules that are allowed to be used by - EnvoyExtensionPolicy resources with a Filesystem code source. Each entry - registers a module by a logical name and the absolute path on the Envoy proxy. + 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 the Wasm files are available + 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 a Filesystem code source. + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. properties: name: description: |- @@ -27859,19 +27859,40 @@ spec: minLength: 1 pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ type: string - path: - description: |- - Path is the absolute filesystem path to the Wasm module on the Envoy proxy. - - The EnvoyProxy owner is responsible for ensuring the file is available on - the proxy container's filesystem (for example via a custom image or volume mount). - maxLength: 4096 - minLength: 1 - pattern: ^/.* - 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 - - path + - source type: object maxItems: 16 type: array 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 dd8cda307c..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,15 +8545,15 @@ spec: code: description: Code is the Wasm code for the extension. properties: - filesystem: + envoyProxyModule: description: |- - Filesystem loads Wasm code from a module registered on the EnvoyProxy + EnvoyProxyModule loads Wasm code from a module registered on the EnvoyProxy wasmModules allowlist. The policy references the module by name only; - the filesystem path is configured by the infrastructure operator on EnvoyProxy. + the module source is configured by the infrastructure operator on EnvoyProxy. - This source 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). + 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 @@ -8775,7 +8775,7 @@ spec: 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 Filesystem. + PullPolicy must not be set when Type is EnvoyProxyModule. enum: - IfNotPresent - Always @@ -8785,14 +8785,14 @@ spec: - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule - enum: - HTTP - Image - - Filesystem + - EnvoyProxyModule description: |- Type is the type of the source of the Wasm code. - Valid WasmCodeSourceType values are "HTTP", "Image", or "Filesystem". + Valid WasmCodeSourceType values are "HTTP", "Image", or "EnvoyProxyModule". type: string required: - type @@ -8802,13 +8802,13 @@ 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 Filesystem, filesystem field needs to - be set. - rule: 'self.type == ''Filesystem'' ? has(self.filesystem) - : !has(self.filesystem)' + - 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 == ''Filesystem'' ? !has(self.pullPolicy) + rule: 'self.type == ''EnvoyProxyModule'' ? !has(self.pullPolicy) : true' config: description: |- @@ -27840,16 +27840,16 @@ spec: wasmModules: description: |- WasmModules defines the set of Wasm modules that are allowed to be used by - EnvoyExtensionPolicy resources with a Filesystem code source. Each entry - registers a module by a logical name and the absolute path on the Envoy proxy. + 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 the Wasm files are available + 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 a Filesystem code source. + by EnvoyExtensionPolicy resources with an EnvoyProxyModule code source. properties: name: description: |- @@ -27859,19 +27859,40 @@ spec: minLength: 1 pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ type: string - path: - description: |- - Path is the absolute filesystem path to the Wasm module on the Envoy proxy. - - The EnvoyProxy owner is responsible for ensuring the file is available on - the proxy container's filesystem (for example via a custom image or volume mount). - maxLength: 4096 - minLength: 1 - pattern: ^/.* - 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 - - path + - source type: object maxItems: 16 type: array From cea6794bbbe34ef1a2ef8347450bdf89dd29cd31 Mon Sep 17 00:00:00 2001 From: daanvinken Date: Tue, 21 Jul 2026 16:44:43 +0200 Subject: [PATCH 6/7] test(e2e): cover EnvoyProxyModule local Wasm load Add a custom Envoy image with the example Wasm module baked in, wire it into EXAMPLE_APPS, and add an e2e that registers the module on EnvoyProxy and asserts the filter response header. Signed-off-by: daanvinken --- examples/wasm-module-test/Dockerfile | 5 ++ examples/wasm-module-test/Makefile | 9 +++ .../envoy_filter_http_wasm_example.wasm | Bin 0 -> 59641 bytes test/e2e/testdata/wasm-envoyproxy-module.yaml | 75 ++++++++++++++++++ test/e2e/tests/wasm_envoyproxy_module.go | 73 +++++++++++++++++ tools/make/examples.mk | 2 +- 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 examples/wasm-module-test/Dockerfile create mode 100644 examples/wasm-module-test/Makefile create mode 100644 examples/wasm-module-test/envoy_filter_http_wasm_example.wasm create mode 100644 test/e2e/testdata/wasm-envoyproxy-module.yaml create mode 100644 test/e2e/tests/wasm_envoyproxy_module.go 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 0000000000000000000000000000000000000000..df2554e971e8538ce58066a2bdacfddf4c1b1bab GIT binary patch literal 59641 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

?y#PhKmAEtSunBwRr_OqWmv+HTSeJ*$kgM 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 c^5luh2Nf+*n*aa+ literal 0 HcmV?d00001 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..a6d1e44299 --- /dev/null +++ b/test/e2e/tests/wasm_envoyproxy_module.go @@ -0,0 +1,73 @@ +// 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", + }, + Response: http.Response{ + StatusCodes: []int{200}, + Headers: map[string]string{ + "x-wasm-custom": "FOO", + }, + }, + Namespace: ns, + } + + // The example Wasm filter also appends "Hello, world" to the response body. + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +} 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 From 550e57ad8a469d45b836dcfa84a753ca7ebde0fd Mon Sep 17 00:00:00 2001 From: daanvinken Date: Tue, 21 Jul 2026 20:39:20 +0200 Subject: [PATCH 7/7] test(e2e): skip request body checks for EnvoyProxyModule Wasm The example Wasm filter appends text to the response body, which breaks JSON extraction of request properties. Match the HTTP Wasm e2e workaround. Signed-off-by: daanvinken --- test/e2e/tests/wasm_envoyproxy_module.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/wasm_envoyproxy_module.go b/test/e2e/tests/wasm_envoyproxy_module.go index a6d1e44299..d5b3abaf9e 100644 --- a/test/e2e/tests/wasm_envoyproxy_module.go +++ b/test/e2e/tests/wasm_envoyproxy_module.go @@ -57,16 +57,26 @@ var WasmEnvoyProxyModuleTest = suite.ConformanceTest{ 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", }, }, - Namespace: ns, } - // The example Wasm filter also appends "Hello, world" to the response body. http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) }) },