Skip to content
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

feat: request and response transformation #10595

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
57 changes: 56 additions & 1 deletion api/v1alpha1/route_policy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// +kubebuilder:rbac:groups=gateway.kgateway.dev,resources=routepolicies,verbs=get;list;watch
Expand Down Expand Up @@ -31,5 +32,59 @@ type RoutePolicyList struct {
type RoutePolicySpec struct {
TargetRef LocalPolicyTargetReference `json:"targetRef,omitempty"`
// +kubebuilder:validation:Minimum=1
Timeout int `json:"timeout,omitempty"`
Timeout int `json:"timeout,omitempty"`
Transformation TransformationPolicy `json:"transformation,omitempty"`
}

type TransformationPolicy struct {
// +optional
Request *Transform `json:"request,omitempty"`
// +optional
Response *Transform `json:"response,omitempty"`
}

type Transform struct {

// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=16
Set []HeaderTransformation `json:"set,omitempty"`

// +optional
// +listType=map
// +listMapKey=name
// +kubebuilder:validation:MaxItems=16
Add []HeaderTransformation `json:"add,omitempty"`

// +optional
// +listType=set
// +kubebuilder:validation:MaxItems=16
Remove []string `json:"remove,omitempty"`

// +optional
//
// If empty, body will not be buffered.
Body *BodyTransformation `json:"body,omitempty"`
}

type InjaTemplate string

type HeaderTransformation struct {
Name gwv1.HeaderName `json:"name,omitempty"`
Value InjaTemplate `json:"value,omitempty"`
}

// +kubebuilder:validation:Enum=AsString;AsJson
type BodyParseBehavior string

const (
BodyParseBehaviorAsString BodyParseBehavior = "AsString"
BodyParseBehaviorAsJSON BodyParseBehavior = "AsJson"
)

type BodyTransformation struct {
// +kubebuilder:default=AsString
ParseAs BodyParseBehavior `json:"parseAs,omitempty"`
Value *InjaTemplate `json:"value,omitempty"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"time"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"k8s.io/apimachinery/pkg/runtime/schema"

Expand All @@ -17,12 +19,26 @@ import (
extensionsplug "github.com/kgateway-dev/kgateway/internal/gateway2/extensions2/plugin"
"github.com/kgateway-dev/kgateway/internal/gateway2/ir"
"github.com/kgateway-dev/kgateway/internal/gateway2/plugins"
"github.com/kgateway-dev/kgateway/internal/gateway2/utils"
"github.com/kgateway-dev/kgateway/internal/gateway2/utils/krtutil"
transformationpb "github.com/solo-io/envoy-gloo/go/config/filter/http/transformation/v2"
)

const filterStage = "route_policy_transformation"

var (
pluginStage = plugins.AfterStage(plugins.AuthZStage)
)

type routeOptsPlugin struct {
ct time.Time
spec v1alpha1.RoutePolicySpec
spec routeSpecIr
}

type routeSpecIr struct {
timeout *durationpb.Duration
transform *anypb.Any
errors []error
}

func (d *routeOptsPlugin) CreationTime() time.Time {
Expand All @@ -34,10 +50,19 @@ func (d *routeOptsPlugin) Equals(in any) bool {
if !ok {
return false
}
return d.spec == d2.spec

if !proto.Equal(d.spec.timeout, d2.spec.timeout) {
return false
}
if !proto.Equal(d.spec.transform, d2.spec.transform) {
return false
}

return true
}

type routeOptsPluginGwPass struct {
needFilter bool
}

func NewPlugin(ctx context.Context, commoncol *common.CommonCollections) extensionplug.Plugin {
Expand All @@ -57,7 +82,7 @@ func NewPlugin(ctx context.Context, commoncol *common.CommonCollections) extensi
Name: policyCR.Name,
},
Policy: policyCR,
PolicyIR: &routeOptsPlugin{ct: policyCR.CreationTimestamp.Time, spec: policyCR.Spec},
PolicyIR: &routeOptsPlugin{ct: policyCR.CreationTimestamp.Time, spec: toSpec(policyCR.Spec)},
TargetRefs: convert(policyCR.Spec.TargetRef),
}
return pol
Expand All @@ -74,6 +99,115 @@ func NewPlugin(ctx context.Context, commoncol *common.CommonCollections) extensi
}
}

func toSpec(spec v1alpha1.RoutePolicySpec) routeSpecIr {
var ret routeSpecIr
if spec.Timeout > 0 {
ret.timeout = durationpb.New(time.Second * time.Duration(spec.Timeout))
}
var err error
ret.transform, err = toTransformFilterConfig(&spec.Transformation)
if err != nil {
ret.errors = append(ret.errors, err)
}

return ret
}

func toTransform(t *v1alpha1.Transform) *transformationpb.Transformation_TransformationTemplate {

hasTransform := false
tt := &transformationpb.Transformation_TransformationTemplate{
TransformationTemplate: &transformationpb.TransformationTemplate{
Headers: map[string]*transformationpb.InjaTemplate{},
},
}
for _, h := range t.Set {
tt.TransformationTemplate.Headers[string(h.Name)] = &transformationpb.InjaTemplate{
Text: string(h.Value),
}
hasTransform = true
}

for _, h := range t.Add {
tt.TransformationTemplate.HeadersToAppend = append(tt.TransformationTemplate.HeadersToAppend, &transformationpb.TransformationTemplate_HeaderToAppend{
Key: string(h.Name),
Value: &transformationpb.InjaTemplate{
Text: string(h.Value),
},
})
hasTransform = true
}

tt.TransformationTemplate.HeadersToRemove = make([]string, 0, len(t.Remove))
for _, h := range t.Remove {
tt.TransformationTemplate.HeadersToRemove = append(tt.TransformationTemplate.HeadersToRemove, string(h))
hasTransform = true
}

//BODY
if t.Body == nil {
tt.TransformationTemplate.BodyTransformation = &transformationpb.TransformationTemplate_Passthrough{
Passthrough: &transformationpb.Passthrough{},
}
} else {
if t.Body.ParseAs == v1alpha1.BodyParseBehaviorAsString {
tt.TransformationTemplate.ParseBodyBehavior = transformationpb.TransformationTemplate_DontParse
}
if value := t.Body.Value; value != nil {
hasTransform = true
tt.TransformationTemplate.BodyTransformation = &transformationpb.TransformationTemplate_Body{
Body: &transformationpb.InjaTemplate{
Text: string(*value),
},
}
}
}

if !hasTransform {
return nil
}
return tt
}

func toTransformFilterConfig(t *v1alpha1.TransformationPolicy) (*anypb.Any, error) {
if t == nil {
return nil, nil
}

var reqt *transformationpb.Transformation
var respt *transformationpb.Transformation

if rtt := toTransform(t.Request); rtt != nil {
reqt = &transformationpb.Transformation{
TransformationType: rtt,
}
}
if rtt := toTransform(t.Response); rtt != nil {
respt = &transformationpb.Transformation{
TransformationType: rtt,
}
}
if reqt == nil && respt == nil {
return nil, nil
}
reqm := &transformationpb.RouteTransformations_RouteTransformation_RequestMatch{
RequestTransformation: reqt,
ResponseTransformation: respt,
}

envoyT := &transformationpb.RouteTransformations{
Transformations: []*transformationpb.RouteTransformations_RouteTransformation{
{
Match: &transformationpb.RouteTransformations_RouteTransformation_RequestMatch_{
RequestMatch: reqm,
},
},
},
}

return utils.MessageToAny(envoyT)
}

func convert(targetRef v1alpha1.LocalPolicyTargetReference) []ir.PolicyTargetRef {
return []ir.PolicyTargetRef{{
Kind: string(targetRef.Kind),
Expand Down Expand Up @@ -103,8 +237,16 @@ func (p *routeOptsPluginGwPass) ApplyForRoute(ctx context.Context, pCtx *ir.Rout
return nil
}

if policy.spec.Timeout > 0 && outputRoute.GetRoute() != nil {
outputRoute.GetRoute().Timeout = durationpb.New(time.Second * time.Duration(policy.spec.Timeout))
if policy.spec.timeout != nil && outputRoute.GetRoute() != nil {
outputRoute.GetRoute().Timeout = policy.spec.timeout
}

if policy.spec.transform != nil {
if outputRoute.TypedPerFilterConfig == nil {
outputRoute.TypedPerFilterConfig = make(map[string]*anypb.Any)
}
outputRoute.TypedPerFilterConfig[filterStage] = policy.spec.transform
p.needFilter = true
}

return nil
Expand All @@ -122,6 +264,15 @@ func (p *routeOptsPluginGwPass) ApplyForRouteBackend(
// if a plugin emits new filters, they must be with a plugin unique name.
// any filter returned from route config must be disabled, so it doesnt impact other routes.
func (p *routeOptsPluginGwPass) HttpFilters(ctx context.Context, fcc ir.FilterChainCommon) ([]plugins.StagedHttpFilter, error) {

if p.needFilter {
return []plugins.StagedHttpFilter{
plugins.MustNewStagedFilter(filterStage,
&transformationpb.FilterTransformations{},
pluginStage),
}, nil
}

return nil, nil
}

Expand Down
3 changes: 2 additions & 1 deletion internal/gateway2/utils/any.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (
// definition for the proto in the form of a *pany.Any, errors if nil or if the proto type doesnt exist or if there is
// a marshalling error
func MessageToAny(msg proto.Message) (*anypb.Any, error) {
return anypb.New(msg)
ret := new(anypb.Any)
return ret, anypb.MarshalFrom(ret, msg, proto.MarshalOptions{Deterministic: true})
}

func AnyToMessage(a *anypb.Any) (proto.Message, error) {
Expand Down
Loading