diff --git a/pkg/ingress/kube/annotations/annotations.go b/pkg/ingress/kube/annotations/annotations.go index 4bb929861..dec2c54cc 100644 --- a/pkg/ingress/kube/annotations/annotations.go +++ b/pkg/ingress/kube/annotations/annotations.go @@ -63,6 +63,8 @@ type Ingress struct { Auth *AuthConfig Destination *DestinationConfig + + IgnoreCase *IgnoreCaseConfig } func (i *Ingress) NeedRegexMatch() bool { @@ -132,6 +134,7 @@ func NewAnnotationHandlerManager() AnnotationHandler { fallback{}, auth{}, destination{}, + ignoreCaseMatching{}, }, gatewayHandlers: []GatewayHandler{ downstreamTLS{}, @@ -146,6 +149,7 @@ func NewAnnotationHandlerManager() AnnotationHandler { ipAccessControl{}, retry{}, fallback{}, + ignoreCaseMatching{}, }, trafficPolicyHandlers: []TrafficPolicyHandler{ upstreamTLS{}, diff --git a/pkg/ingress/kube/annotations/ignore_case.go b/pkg/ingress/kube/annotations/ignore_case.go new file mode 100644 index 000000000..be982153c --- /dev/null +++ b/pkg/ingress/kube/annotations/ignore_case.go @@ -0,0 +1,54 @@ +// 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 ( + networking "istio.io/api/networking/v1alpha3" +) + +const ( + enableIgnoreCase = "ignore-path-case" +) + +type IgnoreCaseConfig struct { + IgnoreUriCase bool +} + +type ignoreCaseMatching struct{} + +func (m ignoreCaseMatching) ApplyRoute(route *networking.HTTPRoute, config *Ingress) { + if config == nil || config.IgnoreCase == nil || !config.IgnoreCase.IgnoreUriCase { + return + } + + for _, v := range route.Match { + v.IgnoreUriCase = true + } +} + +func (m ignoreCaseMatching) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error { + if !needIgnoreCaseMatch(annotations) { + return nil + } + + config.IgnoreCase = &IgnoreCaseConfig{} + config.IgnoreCase.IgnoreUriCase, _ = annotations.ParseBoolASAP(enableIgnoreCase) + + return nil +} + +func needIgnoreCaseMatch(annotation Annotations) bool { + return annotation.HasASAP(enableIgnoreCase) +} diff --git a/pkg/ingress/kube/annotations/ignore_case_test.go b/pkg/ingress/kube/annotations/ignore_case_test.go new file mode 100644 index 000000000..8a4d1d0d4 --- /dev/null +++ b/pkg/ingress/kube/annotations/ignore_case_test.go @@ -0,0 +1,135 @@ +// 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 TestIgnoreCaseMatching_ApplyRoute(t *testing.T) { + handler := ignoreCaseMatching{} + + testCases := []struct { + input *networking.HTTPRoute + config *Ingress + expect *networking.HTTPRoute + }{ + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + IgnoreUriCase: false, + }, + }, + }, + config: &Ingress{ + IgnoreCase: &IgnoreCaseConfig{IgnoreUriCase: true}, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + IgnoreUriCase: true, + }, + }, + }, + }, + { + input: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + IgnoreUriCase: false, + }, + }, + }, + config: &Ingress{ + IgnoreCase: &IgnoreCaseConfig{IgnoreUriCase: false}, + }, + expect: &networking.HTTPRoute{ + Match: []*networking.HTTPMatchRequest{ + { + Uri: &networking.StringMatch{ + MatchType: &networking.StringMatch_Exact{Exact: "/abc"}, + }, + IgnoreUriCase: false, + }, + }, + }, + }, + } + + t.Run("", func(t *testing.T) { + for _, tt := range testCases { + handler.ApplyRoute(tt.input, tt.config) + + if diff := cmp.Diff(tt.expect, tt.input); diff != "" { + t.Fatalf("TestIgnoreCaseMatching_ApplyRoute() mismatch(-want +got): \n%s", diff) + } + } + }) +} + +func TestIgnoreCaseMatching_Parse(t *testing.T) { + parser := ignoreCaseMatching{} + + testCases := []struct { + input Annotations + expect *IgnoreCaseConfig + }{ + { + input: Annotations{}, + expect: nil, + }, + { + input: Annotations{ + buildHigressAnnotationKey(enableIgnoreCase): "true", + }, + expect: &IgnoreCaseConfig{ + IgnoreUriCase: true, + }, + }, + { + input: Annotations{ + buildHigressAnnotationKey(enableIgnoreCase): "false", + }, + expect: &IgnoreCaseConfig{ + IgnoreUriCase: false, + }, + }, + } + + t.Run("", func(t *testing.T) { + for _, tt := range testCases { + config := &Ingress{} + + _ = parser.Parse(tt.input, config, nil) + if diff := cmp.Diff(tt.expect, config.IgnoreCase); diff != "" { + t.Fatalf("TestIgnoreCaseMatching_Parse() mismatch(-want +got): \n%s", diff) + } + } + }) +} diff --git a/test/ingress/conformance/tests/httproute-ignore-case-match.go b/test/ingress/conformance/tests/httproute-ignore-case-match.go new file mode 100644 index 000000000..462e7023d --- /dev/null +++ b/test/ingress/conformance/tests/httproute-ignore-case-match.go @@ -0,0 +1,78 @@ +// 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, HTTPRouteIgnoreCaseMatch) +} + +var HTTPRouteIgnoreCaseMatch = suite.ConformanceTest{ + ShortName: "HTTPRouteIgnoreCaseMatch", + Description: "A Ingress in the higress-conformance-infra namespace that ignores URI case in HTTP match", + Manifests: []string{"tests/httproute-ignore-case-match.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TestCaseName: "case1: normal request", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/foo", + Host: "foo.com", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + }, { + Meta: http.AssertionMeta{ + TestCaseName: "case2: enable ignoreCase", + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/fOO", + Host: "foo.com", + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + }, + } + + t.Run("Enable IgnoreCase Cases Split", 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-ignore-case-match.yaml b/test/ingress/conformance/tests/httproute-ignore-case-match.yaml new file mode 100644 index 000000000..432010222 --- /dev/null +++ b/test/ingress/conformance/tests/httproute-ignore-case-match.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/ignore-path-case: "true" + name: httproute-ignore-case-match + 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 35393e54d..794be87a7 100644 --- a/test/ingress/e2e_test.go +++ b/test/ingress/e2e_test.go @@ -56,6 +56,7 @@ func TestHigressConformanceTests(t *testing.T) { tests.HTTPRouteRewriteHost, tests.HTTPRouteCanaryHeader, tests.HTTPRouteEnableCors, + tests.HTTPRouteIgnoreCaseMatch, } cSuite.Run(t, higressTests)