Support HTTPRoute method and query param matching

This commit is contained in:
Kevin Pollet 2024-06-18 09:48:04 +02:00 committed by GitHub
parent a696f7c654
commit b4f99ae3ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 268 additions and 16 deletions

View file

@ -215,11 +215,6 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
),
EnableAllSupportedFeatures: false,
RunTest: *k8sConformanceRunTest,
// Until the feature are all supported, following tests are skipped.
SkipTests: []string{
tests.HTTPRouteMethodMatching.ShortName,
tests.HTTPRouteQueryParamMatching.ShortName,
},
}
cSuite, err := ksuite.NewExperimentalConformanceTestSuite(ksuite.ExperimentalConformanceOptions{

View file

@ -0,0 +1,50 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- method: GET
path:
type: PathPrefix
value: /foo
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""

View file

@ -0,0 +1,56 @@
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: http-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
hostnames:
- "foo.com"
rules:
- matches:
- queryParams:
- type: Exact
name: foo
value: bar
- type: RegularExpression
name: baz
value: buz
path:
type: PathPrefix
value: /foo
backendRefs:
- name: whoami
port: 80
weight: 1
kind: Service
group: ""

View file

@ -469,11 +469,11 @@ func buildHostRule(hostnames []gatev1.Hostname) (string, int) {
// The current priority computing is rather naive but aims to fulfill Conformance tests suite requirement.
// The priority is computed to match the following precedence order:
//
// * "Exact" path match. (+100000)
// * "Prefix" path match with largest number of characters. (+10000) PathRegex (+1000)
// * Method match. (not implemented)
// * Largest number of header matches. (+100 each) or with PathRegex (+10 each)
// * Largest number of query param matches. (not implemented)
// * "Exact" path match (+100000).
// * "Prefix" path match with largest number of characters (+10000 + nb_characters*100).
// * Method match (+1000).
// * Largest number of header matches (+100 each).
// * Largest number of query param matches (+10 each).
//
// In case of multiple matches for a route, the maximum priority among all matches is retain.
func buildMatchRule(hostnames []gatev1.Hostname, match gatev1.HTTPRouteMatch) (string, int) {
@ -489,10 +489,19 @@ func buildMatchRule(hostnames []gatev1.Hostname, match gatev1.HTTPRouteMatch) (s
matchRules = append(matchRules, pathRule)
priority += pathPriority
if match.Method != nil {
matchRules = append(matchRules, fmt.Sprintf("Method(`%s`)", *match.Method))
priority += 1000
}
headerRules, headersPriority := buildHeaderRules(match.Headers)
matchRules = append(matchRules, headerRules...)
priority += headersPriority
queryParamRules, queryParamsPriority := buildQueryParamRules(match.QueryParams)
matchRules = append(matchRules, queryParamRules...)
priority += queryParamsPriority
matchRulesStr := strings.Join(matchRules, " && ")
hostRule, hostPriority := buildHostRule(hostnames)
@ -525,7 +534,7 @@ func buildPathRule(pathMatch gatev1.HTTPPathMatch) (string, int) {
return fmt.Sprintf("(Path(`%[1]s`) || PathPrefix(`%[1]s/`))", pv), 10000 + len(pathValue)*100
case gatev1.PathMatchRegularExpression:
return fmt.Sprintf("PathRegexp(`%s`)", pathValue), 1000 + len(pathValue)*100
return fmt.Sprintf("PathRegexp(`%s`)", pathValue), 10000 + len(pathValue)*100
default:
return "PathPrefix(`/`)", 1
@ -533,18 +542,38 @@ func buildPathRule(pathMatch gatev1.HTTPPathMatch) (string, int) {
}
func buildHeaderRules(headers []gatev1.HTTPHeaderMatch) ([]string, int) {
var rules []string
var priority int
var (
rules []string
priority int
)
for _, header := range headers {
typ := ptr.Deref(header.Type, gatev1.HeaderMatchExact)
switch typ {
case gatev1.HeaderMatchExact:
rules = append(rules, fmt.Sprintf("Header(`%s`,`%s`)", header.Name, header.Value))
priority += 100
case gatev1.HeaderMatchRegularExpression:
rules = append(rules, fmt.Sprintf("HeaderRegexp(`%s`,`%s`)", header.Name, header.Value))
priority += 10
}
priority += 100
}
return rules, priority
}
func buildQueryParamRules(queryParams []gatev1.HTTPQueryParamMatch) ([]string, int) {
var (
rules []string
priority int
)
for _, qp := range queryParams {
typ := ptr.Deref(qp.Type, gatev1.QueryParamMatchExact)
switch typ {
case gatev1.QueryParamMatchExact:
rules = append(rules, fmt.Sprintf("Query(`%s`,`%s`)", qp.Name, qp.Value))
case gatev1.QueryParamMatchRegularExpression:
rules = append(rules, fmt.Sprintf("QueryRegexp(`%s`,`%s`)", qp.Name, qp.Value))
}
priority += 10
}
return rules, priority

View file

@ -1288,7 +1288,7 @@ func TestLoadHTTPRoutes(t *testing.T) {
"default-http-app-1-my-gateway-web-2-d23f7039bc8036fb918c": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.com`) && PathRegexp(`^/buzz/[0-9]+$`)",
Priority: 2408,
Priority: 11408,
RuleSyntax: "v3",
Service: "default-http-app-1-my-gateway-web-2-wrr",
},
@ -1354,6 +1354,128 @@ func TestLoadHTTPRoutes(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, with method matching",
paths: []string{"services.yml", "httproute/with_method_matching.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-app-1-my-gateway-web-0-74ad70a7cf090becdd3c": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.com`) && (Path(`/foo`) || PathPrefix(`/foo/`)) && Method(`GET`)",
Priority: 11408,
RuleSyntax: "v3",
Service: "default-http-app-1-my-gateway-web-0-wrr",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-http-app-1-my-gateway-web-0-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Simple HTTPRoute, with query param matching",
paths: []string{"services.yml", "httproute/with_query_param_matching.yml"},
entryPoints: map[string]Entrypoint{"web": {
Address: ":80",
}},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-http-app-1-my-gateway-web-0-bb7b03c9610e982fd627": {
EntryPoints: []string{"web"},
Rule: "Host(`foo.com`) && (Path(`/foo`) || PathPrefix(`/foo/`)) && Query(`foo`,`bar`) && QueryRegexp(`baz`,`buz`)",
Priority: 10428,
RuleSyntax: "v3",
Service: "default-http-app-1-my-gateway-web-0-wrr",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-http-app-1-my-gateway-web-0-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-whoami-80",
Weight: ptr.To(1),
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: ptypes.Duration(100 * time.Millisecond),
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTPRoute with Same namespace selector",
paths: []string{"services.yml", "httproute/with_namespace_same.yml"},