istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/telemetry/policy/helper_test.go (about) 1 //go:build integ 2 // +build integ 3 4 // Copyright Istio Authors. All Rights Reserved. 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 18 package policy 19 20 import ( 21 "fmt" 22 "os" 23 "path" 24 "strconv" 25 "testing" 26 27 "istio.io/api/annotation" 28 "istio.io/istio/pkg/config/protocol" 29 "istio.io/istio/pkg/http/headers" 30 "istio.io/istio/pkg/test/echo/common" 31 "istio.io/istio/pkg/test/env" 32 "istio.io/istio/pkg/test/framework" 33 "istio.io/istio/pkg/test/framework/components/echo" 34 "istio.io/istio/pkg/test/framework/components/echo/deployment" 35 "istio.io/istio/pkg/test/framework/components/environment/kube" 36 "istio.io/istio/pkg/test/framework/components/namespace" 37 "istio.io/istio/pkg/test/framework/components/prometheus" 38 "istio.io/istio/pkg/test/util/tmpl" 39 util "istio.io/istio/tests/integration/telemetry" 40 ) 41 42 const ( 43 // ServiceEntry is used to create conflicts on various ports 44 // As defined below, the tcp-conflict and https-conflict ports are 9443 and 9091 45 ServiceEntry = ` 46 apiVersion: networking.istio.io/v1alpha3 47 kind: ServiceEntry 48 metadata: 49 name: http 50 spec: 51 hosts: 52 - istio.io 53 location: MESH_EXTERNAL 54 ports: 55 - name: http-for-https 56 number: 9443 57 protocol: HTTP 58 - name: http-for-tcp 59 number: 9091 60 protocol: HTTP 61 resolution: DNS 62 ` 63 SidecarScope = ` 64 apiVersion: networking.istio.io/v1alpha3 65 kind: Sidecar 66 metadata: 67 name: restrict-to-service-entry-namespace 68 spec: 69 egress: 70 - hosts: 71 - "{{.ImportNamespace}}/*" 72 - "istio-system/*" 73 outboundTrafficPolicy: 74 mode: "{{.TrafficPolicyMode}}" 75 ` 76 77 Gateway = ` 78 apiVersion: networking.istio.io/v1alpha3 79 kind: Gateway 80 metadata: 81 name: istio-egressgateway 82 spec: 83 selector: 84 istio: egressgateway 85 servers: 86 - port: 87 number: 80 88 name: http 89 protocol: HTTP 90 hosts: 91 - "some-external-site.com" 92 --- 93 apiVersion: networking.istio.io/v1alpha3 94 kind: VirtualService 95 metadata: 96 name: route-via-egressgateway 97 spec: 98 hosts: 99 - "some-external-site.com" 100 gateways: 101 - istio-egressgateway 102 - mesh 103 http: 104 - match: 105 - gateways: 106 - mesh # from sidecars, route to egress gateway service 107 port: 80 108 route: 109 - destination: 110 host: istio-egressgateway.istio-system.svc.cluster.local 111 port: 112 number: 80 113 weight: 100 114 - match: 115 - gateways: 116 - istio-egressgateway 117 port: 80 118 route: 119 - destination: 120 host: some-external-site.com 121 headers: 122 request: 123 add: 124 handled-by-egress-gateway: "true" 125 --- 126 apiVersion: networking.istio.io/v1alpha3 127 kind: ServiceEntry 128 metadata: 129 name: ext-service-entry 130 spec: 131 hosts: 132 - "some-external-site.com" 133 location: MESH_EXTERNAL 134 endpoints: 135 - address: destination.{{.AppNamespace}}.svc.cluster.local 136 network: external 137 ports: 138 - number: 80 139 name: http 140 resolution: DNS 141 ` 142 ) 143 144 // TestCase represents what is being tested 145 type TestCase struct { 146 Name string 147 PortName string 148 HTTP2 bool 149 Host string 150 Expected Expected 151 } 152 153 // Expected contains the metric and query to run against 154 // prometheus to validate that expected telemetry information was gathered; 155 // as well as the http response code 156 type Expected struct { 157 Query prometheus.Query 158 StatusCode int 159 Metric string 160 PromQueryFormat string 161 Protocol string 162 RequestHeaders map[string]string 163 } 164 165 // TrafficPolicy is the mode of the outbound traffic policy to use 166 // when configuring the sidecar for the client 167 type TrafficPolicy string 168 169 const ( 170 AllowAny TrafficPolicy = "ALLOW_ANY" 171 RegistryOnly TrafficPolicy = "REGISTRY_ONLY" 172 ) 173 174 // String implements fmt.Stringer 175 func (t TrafficPolicy) String() string { 176 return string(t) 177 } 178 179 // We want to test "external" traffic. To do this without actually hitting an external endpoint, 180 // we can import only the service namespace, so the apps are not known 181 func createSidecarScope(t framework.TestContext, tPolicy TrafficPolicy, appsNamespace namespace.Instance, serviceNamespace namespace.Instance) { 182 args := map[string]string{"ImportNamespace": serviceNamespace.Name(), "TrafficPolicyMode": tPolicy.String()} 183 if err := t.ConfigIstio().Eval(appsNamespace.Name(), args, SidecarScope).Apply(); err != nil { 184 t.Errorf("failed to apply service entries: %v", err) 185 } 186 } 187 188 func mustReadCert(t framework.TestContext, f string) string { 189 t.Helper() 190 b, err := os.ReadFile(path.Join(env.IstioSrc, "tests/testdata/certs", f)) 191 if err != nil { 192 t.Fatalf("failed to read %v: %v", f, err) 193 } 194 return string(b) 195 } 196 197 // We want to test "external" traffic. To do this without actually hitting an external endpoint, 198 // we can import only the service namespace, so the apps are not known 199 func createGateway(t framework.TestContext, appsNamespace namespace.Instance, serviceNamespace namespace.Instance) { 200 t.Helper() 201 b := tmpl.EvaluateOrFail(t, Gateway, map[string]string{"AppNamespace": appsNamespace.Name()}) 202 if err := t.ConfigIstio().YAML(serviceNamespace.Name(), b).Apply(); err != nil { 203 t.Fatalf("failed to apply gateway: %v. template: %v", err, b) 204 } 205 } 206 207 // TODO support native environment for registry only/gateway. Blocked by #13177 because the listeners for native use static 208 // routes and this test relies on the dynamic routes sent through pilot to allow external traffic. 209 210 func RunExternalRequest(t *testing.T, cases []*TestCase, prometheus prometheus.Instance, mode TrafficPolicy) { 211 t.Helper() 212 // Testing of Blackhole and Passthrough clusters: 213 // Setup of environment: 214 // 1. client and destination are deployed to app-1-XXXX namespace 215 // 2. client is restricted to talk to destination via Sidecar scope where outbound policy is set (ALLOW_ANY, REGISTRY_ONLY) 216 // and clients' egress can only be to service-2-XXXX/* and istio-system/* 217 // 3. a namespace service-2-YYYY is created 218 // 4. A gateway is put in service-2-YYYY where its host is set for some-external-site.com on port 80 and 443 219 // 3. a VirtualService is also created in service-2-XXXX to: 220 // a) route requests for some-external-site.com to the istio-egressgateway 221 // * if the request on port 80, then it will add an http header `handled-by-egress-gateway` 222 // b) from the egressgateway it will forward the request to the destination pod deployed in the app-1-XXX 223 // namespace 224 225 // Test cases: 226 // 1. http case: 227 // client -------> Hits listener 0.0.0.0_80 cluster 228 // Metric is istio_requests_total i.e. HTTP 229 // 230 // 2. https case: 231 // client ----> Hits no listener -> 0.0.0.0_150001 -> ALLOW_ANY/REGISTRY_ONLY 232 // Metric is istio_tcp_connections_closed_total i.e. TCP 233 // 234 // 3. https conflict case: 235 // client ----> Hits listener 0.0.0.0_9443 236 // Metric is istio_tcp_connections_closed_total i.e. TCP 237 // 238 // 4. http_egress 239 // client ) ---HTTP request (Host: some-external-site.com----> Hits listener 0.0.0.0_80 -> 240 // VS Routing (add Egress Header) --> Egress Gateway --> destination 241 // Metric is istio_requests_total i.e. HTTP with destination as destination 242 // 243 // 5. TCP 244 // client ---TCP request at port 9090----> Matches no listener -> 0.0.0.0_150001 -> ALLOW_ANY/REGISTRY_ONLY 245 // Metric is istio_tcp_connections_closed_total i.e. TCP 246 // 247 // 5. TCP conflict 248 // client ---TCP request at port 9091 ----> Hits listener 0.0.0.0_9091 -> ALLOW_ANY/REGISTRY_ONLY 249 // Metric is istio_tcp_connections_closed_total i.e. TCP 250 // 251 framework. 252 NewTest(t). 253 Run(func(t framework.TestContext) { 254 client, to := setupEcho(t, mode) 255 256 for _, tc := range cases { 257 t.NewSubTest(tc.Name).Run(func(t framework.TestContext) { 258 client.CallOrFail(t, echo.CallOptions{ 259 To: to, 260 Count: 1, 261 Port: echo.Port{ 262 Name: tc.PortName, 263 }, 264 HTTP: echo.HTTP{ 265 HTTP2: tc.HTTP2, 266 Headers: headers.New().WithHost(tc.Host).Build(), 267 }, 268 Check: func(result echo.CallResult, err error) error { 269 // the expected response from a blackhole test case will have err 270 // set; use the length of the expected code to ignore this condition 271 if err != nil && tc.Expected.StatusCode > 0 { 272 return fmt.Errorf("request failed: %v", err) 273 } 274 codeStr := strconv.Itoa(tc.Expected.StatusCode) 275 for i, r := range result.Responses { 276 if codeStr != r.Code { 277 return fmt.Errorf("response[%d] received status code %s, expected %d", i, r.Code, tc.Expected.StatusCode) 278 } 279 for k, v := range tc.Expected.RequestHeaders { 280 if got := r.RequestHeaders.Get(k); got != v { 281 return fmt.Errorf("expected metadata %v=%v, got %q", k, v, got) 282 } 283 } 284 } 285 return nil 286 }, 287 }) 288 289 if tc.Expected.Query.Metric != "" { 290 util.ValidateMetric(t, t.Clusters().Default(), prometheus, tc.Expected.Query, 1) 291 } 292 }) 293 } 294 }) 295 } 296 297 func setupEcho(t framework.TestContext, mode TrafficPolicy) (echo.Instance, echo.Target) { 298 t.Helper() 299 appsNamespace := namespace.NewOrFail(t, t, namespace.Config{ 300 Prefix: "app", 301 Inject: true, 302 }) 303 serviceNamespace := namespace.NewOrFail(t, t, namespace.Config{ 304 Prefix: "service", 305 Inject: true, 306 }) 307 308 // External traffic should work even if we have service entries on the same ports 309 createSidecarScope(t, mode, appsNamespace, serviceNamespace) 310 311 var client, dest echo.Instance 312 deployment.New(t). 313 With(&client, echo.Config{ 314 Service: "client", 315 Namespace: appsNamespace, 316 Subsets: []echo.SubsetConfig{{}}, 317 }). 318 With(&dest, echo.Config{ 319 Service: "destination", 320 Namespace: appsNamespace, 321 Subsets: []echo.SubsetConfig{{Annotations: map[string]string{annotation.SidecarInject.Name: "false"}}}, 322 Ports: []echo.Port{ 323 { 324 // Plain HTTP port, will match no listeners and fall through 325 Name: "http", 326 Protocol: protocol.HTTP, 327 ServicePort: 80, 328 WorkloadPort: 8080, 329 }, 330 { 331 // HTTPS port, will match no listeners and fall through 332 Name: "https", 333 Protocol: protocol.HTTPS, 334 ServicePort: 443, 335 WorkloadPort: 8443, 336 TLS: true, 337 }, 338 { 339 // HTTPS port, there will be an HTTP service defined on this port that will match 340 Name: "https-conflict", 341 Protocol: protocol.HTTPS, 342 ServicePort: 9443, 343 TLS: true, 344 }, 345 { 346 // TCP port, will match no listeners and fall through 347 Name: "tcp", 348 Protocol: protocol.TCP, 349 ServicePort: 9090, 350 }, 351 { 352 // TCP port, there will be an HTTP service defined on this port that will match 353 Name: "tcp-conflict", 354 Protocol: protocol.TCP, 355 ServicePort: 9091, 356 }, 357 }, 358 TLSSettings: &common.TLSSettings{ 359 // Echo has these test certs baked into the docker image 360 ClientCert: mustReadCert(t, "cert.crt"), 361 Key: mustReadCert(t, "cert.key"), 362 }, 363 }).BuildOrFail(t) 364 365 if err := t.ConfigIstio().YAML(serviceNamespace.Name(), ServiceEntry).Apply(); err != nil { 366 t.Errorf("failed to apply service entries: %v", err) 367 } 368 369 if _, isKube := t.Environment().(*kube.Environment); isKube { 370 createGateway(t, appsNamespace, serviceNamespace) 371 } 372 return client, dest 373 }