Skip to content

Commit

Permalink
Gateway API: implement HTTP query param matching (#4588)
Browse files Browse the repository at this point in the history
Closes #4089.

Signed-off-by: Steve Kriss <[email protected]>
  • Loading branch information
skriss authored Jun 23, 2022
1 parent 23a488b commit 817f857
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 32 deletions.
30 changes: 30 additions & 0 deletions changelogs/unreleased/4588-skriss-minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Gateway API: implement HTTP query parameter matching

Contour now implements Gateway API's [HTTP query parameter matching](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPQueryParamMatch).
Only `Exact` matching is supported.
For example, the following HTTPRoute will send a request with a query string of `?animal=whale` to `s1`, and a request with a querystring of `?animal=dolphin` to `s2`.

```yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: httproute-queryparam-matching
spec:
parentRefs:
- name: contour-gateway
rules:
- matches:
- queryParams:
- type: Exact
name: animal
value: whale
backendRefs:
- name: s1
- matches:
- queryParams:
- type: Exact
name: animal
value: dolphin
backendRefs:
- name: s2
```
172 changes: 172 additions & 0 deletions internal/dag/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2686,6 +2686,178 @@ func TestDAGInsertGatewayAPI(t *testing.T) {
},
),
},
"insert single route with single query param match without type specified and path match": {
gatewayclass: validClass,
gateway: gatewayHTTPAllNamespaces,
objs: []interface{}{
kuardService,
&gatewayapi_v1alpha2.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "basic",
Namespace: "projectcontour",
},
Spec: gatewayapi_v1alpha2.HTTPRouteSpec{
CommonRouteSpec: gatewayapi_v1alpha2.CommonRouteSpec{
ParentRefs: []gatewayapi_v1alpha2.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")},
},
Hostnames: []gatewayapi_v1alpha2.Hostname{
"test.projectcontour.io",
},
Rules: []gatewayapi_v1alpha2.HTTPRouteRule{{
Matches: []gatewayapi_v1alpha2.HTTPRouteMatch{{
Path: &gatewayapi_v1alpha2.HTTPPathMatch{
Type: gatewayapi.PathMatchTypePtr(gatewayapi_v1alpha2.PathMatchPathPrefix),
Value: pointer.StringPtr("/"),
},
QueryParams: []gatewayapi_v1alpha2.HTTPQueryParamMatch{
{
Name: "param-1",
Value: "value-1",
},
},
}},
BackendRefs: gatewayapi.HTTPBackendRef("kuard", 8080, 1),
}},
},
},
},
want: listeners(
&Listener{
Name: HTTP_LISTENER_NAME,
Port: 80,
VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io",
&Route{
PathMatchCondition: prefixString("/"),
QueryParamMatchConditions: []QueryParamMatchCondition{
{Name: "param-1", Value: "value-1", MatchType: QueryParamMatchTypeExact},
},
Clusters: clustersWeight(service(kuardService)),
}),
),
},
),
},
"insert single route with single query param match with type specified and path match": {
gatewayclass: validClass,
gateway: gatewayHTTPAllNamespaces,
objs: []interface{}{
kuardService,
&gatewayapi_v1alpha2.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "basic",
Namespace: "projectcontour",
},
Spec: gatewayapi_v1alpha2.HTTPRouteSpec{
CommonRouteSpec: gatewayapi_v1alpha2.CommonRouteSpec{
ParentRefs: []gatewayapi_v1alpha2.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")},
},
Hostnames: []gatewayapi_v1alpha2.Hostname{
"test.projectcontour.io",
},
Rules: []gatewayapi_v1alpha2.HTTPRouteRule{{
Matches: []gatewayapi_v1alpha2.HTTPRouteMatch{{
Path: &gatewayapi_v1alpha2.HTTPPathMatch{
Type: gatewayapi.PathMatchTypePtr(gatewayapi_v1alpha2.PathMatchPathPrefix),
Value: pointer.StringPtr("/"),
},
QueryParams: []gatewayapi_v1alpha2.HTTPQueryParamMatch{
{
Type: gatewayapi.QueryParamMatchTypePtr(gatewayapi_v1alpha2.QueryParamMatchExact),
Name: "param-1",
Value: "value-1",
},
},
}},
BackendRefs: gatewayapi.HTTPBackendRef("kuard", 8080, 1),
}},
},
},
},
want: listeners(
&Listener{
Name: HTTP_LISTENER_NAME,
Port: 80,
VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io",
&Route{
PathMatchCondition: prefixString("/"),
QueryParamMatchConditions: []QueryParamMatchCondition{
{Name: "param-1", Value: "value-1", MatchType: QueryParamMatchTypeExact},
},
Clusters: clustersWeight(service(kuardService)),
}),
),
},
),
},
"insert single route with multiple query param matches including multiple for the same key": {
gatewayclass: validClass,
gateway: gatewayHTTPAllNamespaces,
objs: []interface{}{
kuardService,
&gatewayapi_v1alpha2.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Name: "basic",
Namespace: "projectcontour",
},
Spec: gatewayapi_v1alpha2.HTTPRouteSpec{
CommonRouteSpec: gatewayapi_v1alpha2.CommonRouteSpec{
ParentRefs: []gatewayapi_v1alpha2.ParentReference{gatewayapi.GatewayParentRef("projectcontour", "contour")},
},
Hostnames: []gatewayapi_v1alpha2.Hostname{
"test.projectcontour.io",
},
Rules: []gatewayapi_v1alpha2.HTTPRouteRule{{
Matches: []gatewayapi_v1alpha2.HTTPRouteMatch{{
Path: &gatewayapi_v1alpha2.HTTPPathMatch{
Type: gatewayapi.PathMatchTypePtr(gatewayapi_v1alpha2.PathMatchPathPrefix),
Value: pointer.StringPtr("/"),
},
QueryParams: []gatewayapi_v1alpha2.HTTPQueryParamMatch{
{
Type: gatewayapi.QueryParamMatchTypePtr(gatewayapi_v1alpha2.QueryParamMatchExact),
Name: "param-1",
Value: "value-1",
},
{
Type: gatewayapi.QueryParamMatchTypePtr(gatewayapi_v1alpha2.QueryParamMatchExact),
Name: "param-2",
Value: "value-2",
},
{
Type: gatewayapi.QueryParamMatchTypePtr(gatewayapi_v1alpha2.QueryParamMatchExact),
Name: "param-1",
Value: "value-3",
},
{
Type: gatewayapi.QueryParamMatchTypePtr(gatewayapi_v1alpha2.QueryParamMatchExact),
Name: "Param-1",
Value: "value-4",
},
},
}},
BackendRefs: gatewayapi.HTTPBackendRef("kuard", 8080, 1),
}},
},
},
},
want: listeners(
&Listener{
Name: HTTP_LISTENER_NAME,
Port: 80,
VirtualHosts: virtualhosts(virtualhost("test.projectcontour.io",
&Route{
PathMatchCondition: prefixString("/"),
QueryParamMatchConditions: []QueryParamMatchCondition{
{Name: "param-1", Value: "value-1", MatchType: QueryParamMatchTypeExact},
{Name: "param-2", Value: "value-2", MatchType: QueryParamMatchTypeExact},
{Name: "Param-1", Value: "value-4", MatchType: QueryParamMatchTypeExact},
},
Clusters: clustersWeight(service(kuardService)),
}),
),
},
),
},
"Route rule with request header modifier": {
gatewayclass: validClass,
gateway: gatewayHTTPAllNamespaces,
Expand Down
29 changes: 29 additions & 0 deletions internal/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,28 @@ func (hc *HeaderMatchCondition) String() string {
return "header: " + details
}

const (
// QueryParamMatchTypeExact matches a querystring parameter value exactly.
QueryParamMatchTypeExact = "exact"
)

// QueryParamMatchCondition matches querystring parameters by MatchType
type QueryParamMatchCondition struct {
Name string
Value string
MatchType string
}

func (qc *QueryParamMatchCondition) String() string {
details := strings.Join([]string{
"name=" + qc.Name,
"value=" + qc.Value,
"matchtype=", qc.MatchType,
}, "&")

return "queryparam: " + details
}

// DirectResponse allows for a specific HTTP status code and body
// to be the response to a route request vs routing to
// an envoy cluster.
Expand Down Expand Up @@ -202,6 +224,10 @@ type Route struct {
// match on the request headers.
HeaderMatchConditions []HeaderMatchCondition

// QueryParamMatchConditions specifies a set of additional Conditions to
// match on the querystring parameters.
QueryParamMatchConditions []QueryParamMatchCondition

Clusters []*Cluster

// Should this route generate a 301 upgrade if accessed
Expand Down Expand Up @@ -522,6 +548,9 @@ func conditionsToString(r *Route) string {
for _, cond := range r.HeaderMatchConditions {
s = append(s, cond.String())
}
for _, cond := range r.QueryParamMatchConditions {
s = append(s, cond.String())
}
return strings.Join(s, ",")
}

Expand Down
60 changes: 49 additions & 11 deletions internal/dag/gatewayapi_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ type GatewayAPIProcessor struct {

// matchConditions holds match rules.
type matchConditions struct {
path MatchCondition
headers []HeaderMatchCondition
path MatchCondition
headers []HeaderMatchCondition
queryParams []QueryParamMatchCondition
}

// Run translates Gateway API types into DAG objects and
Expand Down Expand Up @@ -994,7 +995,7 @@ func (p *GatewayAPIProcessor) computeHTTPRoute(route *gatewayapi_v1alpha2.HTTPRo

headerMatches, err := gatewayHeaderMatchConditions(match.Headers)
if err != nil {
routeAccessor.AddCondition(status.ConditionNotImplemented, metav1.ConditionTrue, status.ReasonHeaderMatchType, "HTTPRoute.Spec.Rules.HeaderMatch: Only Exact match type is supported.")
routeAccessor.AddCondition(status.ConditionNotImplemented, metav1.ConditionTrue, status.ReasonHeaderMatchType, err.Error())
continue
}

Expand All @@ -1008,9 +1009,16 @@ func (p *GatewayAPIProcessor) computeHTTPRoute(route *gatewayapi_v1alpha2.HTTPRo
})
}

queryParamMatches, err := gatewayQueryParamMatchConditions(match.QueryParams)
if err != nil {
routeAccessor.AddCondition(status.ConditionNotImplemented, metav1.ConditionTrue, status.ReasonQueryParamMatchType, err.Error())
continue
}

matchconditions = append(matchconditions, &matchConditions{
path: pathMatch,
headers: headerMatches,
path: pathMatch,
headers: headerMatches,
queryParams: queryParamMatches,
})
}

Expand Down Expand Up @@ -1238,7 +1246,7 @@ func gatewayHeaderMatchConditions(matches []gatewayapi_v1alpha2.HTTPHeaderMatch)
case gatewayapi_v1alpha2.HeaderMatchExact:
headerMatchType = HeaderMatchTypeExact
default:
return nil, fmt.Errorf("HTTPRoute.Spec.Rules.HeaderMatch: Only Exact match type is supported")
return nil, fmt.Errorf("HTTPRoute.Spec.Rules.Matches.Headers: Only Exact match type is supported")
}
}

Expand All @@ -1248,6 +1256,35 @@ func gatewayHeaderMatchConditions(matches []gatewayapi_v1alpha2.HTTPHeaderMatch)
return headerMatchConditions, nil
}

func gatewayQueryParamMatchConditions(matches []gatewayapi_v1alpha2.HTTPQueryParamMatch) ([]QueryParamMatchCondition, error) {
var dagMatchConditions []QueryParamMatchCondition
seenNames := sets.String{}

for _, match := range matches {
// QueryParamMatchTypeExact is the default if not defined in the object.
queryParamMatchType := QueryParamMatchTypeExact

if match.Type != nil && *match.Type != gatewayapi_v1alpha2.QueryParamMatchExact {
return nil, fmt.Errorf("HTTPRoute.Spec.Rules.Matches.QueryParams: Only Exact match type is supported")
}

// If multiple match conditions are found for the same value,
// use the first one and ignore subsequent ones.
if seenNames.Has(match.Name) {
continue
}
seenNames.Insert(match.Name)

dagMatchConditions = append(dagMatchConditions, QueryParamMatchCondition{
MatchType: queryParamMatchType,
Name: match.Name,
Value: match.Value,
})
}

return dagMatchConditions, nil
}

// clusterRoutes builds a []*dag.Route for the supplied set of matchConditions, headerPolicy and backendRefs.
func (p *GatewayAPIProcessor) clusterRoutes(routeNamespace string, matchConditions []*matchConditions, headerPolicy *HeadersPolicy, mirrorPolicy *MirrorPolicy, backendRefs []gatewayapi_v1alpha2.HTTPBackendRef, routeAccessor *status.RouteConditionsUpdate) []*Route {
if len(backendRefs) == 0 {
Expand Down Expand Up @@ -1309,11 +1346,12 @@ func (p *GatewayAPIProcessor) clusterRoutes(routeNamespace string, matchConditio
// we create a separate route per match.
for _, mc := range matchConditions {
routes = append(routes, &Route{
Clusters: clusters,
PathMatchCondition: mc.path,
HeaderMatchConditions: mc.headers,
RequestHeadersPolicy: headerPolicy,
MirrorPolicy: mirrorPolicy,
Clusters: clusters,
PathMatchCondition: mc.path,
HeaderMatchConditions: mc.headers,
QueryParamMatchConditions: mc.queryParams,
RequestHeadersPolicy: headerPolicy,
MirrorPolicy: mirrorPolicy,
})
}

Expand Down
Loading

0 comments on commit 817f857

Please sign in to comment.