istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/echo/server/endpoint/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 endpoint
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/google/uuid"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/admin"
    32  	"google.golang.org/grpc/credentials"
    33  	"google.golang.org/grpc/credentials/insecure"
    34  	xdscreds "google.golang.org/grpc/credentials/xds"
    35  	"google.golang.org/grpc/health"
    36  	grpcHealth "google.golang.org/grpc/health/grpc_health_v1"
    37  	"google.golang.org/grpc/keepalive"
    38  	"google.golang.org/grpc/metadata"
    39  	"google.golang.org/grpc/peer"
    40  	"google.golang.org/grpc/reflection"
    41  	"google.golang.org/grpc/xds"
    42  
    43  	"istio.io/istio/pkg/env"
    44  	"istio.io/istio/pkg/test/echo"
    45  	"istio.io/istio/pkg/test/echo/common"
    46  	"istio.io/istio/pkg/test/echo/proto"
    47  	"istio.io/istio/pkg/test/echo/server/forwarder"
    48  	"istio.io/istio/pkg/test/util/retry"
    49  )
    50  
    51  var _ Instance = &grpcInstance{}
    52  
    53  // grpcServer is the intersection of used methods for grpc.Server and xds.GRPCServer
    54  type grpcServer interface {
    55  	reflection.GRPCServer
    56  	Serve(listener net.Listener) error
    57  	Stop()
    58  }
    59  
    60  type grpcInstance struct {
    61  	Config
    62  	server   grpcServer
    63  	cleanups []func()
    64  	f        *forwarder.Instance
    65  }
    66  
    67  func newGRPC(config Config) Instance {
    68  	return &grpcInstance{
    69  		Config: config,
    70  		f:      forwarder.New(),
    71  	}
    72  }
    73  
    74  func (s *grpcInstance) GetConfig() Config {
    75  	return s.Config
    76  }
    77  
    78  func (s *grpcInstance) newServer(opts ...grpc.ServerOption) grpcServer {
    79  	if s.Port.XDSServer {
    80  		if len(s.Port.XDSTestBootstrap) > 0 {
    81  			opts = append(opts, xds.BootstrapContentsForTesting(s.Port.XDSTestBootstrap))
    82  		}
    83  		epLog.Infof("Using xDS for serverside gRPC on %d", s.Port.Port)
    84  		grpcServer, err := xds.NewGRPCServer(opts...)
    85  		if err != nil {
    86  			return nil
    87  		}
    88  		return grpcServer
    89  	}
    90  	return grpc.NewServer(opts...)
    91  }
    92  
    93  func (s *grpcInstance) Start(onReady OnReadyFunc) error {
    94  	// Listen on the given port and update the port if it changed from what was passed in.
    95  	listener, p, err := listenOnAddress(s.ListenerIP, s.Port.Port)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	// Store the actual listening port back to the argument.
   100  	s.Port.Port = p
   101  
   102  	opts := []grpc.ServerOption{
   103  		grpc.KeepaliveParams(keepalive.ServerParameters{
   104  			MaxConnectionIdle: idleTimeout,
   105  		}),
   106  	}
   107  	if s.Port.TLS {
   108  		epLog.Infof("Listening GRPC (over TLS) on %v", p)
   109  		// Create the TLS credentials
   110  		creds, errCreds := credentials.NewServerTLSFromFile(s.TLSCert, s.TLSKey)
   111  		if errCreds != nil {
   112  			epLog.Errorf("could not load TLS keys: %s", errCreds)
   113  		}
   114  		opts = append(opts, grpc.Creds(creds))
   115  	} else if s.Port.XDSServer {
   116  		epLog.Infof("Listening GRPC (over xDS-configured mTLS) on %v", p)
   117  		creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{
   118  			FallbackCreds: insecure.NewCredentials(),
   119  		})
   120  		if err != nil {
   121  			return err
   122  		}
   123  		opts = append(opts, grpc.Creds(creds))
   124  	} else {
   125  		epLog.Infof("Listening GRPC on %v", p)
   126  	}
   127  	s.server = s.newServer(opts...)
   128  
   129  	// add the standard grpc health check
   130  	healthServer := health.NewServer()
   131  	grpcHealth.RegisterHealthServer(s.server, healthServer)
   132  
   133  	proto.RegisterEchoTestServiceServer(s.server, &EchoGrpcHandler{
   134  		Config:    s.Config,
   135  		Forwarder: s.f,
   136  	})
   137  	reflection.Register(s.server)
   138  	if val := env.Register("EXPOSE_GRPC_ADMIN", false, "").Get(); val {
   139  		cleanup, err := admin.Register(s.server)
   140  		if err != nil {
   141  			return err
   142  		}
   143  		s.cleanups = append(s.cleanups, cleanup)
   144  	}
   145  	// Start serving GRPC traffic.
   146  	go func() {
   147  		err := s.server.Serve(listener)
   148  		epLog.Warnf("Port %d listener terminated with error: %v", p, err)
   149  	}()
   150  
   151  	// Notify the WaitGroup once the port has transitioned to ready.
   152  	go s.awaitReady(func() {
   153  		healthServer.SetServingStatus("", grpcHealth.HealthCheckResponse_SERVING)
   154  		onReady()
   155  	}, listener)
   156  
   157  	return nil
   158  }
   159  
   160  func (s *grpcInstance) awaitReady(onReady OnReadyFunc, listener net.Listener) {
   161  	defer onReady()
   162  
   163  	err := retry.UntilSuccess(func() error {
   164  		cert, key, ca, err := s.certsFromBootstrapForReady()
   165  		if err != nil {
   166  			return err
   167  		}
   168  		req := &proto.ForwardEchoRequest{
   169  			Url:           "grpc://" + listener.Addr().String(),
   170  			Message:       "hello",
   171  			TimeoutMicros: common.DurationToMicros(readyInterval),
   172  		}
   173  		if s.Port.XDSReadinessTLS {
   174  			// TODO: using the servers key/cert is not always valid, it may not be allowed to make requests to itself
   175  			req.CertFile = cert
   176  			req.KeyFile = key
   177  			req.CaCertFile = ca
   178  			req.InsecureSkipVerify = true
   179  		}
   180  		_, err = s.f.ForwardEcho(context.Background(), &forwarder.Config{
   181  			XDSTestBootstrap: s.Port.XDSTestBootstrap,
   182  			Request:          req,
   183  		})
   184  		return err
   185  	}, retry.Timeout(readyTimeout), retry.Delay(readyInterval))
   186  	if err != nil {
   187  		epLog.Errorf("readiness failed for GRPC endpoint %s: %v", listener.Addr().String(), err)
   188  	} else {
   189  		epLog.Infof("ready for GRPC endpoint %s", listener.Addr().String())
   190  	}
   191  }
   192  
   193  // TODO (hack) we have to send certs OR use xds:///fqdn. We don't know our own fqdn, and even if we did
   194  // we could send traffic to another instance. Instead we look into gRPC internals to authenticate with ourself.
   195  func (s *grpcInstance) certsFromBootstrapForReady() (cert string, key string, ca string, err error) {
   196  	if !s.Port.XDSServer {
   197  		return
   198  	}
   199  
   200  	var bootstrapData []byte
   201  	if data := s.Port.XDSTestBootstrap; len(data) > 0 {
   202  		bootstrapData = data
   203  	} else if path := os.Getenv("GRPC_XDS_BOOTSTRAP"); len(path) > 0 {
   204  		bootstrapData, err = os.ReadFile(path)
   205  	} else if data := os.Getenv("GRPC_XDS_BOOTSTRAP_CONFIG"); len(data) > 0 {
   206  		bootstrapData = []byte(data)
   207  	}
   208  	var bootstrap Bootstrap
   209  	if uerr := json.Unmarshal(bootstrapData, &bootstrap); uerr != nil {
   210  		err = uerr
   211  		return
   212  	}
   213  	certs := bootstrap.FileWatcherProvider()
   214  	if certs == nil {
   215  		err = fmt.Errorf("no certs found in bootstrap")
   216  		return
   217  	}
   218  	cert = certs.CertificateFile
   219  	key = certs.PrivateKeyFile
   220  	ca = certs.CACertificateFile
   221  	return
   222  }
   223  
   224  func (s *grpcInstance) Close() error {
   225  	if s.server != nil {
   226  		s.server.Stop()
   227  	}
   228  	if s.f != nil {
   229  		_ = s.f.Close()
   230  	}
   231  	for _, cleanup := range s.cleanups {
   232  		cleanup()
   233  	}
   234  	return nil
   235  }
   236  
   237  type EchoGrpcHandler struct {
   238  	proto.UnimplementedEchoTestServiceServer
   239  	Config
   240  	Forwarder *forwarder.Instance
   241  }
   242  
   243  func (h *EchoGrpcHandler) Echo(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResponse, error) {
   244  	defer common.Metrics.GrpcRequests.With(common.PortLabel.Value(strconv.Itoa(h.Port.Port))).Increment()
   245  	body := bytes.Buffer{}
   246  	md, ok := metadata.FromIncomingContext(ctx)
   247  	if ok {
   248  		for key, values := range md {
   249  			if strings.HasSuffix(key, "-bin") {
   250  				// Skip binary headers.
   251  				continue
   252  			}
   253  
   254  			field := key
   255  
   256  			if key == ":authority" {
   257  				for _, value := range values {
   258  					echo.HostField.Write(&body, value)
   259  				}
   260  			}
   261  
   262  			for _, value := range values {
   263  				echo.RequestHeaderField.WriteKeyValue(&body, field, value)
   264  			}
   265  		}
   266  	}
   267  
   268  	id := uuid.New()
   269  	epLog.WithLabels("message", req.GetMessage(), "headers", md, "id", id).Infof("GRPC Request")
   270  
   271  	portNumber := 0
   272  	if h.Port != nil {
   273  		portNumber = h.Port.Port
   274  	}
   275  
   276  	ip := "0.0.0.0"
   277  	if peerInfo, ok := peer.FromContext(ctx); ok {
   278  		ip, _, _ = net.SplitHostPort(peerInfo.Addr.String())
   279  	}
   280  
   281  	echo.StatusCodeField.Write(&body, strconv.Itoa(http.StatusOK))
   282  	echo.ServiceVersionField.Write(&body, h.Version)
   283  	echo.ServicePortField.Write(&body, strconv.Itoa(portNumber))
   284  	echo.ClusterField.WriteNonEmpty(&body, h.Cluster)
   285  	echo.NamespaceField.WriteNonEmpty(&body, h.Namespace)
   286  	echo.IPField.Write(&body, ip)
   287  	echo.IstioVersionField.WriteNonEmpty(&body, h.IstioVersion)
   288  	echo.ProtocolField.Write(&body, "GRPC")
   289  	echo.Field("Echo").Write(&body, req.GetMessage())
   290  
   291  	if hostname, err := os.Hostname(); err == nil {
   292  		echo.HostnameField.Write(&body, hostname)
   293  	}
   294  
   295  	epLog.WithLabels("id", id).Infof("GRPC Response")
   296  	return &proto.EchoResponse{Message: body.String()}, nil
   297  }
   298  
   299  func (h *EchoGrpcHandler) ForwardEcho(ctx context.Context, req *proto.ForwardEchoRequest) (*proto.ForwardEchoResponse, error) {
   300  	id := uuid.New()
   301  	l := epLog.WithLabels("url", req.Url, "id", id)
   302  	l.Infof("ForwardEcho request")
   303  	t0 := time.Now()
   304  
   305  	ret, err := h.Forwarder.ForwardEcho(ctx, &forwarder.Config{Request: req})
   306  	if err == nil {
   307  		l.WithLabels("latency", time.Since(t0)).Infof("ForwardEcho response complete: %v", ret.GetOutput())
   308  	} else {
   309  		l.WithLabels("latency", time.Since(t0)).Infof("ForwardEcho response failed: %v", err)
   310  	}
   311  	return ret, err
   312  }