pomerium/pkg/grpc/client.go
Caleb Doxsey bbec2cae9f
grpc: send client traffic through envoy (#2469)
* wip

* wip

* handle wildcards in override name

* remove wait for ready, add comment about sync, force initial sync complete in test

* address comments
2021-08-16 16:12:22 -06:00

139 lines
4.4 KiB
Go

package grpc
import (
"context"
"net"
"sync"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry"
"github.com/pomerium/pomerium/internal/telemetry/requestid"
"github.com/pomerium/pomerium/pkg/grpcutil"
)
// Options contains options for connecting to a pomerium rpc service.
type Options struct {
// Address is the location of the service. e.g. "service.corp.example:8443"
Address string
// InstallationID specifies the installation id for telemetry exposition.
InstallationID string
// ServiceName specifies the service name for telemetry exposition
ServiceName string
// SignedJWTKey is the JWT key to use for signing a JWT attached to metadata.
SignedJWTKey []byte
}
// NewGRPCClientConn returns a new gRPC pomerium service client connection.
func NewGRPCClientConn(ctx context.Context, opts *Options, other ...grpc.DialOption) (*grpc.ClientConn, error) {
clientStatsHandler := telemetry.NewGRPCClientStatsHandler(opts.ServiceName)
unaryClientInterceptors := []grpc.UnaryClientInterceptor{
requestid.UnaryClientInterceptor(),
clientStatsHandler.UnaryInterceptor,
}
streamClientInterceptors := []grpc.StreamClientInterceptor{
requestid.StreamClientInterceptor(),
}
if opts.SignedJWTKey != nil {
unaryClientInterceptors = append(unaryClientInterceptors, grpcutil.WithUnarySignedJWT(opts.SignedJWTKey))
streamClientInterceptors = append(streamClientInterceptors, grpcutil.WithStreamSignedJWT(opts.SignedJWTKey))
}
dialOptions := []grpc.DialOption{
grpc.WithChainUnaryInterceptor(unaryClientInterceptors...),
grpc.WithChainStreamInterceptor(streamClientInterceptors...),
grpc.WithStatsHandler(clientStatsHandler.Handler),
grpc.WithDisableServiceConfig(),
grpc.WithInsecure(),
}
dialOptions = append(dialOptions, other...)
log.Info(ctx).Str("address", opts.Address).Msg("dialing")
return grpc.DialContext(ctx, opts.Address, dialOptions...)
}
// grpcTimeoutInterceptor enforces per-RPC request timeouts
func grpcTimeoutInterceptor(timeout time.Duration) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if timeout <= 0 {
return invoker(ctx, method, req, reply, cc, opts...)
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return invoker(ctx, method, req, reply, cc, opts...)
}
}
type grpcClientConnRecord struct {
conn *grpc.ClientConn
opts *Options
}
var grpcClientConns = struct {
sync.Mutex
m map[string]grpcClientConnRecord
}{
m: make(map[string]grpcClientConnRecord),
}
// GetGRPCClientConn returns a gRPC client connection for the given name. If a connection for that name has already been
// established the existing connection will be returned. If any options change for that connection, the existing
// connection will be closed and a new one established.
func GetGRPCClientConn(ctx context.Context, name string, opts *Options) (*grpc.ClientConn, error) {
grpcClientConns.Lock()
defer grpcClientConns.Unlock()
current, ok := grpcClientConns.m[name]
if ok {
if cmp.Equal(current.opts, opts) {
return current.conn, nil
}
err := current.conn.Close()
if err != nil {
log.Error(context.TODO()).Err(err).Msg("grpc: failed to close existing connection")
}
}
cc, err := NewGRPCClientConn(ctx, opts)
if err != nil {
return nil, err
}
grpcClientConns.m[name] = grpcClientConnRecord{
conn: cc,
opts: opts,
}
return cc, nil
}
// OutboundOptions are the options for the outbound gRPC client.
type OutboundOptions struct {
// OutboundPort is the port for the outbound gRPC listener.
OutboundPort string
// InstallationID specifies the installation id for telemetry exposition.
InstallationID string
// ServiceName specifies the service name for telemetry exposition
ServiceName string
// SignedJWTKey is the JWT key to use for signing a JWT attached to metadata.
SignedJWTKey []byte
}
// GetOutboundGRPCClientConn gets the outbound gRPC client.
func GetOutboundGRPCClientConn(ctx context.Context, opts *OutboundOptions) (*grpc.ClientConn, error) {
return GetGRPCClientConn(ctx, "outbound", &Options{
Address: net.JoinHostPort("127.0.0.1", opts.OutboundPort),
InstallationID: opts.InstallationID,
ServiceName: opts.ServiceName,
SignedJWTKey: opts.SignedJWTKey,
})
}