Skip to content

Commit

Permalink
Add request header filter support for gRPC
Browse files Browse the repository at this point in the history
  • Loading branch information
ciarams87 committed Apr 30, 2024
1 parent d3a4846 commit 44058a5
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 263 deletions.
63 changes: 54 additions & 9 deletions internal/mode/static/state/graph/grpcroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,24 @@ func processGRPCRouteRules(
validator validation.HTTPFieldsValidator,
) (rules []RouteRule, atLeastOneValid bool, allRulesErrs field.ErrorList) {
rules = make([]RouteRule, len(specRules))
validFilters := true

for i, rule := range specRules {
rulePath := field.NewPath("spec").Child("rules").Index(i)

var allErrs field.ErrorList

var matchesErrs field.ErrorList
var filtersErrs field.ErrorList

for j, match := range rule.Matches {
matchPath := rulePath.Child("matches").Index(j)
matchesErrs = append(matchesErrs, validateGRPCMatch(validator, match, matchPath)...)
}

if len(rule.Filters) > 0 {
filterPath := rulePath.Child("filters")
allErrs = append(
allErrs,
field.NotSupported(filterPath, rule.Filters, []string{"gRPC filters are not yet supported"}),
)
validFilters = false
for j, filter := range rule.Filters {
filterPath := rulePath.Child("filters").Index(j)
filtersErrs = append(filtersErrs, validateGRPCFilter(validator, filter, filterPath)...)
}
}

backendRefs := make([]RouteBackendRef, 0, len(rule.BackendRefs))
Expand All @@ -114,17 +112,20 @@ func processGRPCRouteRules(
}

allErrs = append(allErrs, matchesErrs...)
allErrs = append(allErrs, filtersErrs...)
allRulesErrs = append(allRulesErrs, allErrs...)

if len(allErrs) == 0 {
atLeastOneValid = true
}

validFilters := len(filtersErrs) == 0

rules[i] = RouteRule{
ValidMatches: len(matchesErrs) == 0,
ValidFilters: validFilters,
Matches: convertGRPCMatches(rule.Matches),
Filters: nil,
Filters: convertGRPCFilters(rule.Filters, validFilters),
RouteBackendRefs: backendRefs,
}
}
Expand Down Expand Up @@ -234,3 +235,47 @@ func validateGRPCMethodMatch(
}
return allErrs
}

func validateGRPCFilter(
validator validation.HTTPFieldsValidator,
filter v1alpha2.GRPCRouteFilter,
filterPath *field.Path,
) field.ErrorList {
var allErrs field.ErrorList

switch filter.Type {
case v1alpha2.GRPCRouteFilterRequestHeaderModifier:
return validateFilterHeaderModifier(validator, filter.RequestHeaderModifier, filterPath)
default:
valErr := field.NotSupported(
filterPath.Child("type"),
filter.Type,
[]string{
string(v1alpha2.GRPCRouteFilterRequestHeaderModifier),
},
)
allErrs = append(allErrs, valErr)
return allErrs
}
}

func convertGRPCFilters(filters []v1alpha2.GRPCRouteFilter, validFilters bool) []v1.HTTPRouteFilter {
// validation has already been done, don't process the filters if they are invalid
if !validFilters || len(filters) == 0 {
return nil
}
httpFilters := make([]v1.HTTPRouteFilter, 0, len(filters))
for _, filter := range filters {
switch filter.Type {
case v1alpha2.GRPCRouteFilterRequestHeaderModifier:
httpRequestHeaderFilter := v1.HTTPRouteFilter{
Type: v1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: filter.RequestHeaderModifier,
}
httpFilters = append(httpFilters, httpRequestHeaderFilter)
default:
continue

Check warning on line 277 in internal/mode/static/state/graph/grpcroute.go

View check run for this annotation

Codecov / codecov/patch

internal/mode/static/state/graph/grpcroute.go#L276-L277

Added lines #L276 - L277 were not covered by tests
}
}
return httpFilters
}
56 changes: 51 additions & 5 deletions internal/mode/static/state/graph/grpcroute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func TestBuildGRPCRoute(t *testing.T) {

grInvalidFilterRule.Filters = []v1alpha2.GRPCRouteFilter{
{
Type: "RequestHeaderModifier",
Type: "RequestMirror",
},
}

Expand All @@ -234,6 +234,24 @@ func TestBuildGRPCRoute(t *testing.T) {
[]v1alpha2.GRPCRouteRule{grInvalidFilterRule},
)

grValidFilterRule := createGRPCMethodMatch("myService", "myMethod", "Exact")

grValidFilterRule.Filters = []v1alpha2.GRPCRouteFilter{
{
Type: "RequestHeaderModifier",
RequestHeaderModifier: &v1.HTTPHeaderFilter{
Remove: []string{"header"},
},
},
}

grValidFilter := createGRPCRoute(
"gr",
gatewayNsName.Name,
"example.com",
[]v1alpha2.GRPCRouteRule{grValidFilterRule},
)

createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator {
v := &validationfakes.FakeHTTPFieldsValidator{}
v.ValidateMethodInMatchReturns(true, nil)
Expand Down Expand Up @@ -310,6 +328,36 @@ func TestBuildGRPCRoute(t *testing.T) {
},
name: "valid rule with empty match",
},
{
validator: createAllValidValidator(),
gr: grValidFilter,
expected: &L7Route{
RouteType: RouteTypeGRPC,
Source: grValidFilter,
ParentRefs: []ParentRef{
{
Idx: 0,
Gateway: gatewayNsName,
SectionName: grValidFilter.Spec.ParentRefs[0].SectionName,
},
},
Valid: true,
Attachable: true,
Spec: L7RouteSpec{
Hostnames: grValidFilter.Spec.Hostnames,
Rules: []RouteRule{
{
ValidMatches: true,
ValidFilters: true,
Matches: convertGRPCMatches(grValidFilter.Spec.Rules[0].Matches),
RouteBackendRefs: []RouteBackendRef{},
Filters: convertGRPCFilters(grValidFilter.Spec.Rules[0].Filters, true),
},
},
},
},
name: "valid rule with filter",
},
{
validator: createAllValidValidator(),
gr: grInvalidMatchesEmptyMethodFields,
Expand Down Expand Up @@ -522,10 +570,8 @@ func TestBuildGRPCRoute(t *testing.T) {
},
Conditions: []conditions.Condition{
staticConds.NewRouteUnsupportedValue(
`All rules are invalid: spec.rules[0].filters: Unsupported value: []v1alpha2.GRPCRouteFilter{v1alpha2.` +
`GRPCRouteFilter{Type:"RequestHeaderModifier", RequestHeaderModifier:(*v1.HTTPHeaderFilter)(nil), ` +
`ResponseHeaderModifier:(*v1.HTTPHeaderFilter)(nil), RequestMirror:(*v1.HTTPRequestMirrorFilter)(nil), ` +
`ExtensionRef:(*v1.LocalObjectReference)(nil)}}: supported values: "gRPC filters are not yet supported"`,
`All rules are invalid: spec.rules[0].filters[0].type: ` +
`Unsupported value: "RequestMirror": supported values: "RequestHeaderModifier"`,
),
},
Spec: L7RouteSpec{
Expand Down
107 changes: 1 addition & 106 deletions internal/mode/static/state/graph/httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package graph

import (
"fmt"
"strings"

"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -247,7 +246,7 @@ func validateFilter(
case v1.HTTPRouteFilterURLRewrite:
return validateFilterRewrite(validator, filter, filterPath)
case v1.HTTPRouteFilterRequestHeaderModifier:
return validateFilterHeaderModifier(validator, filter, filterPath)
return validateFilterHeaderModifier(validator, filter.RequestHeaderModifier, filterPath)
default:
valErr := field.NotSupported(
filterPath.Child("type"),
Expand Down Expand Up @@ -355,107 +354,3 @@ func validateFilterRewrite(

return allErrs
}

func validateFilterHeaderModifier(
validator validation.HTTPFieldsValidator,
filter v1.HTTPRouteFilter,
filterPath *field.Path,
) field.ErrorList {
headerModifier := filter.RequestHeaderModifier

headerModifierPath := filterPath.Child("requestHeaderModifier")

if headerModifier == nil {
return field.ErrorList{field.Required(headerModifierPath, "requestHeaderModifier cannot be nil")}
}

return validateFilterHeaderModifierFields(validator, headerModifier, headerModifierPath)
}

func validateFilterHeaderModifierFields(
validator validation.HTTPFieldsValidator,
headerModifier *v1.HTTPHeaderFilter,
headerModifierPath *field.Path,
) field.ErrorList {
var allErrs field.ErrorList

// Ensure that the header names are case-insensitive unique
allErrs = append(allErrs, validateRequestHeadersCaseInsensitiveUnique(
headerModifier.Add,
headerModifierPath.Child("add"))...,
)
allErrs = append(allErrs, validateRequestHeadersCaseInsensitiveUnique(
headerModifier.Set,
headerModifierPath.Child("set"))...,
)
allErrs = append(allErrs, validateRequestHeaderStringCaseInsensitiveUnique(
headerModifier.Remove,
headerModifierPath.Child("remove"))...,
)

for _, h := range headerModifier.Add {
if err := validator.ValidateRequestHeaderName(string(h.Name)); err != nil {
valErr := field.Invalid(headerModifierPath.Child("add"), h, err.Error())
allErrs = append(allErrs, valErr)
}
if err := validator.ValidateRequestHeaderValue(h.Value); err != nil {
valErr := field.Invalid(headerModifierPath.Child("add"), h, err.Error())
allErrs = append(allErrs, valErr)
}
}
for _, h := range headerModifier.Set {
if err := validator.ValidateRequestHeaderName(string(h.Name)); err != nil {
valErr := field.Invalid(headerModifierPath.Child("set"), h, err.Error())
allErrs = append(allErrs, valErr)
}
if err := validator.ValidateRequestHeaderValue(h.Value); err != nil {
valErr := field.Invalid(headerModifierPath.Child("set"), h, err.Error())
allErrs = append(allErrs, valErr)
}
}
for _, h := range headerModifier.Remove {
if err := validator.ValidateRequestHeaderName(h); err != nil {
valErr := field.Invalid(headerModifierPath.Child("remove"), h, err.Error())
allErrs = append(allErrs, valErr)
}
}

return allErrs
}

func validateRequestHeadersCaseInsensitiveUnique(
headers []v1.HTTPHeader,
path *field.Path,
) field.ErrorList {
var allErrs field.ErrorList

seen := make(map[string]struct{})

for _, h := range headers {
name := strings.ToLower(string(h.Name))
if _, exists := seen[name]; exists {
valErr := field.Invalid(path, h, "header name is not unique")
allErrs = append(allErrs, valErr)
}
seen[name] = struct{}{}
}

return allErrs
}

func validateRequestHeaderStringCaseInsensitiveUnique(headers []string, path *field.Path) field.ErrorList {
var allErrs field.ErrorList

seen := make(map[string]struct{})

for _, h := range headers {
name := strings.ToLower(h)
if _, exists := seen[name]; exists {
valErr := field.Invalid(path, h, "header name is not unique")
allErrs = append(allErrs, valErr)
}
seen[name] = struct{}{}
}

return allErrs
}
Loading

0 comments on commit 44058a5

Please sign in to comment.