Skip to content
Merged
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
13 changes: 13 additions & 0 deletions api/v1alpha1/envoygateway_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,19 @@ func (kcr *KubernetesClientRateLimit) GetQPSAndBurst() (float32, int) {
return float32(qps), int(burst)
}

// GetExtensionManagers normalizes the singular ExtensionManager and plural ExtensionManagers
// fields into a single list. The plural field takes precedence. If only the singular field
// is set, it is returned as a single-element list. Returns nil if neither is set.
func (e *EnvoyGatewaySpec) GetExtensionManagers() []ExtensionManager {
if len(e.ExtensionManagers) > 0 {
return e.ExtensionManagers
}
if e.ExtensionManager != nil {
return []ExtensionManager{*e.ExtensionManager}
}
return nil
}

// ShouldIncludeClusters returns true if clusters should be included in the translation hook.
// When TranslationConfig is nil, defaults to true for backward compatibility.
// When TranslationConfig is explicitly set, uses the configuration.
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/envoygateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type EnvoyGateway struct {
}

// EnvoyGatewaySpec defines the desired state of Envoy Gateway.
// +kubebuilder:validation:XValidation:rule="!(has(self.extensionManager) && has(self.extensionManagers))",message="extensionManager and extensionManagers are mutually exclusive"
type EnvoyGatewaySpec struct {
// Gateway defines desired Gateway API specific configuration. If unset,
// default configuration parameters will apply.
Expand Down Expand Up @@ -98,6 +99,19 @@ type EnvoyGatewaySpec struct {
// +optional
ExtensionManager *ExtensionManager `json:"extensionManager,omitempty"`

// ExtensionManagers defines multiple extension managers to register for the Envoy Gateway Control Plane.
// Each extension's output becomes the next extension's input, enabling sequential chaining.
// Each entry must have a unique Name field for identification.
// This field is mutually exclusive with ExtensionManager.
//
// Warning: Enabling Extension Servers may lead to complete security compromise of your system.
// Users that control Extension Servers can inject arbitrary configuration to proxies,
// leading to high Confidentiality, Integrity and Availability risks.
//
// +optional
// +kubebuilder:validation:MinItems=1
ExtensionManagers []ExtensionManager `json:"extensionManagers,omitempty"`
Comment thread
toffentoffen marked this conversation as resolved.

// ExtensionAPIs defines the settings related to specific Gateway API Extensions
// implemented by Envoy Gateway
//
Expand Down Expand Up @@ -659,6 +673,12 @@ type RateLimitRedisSettings struct {
// ExtensionManager defines the configuration for registering an extension manager to
// the Envoy Gateway control plane.
type ExtensionManager struct {
// Name is a unique identifier for this extension manager. Required when using
// the plural ExtensionManagers field. Used for logging, metrics, and error identification.
//
// +optional
Name string `json:"name,omitempty"`

// Resources defines the set of K8s resources the extension will handle as route
// filter resources
//
Expand Down
33 changes: 32 additions & 1 deletion api/v1alpha1/validation/envoygateway_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func ValidateEnvoyGateway(eg *egv1a1.EnvoyGateway) error {
return err
}

if err := validateEnvoyGatewayExtensionManager(eg.ExtensionManager); err != nil {
if err := validateEnvoyGatewayExtensionManagers(eg); err != nil {
return err
}

Expand Down Expand Up @@ -185,6 +185,37 @@ func validateEnvoyGatewayRateLimit(rateLimit *egv1a1.RateLimit) error {
return nil
}

func validateEnvoyGatewayExtensionManagers(eg *egv1a1.EnvoyGateway) error {
if eg.ExtensionManager != nil && len(eg.ExtensionManagers) > 0 {
return fmt.Errorf("extensionManager and extensionManagers are mutually exclusive")
}

// Mirror +kubebuilder:validation:MinItems=1 for EnvoyGatewaySpec.ExtensionManagers:
// reject an explicitly-set-but-empty list. A nil slice means the field was omitted.
if eg.ExtensionManagers != nil && len(eg.ExtensionManagers) == 0 {
return fmt.Errorf("extensionManagers must contain at least one entry when specified")
}

if len(eg.ExtensionManagers) > 0 {
names := make(map[string]struct{})
for i, em := range eg.ExtensionManagers {
if em.Name == "" {
return fmt.Errorf("extension manager at index %d: name is required", i)
}
if _, exists := names[em.Name]; exists {
return fmt.Errorf("extension manager at index %d: duplicate name %q", i, em.Name)
}
names[em.Name] = struct{}{}
if err := validateEnvoyGatewayExtensionManager(&eg.ExtensionManagers[i]); err != nil {
return fmt.Errorf("extension manager %q: %w", em.Name, err)
}
}
return nil
}

return validateEnvoyGatewayExtensionManager(eg.ExtensionManager)
}

func validateEnvoyGatewayExtensionManager(extensionManager *egv1a1.ExtensionManager) error {
if extensionManager == nil {
return nil
Expand Down
191 changes: 191 additions & 0 deletions api/v1alpha1/validation/envoygateway_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,152 @@ func TestValidateEnvoyGateway(t *testing.T) {
},
expect: false,
},
{
name: "both extensionManager and extensionManagers set",
eg: &egv1a1.EnvoyGateway{
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: egv1a1.DefaultEnvoyGatewayProvider(),
ExtensionManager: &egv1a1.ExtensionManager{
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute},
},
},
Service: &egv1a1.ExtensionService{Host: "foo.extension", Port: 80},
},
ExtensionManagers: []egv1a1.ExtensionManager{
{
Name: "ext1",
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute},
},
},
Service: &egv1a1.ExtensionService{Host: "bar.extension", Port: 80},
},
},
},
},
expect: false,
},
{
name: "extensionManagers explicitly empty",
eg: &egv1a1.EnvoyGateway{
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: egv1a1.DefaultEnvoyGatewayProvider(),
ExtensionManagers: []egv1a1.ExtensionManager{},
},
},
expect: false,
},
{
name: "extensionManagers with duplicate names",
eg: &egv1a1.EnvoyGateway{
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: egv1a1.DefaultEnvoyGatewayProvider(),
ExtensionManagers: []egv1a1.ExtensionManager{
{
Name: "ext1",
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute},
},
},
Service: &egv1a1.ExtensionService{Host: "foo.extension", Port: 80},
},
{
Name: "ext1",
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute},
},
},
Service: &egv1a1.ExtensionService{Host: "bar.extension", Port: 80},
},
},
},
},
expect: false,
},
{
name: "extensionManagers with missing name",
eg: &egv1a1.EnvoyGateway{
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: egv1a1.DefaultEnvoyGatewayProvider(),
ExtensionManagers: []egv1a1.ExtensionManager{
{
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute},
},
},
Service: &egv1a1.ExtensionService{Host: "foo.extension", Port: 80},
},
},
},
},
expect: false,
},
{
name: "extensionManagers with invalid individual extension manager",
eg: &egv1a1.EnvoyGateway{
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: egv1a1.DefaultEnvoyGatewayProvider(),
ExtensionManagers: []egv1a1.ExtensionManager{
{
Name: "good-ext",
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute},
},
},
Service: &egv1a1.ExtensionService{Host: "good.extension", Port: 80},
},
{
Name: "bad-ext",
// Missing hooks → should fail individual validation
Service: &egv1a1.ExtensionService{Host: "bad.extension", Port: 80},
},
},
},
},
expect: false,
},
{
name: "valid extensionManagers plural config",
eg: &egv1a1.EnvoyGateway{
EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{
Gateway: egv1a1.DefaultGateway(),
Provider: egv1a1.DefaultEnvoyGatewayProvider(),
ExtensionManagers: []egv1a1.ExtensionManager{
{
Name: "ai-gateway",
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSRoute, egv1a1.XDSTranslation},
},
},
Service: &egv1a1.ExtensionService{Host: "ai-gw.extension", Port: 80},
},
{
Name: "observability",
Hooks: &egv1a1.ExtensionHooks{
XDSTranslator: &egv1a1.XDSTranslatorHooks{
Post: []egv1a1.XDSTranslatorHook{egv1a1.XDSHTTPListener},
},
},
Service: &egv1a1.ExtensionService{Host: "obs.extension", Port: 80},
},
},
},
},
expect: true,
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -1172,3 +1318,48 @@ func TestEnvoyGatewayTelemetry(t *testing.T) {
assert.False(t, eg.Telemetry.Metrics.Prometheus.Disable)
assert.Nil(t, eg.Telemetry.Metrics.Sinks)
}

func TestGetExtensionManagers(t *testing.T) {
t.Run("both nil returns nil", func(t *testing.T) {
spec := egv1a1.EnvoyGatewaySpec{}
assert.Nil(t, spec.GetExtensionManagers())
})

t.Run("only singular set returns it as slice", func(t *testing.T) {
ext := egv1a1.ExtensionManager{Name: "ext1"}
spec := egv1a1.EnvoyGatewaySpec{
ExtensionManager: &ext,
}
result := spec.GetExtensionManagers()
require.Len(t, result, 1)
assert.Equal(t, "ext1", result[0].Name)
})

t.Run("only plural set returns plural", func(t *testing.T) {
spec := egv1a1.EnvoyGatewaySpec{
ExtensionManagers: []egv1a1.ExtensionManager{
{Name: "ext1"},
{Name: "ext2"},
},
}
result := spec.GetExtensionManagers()
require.Len(t, result, 2)
assert.Equal(t, "ext1", result[0].Name)
assert.Equal(t, "ext2", result[1].Name)
})

t.Run("both set returns plural (plural takes precedence)", func(t *testing.T) {
ext := egv1a1.ExtensionManager{Name: "singular"}
spec := egv1a1.EnvoyGatewaySpec{
ExtensionManager: &ext,
ExtensionManagers: []egv1a1.ExtensionManager{
{Name: "plural1"},
{Name: "plural2"},
},
}
result := spec.GetExtensionManagers()
require.Len(t, result, 2)
assert.Equal(t, "plural1", result[0].Name)
assert.Equal(t, "plural2", result[1].Name)
})
}
7 changes: 7 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.

4 changes: 2 additions & 2 deletions examples/envoy-ext-auth/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/envoyproxy/gateway-grcp-ext-auth
go 1.26.2

require (
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260217184816-6318b674fe2f
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260304210048-a81710db7097
github.com/golang/protobuf v1.5.4
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409
google.golang.org/grpc v1.80.0
Expand All @@ -12,7 +12,7 @@ require (

require (
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions examples/envoy-ext-auth/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260217184816-6318b674fe2f h1:mmro2RvL73rtCCLoFLel4Tw1C4eECzuvRFN9HkHgOV4=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260217184816-6318b674fe2f/go.mod h1:LvH/DJfCl4Wx096uo+ilpBo9tvnMXlAfD0+xIDob/Ic=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260304210048-a81710db7097 h1:Ou9X6qsPOiDOsQgaboj3jlCE5ZYngdYeSVDKBcT95QE=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260304210048-a81710db7097/go.mod h1:237/ZQHepDd4v5BjpRNFI2mMG7WEBd+mQnt8jwbqrnk=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.19.0
controller-gen.kubebuilder.io/version: v0.20.1
name: listenercontextexamples.example.extensions.io
spec:
group: example.extensions.io
Expand Down
4 changes: 2 additions & 2 deletions examples/grpc-ext-proc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ module github.com/envoyproxy/gateway-grpc-ext-proc
go 1.26.2

require (
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260217184816-6318b674fe2f
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260304210048-a81710db7097
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
)

require (
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions examples/grpc-ext-proc/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260217184816-6318b674fe2f h1:mmro2RvL73rtCCLoFLel4Tw1C4eECzuvRFN9HkHgOV4=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260217184816-6318b674fe2f/go.mod h1:LvH/DJfCl4Wx096uo+ilpBo9tvnMXlAfD0+xIDob/Ic=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260304210048-a81710db7097 h1:Ou9X6qsPOiDOsQgaboj3jlCE5ZYngdYeSVDKBcT95QE=
github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260304210048-a81710db7097/go.mod h1:237/ZQHepDd4v5BjpRNFI2mMG7WEBd+mQnt8jwbqrnk=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand Down
Loading
Loading