Skip to content

Commit

Permalink
Merge pull request #606 from hbagdi/feat/filter-validation
Browse files Browse the repository at this point in the history
httproute: add duplicate filter validation
  • Loading branch information
k8s-ci-robot authored Apr 16, 2021
2 parents be61f08 + 977527f commit 71f9311
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 1 deletion.
47 changes: 47 additions & 0 deletions apis/v1alpha1/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
)

var (
// repeatableHTTPRouteFilters are filter types that can are allowed to be
// repeated multiple times in a rule.
repeatableHTTPRouteFilters = []gatewayv1a1.HTTPRouteFilterType{
gatewayv1a1.HTTPRouteFilterExtensionRef,
}
)

// ValidateGateway validates gw according to the Gateway API specification.
// For additional details of the Gateway spec, refer to:
// https://gateway-api.sigs.k8s.io/spec/#networking.x-k8s.io/v1alpha1.Gateway
Expand Down Expand Up @@ -75,3 +83,42 @@ func validateListenerHostname(listeners []gatewayv1a1.Listener, path *field.Path
}
return errs
}

// ValidateHTTPRoute validates HTTPRoute according to the Gateway API specification.
// For additional details of the HTTPRoute spec, refer to:
// https://gateway-api.sigs.k8s.io/spec/#networking.x-k8s.io/v1alpha1.HTTPRoute
func ValidateHTTPRoute(route *gatewayv1a1.HTTPRoute) field.ErrorList {
return validateHTTPRouteSpec(&route.Spec, field.NewPath("spec"))
}

// validateHTTPRouteSpec validates that required fields of spec are set according to the
// HTTPRoute specification.
func validateHTTPRouteSpec(spec *gatewayv1a1.HTTPRouteSpec, path *field.Path) field.ErrorList {
return validateHTTPRouteUniqueFilters(spec.Rules, path.Child("rules"))
}

// validateHTTPRouteUniqueFilters validates whether each core and extended filter
// is used at most once in each rule.
func validateHTTPRouteUniqueFilters(rules []gatewayv1a1.HTTPRouteRule, path *field.Path) field.ErrorList {
var errs field.ErrorList

for i, rule := range rules {
counts := map[gatewayv1a1.HTTPRouteFilterType]int{}
for _, filter := range rule.Filters {
counts[filter.Type]++
}
// custom filters don't have any validation
for _, key := range repeatableHTTPRouteFilters {
counts[key] = 0
}

for filterType, count := range counts {
if count > 1 {
errs = append(errs, field.Invalid(path.Index(i).Child("filters"), filterType, "cannot be used multiple times in the same rule"))
}
}

}

return errs
}
264 changes: 264 additions & 0 deletions apis/v1alpha1/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
gatewayv1a1 "sigs.k8s.io/gateway-api/apis/v1alpha1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilpointer "k8s.io/utils/pointer"
)

func TestValidateGateway(t *testing.T) {
Expand Down Expand Up @@ -160,3 +161,266 @@ func TestValidateGateway(t *testing.T) {
})
}
}

func TestValidateHTTPRoute(t *testing.T) {
testService := "test-service"
specialService := "special-service"
tests := []struct {
name string
hRoute gatewayv1a1.HTTPRoute
errCount int
}{
{
name: "valid httpRoute with no filters",
hRoute: gatewayv1a1.HTTPRoute{
Spec: gatewayv1a1.HTTPRouteSpec{
Rules: []gatewayv1a1.HTTPRouteRule{
{
Matches: []gatewayv1a1.HTTPRouteMatch{
{
Path: &gatewayv1a1.HTTPPathMatch{
Type: pathMatchTypePtr("Prefix"),
Value: utilpointer.String("/"),
},
},
},
ForwardTo: []gatewayv1a1.HTTPRouteForwardTo{
{
ServiceName: &testService,
Port: portNumberPtr(8080),
Weight: utilpointer.Int32(100),
},
},
},
},
},
},
errCount: 0,
},
{
name: "valid httpRoute with 1 filter",
hRoute: gatewayv1a1.HTTPRoute{
Spec: gatewayv1a1.HTTPRouteSpec{
Rules: []gatewayv1a1.HTTPRouteRule{
{
Matches: []gatewayv1a1.HTTPRouteMatch{
{
Path: &gatewayv1a1.HTTPPathMatch{
Type: pathMatchTypePtr("Prefix"),
Value: utilpointer.String("/"),
},
},
},
Filters: []gatewayv1a1.HTTPRouteFilter{
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &testService,
Port: portNumberPtr(8081),
},
},
},
},
},
},
},
errCount: 0,
},
{
name: "invalid httpRoute with 2 extended filters",
hRoute: gatewayv1a1.HTTPRoute{
Spec: gatewayv1a1.HTTPRouteSpec{
Rules: []gatewayv1a1.HTTPRouteRule{
{
Matches: []gatewayv1a1.HTTPRouteMatch{
{
Path: &gatewayv1a1.HTTPPathMatch{
Type: pathMatchTypePtr("Prefix"),
Value: utilpointer.String("/"),
},
},
},
Filters: []gatewayv1a1.HTTPRouteFilter{
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &testService,
Port: portNumberPtr(8080),
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &specialService,
Port: portNumberPtr(8080),
},
},
},
},
},
},
},
errCount: 1,
},
{
name: "invalid httpRoute with mix of filters and one duplicate",
hRoute: gatewayv1a1.HTTPRoute{
Spec: gatewayv1a1.HTTPRouteSpec{
Rules: []gatewayv1a1.HTTPRouteRule{
{
Matches: []gatewayv1a1.HTTPRouteMatch{
{
Path: &gatewayv1a1.HTTPPathMatch{
Type: pathMatchTypePtr("Prefix"),
Value: utilpointer.String("/"),
},
},
},
Filters: []gatewayv1a1.HTTPRouteFilter{
{
Type: gatewayv1a1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1a1.HTTPRequestHeaderFilter{
Set: map[string]string{"special-header": "foo"},
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &testService,
Port: portNumberPtr(8080),
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1a1.HTTPRequestHeaderFilter{
Add: map[string]string{"my-header": "bar"},
},
},
},
},
},
},
},
errCount: 1,
},
{
name: "invalid httpRoute with multiple duplicate filters",
hRoute: gatewayv1a1.HTTPRoute{
Spec: gatewayv1a1.HTTPRouteSpec{
Rules: []gatewayv1a1.HTTPRouteRule{
{
Matches: []gatewayv1a1.HTTPRouteMatch{
{
Path: &gatewayv1a1.HTTPPathMatch{
Type: pathMatchTypePtr("Prefix"),
Value: utilpointer.String("/"),
},
},
},
Filters: []gatewayv1a1.HTTPRouteFilter{
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &testService,
Port: portNumberPtr(8080),
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1a1.HTTPRequestHeaderFilter{
Set: map[string]string{"special-header": "foo"},
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &testService,
Port: portNumberPtr(8080),
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1a1.HTTPRequestHeaderFilter{
Add: map[string]string{"my-header": "bar"},
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &specialService,
Port: portNumberPtr(8080),
},
},
},
},
},
},
},
errCount: 2,
},
{
name: "valid httpRoute with duplicate ExtensionRef filters",
hRoute: gatewayv1a1.HTTPRoute{
Spec: gatewayv1a1.HTTPRouteSpec{
Rules: []gatewayv1a1.HTTPRouteRule{
{
Matches: []gatewayv1a1.HTTPRouteMatch{
{
Path: &gatewayv1a1.HTTPPathMatch{
Type: pathMatchTypePtr("Prefix"),
Value: utilpointer.String("/"),
},
},
},
Filters: []gatewayv1a1.HTTPRouteFilter{
{
Type: gatewayv1a1.HTTPRouteFilterRequestHeaderModifier,
RequestHeaderModifier: &gatewayv1a1.HTTPRequestHeaderFilter{
Set: map[string]string{"special-header": "foo"},
},
},
{
Type: gatewayv1a1.HTTPRouteFilterRequestMirror,
RequestMirror: &gatewayv1a1.HTTPRequestMirrorFilter{
ServiceName: &testService,
Port: portNumberPtr(8080),
},
},
{
Type: "ExtensionRef",
},
{
Type: "ExtensionRef",
},
{
Type: "ExtensionRef",
},
},
},
},
},
},
errCount: 0,
},
}
for _, tt := range tests {
// copy variable to avoid scope problems with ranges
tt := tt
t.Run(tt.name, func(t *testing.T) {
errs := ValidateHTTPRoute(&tt.hRoute)
if len(errs) != tt.errCount {
t.Errorf("ValidateHTTPRoute() got %v errors, want %v errors", len(errs), tt.errCount)
}
})
}
}

func pathMatchTypePtr(s string) *gatewayv1a1.PathMatchType {
result := gatewayv1a1.PathMatchType(s)
return &result
}

func portNumberPtr(p int) *gatewayv1a1.PortNumber {
result := gatewayv1a1.PortNumber(p)
return &result
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
k8s.io/apimachinery v0.21.0
k8s.io/client-go v0.21.0
k8s.io/code-generator v0.21.0
k8s.io/utils v0.0.0-20210305010621-2afb4311ab10
sigs.k8s.io/controller-runtime v0.8.3
sigs.k8s.io/controller-tools v0.5.0
)
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -721,8 +721,9 @@ k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAG
k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0=
k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 h1:0T5IaWHO3sJTEmCP6mUlBvMukxPKUQWqiI/YuiBNMiQ=
k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 h1:u5rPykqiCpL+LBfjRkXvnK71gOgIdmq3eHUEkPrbeTI=
k8s.io/utils v0.0.0-20210305010621-2afb4311ab10/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
Expand Down

0 comments on commit 71f9311

Please sign in to comment.