Skip to content

feat: support dynamic modules #5669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions api/v1alpha1/dynamicmodule_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 v1alpha1

import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

// DynamicModule defines a Dynamic Module extension.
type DynamicModule struct {
// Name is a unique name for this Dynamic Module extension. It is used to identify the
// Dynamic Module extension if multiple extensions are loaded.
// It's also used for logging/debugging.
// If not specified, EG will generate a unique name for the Dynamic Module extension.
//
// +optional
Name *string `json:"name,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would make it ExtensionName to clarify what is this for. I would also want to add the comments about one module can contain multiple extensions so this name is used to specify one out of many


// Module is the name of the dynamic module to load.
// The module name is used to search for the shared library file in the search path.
// The search path is configured by the environment variable ENVOY_DYNAMIC_MODULES_SEARCH_PATH.
// The actual search path is ${ENVOY_DYNAMIC_MODULES_SEARCH_PATH}/lib${name}.so.
//
// +kubebuilder:validation:Required
Module string `json:"module"`

// Config is the configuration for the Dynamic Module extension.
// This configuration will be passed to the Dynamic Module extension.
// +optional
Config *apiextensionsv1.JSON `json:"config,omitempty"`
Copy link
Member

@mathetake mathetake Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure if we want to force JSON all the time. The rationale behind why the Envoy API is Any is that we don't want to force dynamic modules to pull in dependencies to parse the input. Simply making this to string should work i guess?

For example, let's say i develop a module that allows javascript to run inside envoy to modify headers. Then this config will likely be a javascrip string instead of json

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the comment above, I would make this ExtensionConfig but not that strong preference


// DoNotClose prevents the module from being unloaded with dlclose.
// This is useful for modules that have global state that should not be unloaded.
// A module is closed when no more references to it exist in the process.
// For example, no HTTP filters are using the module (e.g. after configuration update).
//
// +optional
DoNotClose *bool `json:"doNotClose,omitempty"`
}
7 changes: 7 additions & 0 deletions api/v1alpha1/envoyextensionypolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ type EnvoyExtensionPolicySpec struct {
// +kubebuilder:validation:MaxItems=16
// +optional
Lua []Lua `json:"lua,omitempty"`

// DynamicModule is an ordered list of Dynamic Module filters
// that should be added to the envoy filter chain
//
// +kubebuilder:validation:MaxItems=16
// +optional
DynamicModule []DynamicModule `json:"dynamicModule,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess plural ?

Suggested change
DynamicModule []DynamicModule `json:"dynamicModule,omitempty"`
DynamicModules []DynamicModule `json:"dynamicModules,omitempty"`

}

//+kubebuilder:object:root=true
Expand Down
7 changes: 6 additions & 1 deletion api/v1alpha1/envoyproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ type EnvoyProxySpec struct {
//
// - envoy.filters.http.wasm
//
// - envoy.filters.http.dynamic_modules
//
// - envoy.filters.http.rbac
//
// - envoy.filters.http.local_ratelimit
Expand Down Expand Up @@ -197,7 +199,7 @@ type FilterPosition struct {
}

// EnvoyFilter defines the type of Envoy HTTP filter.
// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.api_key_auth;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.lua;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.custom_response;envoy.filters.http.compressor
// +kubebuilder:validation:Enum=envoy.filters.http.health_check;envoy.filters.http.fault;envoy.filters.http.cors;envoy.filters.http.ext_authz;envoy.filters.http.api_key_auth;envoy.filters.http.basic_auth;envoy.filters.http.oauth2;envoy.filters.http.jwt_authn;envoy.filters.http.stateful_session;envoy.filters.http.lua;envoy.filters.http.ext_proc;envoy.filters.http.wasm;envoy.filters.http.rbac;envoy.filters.http.local_ratelimit;envoy.filters.http.ratelimit;envoy.filters.http.custom_response;envoy.filters.http.compressor;envoy.filters.http.dynamic_modules
type EnvoyFilter string

const (
Expand Down Expand Up @@ -253,6 +255,9 @@ const (
// EnvoyFilterCompressor defines the Envoy HTTP compressor filter.
EnvoyFilterCompressor EnvoyFilter = "envoy.filters.http.compressor"

// EnvoyFilterDynamicModules defines the Envoy HTTP dynamic modules filter.
EnvoyFilterDynamicModules EnvoyFilter = "envoy.filters.http.dynamic_modules"

// EnvoyFilterRouter defines the Envoy HTTP router filter.
EnvoyFilterRouter EnvoyFilter = "envoy.filters.http.router"
)
Expand Down
37 changes: 37 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,44 @@ spec:
spec:
description: Spec defines the desired state of EnvoyExtensionPolicy.
properties:
dynamicModule:
description: |-
DynamicModule is an ordered list of Dynamic Module filters
that should be added to the envoy filter chain
items:
description: DynamicModule defines a Dynamic Module extension.
properties:
config:
description: |-
Config is the configuration for the Dynamic Module extension.
This configuration will be passed to the Dynamic Module extension.
x-kubernetes-preserve-unknown-fields: true
doNotClose:
description: |-
DoNotClose prevents the module from being unloaded with dlclose.
This is useful for modules that have global state that should not be unloaded.
A module is closed when no more references to it exist in the process.
For example, no HTTP filters are using the module (e.g. after configuration update).
type: boolean
module:
description: |-
Module is the name of the dynamic module to load.
The module name is used to search for the shared library file in the search path.
The search path is configured by the environment variable ENVOY_DYNAMIC_MODULES_SEARCH_PATH.
The actual search path is ${ENVOY_DYNAMIC_MODULES_SEARCH_PATH}/lib${name}.so.
type: string
name:
description: |-
Name is a unique name for this Dynamic Module extension. It is used to identify the
Dynamic Module extension if multiple extensions are loaded.
It's also used for logging/debugging.
If not specified, EG will generate a unique name for the Dynamic Module extension.
type: string
required:
- module
type: object
maxItems: 16
type: array
extProc:
description: |-
ExtProc is an ordered list of external processing filters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ spec:

- envoy.filters.http.wasm

- envoy.filters.http.dynamic_modules

- envoy.filters.http.rbac

- envoy.filters.http.local_ratelimit
Expand Down Expand Up @@ -343,6 +345,7 @@ spec:
- envoy.filters.http.ratelimit
- envoy.filters.http.custom_response
- envoy.filters.http.compressor
- envoy.filters.http.dynamic_modules
type: string
before:
description: |-
Expand All @@ -366,6 +369,7 @@ spec:
- envoy.filters.http.ratelimit
- envoy.filters.http.custom_response
- envoy.filters.http.compressor
- envoy.filters.http.dynamic_modules
type: string
name:
description: Name of the filter.
Expand All @@ -387,6 +391,7 @@ spec:
- envoy.filters.http.ratelimit
- envoy.filters.http.custom_response
- envoy.filters.http.compressor
- envoy.filters.http.dynamic_modules
type: string
required:
- name
Expand Down
16 changes: 16 additions & 0 deletions examples/dynamicmodule/dynamicmodule.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
name: dynamicmodule-example
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: example
dynamicModule:
- name: my-dynamic-module
module: my_module
config:
key: value
# Prevent the module from being unloaded
doNotClose: true
73 changes: 73 additions & 0 deletions internal/gatewayapi/dynamicmodule.go
Original file line number Diff line number Diff line change
@@ -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.

package gatewayapi

import (
"fmt"
"strconv"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
"github.com/envoyproxy/gateway/internal/ir"
)

func (t *Translator) buildDynamicModules(
policy *egv1a1.EnvoyExtensionPolicy,
_ *resource.Resources,
) ([]ir.DynamicModule, error) {
var dynamicModuleIRList []ir.DynamicModule

if policy == nil {
return nil, nil
}

Check warning on line 25 in internal/gatewayapi/dynamicmodule.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/dynamicmodule.go#L24-L25

Added lines #L24 - L25 were not covered by tests

for idx, dm := range policy.Spec.DynamicModule {
name := irConfigNameForDynamicModule(policy, idx)
dynamicModuleIR, err := t.buildDynamicModule(name, dm)
if err != nil {
return nil, err
}
dynamicModuleIRList = append(dynamicModuleIRList, *dynamicModuleIR)
}
return dynamicModuleIRList, nil
}

func (t *Translator) buildDynamicModule(
name string,
config egv1a1.DynamicModule,
) (*ir.DynamicModule, error) {
// Validate required fields
if config.Module == "" {
return nil, fmt.Errorf("module is required")
}

dynamicModuleName := name
if config.Name != nil {
dynamicModuleName = *config.Name
}

// Set DoNotClose if specified
doNotClose := false
if config.DoNotClose != nil {
doNotClose = *config.DoNotClose
}

dynamicModuleIR := &ir.DynamicModule{
Name: dynamicModuleName,
Module: config.Module,
Config: config.Config,
DoNotClose: doNotClose,
}

return dynamicModuleIR, nil
}

func irConfigNameForDynamicModule(policy *egv1a1.EnvoyExtensionPolicy, index int) string {
return fmt.Sprintf(
"%s/dynamicmodule/%s",
irConfigName(policy),
strconv.Itoa(index))
}
39 changes: 26 additions & 13 deletions internal/gatewayapi/envoyextensionpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,10 @@
resources *resource.Resources,
) error {
var (
wasms []ir.Wasm
luas []ir.Lua
err, errs error
wasms []ir.Wasm
luas []ir.Lua
dynamicModules []ir.DynamicModule
err, errs error
)

if wasms, err = t.buildWasms(policy, resources); err != nil {
Expand All @@ -309,6 +310,11 @@
errs = errors.Join(errs, err)
}

if dynamicModules, err = t.buildDynamicModules(policy, resources); err != nil {
err = perr.WithMessage(err, "DynamicModule")
errs = errors.Join(errs, err)
}

// Apply IR to all relevant routes
prefix := irRoutePrefix(route)
parentRefs := GetParentReferences(route)
Expand Down Expand Up @@ -338,9 +344,10 @@
continue
}
r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{
ExtProcs: extProcs,
Wasms: wasms,
Luas: luas,
ExtProcs: extProcs,
Wasms: wasms,
Luas: luas,
DynamicModules: dynamicModules,
}
}
}
Expand All @@ -359,10 +366,11 @@
resources *resource.Resources,
) error {
var (
extProcs []ir.ExtProc
wasms []ir.Wasm
luas []ir.Lua
err, errs error
extProcs []ir.ExtProc
wasms []ir.Wasm
luas []ir.Lua
dynamicModules []ir.DynamicModule
err, errs error
)

if extProcs, err = t.buildExtProcs(policy, resources, gateway.envoyProxy); err != nil {
Expand All @@ -377,6 +385,10 @@
err = perr.WithMessage(err, "Lua")
errs = errors.Join(errs, err)
}
if dynamicModules, err = t.buildDynamicModules(policy, resources); err != nil {
err = perr.WithMessage(err, "DynamicModule")
errs = errors.Join(errs, err)
}

Check warning on line 391 in internal/gatewayapi/envoyextensionpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/envoyextensionpolicy.go#L389-L391

Added lines #L389 - L391 were not covered by tests

irKey := t.getIRKey(gateway.Gateway)
// Should exist since we've validated this
Expand Down Expand Up @@ -407,9 +419,10 @@
}

r.EnvoyExtensions = &ir.EnvoyExtensionFeatures{
ExtProcs: extProcs,
Wasms: wasms,
Luas: luas,
ExtProcs: extProcs,
Wasms: wasms,
Luas: luas,
DynamicModules: dynamicModules,
}
}
}
Expand Down
Loading