Skip to content

Commit 41ab6ed

Browse files
authored
chore: separate validation and translation for HTTPRoute (#4886)
* separte validation and translation for HTTPRoute * update tests
1 parent ed1762a commit 41ab6ed

File tree

2 files changed

+139
-157
lines changed

2 files changed

+139
-157
lines changed

internal/dataplane/parser/translate_httproute.go

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,22 @@ func (p *Parser) ingressRulesFromHTTPRoutes() ingressRules {
3030
return result
3131
}
3232

33+
httpRoutesToTranslate := make([]*gatewayapi.HTTPRoute, 0, len(httpRouteList))
34+
for _, httproute := range httpRouteList {
35+
// Validate each HTTPRoute before translating and register translation failures if an HTTPRoute is invalid.
36+
if err := validateHTTPRoute(httproute, p.featureFlags); err != nil {
37+
p.registerTranslationFailure(fmt.Sprintf("HTTPRoute can't be routed: %v", err), httproute)
38+
continue
39+
}
40+
httpRoutesToTranslate = append(httpRoutesToTranslate, httproute)
41+
}
42+
3343
if p.featureFlags.ExpressionRoutes {
34-
p.ingressRulesFromHTTPRoutesUsingExpressionRoutes(httpRouteList, &result)
44+
p.ingressRulesFromHTTPRoutesUsingExpressionRoutes(httpRoutesToTranslate, &result)
3545
return result
3646
}
3747

38-
for _, httproute := range httpRouteList {
48+
for _, httproute := range httpRoutesToTranslate {
3949
if err := p.ingressRulesFromHTTPRoute(&result, httproute); err != nil {
4050
p.registerTranslationFailure(fmt.Sprintf("HTTPRoute can't be routed: %s", err), httproute)
4151
} else {
@@ -51,9 +61,6 @@ func (p *Parser) ingressRulesFromHTTPRoutes() ingressRules {
5161
// ingressRulesFromHTTPRoute validates and generates a set of proto-Kong routes (ingress rules) from an HTTPRoute.
5262
// If multiple rules in the HTTPRoute use the same Service, it combines them into a single Kong route.
5363
func (p *Parser) ingressRulesFromHTTPRoute(result *ingressRules, httproute *gatewayapi.HTTPRoute) error {
54-
if err := validateHTTPRoute(httproute); err != nil {
55-
return fmt.Errorf("validation failed : %w", err)
56-
}
5764
for _, kongServiceTranslation := range translators.TranslateHTTPRoute(httproute) {
5865
// HTTPRoute uses a wrapper HTTPBackendRef to add optional filters to its BackendRefs
5966
backendRefs := httpBackendRefsToBackendRefs(kongServiceTranslation.BackendRefs)
@@ -82,7 +89,7 @@ func (p *Parser) ingressRulesFromHTTPRoute(result *ingressRules, httproute *gate
8289
return nil
8390
}
8491

85-
func validateHTTPRoute(httproute *gatewayapi.HTTPRoute) error {
92+
func validateHTTPRoute(httproute *gatewayapi.HTTPRoute, featureFlags FeatureFlags) error {
8693
spec := httproute.Spec
8794

8895
// validation for HTTPRoutes will happen at a higher layer, but in spite of that we run
@@ -93,6 +100,18 @@ func validateHTTPRoute(httproute *gatewayapi.HTTPRoute) error {
93100
return translators.ErrRouteValidationNoRules
94101
}
95102

103+
// Kong supports query parameter match only with expression router,
104+
// so we return error when query param match is specified and expression router is not enabled in the parser.
105+
if !featureFlags.ExpressionRoutes {
106+
for _, rule := range spec.Rules {
107+
for _, match := range rule.Matches {
108+
if len(match.QueryParams) > 0 {
109+
return translators.ErrRouteValidationQueryParamMatchesUnsupported
110+
}
111+
}
112+
}
113+
}
114+
96115
return nil
97116
}
98117

@@ -105,10 +124,6 @@ func (p *Parser) ingressRulesFromHTTPRoutesUsingExpressionRoutes(httpRoutes []*g
105124
// first, split HTTPRoutes by hostnames and matches.
106125
splitHTTPRouteMatches := []translators.SplitHTTPRouteMatch{}
107126
for _, httproute := range httpRoutes {
108-
if err := validateHTTPRoute(httproute); err != nil {
109-
p.registerTranslationFailure(fmt.Sprintf("HTTPRoute can't be routed: %s", err), httproute)
110-
continue
111-
}
112127
splitHTTPRouteMatches = append(splitHTTPRouteMatches, translators.SplitHTTPRoute(httproute)...)
113128
}
114129
// assign priorities to split HTTPRoutes.
@@ -232,13 +247,6 @@ func generateKongRoutesFromHTTPRouteMatches(
232247
return []kongstate.Route{r}, nil
233248
}
234249

235-
// Kong supports query parameter match only with expression router.
236-
// This function is only called for Kong with traditional/traditional compatible router,
237-
// so we do not support query parameter match here.
238-
if len(matches[0].QueryParams) > 0 {
239-
return []kongstate.Route{}, translators.ErrRouteValidationQueryParamMatchesUnsupported
240-
}
241-
242250
r := generateKongstateHTTPRoute(routeName, ingressObjectInfo, hostnames)
243251
r.Tags = tags
244252

internal/dataplane/parser/translate_httproute_test.go

Lines changed: 114 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package parser
22

33
import (
4-
"strings"
54
"testing"
65

76
"github.com/go-logr/zapr"
@@ -41,6 +40,120 @@ type testCaseIngressRulesFromHTTPRoutes struct {
4140
errs []error
4241
}
4342

43+
func TestValidateHTTPRoute(t *testing.T) {
44+
testCases := []struct {
45+
name string
46+
httpRoute *gatewayapi.HTTPRoute
47+
expressionRoutes bool
48+
expectedError error
49+
}{
50+
{
51+
name: "valid HTTPRoute should pass the validation",
52+
httpRoute: &gatewayapi.HTTPRoute{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Name: "basic-httproute",
55+
Namespace: corev1.NamespaceDefault,
56+
},
57+
Spec: gatewayapi.HTTPRouteSpec{
58+
CommonRouteSpec: commonRouteSpecMock("fake-gateway-1"),
59+
Hostnames: []gatewayapi.Hostname{
60+
"konghq.com",
61+
"www.konghq.com",
62+
},
63+
Rules: []gatewayapi.HTTPRouteRule{{
64+
BackendRefs: []gatewayapi.HTTPBackendRef{
65+
builder.NewHTTPBackendRef("fake-service").WithPort(80).Build(),
66+
},
67+
}},
68+
},
69+
},
70+
expressionRoutes: false,
71+
expectedError: nil,
72+
},
73+
{
74+
name: "HTTPRoute with no rules should not pass the validation",
75+
httpRoute: &gatewayapi.HTTPRoute{
76+
ObjectMeta: metav1.ObjectMeta{
77+
Name: "httproute-no-rule",
78+
Namespace: corev1.NamespaceDefault,
79+
},
80+
Spec: gatewayapi.HTTPRouteSpec{
81+
CommonRouteSpec: commonRouteSpecMock("fake-gateway-1"),
82+
Hostnames: []gatewayapi.Hostname{
83+
"konghq.com",
84+
"www.konghq.com",
85+
},
86+
},
87+
},
88+
expressionRoutes: false,
89+
expectedError: translators.ErrRouteValidationNoRules,
90+
},
91+
{
92+
name: "HTTPRoute with query param match should pass validation with expression routes",
93+
httpRoute: &gatewayapi.HTTPRoute{
94+
ObjectMeta: metav1.ObjectMeta{
95+
Name: "httproute-query-param-match",
96+
Namespace: corev1.NamespaceDefault,
97+
},
98+
Spec: gatewayapi.HTTPRouteSpec{
99+
CommonRouteSpec: commonRouteSpecMock("fake-gateway-1"),
100+
Hostnames: []gatewayapi.Hostname{
101+
"konghq.com",
102+
"www.konghq.com",
103+
},
104+
Rules: []gatewayapi.HTTPRouteRule{{
105+
BackendRefs: []gatewayapi.HTTPBackendRef{
106+
builder.NewHTTPBackendRef("fake-service").WithPort(80).Build(),
107+
},
108+
Matches: builder.NewHTTPRouteMatch().WithQueryParam("foo", "bar").ToSlice(),
109+
}},
110+
},
111+
},
112+
expressionRoutes: true,
113+
expectedError: nil,
114+
},
115+
{
116+
name: "HTTPRoute with query param match should not pass validation when expression routes disabled",
117+
httpRoute: &gatewayapi.HTTPRoute{
118+
ObjectMeta: metav1.ObjectMeta{
119+
Name: "httproute-query-param-match",
120+
Namespace: corev1.NamespaceDefault,
121+
},
122+
Spec: gatewayapi.HTTPRouteSpec{
123+
CommonRouteSpec: commonRouteSpecMock("fake-gateway-1"),
124+
Hostnames: []gatewayapi.Hostname{
125+
"konghq.com",
126+
"www.konghq.com",
127+
},
128+
Rules: []gatewayapi.HTTPRouteRule{{
129+
BackendRefs: []gatewayapi.HTTPBackendRef{
130+
builder.NewHTTPBackendRef("fake-service").WithPort(80).Build(),
131+
},
132+
Matches: builder.NewHTTPRouteMatch().WithQueryParam("foo", "bar").ToSlice(),
133+
}},
134+
},
135+
},
136+
expressionRoutes: false,
137+
expectedError: translators.ErrRouteValidationQueryParamMatchesUnsupported,
138+
},
139+
}
140+
141+
for _, tc := range testCases {
142+
tc := tc
143+
t.Run(tc.name, func(t *testing.T) {
144+
featureFlags := FeatureFlags{
145+
ExpressionRoutes: tc.expressionRoutes,
146+
}
147+
err := validateHTTPRoute(tc.httpRoute, featureFlags)
148+
if tc.expectedError == nil {
149+
require.NoError(t, err, "should pass the validation")
150+
} else {
151+
require.ErrorIs(t, err, tc.expectedError, "should return expected error")
152+
}
153+
})
154+
}
155+
}
156+
44157
func TestIngressRulesFromHTTPRoutes(t *testing.T) {
45158
fakestore, err := store.NewFakeStore(store.FakeObjects{})
46159
require.NoError(t, err)
@@ -257,58 +370,6 @@ func TestIngressRulesFromHTTPRoutes(t *testing.T) {
257370
}
258371
},
259372
},
260-
{
261-
msg: "an HTTPRoute with no rules can't be routed",
262-
routes: []*gatewayapi.HTTPRoute{{
263-
ObjectMeta: metav1.ObjectMeta{
264-
Name: "basic-httproute",
265-
Namespace: corev1.NamespaceDefault,
266-
},
267-
Spec: gatewayapi.HTTPRouteSpec{
268-
CommonRouteSpec: commonRouteSpecMock("fake-gateway"),
269-
},
270-
}},
271-
expected: func(routes []*gatewayapi.HTTPRoute) ingressRules {
272-
return ingressRules{
273-
SecretNameToSNIs: newSecretNameToSNIs(),
274-
ServiceNameToParent: map[string]client.Object{},
275-
ServiceNameToServices: make(map[string]kongstate.Service),
276-
}
277-
},
278-
errs: []error{
279-
translators.ErrRouteValidationNoRules,
280-
},
281-
},
282-
{
283-
msg: "an HTTPRoute with queryParam matches is not yet supported",
284-
routes: []*gatewayapi.HTTPRoute{{
285-
ObjectMeta: metav1.ObjectMeta{
286-
Name: "basic-httproute",
287-
Namespace: corev1.NamespaceDefault,
288-
},
289-
Spec: gatewayapi.HTTPRouteSpec{
290-
CommonRouteSpec: commonRouteSpecMock("fake-gateway"),
291-
Rules: []gatewayapi.HTTPRouteRule{{
292-
Matches: []gatewayapi.HTTPRouteMatch{
293-
builder.NewHTTPRouteMatch().WithQueryParam("username", "kong").Build(),
294-
},
295-
BackendRefs: []gatewayapi.HTTPBackendRef{
296-
builder.NewHTTPBackendRef("fake-service").WithPort(80).Build(),
297-
},
298-
}},
299-
},
300-
}},
301-
expected: func(routes []*gatewayapi.HTTPRoute) ingressRules {
302-
return ingressRules{
303-
SecretNameToSNIs: newSecretNameToSNIs(),
304-
ServiceNameToParent: map[string]client.Object{},
305-
ServiceNameToServices: make(map[string]kongstate.Service),
306-
}
307-
},
308-
errs: []error{
309-
translators.ErrRouteValidationQueryParamMatchesUnsupported,
310-
},
311-
},
312373
{
313374
msg: "an HTTPRoute with regex path matches is supported",
314375
routes: []*gatewayapi.HTTPRoute{{
@@ -1487,17 +1548,11 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) {
14871548
parser.featureFlags.ExpressionRoutes = true
14881549
httpRouteTypeMeta := metav1.TypeMeta{Kind: "HTTPRoute", APIVersion: gatewayv1beta1.GroupVersion.String()}
14891550

1490-
newResourceFailure := func(reason string, objects ...client.Object) failures.ResourceFailure {
1491-
failure, _ := failures.NewResourceFailure(reason, objects...)
1492-
return failure
1493-
}
1494-
14951551
testCases := []struct {
14961552
name string
14971553
httpRoutes []*gatewayapi.HTTPRoute
14981554
expectedKongServices []kongstate.Service
14991555
expectedKongRoutes map[string][]kongstate.Route
1500-
expectedFailures []failures.ResourceFailure
15011556
}{
15021557
{
15031558
name: "single HTTPRoute with no hostname and multiple matches",
@@ -1744,78 +1799,6 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) {
17441799
},
17451800
},
17461801
},
1747-
{
1748-
name: "multiple HTTPRoutes with translation failures",
1749-
httpRoutes: []*gatewayapi.HTTPRoute{
1750-
{
1751-
TypeMeta: httpRouteTypeMeta,
1752-
ObjectMeta: metav1.ObjectMeta{
1753-
Namespace: "default",
1754-
Name: "httproute-no-host-no-rule",
1755-
},
1756-
Spec: gatewayapi.HTTPRouteSpec{
1757-
Hostnames: []gatewayapi.Hostname{"no-rule.example"},
1758-
},
1759-
},
1760-
{
1761-
TypeMeta: httpRouteTypeMeta,
1762-
ObjectMeta: metav1.ObjectMeta{
1763-
Namespace: "default",
1764-
Name: "httproute-1",
1765-
},
1766-
Spec: gatewayapi.HTTPRouteSpec{
1767-
Hostnames: []gatewayapi.Hostname{
1768-
"foo.com",
1769-
},
1770-
Rules: []gatewayapi.HTTPRouteRule{
1771-
{
1772-
Matches: []gatewayapi.HTTPRouteMatch{
1773-
builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(),
1774-
},
1775-
BackendRefs: []gatewayapi.HTTPBackendRef{
1776-
builder.NewHTTPBackendRef("service1").WithPort(80).Build(),
1777-
},
1778-
},
1779-
},
1780-
},
1781-
},
1782-
},
1783-
expectedKongServices: []kongstate.Service{
1784-
{
1785-
Service: kong.Service{
1786-
Name: kong.String("httproute.default.httproute-1.foo.com.0"),
1787-
},
1788-
Backends: []kongstate.ServiceBackend{
1789-
{
1790-
Name: "service1",
1791-
PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)},
1792-
},
1793-
},
1794-
},
1795-
},
1796-
expectedKongRoutes: map[string][]kongstate.Route{
1797-
"httproute.default.httproute-1.foo.com.0": {
1798-
{
1799-
Route: kong.Route{
1800-
Name: kong.String("httproute.default.httproute-1.foo.com.0.0"),
1801-
Expression: kong.String(`(http.host == "foo.com") && (http.path == "/v1/foo")`),
1802-
PreserveHost: kong.Bool(true),
1803-
},
1804-
Plugins: []kong.Plugin{},
1805-
ExpressionRoutes: true,
1806-
},
1807-
},
1808-
},
1809-
expectedFailures: []failures.ResourceFailure{
1810-
newResourceFailure(translators.ErrRouteValidationNoRules.Error(), &gatewayapi.HTTPRoute{
1811-
TypeMeta: httpRouteTypeMeta,
1812-
ObjectMeta: metav1.ObjectMeta{
1813-
Namespace: "default",
1814-
Name: "httproute-no-host-no-rule",
1815-
},
1816-
}),
1817-
},
1818-
},
18191802
}
18201803

18211804
for _, tc := range testCases {
@@ -1847,15 +1830,6 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) {
18471830
require.Equal(t, *expectedRoute.Expression, *r.Expression)
18481831
}
18491832
}
1850-
// check translation failures
1851-
translationFailures := failureCollector.PopResourceFailures()
1852-
require.Equal(t, len(tc.expectedFailures), len(translationFailures))
1853-
for _, expectedTranslationFailure := range tc.expectedFailures {
1854-
expectedFailureMessage := expectedTranslationFailure.Message()
1855-
require.True(t, lo.ContainsBy(translationFailures, func(failure failures.ResourceFailure) bool {
1856-
return strings.Contains(failure.Message(), expectedFailureMessage)
1857-
}))
1858-
}
18591833
})
18601834
}
18611835
}

0 commit comments

Comments
 (0)