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
8 changes: 8 additions & 0 deletions api/v1alpha1/envoygateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,14 @@ type ExtensionManager struct {
// +optional
PolicyResources []GroupVersionKind `json:"policyResources,omitempty"`

// BackendResources defines the set of K8s resources the extension will handle as
// custom backendRef resources. These resources can be referenced in HTTPRoute
// backendRefs to enable support for custom backend types (e.g., S3, Lambda, etc.)
// that are not natively supported by Envoy Gateway.
//
// +optional
BackendResources []GroupVersionKind `json:"backendResources,omitempty"`

// Hooks defines the set of hooks the extension supports
//
// +kubebuilder:validation:Required
Expand Down
3 changes: 2 additions & 1 deletion api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,14 +387,15 @@ const (
// XDSTranslatorHook defines the types of hooks that an Envoy Gateway extension may support
// for the xds-translator
//
// +kubebuilder:validation:Enum=VirtualHost;Route;HTTPListener;Translation
// +kubebuilder:validation:Enum=VirtualHost;Route;HTTPListener;Translation;Cluster
type XDSTranslatorHook string

const (
XDSVirtualHost XDSTranslatorHook = "VirtualHost"
XDSRoute XDSTranslatorHook = "Route"
XDSHTTPListener XDSTranslatorHook = "HTTPListener"
XDSTranslation XDSTranslatorHook = "Translation"
XDSCluster XDSTranslatorHook = "Cluster"
)

// StringMatch defines how to match any strings.
Expand Down
5 changes: 5 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.

1 change: 1 addition & 0 deletions examples/extension-server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
k8s.io/apimachinery v0.34.0-alpha.0
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c
sigs.k8s.io/gateway-api-inference-extension v0.4.0
)

require (
Expand Down
6 changes: 4 additions & 2 deletions examples/extension-server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -186,6 +186,8 @@ sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3Ew
sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI=
sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c h1:GS4VnGRV90GEUjrgQ2GT5ii6yzWj3KtgUg+sVMdhs5c=
sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
sigs.k8s.io/gateway-api-inference-extension v0.4.0 h1:JoTYxBCkQStJGpV1rwdAR6oDrxquyLsNMECY1My7Ggk=
sigs.k8s.io/gateway-api-inference-extension v0.4.0/go.mod h1:44aUo5kUCGHJ1No/MwLofF2sTarkQ4wQYXr9gz92fhw=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 extensionserver

import (
"context"
"encoding/json"
"log/slog"

clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"google.golang.org/protobuf/types/known/durationpb"
inferencev1alpha2 "sigs.k8s.io/gateway-api-inference-extension/api/v1alpha2"

pb "github.com/envoyproxy/gateway/proto/extension"
)

// PostClusterModify is called after Envoy Gateway is done generating
// a Cluster xDS configuration and before that configuration is passed on to
// Envoy Proxy.
// This implementation modifies the cluster for InferencePool custom backends.
func (s *Server) PostClusterModify(ctx context.Context, req *pb.PostClusterModifyRequest) (*pb.PostClusterModifyResponse, error) {
s.log.Info("postClusterModify callback was invoked", slog.String("cluster_name", req.Cluster.Name))

// Parse extension resources to find InferencePool configurations
var inferencePoolConfigs []*inferencev1alpha2.InferencePool
for _, ext := range req.PostClusterContext.BackendExtensionResources {
// Parse the JSON to check the kind and apiVersion
var resourceInfo map[string]interface{}
if err := json.Unmarshal(ext.GetUnstructuredBytes(), &resourceInfo); err != nil {
s.log.Error("failed to unmarshal extension resource", slog.String("error", err.Error()))
continue
}

kind, _ := resourceInfo["kind"].(string)
apiVersion, _ := resourceInfo["apiVersion"].(string)

s.log.Info("processing extension resource for cluster modification",
slog.String("kind", kind),
slog.String("apiVersion", apiVersion))

// Check if it's an InferencePool
if kind == "InferencePool" && apiVersion == "sigs.k8s.io/gateway-api-inference-extension/v1alpha2" {
// Now unmarshal directly to InferencePool type
var pool inferencev1alpha2.InferencePool
if err := json.Unmarshal(ext.GetUnstructuredBytes(), &pool); err != nil {
s.log.Error("failed to unmarshal InferencePool", slog.String("error", err.Error()))
continue
}

s.log.Info("found InferencePool for cluster modification",
slog.String("name", pool.GetName()),
slog.String("namespace", pool.GetNamespace()),
slog.Int("targetPortNumber", int(pool.Spec.TargetPortNumber)))

inferencePoolConfigs = append(inferencePoolConfigs, &pool)
}
}
if len(inferencePoolConfigs) == 1 {
// Modify the cluster based on InferencePool configurations
modifiedCluster := s.modifyClusterForInferencePool(req.Cluster, inferencePoolConfigs[0])
s.log.Info("successfully processed cluster modification",
slog.String("cluster_name", req.Cluster.Name),
slog.Int("inference_pools", len(inferencePoolConfigs)))

return &pb.PostClusterModifyResponse{
Cluster: modifiedCluster,
}, nil
}

return &pb.PostClusterModifyResponse{
Cluster: req.Cluster,
}, nil
}

// modifyClusterForInferencePool modifies an existing cluster based on InferencePool configurations
func (s *Server) modifyClusterForInferencePool(cluster *clusterv3.Cluster, pool *inferencev1alpha2.InferencePool) *clusterv3.Cluster {
s.log.Info("modifying cluster for InferencePool",
slog.String("cluster_name", cluster.Name),
slog.String("inference_pool", pool.GetName()))

// Convert to ORIGINAL_DST cluster type
modifiedCluster := s.convertToOriginalDestCluster(cluster, pool)
return modifiedCluster
}

// convertToOriginalDestCluster converts a regular cluster to an ORIGINAL_DST cluster for InferencePool
func (s *Server) convertToOriginalDestCluster(originalCluster *clusterv3.Cluster, pool *inferencev1alpha2.InferencePool) *clusterv3.Cluster {
originalCluster.LbPolicy = clusterv3.Cluster_CLUSTER_PROVIDED
originalCluster.ClusterDiscoveryType = &clusterv3.Cluster_Type{
Type: clusterv3.Cluster_ORIGINAL_DST,
}
originalCluster.LbConfig = &clusterv3.Cluster_OriginalDstLbConfig_{
OriginalDstLbConfig: &clusterv3.Cluster_OriginalDstLbConfig{
UseHttpHeader: true,
HttpHeaderName: "x-gateway-destination-endpoint",
},
}
originalCluster.ConnectTimeout = durationpb.New(10 * 1000000000)
originalCluster.EdsClusterConfig = nil
originalCluster.LoadAssignment = nil

s.log.Info("successfully converted cluster to ORIGINAL_DST type",
slog.String("cluster_name", originalCluster.Name),
slog.String("inference_pool", pool.GetName()))

return originalCluster
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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 extensionserver

import (
"context"
"encoding/json"
"fmt"
"log/slog"

corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
bav3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/basic_auth/v3"
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/exampleorg/envoygateway-extension/api/v1alpha1"
"google.golang.org/protobuf/types/known/anypb"

pb "github.com/envoyproxy/gateway/proto/extension"
)

// PostHTTPListenerModify is called after Envoy Gateway is done generating a
// Listener xDS configuration and before that configuration is passed on to
// Envoy Proxy.
// This example adds Basic Authentication on the Listener level as an example.
// Note: This implementation is not secure, and should not be used to protect
// anything important.
func (s *Server) PostHTTPListenerModify(ctx context.Context, req *pb.PostHTTPListenerModifyRequest) (*pb.PostHTTPListenerModifyResponse, error) {
s.log.Info("postHTTPListenerModify callback was invoked")
// Collect all of the required username/password combinations from the
// provided contexts that were attached to the gateway.
passwords := NewHtpasswd()
for _, ext := range req.PostListenerContext.ExtensionResources {
var listenerContext v1alpha1.ListenerContextExample
if err := json.Unmarshal(ext.GetUnstructuredBytes(), &listenerContext); err != nil {
s.log.Error("failed to unmarshal the extension", slog.String("error", err.Error()))
continue
}
s.log.Info("processing an extension context", slog.String("username", listenerContext.Spec.Username))
passwords.AddUser(listenerContext.Spec.Username, listenerContext.Spec.Password)
}

// First, get the filter chains from the listener
filterChains := req.Listener.GetFilterChains()
defaultFC := req.Listener.DefaultFilterChain
if defaultFC != nil {
filterChains = append(filterChains, defaultFC)
}
// Go over all of the chains, and add the basic authentication http filter
for _, currChain := range filterChains {
httpConManager, hcmIndex, err := findHCM(currChain)
if err != nil {
s.log.Error("failed to find an HCM in the current chain", slog.Any("error", err))
continue
}
// If a basic authentication filter already exists, update it. Otherwise, create it.
basicAuth, baIndex, err := findBasicAuthFilter(httpConManager.HttpFilters)
if err != nil {
s.log.Error("failed to unmarshal the existing basicAuth filter", slog.Any("error", err))
continue
}
if baIndex == -1 {
// Create a new basic auth filter
basicAuth = &bav3.BasicAuth{
Users: &corev3.DataSource{
Specifier: &corev3.DataSource_InlineString{
InlineString: passwords.String(),
},
},
ForwardUsernameHeader: "X-Example-Ext",
}
} else {
// Update the basic auth filter
basicAuth.Users.Specifier = &corev3.DataSource_InlineString{
InlineString: passwords.String(),
}
}
// Add or update the Basic Authentication filter in the HCM
anyBAFilter, _ := anypb.New(basicAuth)
if baIndex > -1 {
httpConManager.HttpFilters[baIndex].ConfigType = &hcm.HttpFilter_TypedConfig{
TypedConfig: anyBAFilter,
}
} else {
filters := []*hcm.HttpFilter{
{
Name: "envoy.filters.http.basic_auth",
ConfigType: &hcm.HttpFilter_TypedConfig{
TypedConfig: anyBAFilter,
},
},
}
filters = append(filters, httpConManager.HttpFilters...)
httpConManager.HttpFilters = filters
}

// Write the updated HCM back to the filter chain
anyConnectionMgr, _ := anypb.New(httpConManager)
currChain.Filters[hcmIndex].ConfigType = &listenerv3.Filter_TypedConfig{
TypedConfig: anyConnectionMgr,
}
}

return &pb.PostHTTPListenerModifyResponse{
Listener: req.Listener,
}, nil
}

// Tries to find an HTTP connection manager in the provided filter chain.
func findHCM(filterChain *listenerv3.FilterChain) (*hcm.HttpConnectionManager, int, error) {
for filterIndex, filter := range filterChain.Filters {
if filter.Name == wellknown.HTTPConnectionManager {
hcm := new(hcm.HttpConnectionManager)
if err := filter.GetTypedConfig().UnmarshalTo(hcm); err != nil {
return nil, -1, err
}
return hcm, filterIndex, nil
}
}
return nil, -1, fmt.Errorf("unable to find HTTPConnectionManager in FilterChain: %s", filterChain.Name)
}

// Tries to find the Basic Authentication HTTP filter in the provided chain
func findBasicAuthFilter(chain []*hcm.HttpFilter) (*bav3.BasicAuth, int, error) {
for i, filter := range chain {
if filter.Name == "envoy.filters.http.basic_auth" {
ba := new(bav3.BasicAuth)
if err := filter.GetTypedConfig().UnmarshalTo(ba); err != nil {
return nil, -1, err
}
return ba, i, nil
}
}
return nil, -1, nil
}
Loading