diff --git a/pkg/ingress/kube/annotations/annotations.go b/pkg/ingress/kube/annotations/annotations.go index dec2c54cc..cd326783f 100644 --- a/pkg/ingress/kube/annotations/annotations.go +++ b/pkg/ingress/kube/annotations/annotations.go @@ -65,6 +65,8 @@ type Ingress struct { Destination *DestinationConfig IgnoreCase *IgnoreCaseConfig + + Match *MatchConfig } func (i *Ingress) NeedRegexMatch() bool { @@ -135,6 +137,7 @@ func NewAnnotationHandlerManager() AnnotationHandler { auth{}, destination{}, ignoreCaseMatching{}, + match{}, }, gatewayHandlers: []GatewayHandler{ downstreamTLS{}, @@ -150,6 +153,7 @@ func NewAnnotationHandlerManager() AnnotationHandler { retry{}, fallback{}, ignoreCaseMatching{}, + match{}, }, trafficPolicyHandlers: []TrafficPolicyHandler{ upstreamTLS{}, diff --git a/pkg/ingress/kube/annotations/match.go b/pkg/ingress/kube/annotations/match.go new file mode 100644 index 000000000..cda1060bf --- /dev/null +++ b/pkg/ingress/kube/annotations/match.go @@ -0,0 +1,249 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package annotations + +import ( + "fmt" + "strings" + + . "github.com/alibaba/higress/pkg/ingress/log" + networking "istio.io/api/networking/v1alpha3" +) + +const ( + exact = "exact" + regex = "regex" + prefix = "prefix" + matchMethod = "match-method" + matchQuery = "match-query" + matchHeader = "match-header" + sep = " " +) + +var ( + methodList = []string{"GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"} + methodMap map[string]struct{} +) + +type match struct{} + +type MatchConfig struct { + Methods []string + Headers map[string]map[string]string + QueryParams map[string]map[string]string +} + +func (m match) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) (err error) { + config.Match = &MatchConfig{} + + if err = m.matchByMethod(annotations, config); err != nil { + IngressLog.Errorf("parse methods error %v within ingress %s/%s", err, config.Namespace, config.Name) + } + + if config.Match.Headers, err = m.matchByHeaderOrQueryParma(annotations, matchHeader, config.Match.Headers); err != nil { + IngressLog.Errorf("parse headers error %v within ingress %s/%s", err, config.Namespace, config.Name) + } + + if config.Match.QueryParams, err = m.matchByHeaderOrQueryParma(annotations, matchQuery, config.Match.QueryParams); err != nil { + IngressLog.Errorf("parse query params error %v within ingress %s/%s", err, config.Namespace, config.Name) + } + + return +} + +func (m match) ApplyRoute(route *networking.HTTPRoute, ingressCfg *Ingress) { + // apply route for method + config := ingressCfg.Match + if config.Methods != nil { + for i := 0; i < len(route.Match); i++ { + route.Match[i].Method = createMethodMatch(config.Methods...) + IngressLog.Debug(fmt.Sprintf("match :%v, methods %v", route.Match[i].Name, route.Match[i].Method)) + } + } + + // apply route for headers + if config.Headers != nil { + for i := 0; i < len(route.Match); i++ { + if route.Match[i].Headers == nil { + route.Match[i].Headers = map[string]*networking.StringMatch{} + } + addHeadersMatch(route.Match[i].Headers, config) + IngressLog.Debug(fmt.Sprintf("match headers: %v, headers: %v", route.Match[i].Name, route.Match[i].Headers)) + } + } + + if config.QueryParams != nil { + for i := 0; i < len(route.Match); i++ { + if route.Match[i].QueryParams == nil { + route.Match[i].QueryParams = map[string]*networking.StringMatch{} + } + addQueryParamsMatch(route.Match[i].QueryParams, config) + IngressLog.Debug(fmt.Sprintf("match : %v, queryParams: %v", route.Match[i].Name, route.Match[i].QueryParams)) + } + } +} + +func (m match) matchByMethod(annotations Annotations, ingress *Ingress) error { + if !annotations.HasHigress(matchMethod) { + return nil + } + + config := ingress.Match + str, err := annotations.ParseStringForHigress(matchMethod) + if err != nil { + return err + } + + methods := strings.Split(str, sep) + set := make(map[string]struct{}) + for i := 0; i < len(methods); i++ { + t := strings.ToUpper(methods[i]) + if _, ok := set[t]; !ok && isMethod(t) { + set[t] = struct{}{} + config.Methods = append(config.Methods, t) + } + } + + return nil +} + +// matchByHeader to parse annotations to find matchHeader config +func (m match) matchByHeaderOrQueryParma(annotations Annotations, key string, mmap map[string]map[string]string) (map[string]map[string]string, error) { + for k, v := range annotations { + if idx := strings.Index(k, key); idx != -1 { + if mmap == nil { + mmap = make(map[string]map[string]string) + } + if err := m.doMatch(k, v, mmap, idx+len(key)+1); err != nil { + IngressLog.Errorf("matchByHeader() failed, the key: %v, value : %v, start: %d", k, v, idx+len(key)+1) + return mmap, err + } + } + } + return mmap, nil +} + +func (m match) doMatch(k, v string, mmap map[string]map[string]string, start int) error { + if start >= len(k) { + return ErrInvalidAnnotationName + } + + var ( + idx int + legalIdx = len(HigressAnnotationsPrefix + "/") // the key has a higress prefix + ) + + // if idx == -1, it means don't have exact|regex|prefix + // if idx > legalIdx, it means the user key also has exact|regex|prefix. we just match the first one + if idx = strings.Index(k, exact); idx == legalIdx { + if mmap[exact] == nil { + mmap[exact] = make(map[string]string) + } + mmap[exact][k[start:]] = v + return nil + } + if idx = strings.Index(k, regex); idx == legalIdx { + if mmap[regex] == nil { + mmap[regex] = make(map[string]string) + } + mmap[regex][k[start:]] = v + return nil + } + if idx = strings.Index(k, prefix); idx == legalIdx { + if mmap[prefix] == nil { + mmap[prefix] = make(map[string]string) + } + mmap[prefix][k[start:]] = v + return nil + } + + return ErrInvalidAnnotationName +} + +func isMethod(s string) bool { + if methodMap == nil || len(methodMap) == 0 { + methodMap = make(map[string]struct{}) + for _, v := range methodList { + methodMap[v] = struct{}{} + } + } + + _, ok := methodMap[s] + return ok +} + +func createMethodMatch(methods ...string) *networking.StringMatch { + var sb strings.Builder + for i := 0; i < len(methods); i++ { + if i != 0 { + sb.WriteString("|") + } + sb.WriteString(methods[i]) + } + + return &networking.StringMatch{ + MatchType: &networking.StringMatch_Regex{ + Regex: sb.String(), + }, + } +} + +func addHeadersMatch(headers map[string]*networking.StringMatch, config *MatchConfig) { + merge(headers, config.Headers) +} + +func addQueryParamsMatch(params map[string]*networking.StringMatch, config *MatchConfig) { + merge(params, config.QueryParams) +} + +// merge m2 to m1 +func merge(m1 map[string]*networking.StringMatch, m2 map[string]map[string]string) { + if m1 == nil { + return + } + for typ, mmap := range m2 { + for k, v := range mmap { + switch typ { + case exact: + if _, ok := m1[k]; !ok { + m1[k] = &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{ + Exact: v, + }, + } + } + case prefix: + if _, ok := m1[k]; !ok { + m1[k] = &networking.StringMatch{ + MatchType: &networking.StringMatch_Prefix{ + Prefix: v, + }, + } + } + case regex: + if _, ok := m1[k]; !ok { + m1[k] = &networking.StringMatch{ + MatchType: &networking.StringMatch_Regex{ + Regex: v, + }, + } + } + default: + IngressLog.Errorf("unknown type: %q is not supported Match type", typ) + } + } + + } +} diff --git a/pkg/ingress/kube/annotations/match_test.go b/pkg/ingress/kube/annotations/match_test.go new file mode 100644 index 000000000..e28c97e6d --- /dev/null +++ b/pkg/ingress/kube/annotations/match_test.go @@ -0,0 +1,375 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package annotations + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + networking "istio.io/api/networking/v1alpha3" +) + +func TestMatch_ParseMethods(t *testing.T) { + parser := match{} + testCases := []struct { + input Annotations + expect *MatchConfig + }{ + { + input: Annotations{}, + expect: &MatchConfig{}, + }, + { + input: Annotations{ + buildHigressAnnotationKey(matchMethod): "PUT POST PATCH", + }, + expect: &MatchConfig{ + Methods: []string{"PUT", "POST", "PATCH"}, + }, + }, + { + input: Annotations{ + buildHigressAnnotationKey(matchMethod): "PUT PUT", + }, + expect: &MatchConfig{ + Methods: []string{"PUT"}, + }, + }, + { + input: Annotations{ + buildHigressAnnotationKey(matchMethod): "put post patch", + }, + expect: &MatchConfig{ + Methods: []string{"PUT", "POST", "PATCH"}, + }, + }, + { + input: Annotations{ + buildHigressAnnotationKey(matchMethod): "geet", + }, + expect: &MatchConfig{}, + }, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + config := &Ingress{} + _ = parser.Parse(tt.input, config, nil) + if diff := cmp.Diff(tt.expect, config.Match); diff != "" { + t.Fatalf("TestMatch_Parse() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestMatch_ParseHeaders(t *testing.T) { + parser := match{} + testCases := []struct { + typ string + key string + value string + expect map[string]map[string]string + }{ + { + typ: "exact", + key: "abc", + value: "123", + expect: map[string]map[string]string{ + exact: { + "abc": "123", + }, + }, + }, + { + typ: "prefix", + key: "user-id", + value: "10086-1", + expect: map[string]map[string]string{ + prefix: { + "user-id": "10086-1", + }, + }, + }, + { + typ: "regex", + key: "content-type", + value: "application/(json|xml)", + expect: map[string]map[string]string{ + regex: { + "content-type": "application/(json|xml)", + }, + }, + }, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + key := buildHigressAnnotationKey(tt.typ + "-" + matchHeader + "-" + tt.key) + input := Annotations{key: tt.value} + config := &Ingress{} + _ = parser.Parse(input, config, nil) + if diff := cmp.Diff(tt.expect, config.Match.Headers); diff != "" { + t.Fatalf("TestMatch_ParseHeaders() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestMatch_ParseQueryParams(t *testing.T) { + parser := match{} + testCases := []struct { + typ string + key string + value string + expect map[string]map[string]string + }{ + { + typ: "exact", + key: "abc", + value: "123", + expect: map[string]map[string]string{ + exact: { + "abc": "123", + }, + }, + }, + { + typ: "prefix", + key: "age", + value: "2", + expect: map[string]map[string]string{ + prefix: { + "age": "2", + }, + }, + }, + { + typ: "regex", + key: "name", + value: "B.*", + expect: map[string]map[string]string{ + regex: { + "name": "B.*", + }, + }, + }, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + key := buildHigressAnnotationKey(tt.typ + "-" + matchQuery + "-" + tt.key) + input := Annotations{key: tt.value} + config := &Ingress{} + _ = parser.Parse(input, config, nil) + if diff := cmp.Diff(tt.expect, config.Match.QueryParams); diff != "" { + t.Fatalf("TestMatch_ParseQueryParams() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestMatch_ApplyRoute(t *testing.T) { + handler := match{} + testCases := []struct { + input *networking.HTTPRoute + config *Ingress + expect *networking.HTTPRoute + }{ + // methods + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + config: &Ingress{ + Match: &MatchConfig{ + Methods: []string{"PUT", "GET", "POST"}, + }, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Method: &networking.StringMatch{ + MatchType: &networking.StringMatch_Regex{Regex: "PUT|GET|POST"}, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + }, + // headers + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + config: &Ingress{ + Match: &MatchConfig{ + Headers: map[string]map[string]string{ + exact: {"new": "new"}, + }, + }, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Headers: map[string]*networking.StringMatch{ + "new": { + MatchType: &networking.StringMatch_Exact{Exact: "new"}, + }, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + }, + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Headers: map[string]*networking.StringMatch{ + "origin": { + MatchType: &networking.StringMatch_Exact{Exact: "origin"}, + }, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + config: &Ingress{ + Match: &MatchConfig{ + Headers: map[string]map[string]string{ + exact: {"new": "new"}, + }, + }, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Headers: map[string]*networking.StringMatch{ + "origin": { + MatchType: &networking.StringMatch_Exact{Exact: "origin"}, + }, + "new": { + MatchType: &networking.StringMatch_Exact{Exact: "new"}, + }, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + }, + // queryParams + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + config: &Ingress{ + Match: &MatchConfig{ + QueryParams: map[string]map[string]string{ + exact: {"new": "new"}, + }, + }, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + QueryParams: map[string]*networking.StringMatch{ + "new": { + MatchType: &networking.StringMatch_Exact{Exact: "new"}, + }, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + }, + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + QueryParams: map[string]*networking.StringMatch{ + "origin": { + MatchType: &networking.StringMatch_Exact{Exact: "origin"}, + }, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + config: &Ingress{ + Match: &MatchConfig{ + QueryParams: map[string]map[string]string{ + exact: {"new": "new"}, + }, + }, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + QueryParams: map[string]*networking.StringMatch{ + "origin": { + MatchType: &networking.StringMatch_Exact{Exact: "origin"}, + }, + "new": { + MatchType: &networking.StringMatch_Exact{Exact: "new"}, + }, + }, + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + }, + }, + }, + }, + } + + for _, tt := range testCases { + t.Run("", func(t *testing.T) { + handler.ApplyRoute(tt.input, tt.config) + if diff := cmp.Diff(tt.expect, tt.input); diff != "" { + t.Fatalf("TestMatch_Parse() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/test/ingress/conformance/tests/httproute-match-headers.go b/test/ingress/conformance/tests/httproute-match-headers.go new file mode 100644 index 000000000..0257136ce --- /dev/null +++ b/test/ingress/conformance/tests/httproute-match-headers.go @@ -0,0 +1,106 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/alibaba/higress/test/ingress/conformance/utils/http" + "github.com/alibaba/higress/test/ingress/conformance/utils/suite" +) + +func init() { + HigressConformanceTests = append(HigressConformanceTests, HTTPRouteMatchHeaders) +} + +var HTTPRouteMatchHeaders = suite.ConformanceTest{ + ShortName: "HTTPRouteMatchHeaders", + Description: "A single Ingress in the higress-conformance-infra namespace uses the match headers", + Manifests: []string{"tests/httproute-match-headers.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Host: "foo.com", + Headers: map[string]string{ + "abc": "12", + }, + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Host: "foo.com", + Headers: map[string]string{ + "abc": "123", + "content-type": "application/json", + "user-id": "10086-1-1", + }, + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + } + + t.Run("Match HTTPRoute by headers", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/ingress/conformance/tests/httproute-match-headers.yaml b/test/ingress/conformance/tests/httproute-match-headers.yaml new file mode 100644 index 000000000..795d98e0c --- /dev/null +++ b/test/ingress/conformance/tests/httproute-match-headers.yaml @@ -0,0 +1,39 @@ +# Copyright (c) 2022 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + # exact matching + higress.io/exact-match-header-abc: "123" + # regex matching + higress.io/regex-match-header-content-type: "application/(json|xml)" + # prefix matching + higress.io/prefix-match-header-user-id: "10086-1" + name: httproute-match-headers + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Exact + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 diff --git a/test/ingress/conformance/tests/httproute-match-methods.go b/test/ingress/conformance/tests/httproute-match-methods.go new file mode 100644 index 000000000..584212f55 --- /dev/null +++ b/test/ingress/conformance/tests/httproute-match-methods.go @@ -0,0 +1,101 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/alibaba/higress/test/ingress/conformance/utils/http" + "github.com/alibaba/higress/test/ingress/conformance/utils/suite" +) + +func init() { + HigressConformanceTests = append(HigressConformanceTests, HTTPRouteMatchMethods) +} + +var HTTPRouteMatchMethods = suite.ConformanceTest{ + ShortName: "HTTPRouteMatchMethods", + Description: "A single Ingress in the higress-conformance-infra namespace uses the match methods", + Manifests: []string{"tests/httproute-match-methods.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Method: "GET", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Method: "POST", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Method: "PUT", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + } + + t.Run("Match HTTPRoute by methods", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/ingress/conformance/tests/httproute-match-methods.yaml b/test/ingress/conformance/tests/httproute-match-methods.yaml new file mode 100644 index 000000000..8d418157a --- /dev/null +++ b/test/ingress/conformance/tests/httproute-match-methods.yaml @@ -0,0 +1,34 @@ +# Copyright (c) 2022 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + higress.io/match-method: "POST PUT PATCH" + name: httproute-match-methods + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Exact + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 diff --git a/test/ingress/conformance/tests/httproute-match-query-params.go b/test/ingress/conformance/tests/httproute-match-query-params.go new file mode 100644 index 000000000..ab3c61587 --- /dev/null +++ b/test/ingress/conformance/tests/httproute-match-query-params.go @@ -0,0 +1,79 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "testing" + + "github.com/alibaba/higress/test/ingress/conformance/utils/http" + "github.com/alibaba/higress/test/ingress/conformance/utils/suite" +) + +func init() { + HigressConformanceTests = append(HigressConformanceTests, HTTPRouteMatchQueryParams) +} + +var HTTPRouteMatchQueryParams = suite.ConformanceTest{ + ShortName: "HTTPRouteMatchQueryParams", + Description: "A single Ingress in the higress-conformance-infra namespace uses the match queryParams", + Manifests: []string{"tests/httproute-match-query-params.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo?abc=123&content-type=application/json&user-id=10086-1-1", + Host: "foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + }, + } + + t.Run("Match HTTPRoute by queryParams", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/ingress/conformance/tests/httproute-match-query-params.yaml b/test/ingress/conformance/tests/httproute-match-query-params.yaml new file mode 100644 index 000000000..6c55581ca --- /dev/null +++ b/test/ingress/conformance/tests/httproute-match-query-params.yaml @@ -0,0 +1,39 @@ +# Copyright (c) 2022 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + # exact matching + higress.io/exact-match-query-abc: "123" + # regex matching + higress.io/regex-match-query-content-type: "application/(json|xml)" + # prefix matching + higress.io/prefix-match-query-user-id: "10086-1" + name: httproute-match-query-params + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Exact + path: "/foo" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 diff --git a/test/ingress/e2e_test.go b/test/ingress/e2e_test.go index 794be87a7..0157dcd2b 100644 --- a/test/ingress/e2e_test.go +++ b/test/ingress/e2e_test.go @@ -57,6 +57,9 @@ func TestHigressConformanceTests(t *testing.T) { tests.HTTPRouteCanaryHeader, tests.HTTPRouteEnableCors, tests.HTTPRouteIgnoreCaseMatch, + tests.HTTPRouteMatchMethods, + tests.HTTPRouteMatchQueryParams, + tests.HTTPRouteMatchHeaders, } cSuite.Run(t, higressTests)