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  }