istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/echo/server/forwarder/grpc.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  
    15  package forwarder
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"net"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"google.golang.org/grpc"
    27  	"google.golang.org/grpc/credentials"
    28  	"google.golang.org/grpc/credentials/insecure"
    29  	"google.golang.org/grpc/metadata"
    30  
    31  	"istio.io/istio/pkg/test/echo"
    32  	"istio.io/istio/pkg/test/echo/common"
    33  	"istio.io/istio/pkg/test/echo/proto"
    34  )
    35  
    36  var _ protocol = &grpcProtocol{}
    37  
    38  type grpcProtocol struct {
    39  	e *executor
    40  }
    41  
    42  func newGRPCProtocol(e *executor) protocol {
    43  	return &grpcProtocol{e: e}
    44  }
    45  
    46  type grpcConnectionGetter func() (*grpc.ClientConn, func(), error)
    47  
    48  func (c *grpcProtocol) ForwardEcho(ctx context.Context, cfg *Config) (*proto.ForwardEchoResponse, error) {
    49  	var getConn grpcConnectionGetter
    50  	if cfg.newConnectionPerRequest {
    51  		// Create a new connection per request.
    52  		getConn = func() (*grpc.ClientConn, func(), error) {
    53  			conn, err := newGRPCConnection(cfg)
    54  			if err != nil {
    55  				return nil, nil, err
    56  			}
    57  			return conn, func() { _ = conn.Close() }, nil
    58  		}
    59  	} else {
    60  		// Reuse the connection across all requests.
    61  		conn, err := newGRPCConnection(cfg)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  		defer func() { _ = conn.Close() }()
    66  		getConn = func() (*grpc.ClientConn, func(), error) {
    67  			return conn, func() {}, nil
    68  		}
    69  	}
    70  
    71  	call := &grpcCall{
    72  		e:       c.e,
    73  		getConn: getConn,
    74  	}
    75  	return doForward(ctx, cfg, c.e, call.makeRequest)
    76  }
    77  
    78  func (c *grpcProtocol) Close() error {
    79  	return nil
    80  }
    81  
    82  type grpcCall struct {
    83  	e       *executor
    84  	getConn grpcConnectionGetter
    85  }
    86  
    87  func (c *grpcCall) makeRequest(ctx context.Context, cfg *Config, requestID int) (string, error) {
    88  	conn, closeConn, err := c.getConn()
    89  	if err != nil {
    90  		return "", err
    91  	}
    92  	defer closeConn()
    93  
    94  	// Set the per-request timeout.
    95  	ctx, cancel := context.WithTimeout(ctx, cfg.timeout)
    96  	defer cancel()
    97  
    98  	// Add headers to the request context.
    99  	outMD := make(metadata.MD)
   100  	for k, v := range cfg.headers {
   101  		// Exclude the Host header from the GRPC context.
   102  		if !strings.EqualFold(hostHeader, k) {
   103  			outMD.Set(k, v...)
   104  		}
   105  	}
   106  	outMD.Set("X-Request-Id", strconv.Itoa(requestID))
   107  	ctx = metadata.NewOutgoingContext(ctx, outMD)
   108  
   109  	var outBuffer bytes.Buffer
   110  	grpcReq := &proto.EchoRequest{
   111  		Message: cfg.Request.Message,
   112  	}
   113  	// TODO(nmittler): This doesn't fit in with the field pattern. Do we need this?
   114  	outBuffer.WriteString(fmt.Sprintf("[%d] grpcecho.Echo(%v)\n", requestID, cfg.Request))
   115  
   116  	start := time.Now()
   117  	client := proto.NewEchoTestServiceClient(conn)
   118  	var header metadata.MD
   119  
   120  	resp, err := client.Echo(ctx, grpcReq, grpc.Header(&header))
   121  	if err != nil {
   122  		return "", err
   123  	}
   124  
   125  	echo.LatencyField.WriteForRequest(&outBuffer, requestID, fmt.Sprintf("%v", time.Since(start)))
   126  	echo.ActiveRequestsField.WriteForRequest(&outBuffer, requestID, fmt.Sprintf("%d", c.e.ActiveRequests()))
   127  
   128  	// When the underlying HTTP2 request returns status 404, GRPC
   129  	// request does not return an error in grpc-go.
   130  	// Instead, it just returns an empty response
   131  	for _, line := range strings.Split(resp.GetMessage(), "\n") {
   132  		if line != "" {
   133  			echo.WriteBodyLine(&outBuffer, requestID, line)
   134  		}
   135  	}
   136  	for k, v := range header {
   137  		for _, vv := range v {
   138  			echo.ResponseHeaderField.WriteKeyValueForRequest(&outBuffer, requestID, k, vv)
   139  		}
   140  	}
   141  	return outBuffer.String(), nil
   142  }
   143  
   144  func newGRPCConnection(cfg *Config) (*grpc.ClientConn, error) {
   145  	var security grpc.DialOption
   146  	if cfg.secure {
   147  		security = grpc.WithTransportCredentials(credentials.NewTLS(cfg.tlsConfig))
   148  	} else {
   149  		security = grpc.WithTransportCredentials(insecure.NewCredentials())
   150  	}
   151  
   152  	opts := []grpc.DialOption{
   153  		grpc.WithAuthority(cfg.hostHeader),
   154  		grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
   155  			return newDialer(cfg).DialContext(ctx, "tcp", addr)
   156  		}),
   157  		security,
   158  	}
   159  
   160  	// Strip off the scheme from the address (for regular gRPC).
   161  	address := cfg.Request.Url[len(cfg.scheme+"://"):]
   162  
   163  	// Connect to the GRPC server.
   164  	ctx, cancel := context.WithTimeout(context.Background(), common.ConnectionTimeout)
   165  	defer cancel()
   166  	return grpc.DialContext(ctx, address, opts...)
   167  }