diff --git a/pkg/ingress/kube/annotations/match.go b/pkg/ingress/kube/annotations/match.go index ce58ab71a..ba965fbe0 100644 --- a/pkg/ingress/kube/annotations/match.go +++ b/pkg/ingress/kube/annotations/match.go @@ -23,13 +23,14 @@ import ( ) const ( - exact = "exact" - regex = "regex" - prefix = "prefix" - MatchMethod = "match-method" - MatchQuery = "match-query" - MatchHeader = "match-header" - sep = " " + exact = "exact" + regex = "regex" + prefix = "prefix" + MatchMethod = "match-method" + MatchQuery = "match-query" + MatchHeader = "match-header" + MatchPseudoHeader = "match-pseudo-header" + sep = " " ) var ( @@ -56,6 +57,24 @@ func (m match) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) IngressLog.Errorf("parse headers error %v within ingress %s/%s", err, config.Namespace, config.Name) } + var pseudoHeaderMatches map[string]map[string]string + if pseudoHeaderMatches, err = m.matchByHeaderOrQueryParma(annotations, MatchPseudoHeader, pseudoHeaderMatches); err != nil { + IngressLog.Errorf("parse headers error %v within ingress %s/%s", err, config.Namespace, config.Name) + } + if pseudoHeaderMatches != nil && len(pseudoHeaderMatches) > 0 { + if config.Match.Headers == nil { + config.Match.Headers = make(map[string]map[string]string) + } + for typ, mmap := range pseudoHeaderMatches { + if config.Match.Headers[typ] == nil { + config.Match.Headers[typ] = make(map[string]string) + } + for k, v := range mmap { + config.Match.Headers[typ][":"+k] = v + } + } + } + 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) } diff --git a/pkg/ingress/kube/annotations/match_test.go b/pkg/ingress/kube/annotations/match_test.go index 8d3d7a992..2ddf0f505 100644 --- a/pkg/ingress/kube/annotations/match_test.go +++ b/pkg/ingress/kube/annotations/match_test.go @@ -15,6 +15,7 @@ package annotations import ( + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -112,11 +113,47 @@ func TestMatch_ParseHeaders(t *testing.T) { }, }, }, + { + typ: "exact", + key: ":method", + value: "GET", + expect: map[string]map[string]string{ + exact: { + ":method": "GET", + }, + }, + }, + { + typ: "prefix", + key: ":path", + value: "/foo", + expect: map[string]map[string]string{ + prefix: { + ":path": "/foo", + }, + }, + }, + { + typ: "regex", + key: ":authority", + value: "test\\d+\\.com", + expect: map[string]map[string]string{ + regex: { + ":authority": "test\\d+\\.com", + }, + }, + }, } for _, tt := range testCases { t.Run("", func(t *testing.T) { - key := buildHigressAnnotationKey(tt.typ + "-" + MatchHeader + "-" + tt.key) + matchKeyword := MatchHeader + headerKey := tt.key + if strings.HasPrefix(headerKey, ":") { + headerKey = strings.TrimPrefix(headerKey, ":") + matchKeyword = MatchPseudoHeader + } + key := buildHigressAnnotationKey(tt.typ + "-" + matchKeyword + "-" + headerKey) input := Annotations{key: tt.value} config := &Ingress{} _ = parser.Parse(input, config, nil) diff --git a/pkg/ingress/kube/ingress/controller.go b/pkg/ingress/kube/ingress/controller.go index 2c515f7d0..a71115dc9 100644 --- a/pkg/ingress/kube/ingress/controller.go +++ b/pkg/ingress/kube/ingress/controller.go @@ -1279,8 +1279,10 @@ func createRuleKey(annots map[string]string, hostAndPath string) string { if idx := strings.Index(k, annotations.MatchHeader); idx != -1 { key := k[start:idx] + k[idx+len(annotations.MatchHeader)+1:] headers = append(headers, [2]string{key, val}) - } - if idx := strings.Index(k, annotations.MatchQuery); idx != -1 { + } else if idx := strings.Index(k, annotations.MatchPseudoHeader); idx != -1 { + key := k[start:idx] + ":" + k[idx+len(annotations.MatchPseudoHeader)+1:] + headers = append(headers, [2]string{key, val}) + } else if idx := strings.Index(k, annotations.MatchQuery); idx != -1 { key := k[start:idx] + k[idx+len(annotations.MatchQuery)+1:] params = append(params, [2]string{key, val}) } diff --git a/pkg/ingress/kube/ingress/controller_test.go b/pkg/ingress/kube/ingress/controller_test.go index 08841eb6f..9022a241a 100644 --- a/pkg/ingress/kube/ingress/controller_test.go +++ b/pkg/ingress/kube/ingress/controller_test.go @@ -1302,15 +1302,18 @@ func TestCreateRuleKey(t *testing.T) { } annots := annotations.Annotations{ - buildHigressAnnotationKey(annotations.MatchMethod): "GET PUT", - buildHigressAnnotationKey("exact-" + annotations.MatchHeader + "-abc"): "123", - buildHigressAnnotationKey("prefix-" + annotations.MatchHeader + "-def"): "456", - buildHigressAnnotationKey("exact-" + annotations.MatchQuery + "-region"): "beijing", - buildHigressAnnotationKey("prefix-" + annotations.MatchQuery + "-user-id"): "user-", + buildHigressAnnotationKey(annotations.MatchMethod): "GET PUT", + buildHigressAnnotationKey("exact-" + annotations.MatchHeader + "-abc"): "123", + buildHigressAnnotationKey("prefix-" + annotations.MatchHeader + "-def"): "456", + buildHigressAnnotationKey("exact-" + annotations.MatchPseudoHeader + "-authority"): "foo.bar.com", + buildHigressAnnotationKey("prefix-" + annotations.MatchPseudoHeader + "-scheme"): "htt", + buildHigressAnnotationKey("exact-" + annotations.MatchQuery + "-region"): "beijing", + buildHigressAnnotationKey("prefix-" + annotations.MatchQuery + "-user-id"): "user-", } expect := "higress.com-prefix-/foo" + sep + //host-pathType-path "GET PUT" + sep + // method - "exact-abc\t123" + "\n" + "prefix-def\t456" + sep + // header + "exact-:authority\tfoo.bar.com" + "\n" + "exact-abc\t123" + "\n" + + "prefix-:scheme\thtt" + "\n" + "prefix-def\t456" + sep + // header "exact-region\tbeijing" + "\n" + "prefix-user-id\tuser-" + sep // params key := createRuleKey(annots, wrapperHttpRoute.PathFormat()) diff --git a/pkg/ingress/kube/ingressv1/controller.go b/pkg/ingress/kube/ingressv1/controller.go index 9d5194d7c..e07202375 100644 --- a/pkg/ingress/kube/ingressv1/controller.go +++ b/pkg/ingress/kube/ingressv1/controller.go @@ -1226,8 +1226,10 @@ func createRuleKey(annots map[string]string, hostAndPath string) string { if idx := strings.Index(k, annotations.MatchHeader); idx != -1 { key := k[start:idx] + k[idx+len(annotations.MatchHeader)+1:] headers = append(headers, [2]string{key, val}) - } - if idx := strings.Index(k, annotations.MatchQuery); idx != -1 { + } else if idx := strings.Index(k, annotations.MatchPseudoHeader); idx != -1 { + key := k[start:idx] + ":" + k[idx+len(annotations.MatchPseudoHeader)+1:] + headers = append(headers, [2]string{key, val}) + } else if idx := strings.Index(k, annotations.MatchQuery); idx != -1 { key := k[start:idx] + k[idx+len(annotations.MatchQuery)+1:] params = append(params, [2]string{key, val}) } diff --git a/pkg/ingress/kube/kingress/controller.go b/pkg/ingress/kube/kingress/controller.go index 648d6e127..7bd40810e 100644 --- a/pkg/ingress/kube/kingress/controller.go +++ b/pkg/ingress/kube/kingress/controller.go @@ -699,8 +699,10 @@ func createRuleKey(annots map[string]string, hostAndPath string) string { if idx := strings.Index(k, annotations.MatchHeader); idx != -1 { key := k[start:idx] + k[idx+len(annotations.MatchHeader)+1:] headers = append(headers, [2]string{key, val}) - } - if idx := strings.Index(k, annotations.MatchQuery); idx != -1 { + } else if idx := strings.Index(k, annotations.MatchPseudoHeader); idx != -1 { + key := k[start:idx] + ":" + k[idx+len(annotations.MatchPseudoHeader)+1:] + headers = append(headers, [2]string{key, val}) + } else if idx := strings.Index(k, annotations.MatchQuery); idx != -1 { key := k[start:idx] + k[idx+len(annotations.MatchQuery)+1:] params = append(params, [2]string{key, val}) } diff --git a/pkg/ingress/kube/kingress/controller_test.go b/pkg/ingress/kube/kingress/controller_test.go index ac39ce77c..845bde3f1 100644 --- a/pkg/ingress/kube/kingress/controller_test.go +++ b/pkg/ingress/kube/kingress/controller_test.go @@ -581,15 +581,18 @@ func TestCreateRuleKey(t *testing.T) { } annots := annotations.Annotations{ - buildHigressAnnotationKey(annotations.MatchMethod): "GET PUT", - buildHigressAnnotationKey("exact-" + annotations.MatchHeader + "-abc"): "123", - buildHigressAnnotationKey("prefix-" + annotations.MatchHeader + "-def"): "456", - buildHigressAnnotationKey("exact-" + annotations.MatchQuery + "-region"): "beijing", - buildHigressAnnotationKey("prefix-" + annotations.MatchQuery + "-user-id"): "user-", + buildHigressAnnotationKey(annotations.MatchMethod): "GET PUT", + buildHigressAnnotationKey("exact-" + annotations.MatchHeader + "-abc"): "123", + buildHigressAnnotationKey("prefix-" + annotations.MatchHeader + "-def"): "456", + buildHigressAnnotationKey("exact-" + annotations.MatchPseudoHeader + "-authority"): "foo.bar.com", + buildHigressAnnotationKey("prefix-" + annotations.MatchPseudoHeader + "-scheme"): "htt", + buildHigressAnnotationKey("exact-" + annotations.MatchQuery + "-region"): "beijing", + buildHigressAnnotationKey("prefix-" + annotations.MatchQuery + "-user-id"): "user-", } expect := "higress.com-prefix-/foo" + sep + //host-pathType-path "GET PUT" + sep + // method - "exact-abc\t123" + "\n" + "prefix-def\t456" + sep + // header + "exact-:authority\tfoo.bar.com" + "\n" + "exact-abc\t123" + "\n" + + "prefix-:scheme\thtt" + "\n" + "prefix-def\t456" + sep + // header "exact-region\tbeijing" + "\n" + "prefix-user-id\tuser-" + sep // params key := createRuleKey(annots, wrapperHttpRoute.PathFormat()) diff --git a/test/e2e/conformance/tests/httproute-match-pseudo-headers.go b/test/e2e/conformance/tests/httproute-match-pseudo-headers.go new file mode 100644 index 000000000..f3dfb1a58 --- /dev/null +++ b/test/e2e/conformance/tests/httproute-match-pseudo-headers.go @@ -0,0 +1,148 @@ +// 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/e2e/conformance/utils/http" + "github.com/alibaba/higress/test/e2e/conformance/utils/suite" +) + +func init() { + Register(HTTPRouteMatchPseudoHeaders) +} + +var HTTPRouteMatchPseudoHeaders = suite.ConformanceTest{ + ShortName: "HTTPRouteMatchPseudoHeaders", + Description: "Ingresses in the higress-conformance-infra namespace uses the match pseudo-headers.", + Manifests: []string{"tests/httproute-match-pseudo-headers.yaml"}, + Features: []suite.SupportedFeature{suite.HTTPConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/v1", + Host: "bad.foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{}, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/v1", + Host: "test.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: "/v2", + Host: "test.foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{}, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/v2", + Host: "test2.foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v2", + TargetNamespace: "higress-conformance-infra", + }, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/v3", + Host: "bar.foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 404, + }, + }, + + Meta: http.AssertionMeta{}, + }, + { + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Path: "/v3/bar", + Host: "bar.foo.com", + }, + }, + + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v3", + TargetNamespace: "higress-conformance-infra", + }, + }, + } + + t.Run("Match HTTPRoute by pseudo-headers", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/httproute-match-pseudo-headers.yaml b/test/e2e/conformance/tests/httproute-match-pseudo-headers.yaml new file mode 100644 index 000000000..ea0b52fc0 --- /dev/null +++ b/test/e2e/conformance/tests/httproute-match-pseudo-headers.yaml @@ -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. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + # exact matching + higress.io/exact-match-pseudo-header-authority: "test.foo.com" + name: httproute-match-pseudo-headers-1 + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "*.foo.com" + http: + paths: + - pathType: Prefix + path: "/v1" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + # regex matching + higress.io/regex-match-pseudo-header-authority: "test.+\\.foo\\.com" + name: httproute-match-pseudo-headers-2 + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "*.foo.com" + http: + paths: + - pathType: Prefix + path: "/v2" + backend: + service: + name: infra-backend-v2 + port: + number: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + # prefix matching + higress.io/prefix-match-pseudo-header-path: "/v3/bar" + name: httproute-match-pseudo-headers-3 + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "*.foo.com" + http: + paths: + - pathType: Prefix + path: "/v3" + backend: + service: + name: infra-backend-v3 + port: + number: 8080