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

validator: prevent multiple filters/predicates per yaml item #2691

Merged
merged 5 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 10 additions & 0 deletions cmd/webhook/admission/admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ func TestRouteGroupAdmitter(t *testing.T) {
inputFile: "rg-with-invalid-eskip-filters-and-predicates.json",
message: "parse failed after token status, last route id: , position 11: syntax error\\nparse failed after token Method, last route id: Method, position 6: syntax error",
},
{
name: "invalid routgroup multiple filters per json/yaml array item",
inputFile: "rg-with-multiple-filters.json",
message: "multiple filters per yaml/json array item is not supported",
},
{
name: "invalid routgroup multiple predicates per json/yaml array item",
inputFile: "rg-with-multiple-predicates.json",
message: "multiple predicates per yaml/json array item is not supported",
},
} {
t.Run(tc.name, func(t *testing.T) {
expectedResponse := responseAllowedFmt
Expand Down
48 changes: 48 additions & 0 deletions cmd/webhook/admission/testdata/rg/rg-with-multiple-filters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"request": {
"uid": "req-uid",
"name": "req1",
"operation": "create",
"kind": {
"group": "zalando",
"version": "v1",
"kind": "RouteGroup"
},
"namespace": "n1",
"object": {
"metadata": {
"name": "rg1",
"namespace": "n1"
},
"spec": {
"backends": [
{
"name": "backend",
"type": "shunt"
}
],
"defaultBackends": [
{
"backendName": "backend"
}
],
"routes": [
{
"backends": [
{
"backendName": "backend"
}
],
"filters": [
"status(201) -> inlineContent(\"hi\")"
],
"path": "/",
"predicates": [
"Method(\"GET\")"
]
}
]
}
}
}
}
48 changes: 48 additions & 0 deletions cmd/webhook/admission/testdata/rg/rg-with-multiple-predicates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"request": {
"uid": "req-uid",
"name": "req1",
"operation": "create",
"kind": {
"group": "zalando",
"version": "v1",
"kind": "RouteGroup"
},
"namespace": "n1",
"object": {
"metadata": {
"name": "rg1",
"namespace": "n1"
},
"spec": {
"backends": [
{
"name": "backend",
"type": "shunt"
}
],
"defaultBackends": [
{
"backendName": "backend"
}
],
"routes": [
{
"backends": [
{
"backendName": "backend"
}
],
"filters": [
"status(201)"
],
"path": "/",
"predicates": [
"Method(\"GET\") && Path(\"/\")"
]
}
]
}
}
}
}
8 changes: 7 additions & 1 deletion dataclients/kubernetes/definitions/definitions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package definitions_test

import (
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -18,10 +19,15 @@ func TestValidateRouteGroups(t *testing.T) {
data, err := os.ReadFile("testdata/errorwrapdata/routegroups.json")
require.NoError(t, err)

logs, err := os.ReadFile("testdata/errorwrapdata/errors.log")
require.NoError(t, err)

logsString := strings.TrimSuffix(string(logs), "\n")

rgl, err := definitions.ParseRouteGroupsJSON(data)
require.NoError(t, err)

err = definitions.ValidateRouteGroups(&rgl)

assert.EqualError(t, err, "route group without name\nerror in route group default/rg1: route group without backend")
assert.EqualError(t, err, logsString)
}
17 changes: 15 additions & 2 deletions dataclients/kubernetes/definitions/routegroupvalidator.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package definitions

import (
"errors"

"github.com/zalando/skipper/eskip"
)

type RouteGroupValidator struct{}

var (
errMultipleFilters = errors.New("multiple filters per yaml/json array item is not supported")
errMultiplePredicates = errors.New("multiple predicates per yaml/json array item is not supported")
)

var defaultRouteGroupValidator = &RouteGroupValidator{}

// ValidateRouteGroup validates a RouteGroupItem
Expand Down Expand Up @@ -56,7 +63,10 @@ func (rgv *RouteGroupValidator) filtersValidation(item *RouteGroupItem) error {
var errs []error
for _, r := range item.Spec.Routes {
for _, f := range r.Filters {
_, err := eskip.ParseFilters(f)
filters, err := eskip.ParseFilters(f)
if len(filters) > 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should check number of filters only when err is nil.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? I think we should send both errors to decrease roundtrips of fixes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add a test case for empty value, maybe eskip.Parse already handles it

Copy link
Member Author

@MustafaSaber MustafaSaber Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without validation ParseFilter/predicate return no error for empty string

errs = append(errs, errMultipleFilters)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can check len(filters) == 1 and make error message like "single filter expected".
This would cover both chain and empty string case (not sure if ParseFilters fails on empty string or not).

I also think we should not mention json/yaml in the error message as it is ambiguous and also routegroup could be created programmatically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we check empty first > 1 makes more sense because if the filter was invalid, ParseFilters will return empty filter which will then push 2 errors (parsing & single)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then maybe we should move empty check here to have filter validation in one place.

Note that value might also contain only whitespaces " " which would produce zero filters but will not be empty - currently we do not catch this case. Here parse would correctly handle empty value like that

errs = append(errs, err)
}
}
Expand All @@ -68,7 +78,10 @@ func (rgv *RouteGroupValidator) predicatesValidation(item *RouteGroupItem) error
var errs []error
for _, r := range item.Spec.Routes {
for _, p := range r.Predicates {
_, err := eskip.ParsePredicates(p)
predicates, err := eskip.ParsePredicates(p)
if len(predicates) > 1 {
errs = append(errs, errMultiplePredicates)
}
errs = append(errs, err)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
route group without name
error in route group default/rg1: route group without backend
multiple predicates per yaml/json array item is not supported
multiple filters per yaml/json array item is not supported
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,41 @@
"kind": "RouteGroup",
"spec": {
"backends": [
{
{
"name": "shunt",
"type": "shunt"
}
],
"hosts": [
"test.example.com"
],
"routes": [
{
"backends": [
{
"backendName": "shunt"
}
],
"filters": [
"inlineContent(\"/foo\")"
],
"path": "/foo"
}
]
}
},
{
"apiVersion": "zalando.org/v1",
"metadata": {
"name": "rg1"
},
"kind": "RouteGroup",
"spec": {
"backends": [
{
"name": "shunt",
"type": "shunt"
}
}
],
"hosts": [
"test.example.com"
Expand All @@ -60,13 +91,47 @@
"backendName": "shunt"
}
],
"predicates": [
"Path(\"/foo\") && Method(\"GET\")"
],
"filters": [
"inlineContent(\"/foo\")"
],
"path": "/foo"
}
]
}
},
{
"apiVersion": "zalando.org/v1",
"metadata": {
"name": "rg1"
},
"kind": "RouteGroup",
"spec": {
"backends": [
{
"name": "shunt",
"type": "shunt"
}
],
"hosts": [
"test.example.com"
],
"routes": [
{
"backends": [
{
"backendName": "shunt"
}
],
"filters": [
"inlineContent(\"/foo\") -> status(200)"
],
"path": "/foo"
}
]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
multiple filters per yaml/json array item is not supported
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: test-route-group
spec:
hosts:
- example.org
backends:
- name: app
type: service
serviceName: app-svc
servicePort: 80
defaultBackends:
- backendName: app
routes:
- path: /
methods:
- GET
- HEAD
predicates:
- Foo("X-Bar", "42")
filters:
- foo(42) -> bar(24)
backends:
- backendName: app
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
multiple predicates per yaml/json array item is not supported
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: test-route-group
spec:
hosts:
- example.org
backends:
- name: app
type: service
serviceName: app-svc
servicePort: 80
defaultBackends:
- backendName: app
routes:
- path: /
methods:
- GET
- HEAD
predicates:
- Foo("X-Bar", "42") && Bar("X-Foo", "24")
filters:
- foo(42)
- bar(24)
backends:
- backendName: app