istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/grpcgen/grpcecho_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package grpcgen_test 15 16 import ( 17 "context" 18 "fmt" 19 "math" 20 "net" 21 "runtime" 22 "strconv" 23 "sync" 24 "testing" 25 "time" 26 27 "google.golang.org/grpc" 28 // To install the xds resolvers and balancers. 29 _ "google.golang.org/grpc/xds" 30 31 networking "istio.io/api/networking/v1alpha3" 32 "istio.io/istio/pilot/test/xds" 33 "istio.io/istio/pkg/config" 34 "istio.io/istio/pkg/config/protocol" 35 "istio.io/istio/pkg/config/schema/gvk" 36 "istio.io/istio/pkg/test/echo" 37 "istio.io/istio/pkg/test/echo/common" 38 "istio.io/istio/pkg/test/echo/proto" 39 "istio.io/istio/pkg/test/echo/server/endpoint" 40 "istio.io/istio/pkg/test/util/retry" 41 ) 42 43 type echoCfg struct { 44 version string 45 namespace string 46 tls bool 47 } 48 49 type configGenTest struct { 50 *testing.T 51 endpoints []endpoint.Instance 52 ds *xds.FakeDiscoveryServer 53 xdsPort int 54 } 55 56 // newConfigGenTest creates a FakeDiscoveryServer that listens for gRPC on grpcXdsAddr 57 // For each of the given servers, we serve echo (only supporting Echo, no ForwardEcho) and 58 // create a corresponding WorkloadEntry. The WorkloadEntry will have the given format: 59 // 60 // meta: 61 // name: echo-{generated portnum}-{server.version} 62 // namespace: {server.namespace or "default"} 63 // labels: {"app": "grpc", "version": "{server.version}"} 64 // spec: 65 // address: {grpcEchoHost} 66 // ports: 67 // grpc: {generated portnum} 68 func newConfigGenTest(t *testing.T, discoveryOpts xds.FakeOptions, servers ...echoCfg) *configGenTest { 69 if runtime.GOOS == "darwin" && len(servers) > 1 { 70 // TODO always skip if this breaks anywhere else 71 t.Skip("cannot use 127.0.0.2-255 on OSX without manual setup") 72 } 73 74 cgt := &configGenTest{T: t} 75 wg := sync.WaitGroup{} 76 var cfgs []config.Config 77 78 discoveryOpts.ListenerBuilder = func() (net.Listener, error) { 79 return net.Listen("tcp", "127.0.0.1:0") 80 } 81 // Start XDS server 82 cgt.ds = xds.NewFakeDiscoveryServer(t, discoveryOpts) 83 _, xdsPorts, _ := net.SplitHostPort(cgt.ds.Listener.Addr().String()) 84 xdsPort, _ := strconv.Atoi(xdsPorts) 85 cgt.xdsPort = xdsPort 86 for i, s := range servers { 87 if s.namespace == "" { 88 s.namespace = "default" 89 } 90 // TODO this breaks without extra ifonfig aliases on OSX, and probably elsewhere 91 ip := fmt.Sprintf("127.0.0.%d", i+1) 92 93 ep, err := endpoint.New(endpoint.Config{ 94 Port: &common.Port{ 95 Name: "grpc", 96 Port: 0, 97 Protocol: protocol.GRPC, 98 XDSServer: true, 99 XDSReadinessTLS: s.tls, 100 XDSTestBootstrap: GRPCBootstrap("echo-"+s.version, s.namespace, ip, xdsPort), 101 }, 102 ListenerIP: ip, 103 Version: s.version, 104 }) 105 if err != nil { 106 t.Fatal(err) 107 } 108 wg.Add(1) 109 if err := ep.Start(func() { 110 wg.Done() 111 }); err != nil { 112 t.Fatal(err) 113 } 114 115 cfgs = append(cfgs, makeWE(s, ip, ep.GetConfig().Port.Port)) 116 cgt.endpoints = append(cgt.endpoints, ep) 117 t.Cleanup(func() { 118 if err := ep.Close(); err != nil { 119 t.Errorf("failed to close endpoint %s: %v", ip, err) 120 } 121 }) 122 } 123 for _, cfg := range cfgs { 124 if _, err := cgt.ds.Env().Create(cfg); err != nil { 125 t.Fatalf("failed to create config %v: %v", cfg.Name, err) 126 } 127 } 128 // we know onReady will get called because there are internal timeouts for this 129 wg.Wait() 130 return cgt 131 } 132 133 func makeWE(s echoCfg, host string, port int) config.Config { 134 ns := "default" 135 if s.namespace != "" { 136 ns = s.namespace 137 } 138 return config.Config{ 139 Meta: config.Meta{ 140 Name: fmt.Sprintf("echo-%d-%s", port, s.version), 141 Namespace: ns, 142 GroupVersionKind: gvk.WorkloadEntry, 143 Labels: map[string]string{ 144 "app": "echo", 145 "version": s.version, 146 }, 147 }, 148 Spec: &networking.WorkloadEntry{ 149 Address: host, 150 Ports: map[string]uint32{"grpc": uint32(port)}, 151 }, 152 } 153 } 154 155 func (t *configGenTest) dialEcho(addr string) *echo.Client { 156 resolver := resolverForTest(t, t.xdsPort, "default") 157 out, err := echo.New(addr, nil, grpc.WithResolvers(resolver)) 158 if err != nil { 159 t.Fatal(err) 160 } 161 return out 162 } 163 164 func TestTrafficShifting(t *testing.T) { 165 tt := newConfigGenTest(t, xds.FakeOptions{ 166 KubernetesObjectString: ` 167 apiVersion: v1 168 kind: Service 169 metadata: 170 labels: 171 app: echo-app 172 name: echo-app 173 namespace: default 174 spec: 175 clusterIP: 1.2.3.4 176 selector: 177 app: echo 178 ports: 179 - name: grpc 180 targetPort: grpc 181 port: 7070 182 `, 183 ConfigString: ` 184 apiVersion: networking.istio.io/v1alpha3 185 kind: DestinationRule 186 metadata: 187 name: echo-dr 188 namespace: default 189 spec: 190 host: echo-app.default.svc.cluster.local 191 subsets: 192 - name: v1 193 labels: 194 version: v1 195 - name: v2 196 labels: 197 version: v2 198 --- 199 apiVersion: networking.istio.io/v1alpha3 200 kind: VirtualService 201 metadata: 202 name: echo-vs 203 namespace: default 204 spec: 205 hosts: 206 - echo-app.default.svc.cluster.local 207 http: 208 - route: 209 - destination: 210 host: echo-app.default.svc.cluster.local 211 subset: v1 212 weight: 20 213 - destination: 214 host: echo-app.default.svc.cluster.local 215 subset: v2 216 weight: 80 217 218 `, 219 }, echoCfg{version: "v1"}, echoCfg{version: "v2"}) 220 221 retry.UntilSuccessOrFail(tt.T, func() error { 222 cw := tt.dialEcho("xds:///echo-app.default.svc.cluster.local:7070") 223 distribution := map[string]int{} 224 for i := 0; i < 100; i++ { 225 res, err := cw.Echo(context.Background(), &proto.EchoRequest{Message: "needle"}) 226 if err != nil { 227 return err 228 } 229 distribution[res.Version]++ 230 } 231 232 if err := expectAlmost(distribution["v1"], 20); err != nil { 233 return err 234 } 235 if err := expectAlmost(distribution["v2"], 80); err != nil { 236 return err 237 } 238 return nil 239 }, retry.Timeout(5*time.Second), retry.Delay(0)) 240 } 241 242 func TestMtls(t *testing.T) { 243 tt := newConfigGenTest(t, xds.FakeOptions{ 244 KubernetesObjectString: ` 245 apiVersion: v1 246 kind: Service 247 metadata: 248 labels: 249 app: echo-app 250 name: echo-app 251 namespace: default 252 spec: 253 clusterIP: 1.2.3.4 254 selector: 255 app: echo 256 ports: 257 - name: grpc 258 targetPort: grpc 259 port: 7070 260 `, 261 ConfigString: ` 262 apiVersion: networking.istio.io/v1alpha3 263 kind: DestinationRule 264 metadata: 265 name: echo-dr 266 namespace: default 267 spec: 268 host: echo-app.default.svc.cluster.local 269 trafficPolicy: 270 tls: 271 mode: ISTIO_MUTUAL 272 --- 273 apiVersion: security.istio.io/v1beta1 274 kind: PeerAuthentication 275 metadata: 276 name: default 277 namespace: default 278 spec: 279 mtls: 280 mode: STRICT 281 `, 282 }, echoCfg{version: "v1", tls: true}) 283 284 // ensure we can make 10 consecutive successful requests 285 retry.UntilSuccessOrFail(tt.T, func() error { 286 cw := tt.dialEcho("xds:///echo-app.default.svc.cluster.local:7070") 287 for i := 0; i < 10; i++ { 288 _, err := cw.Echo(context.Background(), &proto.EchoRequest{Message: "needle"}) 289 if err != nil { 290 return err 291 } 292 } 293 return nil 294 }, retry.Timeout(5*time.Second), retry.Delay(0)) 295 } 296 297 func TestFault(t *testing.T) { 298 tt := newConfigGenTest(t, xds.FakeOptions{ 299 KubernetesObjectString: ` 300 apiVersion: v1 301 kind: Service 302 metadata: 303 labels: 304 app: echo-app 305 name: echo-app 306 namespace: default 307 spec: 308 clusterIP: 1.2.3.4 309 selector: 310 app: echo 311 ports: 312 - name: grpc 313 targetPort: grpc 314 port: 7071 315 `, 316 ConfigString: ` 317 apiVersion: networking.istio.io/v1alpha3 318 kind: VirtualService 319 metadata: 320 name: echo-delay 321 spec: 322 hosts: 323 - echo-app.default.svc.cluster.local 324 http: 325 - fault: 326 delay: 327 percent: 100 328 fixedDelay: 100ms 329 route: 330 - destination: 331 host: echo-app.default.svc.cluster.local 332 `, 333 }, echoCfg{version: "v1"}) 334 c := tt.dialEcho("xds:///echo-app.default.svc.cluster.local:7071") 335 336 // without a delay it usually takes ~500us 337 st := time.Now() 338 _, err := c.Echo(context.Background(), &proto.EchoRequest{}) 339 duration := time.Since(st) 340 if err != nil { 341 t.Fatal(err) 342 } 343 if duration < time.Millisecond*100 { 344 t.Fatalf("expected to take over 1s but took %v", duration) 345 } 346 347 // TODO test timeouts, aborts 348 } 349 350 func expectAlmost(got, want int) error { 351 if math.Abs(float64(want-got)) > 10 { 352 return fmt.Errorf("expected within %d of %d but got %d", 10, want, got) 353 } 354 return nil 355 }