| //go:build integ |
| // +build integ |
| |
| // Copyright Istio Authors |
| // |
| // 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 common |
| |
| import ( |
| "fmt" |
| "net/http" |
| "net/url" |
| "reflect" |
| "sort" |
| "strings" |
| "time" |
| ) |
| |
| import ( |
| wrappers "google.golang.org/protobuf/types/known/wrapperspb" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/host" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/protocol" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/security" |
| "github.com/apache/dubbo-go-pixiu/pkg/http/headers" |
| echoClient "github.com/apache/dubbo-go-pixiu/pkg/test/echo" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/echo/common/scheme" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/check" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/common/deployment" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/common/ports" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/echotest" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/echo/match" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/istio" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/istio/ingress" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/scopes" |
| "github.com/apache/dubbo-go-pixiu/pkg/test/util/tmpl" |
| "github.com/apache/dubbo-go-pixiu/pkg/util/sets" |
| "github.com/apache/dubbo-go-pixiu/tests/common/jwt" |
| ingressutil "github.com/apache/dubbo-go-pixiu/tests/integration/security/sds_ingress/util" |
| ) |
| |
| const httpVirtualServiceTmpl = ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: {{.VirtualServiceHost}} |
| spec: |
| gateways: |
| - {{.Gateway}} |
| hosts: |
| - {{.VirtualServiceHost}} |
| http: |
| - route: |
| - destination: |
| host: {{.VirtualServiceHost}} |
| port: |
| number: {{.Port}} |
| {{- if .MatchScheme }} |
| match: |
| - scheme: |
| exact: {{.MatchScheme}} |
| headers: |
| request: |
| add: |
| istio-custom-header: user-defined-value |
| {{- end }} |
| --- |
| ` |
| |
| func httpVirtualService(gateway, host string, port int) string { |
| return tmpl.MustEvaluate(httpVirtualServiceTmpl, struct { |
| Gateway string |
| VirtualServiceHost string |
| Port int |
| MatchScheme string |
| }{gateway, host, port, ""}) |
| } |
| |
| const gatewayTmpl = ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: {{.GatewayPort}} |
| name: {{.GatewayPortName}} |
| protocol: {{.GatewayProtocol}} |
| {{- if .Credential }} |
| tls: |
| mode: SIMPLE |
| credentialName: {{.Credential}} |
| {{- if .Ciphers }} |
| cipherSuites: |
| {{- range $cipher := .Ciphers }} |
| - "{{$cipher}}" |
| {{- end }} |
| {{- end }} |
| {{- end }} |
| hosts: |
| - "{{.GatewayHost}}" |
| --- |
| ` |
| |
| func httpGateway(host string) string { |
| return tmpl.MustEvaluate(gatewayTmpl, struct { |
| GatewayHost string |
| GatewayPort int |
| GatewayPortName string |
| GatewayProtocol string |
| Credential string |
| }{ |
| host, 80, "http", "HTTP", "", |
| }) |
| } |
| |
| func virtualServiceCases(t framework.TestContext, skipVM bool) []TrafficTestCase { |
| var cases []TrafficTestCase |
| cases = append(cases, |
| TrafficTestCase{ |
| name: "added header", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - route: |
| - destination: |
| host: {{ .dstSvc }} |
| headers: |
| request: |
| add: |
| istio-custom-header: user-defined-value`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.RequestHeader("Istio-Custom-Header", "user-defined-value")), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "set header", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| x-custom: some-value`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.RequestHeader("X-Custom", "some-value")), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "set authority header", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| :authority: my-custom-authority`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.Host("my-custom-authority")), |
| }, |
| workloadAgnostic: true, |
| minIstioVersion: "1.10.0", |
| }, |
| TrafficTestCase{ |
| name: "set host header in destination", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| Host: my-custom-authority`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.Host("my-custom-authority")), |
| }, |
| workloadAgnostic: true, |
| minIstioVersion: "1.10.0", |
| }, |
| TrafficTestCase{ |
| name: "set host header in route and destination", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| Host: dest-authority |
| headers: |
| request: |
| set: |
| :authority: route-authority`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.Host("route-authority")), |
| }, |
| workloadAgnostic: true, |
| minIstioVersion: "1.12.0", |
| }, |
| TrafficTestCase{ |
| name: "set host header in route and multi destination", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| Host: dest-authority |
| weight: 50 |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| weight: 50 |
| headers: |
| request: |
| set: |
| :authority: route-authority`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.Host("route-authority")), |
| }, |
| workloadAgnostic: true, |
| minIstioVersion: "1.12.0", |
| }, |
| TrafficTestCase{ |
| name: "set host header multi destination", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| Host: dest-authority |
| weight: 50 |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| headers: |
| request: |
| set: |
| Host: dest-authority |
| weight: 50`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.Host("dest-authority")), |
| }, |
| workloadAgnostic: true, |
| minIstioVersion: "1.12.0", |
| }, |
| TrafficTestCase{ |
| name: "redirect", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - match: |
| - uri: |
| exact: /foo |
| redirect: |
| uri: /new/path |
| - match: |
| - uri: |
| exact: /new/path |
| route: |
| - destination: |
| host: {{ .dstSvc }}`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Path: "/foo?key=value", |
| FollowRedirects: true, |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.URL("/new/path?key=value")), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "redirect port and scheme", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - match: |
| - uri: |
| exact: /foo |
| redirect: |
| derivePort: FROM_REQUEST_PORT |
| scheme: https |
| `, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Path: "/foo", |
| FollowRedirects: false, |
| }, |
| Count: 1, |
| Check: check.And( |
| check.Status(http.StatusMovedPermanently), |
| check.Each( |
| func(r echoClient.Response) error { |
| originalHostname, err := url.Parse(r.RequestURL) |
| if err != nil { |
| return err |
| } |
| return ExpectString(r.ResponseHeaders.Get("Location"), |
| fmt.Sprintf("https://%s:%d/foo", originalHostname.Hostname(), ports.All().MustForName("http").ServicePort), |
| "Location") |
| })), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "rewrite uri", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - match: |
| - uri: |
| exact: /foo |
| rewrite: |
| uri: /new/path |
| route: |
| - destination: |
| host: {{ .dstSvc }}`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Path: "/foo?key=value#hash", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.URL("/new/path?key=value")), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "rewrite authority", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - match: |
| - uri: |
| exact: /foo |
| rewrite: |
| authority: new-authority |
| route: |
| - destination: |
| host: {{ .dstSvc }}`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Path: "/foo", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.Host("new-authority")), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "cors", |
| // TODO https://github.com/istio/istio/issues/31532 |
| targetMatchers: []match.Matcher{match.NotTProxy, match.NotVM}, |
| |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - corsPolicy: |
| allowOrigins: |
| - exact: cors.com |
| allowMethods: |
| - POST |
| - GET |
| allowCredentials: false |
| allowHeaders: |
| - X-Foo-Bar |
| - X-Foo-Baz |
| maxAge: "24h" |
| route: |
| - destination: |
| host: {{ .dstSvc }} |
| `, |
| children: []TrafficCall{ |
| { |
| name: "preflight", |
| opts: func() echo.CallOptions { |
| return echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Method: "OPTIONS", |
| Headers: headers.New(). |
| With(headers.Origin, "cors.com"). |
| With(headers.AccessControlRequestMethod, "DELETE"). |
| Build(), |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.ResponseHeaders(map[string]string{ |
| "Access-Control-Allow-Origin": "cors.com", |
| "Access-Control-Allow-Methods": "POST,GET", |
| "Access-Control-Allow-Headers": "X-Foo-Bar,X-Foo-Baz", |
| "Access-Control-Max-Age": "86400", |
| })), |
| } |
| }(), |
| }, |
| { |
| name: "get", |
| opts: func() echo.CallOptions { |
| return echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headers.New().With(headers.Origin, "cors.com").Build(), |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.ResponseHeader("Access-Control-Allow-Origin", "cors.com")), |
| } |
| }(), |
| }, |
| { |
| // GET without matching origin |
| name: "get no origin match", |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.And( |
| check.OK(), |
| check.ResponseHeader("Access-Control-Allow-Origin", "")), |
| }, |
| }, |
| }, |
| workloadAgnostic: true, |
| }, |
| // Retry conditions have been added to just check that config is correct. |
| // Retries are not specifically tested. |
| TrafficTestCase{ |
| name: "retry conditions", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ .dstSvc }} |
| http: |
| - route: |
| - destination: |
| host: {{ .dstSvc }} |
| retries: |
| attempts: 3 |
| perTryTimeout: 2s |
| retryOn: gateway-error,connect-failure,refused-stream |
| retryRemoteLocalities: true`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.OK(), |
| }, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: "fault abort", |
| config: ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ (index .dst 0).Config.Service }} |
| http: |
| - route: |
| - destination: |
| host: {{ (index .dst 0).Config.Service }} |
| fault: |
| abort: |
| percentage: |
| value: 100 |
| httpStatus: 418`, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| Check: check.Status(http.StatusTeapot), |
| }, |
| workloadAgnostic: true, |
| }, |
| ) |
| |
| // reduce the total # of subtests that don't give valuable coverage or just don't work |
| for i, tc := range cases { |
| // TODO include proxyless as different features become supported |
| tc.sourceMatchers = append(tc.sourceMatchers, match.NotNaked, match.NotHeadless, match.NotProxylessGRPC) |
| tc.targetMatchers = append(tc.targetMatchers, match.NotNaked, match.NotHeadless, match.NotProxylessGRPC) |
| cases[i] = tc |
| } |
| |
| splits := [][]int{ |
| {50, 25, 25}, |
| {80, 10, 10}, |
| } |
| if skipVM { |
| splits = [][]int{ |
| {50, 50}, |
| {80, 20}, |
| } |
| } |
| for _, split := range splits { |
| split := split |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("shifting-%d", split[0]), |
| toN: len(split), |
| sourceMatchers: []match.Matcher{match.NotHeadless, match.NotNaked}, |
| targetMatchers: []match.Matcher{match.NotHeadless, match.NotExternal}, |
| templateVars: func(_ echo.Callers, _ echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "split": split, |
| } |
| }, |
| config: ` |
| {{ $split := .split }} |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - {{ ( index .dstSvcs 0) }} |
| http: |
| - route: |
| {{- range $idx, $svc := .dstSvcs }} |
| - destination: |
| host: {{ $svc }} |
| weight: {{ ( index $split $idx ) }} |
| {{- end }} |
| `, |
| checkForN: func(src echo.Caller, dests echo.Services, opts *echo.CallOptions) echo.Checker { |
| return check.And( |
| check.OK(), |
| func(result echo.CallResult, err error) error { |
| errorThreshold := 10 |
| if len(split) != len(dests) { |
| // shouldn't happen |
| return fmt.Errorf("split configured for %d destinations, but framework gives %d", len(split), len(dests)) |
| } |
| splitPerHost := map[echo.NamespacedName]int{} |
| destNames := dests.NamespacedNames() |
| for i, pct := range split { |
| splitPerHost[destNames[i]] = pct |
| } |
| for serviceName, exp := range splitPerHost { |
| hostResponses := result.Responses.Match(func(r echoClient.Response) bool { |
| return strings.HasPrefix(r.Hostname, serviceName.Name) |
| }) |
| if !AlmostEquals(len(hostResponses), exp, errorThreshold) { |
| return fmt.Errorf("expected %v calls to %s, got %v", exp, serviceName, len(hostResponses)) |
| } |
| // echotest should have filtered the deployment to only contain reachable clusters |
| to := match.ServiceName(serviceName).GetMatches(dests.Instances()) |
| fromCluster := src.(echo.Instance).Config().Cluster |
| toClusters := to.Clusters() |
| // don't check headless since lb is unpredictable |
| headlessTarget := match.Headless.Any(to) |
| if !headlessTarget && len(toClusters.ByNetwork()[fromCluster.NetworkName()]) > 1 { |
| // Conditionally check reached clusters to work around connection load balancing issues |
| // See https://github.com/istio/istio/issues/32208 for details |
| // We want to skip this for requests from the cross-network pod |
| if err := check.ReachedClusters(t.AllClusters(), toClusters).Check(echo.CallResult{ |
| From: result.From, |
| Opts: result.Opts, |
| Responses: hostResponses, |
| }, nil); err != nil { |
| return fmt.Errorf("did not reach all clusters for %s: %v", serviceName, err) |
| } |
| } |
| } |
| return nil |
| }) |
| }, |
| setupOpts: func(src echo.Caller, opts *echo.CallOptions) { |
| // TODO force this globally in echotest? |
| if src, ok := src.(echo.Instance); ok && src.Config().IsProxylessGRPC() { |
| opts.Port.Name = "grpc" |
| } |
| }, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 100, |
| }, |
| workloadAgnostic: true, |
| }) |
| } |
| |
| return cases |
| } |
| |
| func HostHeader(header string) http.Header { |
| return headers.New().WithHost(header).Build() |
| } |
| |
| // tlsOriginationCases contains tests TLS origination from DestinationRule |
| func tlsOriginationCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| tc := TrafficTestCase{ |
| name: "", |
| config: fmt.Sprintf(` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: DestinationRule |
| metadata: |
| name: external |
| spec: |
| host: %s |
| trafficPolicy: |
| tls: |
| mode: SIMPLE |
| `, apps.External.All.Config().DefaultHostHeader), |
| children: []TrafficCall{}, |
| } |
| expects := []struct { |
| port int |
| alpn string |
| }{ |
| {8888, "http/1.1"}, |
| {8882, "h2"}, |
| } |
| for _, c := range apps.A { |
| for _, e := range expects { |
| c := c |
| e := e |
| |
| tc.children = append(tc.children, TrafficCall{ |
| name: fmt.Sprintf("%s: %s", c.Config().Cluster.StableName(), e.alpn), |
| opts: echo.CallOptions{ |
| Port: echo.Port{ServicePort: e.port, Protocol: protocol.HTTP}, |
| Count: 1, |
| Address: apps.External.All[0].Address(), |
| HTTP: echo.HTTP{ |
| Headers: HostHeader(apps.External.All[0].Config().DefaultHostHeader), |
| }, |
| Scheme: scheme.HTTP, |
| Check: check.And( |
| check.OK(), |
| check.Alpn(e.alpn)), |
| }, |
| call: c.CallOrFail, |
| }) |
| } |
| } |
| return []TrafficTestCase{tc} |
| } |
| |
| // useClientProtocolCases contains tests use_client_protocol from DestinationRule |
| func useClientProtocolCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| client := apps.A |
| to := apps.C |
| cases = append(cases, |
| TrafficTestCase{ |
| name: "use client protocol with h2", |
| config: useClientProtocolDestinationRule(to.Config().Service), |
| call: client[0].CallOrFail, |
| opts: echo.CallOptions{ |
| To: to, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| HTTP: echo.HTTP{ |
| HTTP2: true, |
| }, |
| Check: check.And( |
| check.OK(), |
| check.Protocol("HTTP/2.0"), |
| ), |
| }, |
| minIstioVersion: "1.10.0", |
| }, |
| TrafficTestCase{ |
| name: "use client protocol with h1", |
| config: useClientProtocolDestinationRule(to.Config().Service), |
| call: client[0].CallOrFail, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| To: to, |
| HTTP: echo.HTTP{ |
| HTTP2: false, |
| }, |
| Check: check.And( |
| check.OK(), |
| check.Protocol("HTTP/1.1"), |
| ), |
| }, |
| }, |
| ) |
| return cases |
| } |
| |
| // destinationRuleCases contains tests some specific DestinationRule tests. |
| func destinationRuleCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| from := apps.A |
| to := apps.C |
| cases = append(cases, |
| // Validates the config is generated correctly when only idletimeout is specified in DR. |
| TrafficTestCase{ |
| name: "only idletimeout specified in DR", |
| config: idletimeoutDestinationRule("idletimeout-dr", to.Config().Service), |
| call: from[0].CallOrFail, |
| opts: echo.CallOptions{ |
| To: to, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Count: 1, |
| HTTP: echo.HTTP{ |
| HTTP2: true, |
| }, |
| Check: check.OK(), |
| }, |
| minIstioVersion: "1.10.0", |
| }, |
| ) |
| return cases |
| } |
| |
| // trafficLoopCases contains tests to ensure traffic does not loop through the sidecar |
| func trafficLoopCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| for _, c := range apps.A { |
| for _, d := range apps.B { |
| for _, port := range []int{15001, 15006} { |
| c, d, port := c, d, port |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprint(port), |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| ToWorkload: d, |
| Port: echo.Port{ServicePort: port, Protocol: protocol.HTTP}, |
| // Ideally we would actually check to make sure we do not blow up the pod, |
| // but I couldn't find a way to reliably detect this. |
| Check: check.Error(), |
| }, |
| }) |
| } |
| } |
| } |
| return cases |
| } |
| |
| // autoPassthroughCases tests that we cannot hit unexpected destinations when using AUTO_PASSTHROUGH |
| func autoPassthroughCases(t framework.TestContext, apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| t.Helper() |
| var cases []TrafficTestCase |
| // We test the cross product of all Istio ALPNs (or no ALPN), all mTLS modes, and various backends |
| alpns := []string{"istio", "istio-peer-exchange", "istio-http/1.0", "istio-http/1.1", "istio-h2", ""} |
| modes := []string{"STRICT", "PERMISSIVE", "DISABLE"} |
| |
| mtlsHost := host.Name(apps.A.Config().ClusterLocalFQDN()) |
| nakedHost := host.Name(apps.Naked.Config().ClusterLocalFQDN()) |
| httpsPort := ports.All().MustForName("https").ServicePort |
| httpsAutoPort := ports.All().MustForName("auto-https").ServicePort |
| snis := []string{ |
| model.BuildSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsPort), |
| model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsPort), |
| model.BuildSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsPort), |
| model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsPort), |
| model.BuildSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsAutoPort), |
| model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", mtlsHost, httpsAutoPort), |
| model.BuildSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsAutoPort), |
| model.BuildDNSSrvSubsetKey(model.TrafficDirectionOutbound, "", nakedHost, httpsAutoPort), |
| } |
| defaultIngress := istio.DefaultIngressOrFail(t, t) |
| for _, mode := range modes { |
| var childs []TrafficCall |
| for _, sni := range snis { |
| for _, alpn := range alpns { |
| alpn, sni, mode := alpn, sni, mode |
| al := []string{alpn} |
| if alpn == "" { |
| al = nil |
| } |
| childs = append(childs, TrafficCall{ |
| name: fmt.Sprintf("mode:%v,sni:%v,alpn:%v", mode, sni, alpn), |
| call: defaultIngress.CallOrFail, |
| opts: echo.CallOptions{ |
| Port: echo.Port{ |
| ServicePort: 443, |
| Protocol: protocol.HTTPS, |
| }, |
| TLS: echo.TLS{ |
| ServerName: sni, |
| Alpn: al, |
| }, |
| Check: check.Error(), |
| Timeout: 5 * time.Second, |
| }, |
| }, |
| ) |
| } |
| } |
| cases = append(cases, TrafficTestCase{ |
| config: globalPeerAuthentication(mode) + ` |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: cross-network-gateway-test |
| namespace: dubbo-system |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 443 |
| name: tls |
| protocol: TLS |
| tls: |
| mode: AUTO_PASSTHROUGH |
| hosts: |
| - "*.local" |
| `, |
| children: childs, |
| }) |
| } |
| |
| return cases |
| } |
| |
| func gatewayCases() []TrafficTestCase { |
| templateParams := func(protocol protocol.Instance, src echo.Callers, dests echo.Instances, ciphers []string) map[string]interface{} { |
| hostName, dest, portN, cred := "*", dests[0], 80, "" |
| if protocol.IsTLS() { |
| hostName, portN, cred = dest.Config().ClusterLocalFQDN(), 443, "cred" |
| } |
| return map[string]interface{}{ |
| "IngressNamespace": src[0].(ingress.Instance).Namespace(), |
| "GatewayHost": hostName, |
| "GatewayPort": portN, |
| "GatewayPortName": strings.ToLower(string(protocol)), |
| "GatewayProtocol": string(protocol), |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": dest.PortForName("http").ServicePort, |
| "Credential": cred, |
| "Ciphers": ciphers, |
| } |
| } |
| |
| // clears the To to avoid echo internals trying to match the protocol with the port on echo.Config |
| noTarget := func(_ echo.Caller, opts *echo.CallOptions) { |
| opts.To = nil |
| } |
| // allows setting the target indirectly via the host header |
| fqdnHostHeader := func(src echo.Caller, opts *echo.CallOptions) { |
| if opts.HTTP.Headers == nil { |
| opts.HTTP.Headers = make(http.Header) |
| } |
| opts.HTTP.Headers.Set(headers.Host, opts.To.Config().ClusterLocalFQDN()) |
| noTarget(src, opts) |
| } |
| |
| // SingleRegualrPod is already applied leaving one regular pod, to only regular pods should leave a single workload. |
| singleTarget := []match.Matcher{match.RegularPod} |
| // the following cases don't actually target workloads, we use the singleTarget filter to avoid duplicate cases |
| cases := []TrafficTestCase{ |
| { |
| name: "404", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: httpGateway("*"), |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headers.New().WithHost("foo.bar").Build(), |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| setupOpts: noTarget, |
| }, |
| { |
| name: "https redirect", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| tls: |
| httpsRedirect: true |
| --- |
| `, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| Check: check.Status(http.StatusMovedPermanently), |
| }, |
| setupOpts: fqdnHostHeader, |
| }, |
| { |
| // See https://github.com/istio/istio/issues/27315 |
| name: "https with x-forwarded-proto", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| tls: |
| httpsRedirect: true |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: EnvoyFilter |
| metadata: |
| name: ingressgateway-redirect-config |
| namespace: dubbo-system |
| spec: |
| configPatches: |
| - applyTo: NETWORK_FILTER |
| match: |
| context: GATEWAY |
| listener: |
| filterChain: |
| filter: |
| name: envoy.filters.network.http_connection_manager |
| patch: |
| operation: MERGE |
| value: |
| typed_config: |
| '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager |
| xff_num_trusted_hops: 1 |
| normalize_path: true |
| workloadSelector: |
| labels: |
| istio: ingressgateway |
| --- |
| ` + httpVirtualServiceTmpl, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| // In real world, this may be set by a downstream LB that terminates the TLS |
| Headers: headers.New().With(headers.XForwardedProto, "https").Build(), |
| }, |
| Check: check.OK(), |
| }, |
| setupOpts: fqdnHostHeader, |
| templateVars: func(_ echo.Callers, dests echo.Instances) map[string]interface{} { |
| dest := dests[0] |
| return map[string]interface{}{ |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": dest.PortForName("http").ServicePort, |
| } |
| }, |
| }, |
| { |
| name: "cipher suite", |
| config: gatewayTmpl + httpVirtualServiceTmpl + |
| ingressutil.IngressKubeSecretYAML("cred", "{{.IngressNamespace}}", ingressutil.TLS, ingressutil.IngressCredentialA), |
| templateVars: func(src echo.Callers, dests echo.Instances) map[string]interface{} { |
| // Test all cipher suites, including a fake one. Envoy should accept all of the ones on the "valid" list, |
| // and control plane should filter our invalid one. |
| return templateParams(protocol.HTTPS, src, dests, append(security.ValidCipherSuites.SortedList(), "fake")) |
| }, |
| setupOpts: fqdnHostHeader, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTPS, |
| }, |
| }, |
| viaIngress: true, |
| workloadAgnostic: true, |
| }, |
| { |
| // See https://github.com/istio/istio/issues/34609 |
| name: "http redirect when vs port specify https", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| tls: |
| httpsRedirect: true |
| --- |
| ` + httpVirtualServiceTmpl, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| Check: check.Status(http.StatusMovedPermanently), |
| }, |
| setupOpts: fqdnHostHeader, |
| templateVars: func(_ echo.Callers, dests echo.Instances) map[string]interface{} { |
| dest := dests[0] |
| return map[string]interface{}{ |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": 443, |
| } |
| }, |
| }, |
| { |
| // See https://github.com/istio/istio/issues/27315 |
| // See https://github.com/istio/istio/issues/34609 |
| name: "http return 400 with with x-forwarded-proto https when vs port specify https", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| tls: |
| httpsRedirect: true |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: EnvoyFilter |
| metadata: |
| name: ingressgateway-redirect-config |
| namespace: dubbo-system |
| spec: |
| configPatches: |
| - applyTo: NETWORK_FILTER |
| match: |
| context: GATEWAY |
| listener: |
| filterChain: |
| filter: |
| name: envoy.filters.network.http_connection_manager |
| patch: |
| operation: MERGE |
| value: |
| typed_config: |
| '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager |
| xff_num_trusted_hops: 1 |
| normalize_path: true |
| workloadSelector: |
| labels: |
| istio: ingressgateway |
| --- |
| ` + httpVirtualServiceTmpl, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| // In real world, this may be set by a downstream LB that terminates the TLS |
| Headers: headers.New().With(headers.XForwardedProto, "https").Build(), |
| }, |
| Check: check.Status(http.StatusBadRequest), |
| }, |
| setupOpts: fqdnHostHeader, |
| templateVars: func(_ echo.Callers, dests echo.Instances) map[string]interface{} { |
| dest := dests[0] |
| return map[string]interface{}{ |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": 443, |
| } |
| }, |
| }, |
| { |
| // https://github.com/istio/istio/issues/37196 |
| name: "client protocol - http1", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| --- |
| ` + httpVirtualServiceTmpl, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| Check: check.And( |
| check.OK(), |
| check.Protocol("HTTP/1.1")), |
| }, |
| setupOpts: fqdnHostHeader, |
| templateVars: func(_ echo.Callers, dests echo.Instances) map[string]interface{} { |
| dest := dests[0] |
| return map[string]interface{}{ |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": ports.All().MustForName("auto-http").ServicePort, |
| } |
| }, |
| }, |
| { |
| // https://github.com/istio/istio/issues/37196 |
| name: "client protocol - http2", |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| --- |
| ` + httpVirtualServiceTmpl, |
| opts: echo.CallOptions{ |
| HTTP: echo.HTTP{ |
| HTTP2: true, |
| }, |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| Check: check.And( |
| check.OK(), |
| // Gateway doesn't implicitly use downstream |
| check.Protocol("HTTP/1.1"), |
| // Regression test; if this is set it means the inbound sidecar is treating it as TCP |
| check.RequestHeader("X-Envoy-Peer-Metadata", "")), |
| }, |
| setupOpts: fqdnHostHeader, |
| templateVars: func(_ echo.Callers, dests echo.Instances) map[string]interface{} { |
| dest := dests[0] |
| return map[string]interface{}{ |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": ports.All().MustForName("auto-http").ServicePort, |
| } |
| }, |
| }, |
| } |
| for _, port := range []string{"auto-http", "http", "http2"} { |
| for _, h2 := range []bool{true, false} { |
| port, h2 := port, h2 |
| protoName := "http1" |
| expectedProto := "HTTP/1.1" |
| if h2 { |
| protoName = "http2" |
| expectedProto = "HTTP/2.0" |
| } |
| |
| cases = append(cases, |
| TrafficTestCase{ |
| // https://github.com/istio/istio/issues/37196 |
| name: fmt.Sprintf("client protocol - %v use client with %v", protoName, port), |
| targetMatchers: singleTarget, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: `apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| --- |
| ` + httpVirtualServiceTmpl + useClientProtocolDestinationRuleTmpl, |
| opts: echo.CallOptions{ |
| HTTP: echo.HTTP{ |
| HTTP2: h2, |
| }, |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: protocol.HTTP, |
| }, |
| Check: check.And( |
| check.OK(), |
| // We did configure to use client protocol |
| check.Protocol(expectedProto), |
| // Regression test; if this is set it means the inbound sidecar is treating it as TCP |
| check.RequestHeader("X-Envoy-Peer-Metadata", "")), |
| }, |
| setupOpts: fqdnHostHeader, |
| templateVars: func(_ echo.Callers, dests echo.Instances) map[string]interface{} { |
| dest := dests[0] |
| return map[string]interface{}{ |
| "Gateway": "gateway", |
| "VirtualServiceHost": dest.Config().ClusterLocalFQDN(), |
| "Port": ports.All().MustForName(port).ServicePort, |
| } |
| }, |
| }) |
| } |
| } |
| |
| for _, proto := range []protocol.Instance{protocol.HTTP, protocol.HTTPS} { |
| proto, secret := proto, "" |
| if proto.IsTLS() { |
| secret = ingressutil.IngressKubeSecretYAML("cred", "{{.IngressNamespace}}", ingressutil.TLS, ingressutil.IngressCredentialA) |
| } |
| cases = append( |
| cases, |
| TrafficTestCase{ |
| name: string(proto), |
| config: gatewayTmpl + httpVirtualServiceTmpl + secret, |
| templateVars: func(src echo.Callers, dests echo.Instances) map[string]interface{} { |
| return templateParams(proto, src, dests, nil) |
| }, |
| setupOpts: fqdnHostHeader, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: proto, |
| }, |
| }, |
| viaIngress: true, |
| workloadAgnostic: true, |
| }, |
| TrafficTestCase{ |
| name: fmt.Sprintf("%s scheme match", proto), |
| config: gatewayTmpl + httpVirtualServiceTmpl + secret, |
| templateVars: func(src echo.Callers, dests echo.Instances) map[string]interface{} { |
| params := templateParams(proto, src, dests, nil) |
| params["MatchScheme"] = strings.ToLower(string(proto)) |
| return params |
| }, |
| setupOpts: fqdnHostHeader, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Protocol: proto, |
| }, |
| Check: check.And( |
| check.OK(), |
| check.RequestHeader("Istio-Custom-Header", "user-defined-value")), |
| }, |
| // to keep tests fast, we only run the basic protocol test per-workload and scheme match once (per cluster) |
| targetMatchers: singleTarget, |
| viaIngress: true, |
| workloadAgnostic: true, |
| }, |
| ) |
| } |
| |
| return cases |
| } |
| |
| func XFFGatewayCase(apps *deployment.SingleNamespaceView, gateway string) []TrafficTestCase { |
| var cases []TrafficTestCase |
| |
| destinationSets := []echo.Instances{ |
| apps.A, |
| } |
| |
| for _, d := range destinationSets { |
| d := d |
| if len(d) == 0 { |
| continue |
| } |
| fqdn := d[0].Config().ClusterLocalFQDN() |
| cases = append(cases, TrafficTestCase{ |
| name: d[0].Config().Service, |
| config: httpGateway("*") + httpVirtualService("gateway", fqdn, d[0].PortForName("http").ServicePort), |
| call: apps.Naked[0].CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ServicePort: 80}, |
| Scheme: scheme.HTTP, |
| Address: gateway, |
| HTTP: echo.HTTP{ |
| Headers: headers.New(). |
| WithHost(fqdn). |
| With(headers.XForwardedFor, "56.5.6.7, 72.9.5.6, 98.1.2.3"). |
| Build(), |
| }, |
| Check: check.Each( |
| func(r echoClient.Response) error { |
| externalAddress, ok := r.RequestHeaders["X-Envoy-External-Address"] |
| if !ok { |
| return fmt.Errorf("missing X-Envoy-External-Address Header") |
| } |
| if err := ExpectString(externalAddress[0], "72.9.5.6", "envoy-external-address header"); err != nil { |
| return err |
| } |
| xffHeader, ok := r.RequestHeaders["X-Forwarded-For"] |
| if !ok { |
| return fmt.Errorf("missing X-Forwarded-For Header") |
| } |
| |
| xffIPs := strings.Split(xffHeader[0], ",") |
| if len(xffIPs) != 4 { |
| return fmt.Errorf("did not receive expected 4 hosts in X-Forwarded-For header") |
| } |
| |
| return ExpectString(strings.TrimSpace(xffIPs[1]), "72.9.5.6", "ip in xff header") |
| }), |
| }, |
| }) |
| } |
| return cases |
| } |
| |
| func envoyFilterCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| // Test adding envoyfilter to inbound and outbound route/cluster/listeners |
| cfg := ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: EnvoyFilter |
| metadata: |
| name: outbound |
| spec: |
| workloadSelector: |
| labels: |
| app: a |
| configPatches: |
| - applyTo: HTTP_FILTER |
| match: |
| context: SIDECAR_OUTBOUND |
| listener: |
| filterChain: |
| filter: |
| name: "envoy.filters.network.http_connection_manager" |
| subFilter: |
| name: "envoy.filters.http.router" |
| patch: |
| operation: INSERT_BEFORE |
| value: |
| name: envoy.lua |
| typed_config: |
| "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" |
| inlineCode: | |
| function envoy_on_request(request_handle) |
| request_handle:headers():add("x-lua-outbound", "hello world") |
| end |
| - applyTo: VIRTUAL_HOST |
| match: |
| context: SIDECAR_OUTBOUND |
| patch: |
| operation: MERGE |
| value: |
| request_headers_to_add: |
| - header: |
| key: x-vhost-outbound |
| value: "hello world" |
| - applyTo: CLUSTER |
| match: |
| context: SIDECAR_OUTBOUND |
| cluster: {} |
| patch: |
| operation: MERGE |
| value: |
| http2_protocol_options: {} |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: EnvoyFilter |
| metadata: |
| name: inbound |
| spec: |
| workloadSelector: |
| labels: |
| app: b |
| configPatches: |
| - applyTo: HTTP_FILTER |
| match: |
| context: SIDECAR_INBOUND |
| listener: |
| filterChain: |
| filter: |
| name: "envoy.filters.network.http_connection_manager" |
| subFilter: |
| name: "envoy.filters.http.router" |
| patch: |
| operation: INSERT_BEFORE |
| value: |
| name: envoy.lua |
| typed_config: |
| "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" |
| inlineCode: | |
| function envoy_on_request(request_handle) |
| request_handle:headers():add("x-lua-inbound", "hello world") |
| end |
| - applyTo: VIRTUAL_HOST |
| match: |
| context: SIDECAR_INBOUND |
| patch: |
| operation: MERGE |
| value: |
| request_headers_to_add: |
| - header: |
| key: x-vhost-inbound |
| value: "hello world" |
| - applyTo: CLUSTER |
| match: |
| context: SIDECAR_INBOUND |
| cluster: {} |
| patch: |
| operation: MERGE |
| value: |
| http2_protocol_options: {} |
| ` |
| for _, c := range apps.A { |
| cases = append(cases, TrafficTestCase{ |
| config: cfg, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| To: apps.B, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Check: check.And( |
| check.OK(), |
| check.Protocol("HTTP/2.0"), |
| check.RequestHeaders(map[string]string{ |
| "X-Vhost-Inbound": "hello world", |
| "X-Vhost-Outbound": "hello world", |
| "X-Lua-Inbound": "hello world", |
| "X-Lua-Outbound": "hello world", |
| }), |
| ), |
| }, |
| }) |
| } |
| return cases |
| } |
| |
| // hostCases tests different forms of host header to use |
| func hostCases(apps *deployment.SingleNamespaceView) ([]TrafficTestCase, error) { |
| var cases []TrafficTestCase |
| for _, c := range apps.A { |
| cfg := apps.Headless.Config() |
| port := ports.All().MustForName("auto-http").WorkloadPort |
| wl, err := apps.Headless[0].Workloads() |
| if err != nil { |
| return nil, err |
| } |
| if len(wl) == 0 { |
| return nil, fmt.Errorf("no workloads found") |
| } |
| address := wl[0].Address() |
| hosts := []string{ |
| cfg.ClusterLocalFQDN(), |
| fmt.Sprintf("%s:%d", cfg.ClusterLocalFQDN(), port), |
| fmt.Sprintf("%s.%s.svc", cfg.Service, cfg.Namespace.Name()), |
| fmt.Sprintf("%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port), |
| cfg.Service, |
| fmt.Sprintf("%s:%d", cfg.Service, port), |
| fmt.Sprintf("some-instances.%s:%d", cfg.ClusterLocalFQDN(), port), |
| fmt.Sprintf("some-instances.%s.%s.svc", cfg.Service, cfg.Namespace.Name()), |
| fmt.Sprintf("some-instances.%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port), |
| fmt.Sprintf("some-instances.%s", cfg.Service), |
| fmt.Sprintf("some-instances.%s:%d", cfg.Service, port), |
| address, |
| fmt.Sprintf("%s:%d", address, port), |
| } |
| for _, h := range hosts { |
| name := strings.Replace(h, address, "ip", -1) + "/auto-http" |
| cases = append(cases, TrafficTestCase{ |
| name: name, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| To: apps.Headless, |
| Port: echo.Port{ |
| Name: "auto-http", |
| }, |
| HTTP: echo.HTTP{ |
| Headers: HostHeader(h), |
| }, |
| Check: check.OK(), |
| }, |
| }) |
| } |
| port = ports.All().MustForName("http").WorkloadPort |
| hosts = []string{ |
| cfg.ClusterLocalFQDN(), |
| fmt.Sprintf("%s:%d", cfg.ClusterLocalFQDN(), port), |
| fmt.Sprintf("%s.%s.svc", cfg.Service, cfg.Namespace.Name()), |
| fmt.Sprintf("%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port), |
| cfg.Service, |
| fmt.Sprintf("%s:%d", cfg.Service, port), |
| fmt.Sprintf("some-instances.%s:%d", cfg.ClusterLocalFQDN(), port), |
| fmt.Sprintf("some-instances.%s.%s.svc", cfg.Service, cfg.Namespace.Name()), |
| fmt.Sprintf("some-instances.%s.%s.svc:%d", cfg.Service, cfg.Namespace.Name(), port), |
| fmt.Sprintf("some-instances.%s", cfg.Service), |
| fmt.Sprintf("some-instances.%s:%d", cfg.Service, port), |
| address, |
| fmt.Sprintf("%s:%d", address, port), |
| } |
| for _, h := range hosts { |
| name := strings.Replace(h, address, "ip", -1) + "/http" |
| cases = append(cases, TrafficTestCase{ |
| name: name, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| To: apps.Headless, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| HTTP: echo.HTTP{ |
| Headers: HostHeader(h), |
| }, |
| Check: check.OK(), |
| }, |
| }) |
| } |
| } |
| return cases, nil |
| } |
| |
| // serviceCases tests overlapping Services. There are a few cases. |
| // Consider we have our base service B, with service port P and target port T |
| // 1) Another service, B', with P -> T. In this case, both the listener and the cluster will conflict. |
| // Because everything is workload oriented, this is not a problem unless they try to make them different |
| // protocols (this is explicitly called out as "not supported") or control inbound connectionPool settings |
| // (which is moving to Sidecar soon) |
| // 2) Another service, B', with P -> T'. In this case, the listener will be distinct, since its based on the target. |
| // The cluster, however, will be shared, which is broken, because we should be forwarding to T when we call B, and T' when we call B'. |
| // 3) Another service, B', with P' -> T. In this case, the listener is shared. This is fine, with the exception of different protocols |
| // The cluster is distinct. |
| // 4) Another service, B', with P' -> T'. There is no conflicts here at all. |
| func serviceCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| for _, c := range apps.A { |
| c := c |
| |
| // Case 1 |
| // Identical to port "http" or service B, just behind another service name |
| svc := fmt.Sprintf(`apiVersion: v1 |
| kind: Service |
| metadata: |
| name: b-alt-1 |
| labels: |
| app: b |
| spec: |
| ports: |
| - name: http |
| port: %d |
| targetPort: %d |
| selector: |
| app: b`, ports.All().MustForName("http").ServicePort, ports.All().MustForName("http").WorkloadPort) |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("case 1 both match in cluster %v", c.Config().Cluster.StableName()), |
| config: svc, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Address: "b-alt-1", |
| Port: echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.HTTP}, |
| Timeout: time.Millisecond * 100, |
| Check: check.OK(), |
| }, |
| }) |
| |
| // Case 2 |
| // We match the service port, but forward to a different port |
| // Here we make the new target tcp so the test would fail if it went to the http port |
| svc = fmt.Sprintf(`apiVersion: v1 |
| kind: Service |
| metadata: |
| name: b-alt-2 |
| labels: |
| app: b |
| spec: |
| ports: |
| - name: tcp |
| port: %d |
| targetPort: %d |
| selector: |
| app: b`, ports.All().MustForName("http").ServicePort, ports.All().GetWorkloadOnlyPorts()[0].WorkloadPort) |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("case 2 service port match in cluster %v", c.Config().Cluster.StableName()), |
| config: svc, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Address: "b-alt-2", |
| Port: echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.TCP}, |
| Scheme: scheme.TCP, |
| Timeout: time.Millisecond * 100, |
| Check: check.OK(), |
| }, |
| }) |
| |
| // Case 3 |
| // We match the target port, but front with a different service port |
| svc = fmt.Sprintf(`apiVersion: v1 |
| kind: Service |
| metadata: |
| name: b-alt-3 |
| labels: |
| app: b |
| spec: |
| ports: |
| - name: http |
| port: 12345 |
| targetPort: %d |
| selector: |
| app: b`, ports.All().MustForName("http").WorkloadPort) |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("case 3 target port match in cluster %v", c.Config().Cluster.StableName()), |
| config: svc, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Address: "b-alt-3", |
| Port: echo.Port{ServicePort: 12345, Protocol: protocol.HTTP}, |
| Timeout: time.Millisecond * 100, |
| Check: check.OK(), |
| }, |
| }) |
| |
| // Case 4 |
| // Completely new set of ports |
| svc = fmt.Sprintf(`apiVersion: v1 |
| kind: Service |
| metadata: |
| name: b-alt-4 |
| labels: |
| app: b |
| spec: |
| ports: |
| - name: http |
| port: 12346 |
| targetPort: %d |
| selector: |
| app: b`, ports.All().GetWorkloadOnlyPorts()[1].WorkloadPort) |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("case 4 no match in cluster %v", c.Config().Cluster.StableName()), |
| config: svc, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Address: "b-alt-4", |
| Port: echo.Port{ServicePort: 12346, Protocol: protocol.HTTP}, |
| Timeout: time.Millisecond * 100, |
| Check: check.OK(), |
| }, |
| }) |
| } |
| |
| return cases |
| } |
| |
| // consistentHashCases tests destination rule's consistent hashing mechanism |
| func consistentHashCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| for _, app := range []echo.Instances{apps.A, apps.B} { |
| app := app |
| for _, c := range app { |
| c := c |
| |
| // First setup a service selecting a few services. This is needed to ensure we can load balance across many pods. |
| svcName := "consistent-hash" |
| if nw := c.Config().Cluster.NetworkName(); nw != "" { |
| svcName += "-" + nw |
| } |
| svc := tmpl.MustEvaluate(`apiVersion: v1 |
| kind: Service |
| metadata: |
| name: {{.Service}} |
| spec: |
| ports: |
| - name: http |
| port: {{.Port}} |
| targetPort: {{.TargetPort}} |
| - name: tcp |
| port: {{.TcpPort}} |
| targetPort: {{.TcpTargetPort}} |
| selector: |
| test.istio.io/class: standard |
| {{- if .Network }} |
| topology.istio.io/network: {{.Network}} |
| {{- end }} |
| `, map[string]interface{}{ |
| "Service": svcName, |
| "Network": c.Config().Cluster.NetworkName(), |
| "Port": ports.All().MustForName("http").ServicePort, |
| "TargetPort": ports.All().MustForName("http").WorkloadPort, |
| "TcpPort": ports.All().MustForName("tcp").ServicePort, |
| "TcpTargetPort": ports.All().MustForName("tcp").WorkloadPort, |
| "GrpcPort": ports.All().MustForName("grpc").ServicePort, |
| "GrpcTargetPort": ports.All().MustForName("grpc").WorkloadPort, |
| }) |
| |
| destRule := fmt.Sprintf(` |
| --- |
| apiVersion: networking.istio.io/v1beta1 |
| kind: DestinationRule |
| metadata: |
| name: %s |
| spec: |
| host: %s |
| trafficPolicy: |
| loadBalancer: |
| consistentHash: |
| {{. | indent 8}} |
| `, svcName, svcName) |
| // Add a negative test case. This ensures that the test is actually valid; its not a super trivial check |
| // and could be broken by having only 1 pod so its good to have this check in place |
| cases = append(cases, TrafficTestCase{ |
| name: "no consistent", |
| config: svc, |
| call: c.CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 10, |
| Address: svcName, |
| Port: echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.HTTP}, |
| Check: check.And( |
| check.OK(), |
| func(result echo.CallResult, rerr error) error { |
| err := ConsistentHostChecker.Check(result, rerr) |
| if err == nil { |
| return fmt.Errorf("expected inconsistent hash, but it was consistent") |
| } |
| return nil |
| }, |
| ), |
| }, |
| }) |
| callOpts := echo.CallOptions{ |
| Count: 10, |
| Address: svcName, |
| HTTP: echo.HTTP{ |
| Path: "/?some-query-param=bar", |
| Headers: headers.New().With("x-some-header", "baz").Build(), |
| }, |
| Port: echo.Port{ServicePort: ports.All().MustForName("http").ServicePort, Protocol: protocol.HTTP}, |
| Check: check.And( |
| check.OK(), |
| ConsistentHostChecker, |
| ), |
| } |
| tcpCallopts := echo.CallOptions{ |
| Count: 10, |
| Address: svcName, |
| Port: echo.Port{ServicePort: ports.All().MustForName("tcp").ServicePort, Protocol: protocol.TCP}, |
| Check: check.And( |
| check.OK(), |
| ConsistentHostChecker, |
| ), |
| } |
| if c.Config().WorkloadClass() == echo.Proxyless { |
| callOpts.Port = echo.Port{ServicePort: ports.All().MustForName("grpc").ServicePort, Protocol: protocol.GRPC} |
| } |
| // Setup tests for various forms of the API |
| // TODO: it may be necessary to vary the inputs of the hash and ensure we get a different backend |
| // But its pretty hard to test that, so for now just ensure we hit the same one. |
| cases = append(cases, TrafficTestCase{ |
| name: "source ip " + c.Config().Service, |
| config: svc + tmpl.MustEvaluate(destRule, "useSourceIp: true"), |
| call: c.CallOrFail, |
| opts: callOpts, |
| }, TrafficTestCase{ |
| name: "query param" + c.Config().Service, |
| config: svc + tmpl.MustEvaluate(destRule, "httpQueryParameterName: some-query-param"), |
| call: c.CallOrFail, |
| opts: callOpts, |
| }, TrafficTestCase{ |
| name: "http header" + c.Config().Service, |
| config: svc + tmpl.MustEvaluate(destRule, "httpHeaderName: x-some-header"), |
| call: c.CallOrFail, |
| opts: callOpts, |
| }, TrafficTestCase{ |
| name: "tcp source ip " + c.Config().Service, |
| config: svc + tmpl.MustEvaluate(destRule, "useSourceIp: true"), |
| call: c.CallOrFail, |
| opts: tcpCallopts, |
| skip: skip{ |
| skip: c.Config().WorkloadClass() == echo.Proxyless, |
| reason: "", // TODO: is this a bug or WAI? |
| }, |
| }) |
| } |
| } |
| |
| return cases |
| } |
| |
| var ConsistentHostChecker echo.Checker = func(result echo.CallResult, _ error) error { |
| hostnames := make([]string, len(result.Responses)) |
| for i, r := range result.Responses { |
| hostnames[i] = r.Hostname |
| } |
| scopes.Framework.Infof("requests landed on hostnames: %v", hostnames) |
| unique := sets.New(hostnames...).SortedList() |
| if len(unique) != 1 { |
| return fmt.Errorf("excepted only one destination, got: %v", unique) |
| } |
| return nil |
| } |
| |
| func flatten(clients ...[]echo.Instance) []echo.Instance { |
| var instances []echo.Instance |
| for _, c := range clients { |
| instances = append(instances, c...) |
| } |
| return instances |
| } |
| |
| // selfCallsCases checks that pods can call themselves |
| func selfCallsCases() []TrafficTestCase { |
| cases := []TrafficTestCase{ |
| // Calls to the Service will go through envoy outbound and inbound, so we get envoy headers added |
| { |
| name: "to service", |
| workloadAgnostic: true, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Check: check.And( |
| check.OK(), |
| check.RequestHeader("X-Envoy-Attempt-Count", "1")), |
| }, |
| }, |
| // Localhost calls will go directly to localhost, bypassing Envoy. No envoy headers added. |
| { |
| name: "to localhost", |
| workloadAgnostic: true, |
| setupOpts: func(_ echo.Caller, opts *echo.CallOptions) { |
| // the framework will try to set this when enumerating test cases |
| opts.To = nil |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Address: "localhost", |
| Port: echo.Port{ServicePort: 8080}, |
| Scheme: scheme.HTTP, |
| Check: check.And( |
| check.OK(), |
| check.RequestHeader("X-Envoy-Attempt-Count", "")), |
| }, |
| }, |
| // PodIP calls will go directly to podIP, bypassing Envoy. No envoy headers added. |
| { |
| name: "to podIP", |
| workloadAgnostic: true, |
| setupOpts: func(srcCaller echo.Caller, opts *echo.CallOptions) { |
| src := srcCaller.(echo.Instance) |
| workloads, _ := src.Workloads() |
| opts.Address = workloads[0].Address() |
| // the framework will try to set this when enumerating test cases |
| opts.To = nil |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Scheme: scheme.HTTP, |
| Port: echo.Port{ServicePort: 8080}, |
| Check: check.And( |
| check.OK(), |
| check.RequestHeader("X-Envoy-Attempt-Count", "")), |
| }, |
| }, |
| } |
| for i, tc := range cases { |
| // proxyless doesn't get valuable coverage here |
| tc.sourceMatchers = []match.Matcher{ |
| match.NotExternal, |
| match.NotNaked, |
| match.NotHeadless, |
| match.NotProxylessGRPC, |
| } |
| tc.comboFilters = []echotest.CombinationFilter{func(from echo.Instance, to echo.Instances) echo.Instances { |
| return match.ServiceName(from.NamespacedName()).GetMatches(to) |
| }} |
| cases[i] = tc |
| } |
| |
| return cases |
| } |
| |
| // TODO: merge with security TestReachability code |
| func protocolSniffingCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| |
| type protocolCase struct { |
| // The port we call |
| port string |
| // The actual type of traffic we send to the port |
| scheme scheme.Instance |
| } |
| protocols := []protocolCase{ |
| {"http", scheme.HTTP}, |
| {"auto-http", scheme.HTTP}, |
| {"tcp", scheme.TCP}, |
| {"auto-tcp", scheme.TCP}, |
| {"grpc", scheme.GRPC}, |
| {"auto-grpc", scheme.GRPC}, |
| } |
| |
| // so we can check all clusters are hit |
| for _, call := range protocols { |
| call := call |
| cases = append(cases, TrafficTestCase{ |
| skip: skip{ |
| skip: call.scheme == scheme.TCP, |
| reason: "https://github.com/istio/istio/issues/26798: enable sniffing tcp", |
| }, |
| name: call.port, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: call.port, |
| }, |
| Scheme: call.scheme, |
| Timeout: time.Second * 5, |
| }, |
| check: func(src echo.Caller, opts *echo.CallOptions) echo.Checker { |
| if call.scheme == scheme.TCP || src.(echo.Instance).Config().IsProxylessGRPC() { |
| // no host header for TCP |
| // TODO understand why proxyless adds the port to :authority md |
| return check.OK() |
| } |
| return check.And( |
| check.OK(), |
| check.Host(opts.GetHost())) |
| }, |
| comboFilters: func() []echotest.CombinationFilter { |
| if call.scheme != scheme.GRPC { |
| return []echotest.CombinationFilter{func(from echo.Instance, to echo.Instances) echo.Instances { |
| if from.Config().IsProxylessGRPC() && match.VM.Any(to) { |
| return nil |
| } |
| return to |
| }} |
| } |
| return nil |
| }(), |
| workloadAgnostic: true, |
| }) |
| } |
| |
| autoPort := ports.All().MustForName("auto-http") |
| httpPort := ports.All().MustForName("http") |
| // Tests for http1.0. Golang does not support 1.0 client requests at all |
| // To simulate these, we use TCP and hand-craft the requests. |
| cases = append(cases, TrafficTestCase{ |
| name: "http10 to http", |
| call: apps.A[0].CallOrFail, |
| opts: echo.CallOptions{ |
| To: apps.B, |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Scheme: scheme.TCP, |
| Message: `GET / HTTP/1.0 |
| `, |
| Timeout: time.Second * 5, |
| TCP: echo.TCP{ |
| // Explicitly declared as HTTP, so we always go through http filter which fails |
| ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.1 426 Upgrade Required`}, |
| }, |
| }, |
| }, |
| TrafficTestCase{ |
| name: "http10 to auto", |
| call: apps.A[0].CallOrFail, |
| opts: echo.CallOptions{ |
| To: apps.B, |
| Count: 1, |
| Port: echo.Port{ |
| Name: "auto-http", |
| }, |
| Scheme: scheme.TCP, |
| Message: `GET / HTTP/1.0 |
| `, |
| Timeout: time.Second * 5, |
| TCP: echo.TCP{ |
| // Auto should be detected as TCP |
| ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.0 200 OK`}, |
| }, |
| }, |
| }, |
| TrafficTestCase{ |
| name: "http10 to external", |
| call: apps.A[0].CallOrFail, |
| opts: echo.CallOptions{ |
| Address: apps.External.All[0].Address(), |
| HTTP: echo.HTTP{ |
| Headers: HostHeader(apps.External.All.Config().DefaultHostHeader), |
| }, |
| Port: httpPort, |
| Count: 1, |
| Scheme: scheme.TCP, |
| Message: `GET / HTTP/1.0 |
| `, |
| Timeout: time.Second * 5, |
| TCP: echo.TCP{ |
| // There is no VIP so we fall back to 0.0.0.0 listener which sniffs |
| ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.0 200 OK`}, |
| }, |
| }, |
| }, |
| TrafficTestCase{ |
| name: "http10 to external auto", |
| call: apps.A[0].CallOrFail, |
| opts: echo.CallOptions{ |
| Address: apps.External.All[0].Address(), |
| HTTP: echo.HTTP{ |
| Headers: HostHeader(apps.External.All.Config().DefaultHostHeader), |
| }, |
| Port: autoPort, |
| Count: 1, |
| Scheme: scheme.TCP, |
| Message: `GET / HTTP/1.0 |
| `, |
| Timeout: time.Second * 5, |
| TCP: echo.TCP{ |
| // Auto should be detected as TCP |
| ExpectedResponse: &wrappers.StringValue{Value: `HTTP/1.0 200 OK`}, |
| }, |
| }, |
| }, |
| ) |
| //check: func(src echo.Caller, dst echo.Instances, opts *echo.CallOptions) echo.Validator { |
| // if call.scheme == scheme.TCP || src.(echo.Instance).Config().ProxylessGRPC() { |
| // // no host header for TCP |
| // // TODO understand why proxyless adds the port to :authority md |
| // return echo.ExpectOK() |
| // } |
| // return echo.And( |
| // echo.ExpectOK(), |
| // echo.ExpectHost(opts.GetHost())) |
| //}, |
| //comboFilters: func() []echotest.CombinationFilter { |
| // if call.scheme != scheme.GRPC { |
| // return []echotest.CombinationFilter{func(from echo.Instance, to echo.Instances) echo.Instances { |
| // if from.Config().ProxylessGRPC() && to.ContainsMatch(echo.VM()) { |
| // return nil |
| // } |
| // return to |
| // }} |
| // } |
| // return nil |
| //}(), |
| return cases |
| } |
| |
| // Todo merge with security TestReachability code |
| func instanceIPTests(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var cases []TrafficTestCase |
| ipCases := []struct { |
| name string |
| endpoint string |
| disableSidecar bool |
| port string |
| code int |
| minIstioVersion string |
| }{ |
| // instance IP bind |
| { |
| name: "instance IP without sidecar", |
| disableSidecar: true, |
| port: "http-instance", |
| code: http.StatusOK, |
| }, |
| { |
| name: "instance IP with wildcard sidecar", |
| endpoint: "0.0.0.0", |
| port: "http-instance", |
| code: http.StatusOK, |
| }, |
| { |
| name: "instance IP with localhost sidecar", |
| endpoint: "127.0.0.1", |
| port: "http-instance", |
| code: http.StatusServiceUnavailable, |
| }, |
| { |
| name: "instance IP with empty sidecar", |
| endpoint: "", |
| port: "http-instance", |
| code: http.StatusOK, |
| }, |
| |
| // Localhost bind |
| { |
| name: "localhost IP without sidecar", |
| disableSidecar: true, |
| port: "http-localhost", |
| code: http.StatusServiceUnavailable, |
| // when testing with pre-1.10 versions this request succeeds |
| minIstioVersion: "1.10.0", |
| }, |
| { |
| name: "localhost IP with wildcard sidecar", |
| endpoint: "0.0.0.0", |
| port: "http-localhost", |
| code: http.StatusServiceUnavailable, |
| }, |
| { |
| name: "localhost IP with localhost sidecar", |
| endpoint: "127.0.0.1", |
| port: "http-localhost", |
| code: http.StatusOK, |
| }, |
| { |
| name: "localhost IP with empty sidecar", |
| endpoint: "", |
| port: "http-localhost", |
| code: http.StatusServiceUnavailable, |
| // when testing with pre-1.10 versions this request succeeds |
| minIstioVersion: "1.10.0", |
| }, |
| |
| // Wildcard bind |
| { |
| name: "wildcard IP without sidecar", |
| disableSidecar: true, |
| port: "http", |
| code: http.StatusOK, |
| }, |
| { |
| name: "wildcard IP with wildcard sidecar", |
| endpoint: "0.0.0.0", |
| port: "http", |
| code: http.StatusOK, |
| }, |
| { |
| name: "wildcard IP with localhost sidecar", |
| endpoint: "127.0.0.1", |
| port: "http", |
| code: http.StatusOK, |
| }, |
| { |
| name: "wildcard IP with empty sidecar", |
| endpoint: "", |
| port: "http", |
| code: http.StatusOK, |
| }, |
| } |
| for _, ipCase := range ipCases { |
| for _, client := range apps.A { |
| ipCase := ipCase |
| client := client |
| to := apps.B |
| var config string |
| if !ipCase.disableSidecar { |
| config = fmt.Sprintf(` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: Sidecar |
| metadata: |
| name: sidecar |
| spec: |
| workloadSelector: |
| labels: |
| app: b |
| egress: |
| - hosts: |
| - "./*" |
| ingress: |
| - port: |
| number: %d |
| protocol: HTTP |
| defaultEndpoint: %s:%d |
| `, ports.All().MustForName(ipCase.port).WorkloadPort, ipCase.endpoint, ports.All().MustForName(ipCase.port).WorkloadPort) |
| } |
| cases = append(cases, |
| TrafficTestCase{ |
| name: ipCase.name, |
| call: client.CallOrFail, |
| config: config, |
| opts: echo.CallOptions{ |
| Count: 1, |
| To: to, |
| Port: echo.Port{ |
| Name: ipCase.port, |
| }, |
| Scheme: scheme.HTTP, |
| Timeout: time.Second * 5, |
| Check: check.Status(ipCase.code), |
| }, |
| minIstioVersion: ipCase.minIstioVersion, |
| }) |
| } |
| } |
| |
| for _, tc := range cases { |
| // proxyless doesn't get valuable coverage here |
| tc.sourceMatchers = append(tc.sourceMatchers, match.NotProxylessGRPC) |
| tc.targetMatchers = append(tc.targetMatchers, match.NotProxylessGRPC) |
| } |
| |
| return cases |
| } |
| |
| type vmCase struct { |
| name string |
| from echo.Instance |
| to echo.Instances |
| host string |
| } |
| |
| func DNSTestCases(apps *deployment.SingleNamespaceView, cniEnabled bool) []TrafficTestCase { |
| makeSE := func(ips ...string) string { |
| return tmpl.MustEvaluate(` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: ServiceEntry |
| metadata: |
| name: dns |
| spec: |
| hosts: |
| - "fake.service.local" |
| addresses: |
| {{ range $ip := .IPs }} |
| - "{{$ip}}" |
| {{ end }} |
| resolution: STATIC |
| endpoints: [] |
| ports: |
| - number: 80 |
| name: http |
| protocol: HTTP |
| `, map[string]interface{}{"IPs": ips}) |
| } |
| var tcases []TrafficTestCase |
| ipv4 := "1.2.3.4" |
| ipv6 := "1234:1234:1234::1234:1234:1234" |
| dummyLocalhostServer := "127.0.0.1" |
| cases := []struct { |
| name string |
| // TODO(https://github.com/istio/istio/issues/30282) support multiple vips |
| ips string |
| protocol string |
| server string |
| skipCNI bool |
| expected []string |
| }{ |
| { |
| name: "tcp ipv4", |
| ips: ipv4, |
| expected: []string{ipv4}, |
| protocol: "tcp", |
| }, |
| { |
| name: "udp ipv4", |
| ips: ipv4, |
| expected: []string{ipv4}, |
| protocol: "udp", |
| }, |
| { |
| name: "tcp ipv6", |
| ips: ipv6, |
| expected: []string{ipv6}, |
| protocol: "tcp", |
| }, |
| { |
| name: "udp ipv6", |
| ips: ipv6, |
| expected: []string{ipv6}, |
| protocol: "udp", |
| }, |
| { |
| // We should only capture traffic to servers in /etc/resolv.conf nameservers |
| // This checks we do not capture traffic to other servers. |
| // This is important for cases like app -> istio dns server -> dnsmasq -> upstream |
| // If we captured all DNS traffic, we would loop dnsmasq traffic back to our server. |
| name: "tcp localhost server", |
| ips: ipv4, |
| expected: nil, |
| protocol: "tcp", |
| skipCNI: true, |
| server: dummyLocalhostServer, |
| }, |
| { |
| name: "udp localhost server", |
| ips: ipv4, |
| expected: nil, |
| protocol: "udp", |
| skipCNI: true, |
| server: dummyLocalhostServer, |
| }, |
| } |
| for _, client := range flatten(apps.VM, apps.A, apps.Tproxy) { |
| for _, tt := range cases { |
| if tt.skipCNI && cniEnabled { |
| continue |
| } |
| tt, client := tt, client |
| address := "fake.service.local?" |
| if tt.protocol != "" { |
| address += "&protocol=" + tt.protocol |
| } |
| if tt.server != "" { |
| address += "&server=" + tt.server |
| } |
| var checker echo.Checker = func(result echo.CallResult, _ error) error { |
| for _, r := range result.Responses { |
| if !reflect.DeepEqual(r.Body(), tt.expected) { |
| return fmt.Errorf("unexpected dns response: wanted %v, got %v", tt.expected, r.Body()) |
| } |
| } |
| return nil |
| } |
| if tt.expected == nil { |
| checker = check.Error() |
| } |
| tcases = append(tcases, TrafficTestCase{ |
| name: fmt.Sprintf("%s/%s", client.Config().Service, tt.name), |
| config: makeSE(tt.ips), |
| call: client.CallOrFail, |
| opts: echo.CallOptions{ |
| Scheme: scheme.DNS, |
| Count: 1, |
| Address: address, |
| Check: checker, |
| }, |
| }) |
| } |
| } |
| svcCases := []struct { |
| name string |
| protocol string |
| server string |
| }{ |
| { |
| name: "tcp", |
| protocol: "tcp", |
| }, |
| { |
| name: "udp", |
| protocol: "udp", |
| }, |
| } |
| for _, client := range flatten(apps.VM, apps.A, apps.Tproxy) { |
| for _, tt := range svcCases { |
| tt, client := tt, client |
| aInCluster := match.Cluster(client.Config().Cluster).GetMatches(apps.A) |
| if len(aInCluster) == 0 { |
| // The cluster doesn't contain A, but connects to a cluster containing A |
| aInCluster = match.Cluster(client.Config().Cluster.Config()).GetMatches(apps.A) |
| } |
| address := aInCluster[0].Config().ClusterLocalFQDN() + "?" |
| if tt.protocol != "" { |
| address += "&protocol=" + tt.protocol |
| } |
| if tt.server != "" { |
| address += "&server=" + tt.server |
| } |
| expected := aInCluster[0].Address() |
| tcases = append(tcases, TrafficTestCase{ |
| name: fmt.Sprintf("svc/%s/%s", client.Config().Service, tt.name), |
| call: client.CallOrFail, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Scheme: scheme.DNS, |
| Address: address, |
| Check: func(result echo.CallResult, _ error) error { |
| for _, r := range result.Responses { |
| ips := r.Body() |
| sort.Strings(ips) |
| exp := []string{expected} |
| if !reflect.DeepEqual(ips, exp) { |
| return fmt.Errorf("unexpected dns response: wanted %v, got %v", exp, ips) |
| } |
| } |
| return nil |
| }, |
| }, |
| }) |
| } |
| } |
| return tcases |
| } |
| |
| func VMTestCases(t framework.TestContext, vms echo.Instances, apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| var testCases []vmCase |
| |
| for _, vm := range vms { |
| testCases = append(testCases, |
| vmCase{ |
| name: "dns: VM to k8s cluster IP service name.namespace host", |
| from: vm, |
| to: apps.A, |
| host: deployment.ASvc + "." + apps.Namespace.Name(), |
| }, |
| vmCase{ |
| name: "dns: VM to k8s cluster IP service fqdn host", |
| from: vm, |
| to: apps.A, |
| host: apps.A[0].Config().ClusterLocalFQDN(), |
| }, |
| vmCase{ |
| name: "dns: VM to k8s cluster IP service short name host", |
| from: vm, |
| to: apps.A, |
| host: deployment.ASvc, |
| }, |
| vmCase{ |
| name: "dns: VM to k8s headless service", |
| from: vm, |
| to: match.Cluster(vm.Config().Cluster.Config()).GetMatches(apps.Headless), |
| host: apps.Headless.Config().ClusterLocalFQDN(), |
| }, |
| vmCase{ |
| name: "dns: VM to k8s statefulset service", |
| from: vm, |
| to: match.Cluster(vm.Config().Cluster.Config()).GetMatches(apps.StatefulSet), |
| host: apps.StatefulSet.Config().ClusterLocalFQDN(), |
| }, |
| // TODO(https://github.com/istio/istio/issues/32552) re-enable |
| //vmCase{ |
| // name: "dns: VM to k8s statefulset instance.service", |
| // from: vm, |
| // to: apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())), |
| // host: fmt.Sprintf("%s-v1-0.%s", StatefulSetSvc, StatefulSetSvc), |
| //}, |
| //vmCase{ |
| // name: "dns: VM to k8s statefulset instance.service.namespace", |
| // from: vm, |
| // to: apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())), |
| // host: fmt.Sprintf("%s-v1-0.%s.%s", StatefulSetSvc, StatefulSetSvc, apps.Namespace.Name()), |
| //}, |
| //vmCase{ |
| // name: "dns: VM to k8s statefulset instance.service.namespace.svc", |
| // from: vm, |
| // to: apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())), |
| // host: fmt.Sprintf("%s-v1-0.%s.%s.svc", StatefulSetSvc, StatefulSetSvc, apps.Namespace.Name()), |
| //}, |
| //vmCase{ |
| // name: "dns: VM to k8s statefulset instance FQDN", |
| // from: vm, |
| // to: apps.StatefulSet.Match(echo.Cluster(vm.Config().Cluster.Config())), |
| // host: fmt.Sprintf("%s-v1-0.%s", StatefulSetSvc, apps.StatefulSet[0].Config().ClusterLocalFQDN()), |
| //}, |
| ) |
| } |
| for _, podA := range apps.A { |
| testCases = append(testCases, vmCase{ |
| name: "k8s to vm", |
| from: podA, |
| to: vms, |
| }) |
| } |
| cases := make([]TrafficTestCase, 0) |
| for _, c := range testCases { |
| c := c |
| checker := check.OK() |
| if !match.Headless.Any(c.to) { |
| // headless load-balancing can be inconsistent |
| checker = check.And(checker, check.ReachedTargetClusters(t.AllClusters())) |
| } |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("%s from %s", c.name, c.from.Config().Cluster.StableName()), |
| call: c.from.CallOrFail, |
| opts: echo.CallOptions{ |
| // assume that all echos in `to` only differ in which cluster they're deployed in |
| To: c.to, |
| Port: echo.Port{ |
| Name: "http", |
| }, |
| Address: c.host, |
| Count: callCountMultiplier * c.to.MustWorkloads().Clusters().Len(), |
| Check: checker, |
| }, |
| }) |
| } |
| return cases |
| } |
| |
| func destinationRule(app, mode string) string { |
| return fmt.Sprintf(`apiVersion: networking.istio.io/v1beta1 |
| kind: DestinationRule |
| metadata: |
| name: %s |
| spec: |
| host: %s |
| trafficPolicy: |
| tls: |
| mode: %s |
| --- |
| `, app, app, mode) |
| } |
| |
| const useClientProtocolDestinationRuleTmpl = `apiVersion: networking.istio.io/v1beta1 |
| kind: DestinationRule |
| metadata: |
| name: use-client-protocol |
| spec: |
| host: {{.VirtualServiceHost}} |
| trafficPolicy: |
| tls: |
| mode: DISABLE |
| connectionPool: |
| http: |
| useClientProtocol: true |
| --- |
| ` |
| |
| func useClientProtocolDestinationRule(app string) string { |
| return tmpl.MustEvaluate(useClientProtocolDestinationRuleTmpl, map[string]string{"VirtualServiceHost": app}) |
| } |
| |
| func idletimeoutDestinationRule(name, app string) string { |
| return fmt.Sprintf(`apiVersion: networking.istio.io/v1beta1 |
| kind: DestinationRule |
| metadata: |
| name: %s |
| spec: |
| host: %s |
| trafficPolicy: |
| tls: |
| mode: DISABLE |
| connectionPool: |
| http: |
| idleTimeout: 100s |
| --- |
| `, name, app) |
| } |
| |
| func peerAuthentication(app, mode string) string { |
| return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1 |
| kind: PeerAuthentication |
| metadata: |
| name: %s |
| spec: |
| selector: |
| matchLabels: |
| app: %s |
| mtls: |
| mode: %s |
| --- |
| `, app, app, mode) |
| } |
| |
| func globalPeerAuthentication(mode string) string { |
| return fmt.Sprintf(`apiVersion: security.istio.io/v1beta1 |
| kind: PeerAuthentication |
| metadata: |
| name: default |
| spec: |
| mtls: |
| mode: %s |
| --- |
| `, mode) |
| } |
| |
| func serverFirstTestCases(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| cases := make([]TrafficTestCase, 0) |
| from := apps.A |
| to := apps.C |
| configs := []struct { |
| port string |
| dest string |
| auth string |
| checker echo.Checker |
| }{ |
| // TODO: All these cases *should* succeed (except the TLS mismatch cases) - but don't due to issues in our implementation |
| |
| // For auto port, outbound request will be delayed by the protocol sniffer, regardless of configuration |
| {"auto-tcp-server", "DISABLE", "DISABLE", check.Error()}, |
| {"auto-tcp-server", "DISABLE", "PERMISSIVE", check.Error()}, |
| {"auto-tcp-server", "DISABLE", "STRICT", check.Error()}, |
| {"auto-tcp-server", "ISTIO_MUTUAL", "DISABLE", check.Error()}, |
| {"auto-tcp-server", "ISTIO_MUTUAL", "PERMISSIVE", check.Error()}, |
| {"auto-tcp-server", "ISTIO_MUTUAL", "STRICT", check.Error()}, |
| |
| // These is broken because we will still enable inbound sniffing for the port. Since there is no tls, |
| // there is no server-first "upgrading" to client-first |
| {"tcp-server", "DISABLE", "DISABLE", check.OK()}, |
| {"tcp-server", "DISABLE", "PERMISSIVE", check.Error()}, |
| |
| // Expected to fail, incompatible configuration |
| {"tcp-server", "DISABLE", "STRICT", check.Error()}, |
| {"tcp-server", "ISTIO_MUTUAL", "DISABLE", check.Error()}, |
| |
| // In these cases, we expect success |
| // There is no sniffer on either side |
| {"tcp-server", "DISABLE", "DISABLE", check.OK()}, |
| |
| // On outbound, we have no sniffer involved |
| // On inbound, the request is TLS, so its not server first |
| {"tcp-server", "ISTIO_MUTUAL", "PERMISSIVE", check.OK()}, |
| {"tcp-server", "ISTIO_MUTUAL", "STRICT", check.OK()}, |
| } |
| for _, client := range from { |
| for _, c := range configs { |
| client, c := client, c |
| cases = append(cases, TrafficTestCase{ |
| name: fmt.Sprintf("%v:%v/%v", c.port, c.dest, c.auth), |
| skip: skip{ |
| skip: apps.All.Instances().Clusters().IsMulticluster(), |
| reason: "https://github.com/istio/istio/issues/37305: stabilize tcp connection breaks", |
| }, |
| config: destinationRule(to.Config().Service, c.dest) + peerAuthentication(to.Config().Service, c.auth), |
| call: client.CallOrFail, |
| opts: echo.CallOptions{ |
| To: to, |
| Port: echo.Port{ |
| Name: c.port, |
| }, |
| Scheme: scheme.TCP, |
| // Inbound timeout is 1s. We want to test this does not hit the listener filter timeout |
| Timeout: time.Millisecond * 100, |
| Count: 1, |
| Check: c.checker, |
| }, |
| }) |
| } |
| } |
| |
| return cases |
| } |
| |
| func jwtClaimRoute(apps *deployment.SingleNamespaceView) []TrafficTestCase { |
| configRoute := ` |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: Gateway |
| metadata: |
| name: gateway |
| spec: |
| selector: |
| istio: ingressgateway |
| servers: |
| - port: |
| number: 80 |
| name: http |
| protocol: HTTP |
| hosts: |
| - "*" |
| --- |
| apiVersion: networking.istio.io/v1alpha3 |
| kind: VirtualService |
| metadata: |
| name: default |
| spec: |
| hosts: |
| - foo.bar |
| gateways: |
| - gateway |
| http: |
| - match: |
| - uri: |
| prefix: / |
| {{- if .Headers }} |
| headers: |
| {{- range $data := .Headers }} |
| "{{$data.Name}}": |
| {{$data.Match}}: {{$data.Value}} |
| {{- end }} |
| {{- end }} |
| {{- if .WithoutHeaders }} |
| withoutHeaders: |
| {{- range $data := .WithoutHeaders }} |
| "{{$data.Name}}": |
| {{$data.Match}}: {{$data.Value}} |
| {{- end }} |
| {{- end }} |
| route: |
| - destination: |
| host: {{ .dstSvc }} |
| --- |
| ` |
| configAll := configRoute + ` |
| apiVersion: security.istio.io/v1beta1 |
| kind: RequestAuthentication |
| metadata: |
| name: default |
| namespace: dubbo-system |
| spec: |
| jwtRules: |
| - issuer: "test-issuer-1@istio.io" |
| jwksUri: "https://raw.githubusercontent.com/istio/istio/master/tests/common/jwt/jwks.json" |
| --- |
| ` |
| podB := []match.Matcher{match.ServiceName(apps.B.NamespacedName())} |
| headersWithToken := map[string][]string{ |
| "Host": {"foo.bar"}, |
| "Authorization": {"Bearer " + jwt.TokenIssuer1WithNestedClaims1}, |
| } |
| headersWithInvalidToken := map[string][]string{ |
| "Host": {"foo.bar"}, |
| "Authorization": {"Bearer " + jwt.TokenExpired}, |
| } |
| headersWithNoToken := map[string][]string{"Host": {"foo.bar"}} |
| headersWithNoTokenButSameHeader := map[string][]string{ |
| "Host": {"foo.bar"}, |
| "request.auth.claims.nested.key1": {"valueA"}, |
| } |
| |
| type configData struct { |
| Name, Match, Value string |
| } |
| cases := []TrafficTestCase{ |
| { |
| name: "matched with nested claims:200", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusOK), |
| }, |
| }, |
| { |
| name: "matched with single claim:200", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.sub", "prefix", "sub"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusOK), |
| }, |
| }, |
| { |
| name: "matched multiple claims:200", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{ |
| {"@request.auth.claims.nested.key1", "exact", "valueA"}, |
| {"@request.auth.claims.sub", "prefix", "sub"}, |
| }, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusOK), |
| }, |
| }, |
| { |
| name: "matched without claim:200", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "WithoutHeaders": []configData{{"@request.auth.claims.nested.key1", "exact", "value-not-matched"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusOK), |
| }, |
| }, |
| { |
| name: "unmatched without claim:404", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "WithoutHeaders": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| }, |
| { |
| name: "matched both with and without claims:200", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.sub", "prefix", "sub"}}, |
| "WithoutHeaders": []configData{{"@request.auth.claims.nested.key1", "exact", "value-not-matched"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusOK), |
| }, |
| }, |
| { |
| name: "unmatched multiple claims:404", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{ |
| {"@request.auth.claims.nested.key1", "exact", "valueA"}, |
| {"@request.auth.claims.sub", "prefix", "value-not-matched"}, |
| }, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| }, |
| { |
| name: "unmatched token:404", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.sub", "exact", "value-not-matched"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| }, |
| { |
| name: "unmatched with invalid token:401", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithInvalidToken, |
| }, |
| Check: check.Status(http.StatusUnauthorized), |
| }, |
| }, |
| { |
| name: "unmatched with no token:404", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithNoToken, |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| }, |
| { |
| name: "unmatched with no token but same header:404", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configAll, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| // Include a header @request.auth.claims.nested.key1 and value same as the JWT claim, should not be routed. |
| Headers: headersWithNoTokenButSameHeader, |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| }, |
| { |
| name: "unmatched with no request authentication:404", |
| targetMatchers: podB, |
| workloadAgnostic: true, |
| viaIngress: true, |
| config: configRoute, |
| templateVars: func(src echo.Callers, dest echo.Instances) map[string]interface{} { |
| return map[string]interface{}{ |
| "Headers": []configData{{"@request.auth.claims.nested.key1", "exact", "valueA"}}, |
| } |
| }, |
| opts: echo.CallOptions{ |
| Count: 1, |
| Port: echo.Port{ |
| Name: "http", |
| Protocol: protocol.HTTP, |
| }, |
| HTTP: echo.HTTP{ |
| Headers: headersWithToken, |
| }, |
| Check: check.Status(http.StatusNotFound), |
| }, |
| }, |
| } |
| return cases |
| } |