go.uber.org/yarpc@v1.72.1/transport/grpc/config.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package grpc
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"net"
    27  	"time"
    28  
    29  	"go.uber.org/yarpc/api/peer"
    30  	"go.uber.org/yarpc/api/transport"
    31  	yarpctls "go.uber.org/yarpc/api/transport/tls"
    32  	peerchooser "go.uber.org/yarpc/peer"
    33  	"go.uber.org/yarpc/peer/hostport"
    34  	"go.uber.org/yarpc/yarpcconfig"
    35  	"google.golang.org/grpc/credentials"
    36  	"google.golang.org/grpc/keepalive"
    37  )
    38  
    39  // TransportSpec returns a TransportSpec for the gRPC transport.
    40  //
    41  // See TransportConfig, InboundConfig, and OutboundConfig for details on the
    42  // different configuration parameters supported by this Transport.
    43  //
    44  // Any TransportOption, InboundOption, or OutboundOption may be passed to this function.
    45  // These options will be applied BEFORE configuration parameters are
    46  // interpreted. This allows configuration parameters to override Options
    47  // provided to TransportSpec.
    48  func TransportSpec(opts ...Option) yarpcconfig.TransportSpec {
    49  	transportSpec, err := newTransportSpec(opts...)
    50  	if err != nil {
    51  		panic(err.Error())
    52  	}
    53  	return yarpcconfig.TransportSpec{
    54  		Name:                TransportName,
    55  		BuildTransport:      transportSpec.buildTransport,
    56  		BuildInbound:        transportSpec.buildInbound,
    57  		BuildUnaryOutbound:  transportSpec.buildUnaryOutbound,
    58  		BuildStreamOutbound: transportSpec.buildStreamOutbound,
    59  	}
    60  }
    61  
    62  // TransportConfig configures a gRPC Transport. This is shared
    63  // between all gRPC inbounds and outbounds of a Dispatcher.
    64  //
    65  //  transports:
    66  //    grpc:
    67  //      backoff:
    68  //        exponential:
    69  //          first: 10ms
    70  //          max: 30s
    71  //      clientMaxHeaderListSize: 1024
    72  //      serverMaxHeaderListSize: 2048
    73  //
    74  // All parameters of TransportConfig are optional. This section
    75  // may be omitted in the transports section.
    76  type TransportConfig struct {
    77  	ServerMaxRecvMsgSize int `config:"serverMaxRecvMsgSize"`
    78  	ServerMaxSendMsgSize int `config:"serverMaxSendMsgSize"`
    79  	ClientMaxRecvMsgSize int `config:"clientMaxRecvMsgSize"`
    80  	ClientMaxSendMsgSize int `config:"clientMaxSendMsgSize"`
    81  	// GRPC header lise size options accept uint32 param.
    82  	// see: https://pkg.go.dev/google.golang.org/grpc#WithMaxHeaderListSize
    83  	ServerMaxHeaderListSize uint32              `config:"serverMaxHeaderListSize"`
    84  	ClientMaxHeaderListSize uint32              `config:"clientMaxHeaderListSize"`
    85  	Backoff                 yarpcconfig.Backoff `config:"backoff"`
    86  }
    87  
    88  // InboundConfig configures a gRPC Inbound.
    89  //
    90  // inbounds:
    91  //   grpc:
    92  //     address: ":80"
    93  //
    94  // A gRPC inbound can also enable TLS from key and cert files.
    95  //
    96  // inbounds:
    97  //   grpc:
    98  //     address: ":443"
    99  //     tls:
   100  //       enabled: true
   101  //       keyFile: "/path/to/key"
   102  //       certFile: "/path/to/cert"
   103  type InboundConfig struct {
   104  	// Address to listen on. This field is required.
   105  	Address string           `config:"address,interpolate"`
   106  	TLS     InboundTLSConfig `config:"tls"`
   107  }
   108  
   109  func (c InboundConfig) inboundOptions() ([]InboundOption, error) {
   110  	return c.TLS.inboundOptions()
   111  }
   112  
   113  // InboundTLSConfig specifies the TLS configuration for the gRPC inbound.
   114  type InboundTLSConfig struct {
   115  	Enabled  bool   `config:"enabled"` // disabled by default
   116  	CertFile string `config:"certFile,interpolate"`
   117  	KeyFile  string `config:"keyFile,interpolate"`
   118  
   119  	// Mode when set to Permissive or Enforced enables TLS inbound and
   120  	// TLS configuration must be passed as an inbound option.
   121  	// Note: enable, cert and key fields are ignored when mode is set.
   122  	Mode yarpctls.Mode `config:"mode,interpolate"`
   123  }
   124  
   125  func (c InboundTLSConfig) inboundOptions() ([]InboundOption, error) {
   126  	if c.Mode != yarpctls.Disabled {
   127  		return []InboundOption{InboundTLSMode(c.Mode)}, nil
   128  	}
   129  
   130  	if !c.Enabled {
   131  		return nil, nil
   132  	}
   133  	creds, err := c.newInboundCredentials()
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	return []InboundOption{InboundCredentials(creds)}, nil
   138  }
   139  
   140  func (c InboundTLSConfig) newInboundCredentials() (credentials.TransportCredentials, error) {
   141  	if c.CertFile != "" && c.KeyFile != "" {
   142  		return credentials.NewServerTLSFromFile(c.CertFile, c.KeyFile)
   143  	}
   144  	return nil, fmt.Errorf("both certFile and keyFile are necessary to construct gRPC transport credentials, got certFile=%q and keyFile=%q", c.CertFile, c.KeyFile)
   145  }
   146  
   147  // OutboundConfig configures a gRPC Outbound.
   148  //
   149  // outbounds:
   150  //   myservice:
   151  //     grpc:
   152  //       address: ":80"
   153  //
   154  // A gRPC outbound can also configure a peer list.
   155  //
   156  //  outbounds:
   157  //    myservice:
   158  //      grpc:
   159  //        round-robin:
   160  //          peers:
   161  //            - 127.0.0.1:8080
   162  //            - 127.0.0.1:8081
   163  //
   164  // A gRPC outbound can enable TLS using the system cert.Pool.
   165  //
   166  //  outbounds:
   167  //    theirsecureservice:
   168  //      grpc:
   169  //        address: ":443"
   170  //        tls:
   171  //          enabled: true
   172  //        compressor: gzip
   173  //        grpc-keepalive:
   174  //          enabled: true
   175  //          time:    10s
   176  //          timeout: 30s
   177  //          permit-without-stream: true
   178  //
   179  type OutboundConfig struct {
   180  	yarpcconfig.PeerChooser
   181  
   182  	// Address to connect to if no peer options set.
   183  	Address string            `config:"address,interpolate"`
   184  	TLS     OutboundTLSConfig `config:"tls"`
   185  	// Compressor to use by default if the server side supports it
   186  	Compressor string                  `config:"compressor"`
   187  	Keepalive  OutboundKeepaliveConfig `config:"grpc-keepalive"`
   188  }
   189  
   190  func (c OutboundConfig) dialOptions(kit *yarpcconfig.Kit, tlsConfigProvider yarpctls.OutboundTLSConfigProvider) ([]DialOption, error) {
   191  	opts, err := c.TLS.dialOptions(tlsConfigProvider)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	keepaliveOpts, err := c.Keepalive.dialOptions()
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	opts = append(opts, keepaliveOpts...)
   202  	return opts, nil
   203  }
   204  
   205  // OutboundTLSConfig configures TLS for a gRPC outbound.
   206  type OutboundTLSConfig struct {
   207  	// Enabled field is deprecated, use Mode and SpiffeIDs fields instead.
   208  	Enabled bool `config:"enabled"`
   209  	// Mode when set to Enforced enables TLS outbound and
   210  	// outbound TLS configuration providered option will be used for fetching
   211  	// outbound tls.Config.
   212  	// Note: enable field is ignored when mode is set.
   213  	Mode yarpctls.Mode `config:"mode,interpolate"`
   214  	// SpiffeIDs is a list of the accepted server spiffe IDs.
   215  	SpiffeIDs []string `config:"spiffe-ids"`
   216  }
   217  
   218  func (c OutboundTLSConfig) dialOptions(tlsConfigProvider yarpctls.OutboundTLSConfigProvider) ([]DialOption, error) {
   219  	if c.Mode != yarpctls.Disabled {
   220  		if c.Mode == yarpctls.Permissive {
   221  			return nil, errors.New("outbound does not support permissive TLS mode")
   222  		}
   223  
   224  		if tlsConfigProvider == nil {
   225  			return nil, errors.New("outbound TLS enforced but outbound TLS config provider is nil")
   226  		}
   227  
   228  		config, err := tlsConfigProvider.ClientTLSConfig(c.SpiffeIDs)
   229  		if err != nil {
   230  			return nil, err
   231  		}
   232  		return []DialOption{DialerTLSConfig(config)}, nil
   233  	}
   234  
   235  	if !c.Enabled {
   236  		return nil, nil
   237  	}
   238  	creds := credentials.NewClientTLSFromCert(nil, "")
   239  	option := DialerCredentials(creds)
   240  	return []DialOption{option}, nil
   241  }
   242  
   243  // OutboundKeepaliveConfig configures gRPC keepalive for a gRPC outbound.
   244  type OutboundKeepaliveConfig struct {
   245  	Enabled             bool   `config:"enabled"`
   246  	Time                string `config:"time"`
   247  	Timeout             string `config:"timeout"`
   248  	PermitWithoutStream bool   `config:"permit-without-stream"`
   249  }
   250  
   251  func (c OutboundKeepaliveConfig) dialOptions() ([]DialOption, error) {
   252  	if !c.Enabled {
   253  		return nil, nil
   254  	}
   255  
   256  	var err error
   257  
   258  	// gRPC keepalive expects time to be minimum 10s.
   259  	// read more: https://pkg.go.dev/google.golang.org/grpc/keepalive#ClientParameters
   260  	keepaliveTime := time.Second * 10
   261  	if c.Time != "" {
   262  		keepaliveTime, err = time.ParseDuration(c.Time)
   263  		if err != nil {
   264  			return nil, fmt.Errorf("could not parse gRPC keepalive time: %v", err)
   265  		}
   266  	}
   267  
   268  	// gRPC keepalive defaults timeout to 20s.
   269  	// read more: https://pkg.go.dev/google.golang.org/grpc/keepalive#ClientParameters
   270  	keepaliveTimeout := time.Second * 20
   271  	if c.Timeout != "" {
   272  		keepaliveTimeout, err = time.ParseDuration(c.Timeout)
   273  		if err != nil {
   274  			return nil, fmt.Errorf("could not parse gRPC keepalive timeout: %v", err)
   275  		}
   276  	}
   277  
   278  	option := KeepaliveParams(keepalive.ClientParameters{
   279  		Time:                keepaliveTime,
   280  		Timeout:             keepaliveTimeout,
   281  		PermitWithoutStream: c.PermitWithoutStream,
   282  	})
   283  	return []DialOption{option}, nil
   284  }
   285  
   286  type transportSpec struct {
   287  	TransportOptions []TransportOption
   288  	InboundOptions   []InboundOption
   289  	OutboundOptions  []OutboundOption
   290  	DialOptions      []DialOption
   291  }
   292  
   293  func newTransportSpec(opts ...Option) (*transportSpec, error) {
   294  	transportSpec := &transportSpec{}
   295  	for _, o := range opts {
   296  		switch opt := o.(type) {
   297  		case TransportOption:
   298  			transportSpec.TransportOptions = append(transportSpec.TransportOptions, opt)
   299  		case InboundOption:
   300  			transportSpec.InboundOptions = append(transportSpec.InboundOptions, opt)
   301  		case OutboundOption:
   302  			transportSpec.OutboundOptions = append(transportSpec.OutboundOptions, opt)
   303  		case DialOption:
   304  			transportSpec.DialOptions = append(transportSpec.DialOptions, opt)
   305  		default:
   306  			return nil, fmt.Errorf("unknown option of type %T: %v", o, o)
   307  		}
   308  	}
   309  	return transportSpec, nil
   310  }
   311  
   312  func (t *transportSpec) buildTransport(transportConfig *TransportConfig, kit *yarpcconfig.Kit) (transport.Transport, error) {
   313  	options := t.TransportOptions
   314  	if transportConfig.ServerMaxRecvMsgSize > 0 {
   315  		options = append(options, ServerMaxRecvMsgSize(transportConfig.ServerMaxRecvMsgSize))
   316  	}
   317  	if transportConfig.ServerMaxSendMsgSize > 0 {
   318  		options = append(options, ServerMaxSendMsgSize(transportConfig.ServerMaxSendMsgSize))
   319  	}
   320  	if transportConfig.ClientMaxRecvMsgSize > 0 {
   321  		options = append(options, ClientMaxRecvMsgSize(transportConfig.ClientMaxRecvMsgSize))
   322  	}
   323  	if transportConfig.ClientMaxSendMsgSize > 0 {
   324  		options = append(options, ClientMaxSendMsgSize(transportConfig.ClientMaxSendMsgSize))
   325  	}
   326  	if transportConfig.ServerMaxHeaderListSize > 0 {
   327  		options = append(options, ServerMaxHeaderListSize(transportConfig.ServerMaxHeaderListSize))
   328  	}
   329  	if transportConfig.ClientMaxHeaderListSize > 0 {
   330  		options = append(options, ClientMaxHeaderListSize(transportConfig.ClientMaxHeaderListSize))
   331  	}
   332  	backoffStrategy, err := transportConfig.Backoff.Strategy()
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	options = append(options, BackoffStrategy(backoffStrategy), ServiceName(kit.ServiceName()))
   337  	return newTransport(newTransportOptions(options)), nil
   338  }
   339  
   340  func (t *transportSpec) buildInbound(inboundConfig *InboundConfig, tr transport.Transport, _ *yarpcconfig.Kit) (transport.Inbound, error) {
   341  	trans, ok := tr.(*Transport)
   342  	if !ok {
   343  		return nil, newTransportCastError(tr)
   344  	}
   345  	if inboundConfig.Address == "" {
   346  		return nil, newRequiredFieldMissingError("address")
   347  	}
   348  	listener, err := net.Listen("tcp", inboundConfig.Address)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  	inboundOptions, err := inboundConfig.inboundOptions()
   353  	if err != nil {
   354  		return nil, fmt.Errorf("cannot build gRPC inbound from given configuration: %v", err)
   355  	}
   356  	return trans.NewInbound(listener, append(t.InboundOptions, inboundOptions...)...), nil
   357  }
   358  
   359  func (t *transportSpec) buildUnaryOutbound(outboundConfig *OutboundConfig, tr transport.Transport, kit *yarpcconfig.Kit) (transport.UnaryOutbound, error) {
   360  	return t.buildOutbound(outboundConfig, tr, kit)
   361  }
   362  
   363  func (t *transportSpec) buildStreamOutbound(outboundConfig *OutboundConfig, tr transport.Transport, kit *yarpcconfig.Kit) (transport.StreamOutbound, error) {
   364  	return t.buildOutbound(outboundConfig, tr, kit)
   365  }
   366  
   367  func (t *transportSpec) buildOutbound(outboundConfig *OutboundConfig, tr transport.Transport, kit *yarpcconfig.Kit) (*Outbound, error) {
   368  	trans, ok := tr.(*Transport)
   369  	if !ok {
   370  		return nil, newTransportCastError(tr)
   371  	}
   372  
   373  	dialOpts, err := outboundConfig.dialOptions(kit, newOutboundOptions(t.OutboundOptions).tlsConfigProvider)
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  
   378  	opts := append(dialOpts, t.DialOptions...)
   379  	dialer := trans.NewDialer(append([]DialOption{DialerDestinationServiceName(kit.OutboundServiceName())}, opts...)...)
   380  	var chooser peer.Chooser
   381  	if outboundConfig.Empty() {
   382  		if outboundConfig.Address == "" {
   383  			return nil, newRequiredFieldMissingError("address")
   384  		}
   385  		chooser = peerchooser.NewSingle(hostport.PeerIdentifier(outboundConfig.Address), dialer)
   386  	} else {
   387  		var err error
   388  		chooser, err = outboundConfig.BuildPeerChooser(dialer, hostport.Identify, kit)
   389  		if err != nil {
   390  			return nil, err
   391  		}
   392  	}
   393  
   394  	outboundOpts := []OutboundOption{OutboundCompressor(kit.Compressor(outboundConfig.Compressor))}
   395  	outboundOpts = append(outboundOpts, t.OutboundOptions...)
   396  	return trans.NewOutbound(chooser, outboundOpts...), nil
   397  }
   398  
   399  func newTransportCastError(tr transport.Transport) error {
   400  	return fmt.Errorf("could not cast %T to a *grpc.Transport", tr)
   401  }
   402  
   403  func newRequiredFieldMissingError(field string) error {
   404  	return fmt.Errorf("required field missing: %v", field)
   405  }