Skip to content

Commit d16cec3

Browse files
authored
Support muxing gRPC broker connections over a single net.Conn (#288)
* Support muxing gRPC broker connections over a single net.Conn, via ClientConfig.GRPCBrokerMultiplex * upgrade yamux, fix yamux config, and go mod tidy -compat=1.17 * Check for multiplexing support in protocol negotiation if enabled
1 parent 017b758 commit d16cec3

22 files changed

+1037
-196
lines changed

client.go

+82-11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/hashicorp/go-hclog"
2929
"github.com/hashicorp/go-plugin/internal/cmdrunner"
30+
"github.com/hashicorp/go-plugin/internal/grpcmux"
3031
"github.com/hashicorp/go-plugin/runner"
3132
"google.golang.org/grpc"
3233
)
@@ -63,6 +64,13 @@ var (
6364
// ErrSecureConfigAndReattach is returned when both Reattach and
6465
// SecureConfig are set.
6566
ErrSecureConfigAndReattach = errors.New("only one of Reattach or SecureConfig can be set")
67+
68+
// ErrGRPCBrokerMuxNotSupported is returned when the client requests
69+
// multiplexing over the gRPC broker, but the plugin does not support the
70+
// feature. In most cases, this should be resolvable by updating and
71+
// rebuilding the plugin, or restarting the plugin with
72+
// ClientConfig.GRPCBrokerMultiplex set to false.
73+
ErrGRPCBrokerMuxNotSupported = errors.New("client requested gRPC broker multiplexing but plugin does not support the feature")
6674
)
6775

6876
// Client handles the lifecycle of a plugin application. It launches
@@ -102,6 +110,9 @@ type Client struct {
102110
processKilled bool
103111

104112
unixSocketCfg UnixSocketConfig
113+
114+
grpcMuxerOnce sync.Once
115+
grpcMuxer *grpcmux.GRPCClientMuxer
105116
}
106117

107118
// NegotiatedVersion returns the protocol version negotiated with the server.
@@ -237,6 +248,19 @@ type ClientConfig struct {
237248
// protocol.
238249
GRPCDialOptions []grpc.DialOption
239250

251+
// GRPCBrokerMultiplex turns on multiplexing for the gRPC broker. The gRPC
252+
// broker will multiplex all brokered gRPC servers over the plugin's original
253+
// listener socket instead of making a new listener for each server. The
254+
// go-plugin library currently only includes a Go implementation for the
255+
// server (i.e. plugin) side of gRPC broker multiplexing.
256+
//
257+
// Does not support reattaching.
258+
//
259+
// Multiplexed gRPC streams MUST be established sequentially, i.e. after
260+
// calling AcceptAndServe from one side, wait for the other side to Dial
261+
// before calling AcceptAndServe again.
262+
GRPCBrokerMultiplex bool
263+
240264
// SkipHostEnv allows plugins to run without inheriting the parent process'
241265
// environment variables.
242266
SkipHostEnv bool
@@ -352,7 +376,7 @@ func CleanupClients() {
352376
wg.Wait()
353377
}
354378

355-
// Creates a new plugin client which manages the lifecycle of an external
379+
// NewClient creates a new plugin client which manages the lifecycle of an external
356380
// plugin and gets the address for the RPC connection.
357381
//
358382
// The client must be cleaned up at some point by calling Kill(). If
@@ -374,10 +398,10 @@ func NewClient(config *ClientConfig) (c *Client) {
374398
}
375399

376400
if config.SyncStdout == nil {
377-
config.SyncStdout = ioutil.Discard
401+
config.SyncStdout = io.Discard
378402
}
379403
if config.SyncStderr == nil {
380-
config.SyncStderr = ioutil.Discard
404+
config.SyncStderr = io.Discard
381405
}
382406

383407
if config.AllowedProtocols == nil {
@@ -572,6 +596,10 @@ func (c *Client) Start() (addr net.Addr, err error) {
572596
if c.config.SecureConfig != nil && c.config.Reattach != nil {
573597
return nil, ErrSecureConfigAndReattach
574598
}
599+
600+
if c.config.GRPCBrokerMultiplex && c.config.Reattach != nil {
601+
return nil, fmt.Errorf("gRPC broker multiplexing is not supported with Reattach config")
602+
}
575603
}
576604

577605
if c.config.Reattach != nil {
@@ -603,6 +631,9 @@ func (c *Client) Start() (addr net.Addr, err error) {
603631
fmt.Sprintf("PLUGIN_MAX_PORT=%d", c.config.MaxPort),
604632
fmt.Sprintf("PLUGIN_PROTOCOL_VERSIONS=%s", strings.Join(versionStrings, ",")),
605633
}
634+
if c.config.GRPCBrokerMultiplex {
635+
env = append(env, fmt.Sprintf("%s=true", envMultiplexGRPC))
636+
}
606637

607638
cmd := c.config.Cmd
608639
if cmd == nil {
@@ -790,7 +821,7 @@ func (c *Client) Start() (addr net.Addr, err error) {
790821
// Trim the line and split by "|" in order to get the parts of
791822
// the output.
792823
line = strings.TrimSpace(line)
793-
parts := strings.SplitN(line, "|", 6)
824+
parts := strings.Split(line, "|")
794825
if len(parts) < 4 {
795826
errText := fmt.Sprintf("Unrecognized remote plugin message: %s", line)
796827
if !ok {
@@ -878,6 +909,18 @@ func (c *Client) Start() (addr net.Addr, err error) {
878909
return nil, fmt.Errorf("error parsing server cert: %s", err)
879910
}
880911
}
912+
913+
if c.config.GRPCBrokerMultiplex && c.protocol == ProtocolGRPC {
914+
if len(parts) <= 6 {
915+
return nil, fmt.Errorf("%w; for Go plugins, you will need to update the "+
916+
"github.com/hashicorp/go-plugin dependency and recompile", ErrGRPCBrokerMuxNotSupported)
917+
}
918+
if muxSupported, err := strconv.ParseBool(parts[6]); err != nil {
919+
return nil, fmt.Errorf("error parsing %q as a boolean for gRPC broker multiplexing support", parts[6])
920+
} else if !muxSupported {
921+
return nil, ErrGRPCBrokerMuxNotSupported
922+
}
923+
}
881924
}
882925

883926
c.address = addr
@@ -951,12 +994,11 @@ func (c *Client) reattach() (net.Addr, error) {
951994

952995
if c.config.Reattach.Test {
953996
c.negotiatedVersion = c.config.Reattach.ProtocolVersion
954-
}
955-
956-
// If we're in test mode, we do NOT set the process. This avoids the
957-
// process being killed (the only purpose we have for c.process), since
958-
// in test mode the process is responsible for exiting on its own.
959-
if !c.config.Reattach.Test {
997+
} else {
998+
// If we're in test mode, we do NOT set the runner. This avoids the
999+
// runner being killed (the only purpose we have for setting c.runner
1000+
// when reattaching), since in test mode the process is responsible for
1001+
// exiting on its own.
9601002
c.runner = r
9611003
}
9621004

@@ -1061,11 +1103,24 @@ func netAddrDialer(addr net.Addr) func(string, time.Duration) (net.Conn, error)
10611103
// dialer is compatible with grpc.WithDialer and creates the connection
10621104
// to the plugin.
10631105
func (c *Client) dialer(_ string, timeout time.Duration) (net.Conn, error) {
1064-
conn, err := netAddrDialer(c.address)("", timeout)
1106+
muxer, err := c.getGRPCMuxer(c.address)
10651107
if err != nil {
10661108
return nil, err
10671109
}
10681110

1111+
var conn net.Conn
1112+
if muxer.Enabled() {
1113+
conn, err = muxer.Dial()
1114+
if err != nil {
1115+
return nil, err
1116+
}
1117+
} else {
1118+
conn, err = netAddrDialer(c.address)("", timeout)
1119+
if err != nil {
1120+
return nil, err
1121+
}
1122+
}
1123+
10691124
// If we have a TLS config we wrap our connection. We only do this
10701125
// for net/rpc since gRPC uses its own mechanism for TLS.
10711126
if c.protocol == ProtocolNetRPC && c.config.TLSConfig != nil {
@@ -1075,6 +1130,22 @@ func (c *Client) dialer(_ string, timeout time.Duration) (net.Conn, error) {
10751130
return conn, nil
10761131
}
10771132

1133+
func (c *Client) getGRPCMuxer(addr net.Addr) (*grpcmux.GRPCClientMuxer, error) {
1134+
if c.protocol != ProtocolGRPC || !c.config.GRPCBrokerMultiplex {
1135+
return nil, nil
1136+
}
1137+
1138+
var err error
1139+
c.grpcMuxerOnce.Do(func() {
1140+
c.grpcMuxer, err = grpcmux.NewGRPCClientMuxer(c.logger, addr)
1141+
})
1142+
if err != nil {
1143+
return nil, err
1144+
}
1145+
1146+
return c.grpcMuxer, nil
1147+
}
1148+
10781149
var stdErrBufferSize = 64 * 1024
10791150

10801151
func (c *Client) logStderr(name string, r io.Reader) {

client_test.go

+32-10
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import (
77
"bytes"
88
"context"
99
"crypto/sha256"
10+
"errors"
1011
"fmt"
1112
"io"
12-
"io/ioutil"
1313
"log"
1414
"net"
1515
"os"
@@ -65,10 +65,7 @@ func TestClient(t *testing.T) {
6565
// This tests a bug where Kill would start
6666
func TestClient_killStart(t *testing.T) {
6767
// Create a temporary dir to store the result file
68-
td, err := ioutil.TempDir("", "plugin")
69-
if err != nil {
70-
t.Fatalf("err: %s", err)
71-
}
68+
td := t.TempDir()
7269
defer os.RemoveAll(td)
7370

7471
// Start the client
@@ -115,10 +112,7 @@ func TestClient_killStart(t *testing.T) {
115112

116113
func TestClient_testCleanup(t *testing.T) {
117114
// Create a temporary dir to store the result file
118-
td, err := ioutil.TempDir("", "plugin")
119-
if err != nil {
120-
t.Fatalf("err: %s", err)
121-
}
115+
td := t.TempDir()
122116
defer os.RemoveAll(td)
123117

124118
// Create a path that the helper process will write on cleanup
@@ -825,7 +819,7 @@ func TestClient_textLogLevel(t *testing.T) {
825819

826820
func TestClient_Stdin(t *testing.T) {
827821
// Overwrite stdin for this test with a temporary file
828-
tf, err := ioutil.TempFile("", "terraform")
822+
tf, err := os.CreateTemp("", "terraform")
829823
if err != nil {
830824
t.Fatalf("err: %s", err)
831825
}
@@ -914,6 +908,34 @@ func TestClient_SkipHostEnv(t *testing.T) {
914908
}
915909
}
916910

911+
func TestClient_RequestGRPCMultiplexing_UnsupportedByPlugin(t *testing.T) {
912+
for _, name := range []string{
913+
"mux-grpc-with-old-plugin",
914+
"mux-grpc-with-unsupported-plugin",
915+
} {
916+
t.Run(name, func(t *testing.T) {
917+
process := helperProcess(name)
918+
c := NewClient(&ClientConfig{
919+
Cmd: process,
920+
HandshakeConfig: testHandshake,
921+
Plugins: testGRPCPluginMap,
922+
AllowedProtocols: []Protocol{ProtocolGRPC},
923+
GRPCBrokerMultiplex: true,
924+
})
925+
defer c.Kill()
926+
927+
_, err := c.Start()
928+
if err == nil {
929+
t.Fatal("expected error")
930+
}
931+
932+
if !errors.Is(err, ErrGRPCBrokerMuxNotSupported) {
933+
t.Fatalf("expected %s, but got %s", ErrGRPCBrokerMuxNotSupported, err)
934+
}
935+
})
936+
}
937+
}
938+
917939
func TestClient_SecureConfig(t *testing.T) {
918940
// Test failure case
919941
secureConfig := &SecureConfig{

constants.go

+2
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ const (
1111
// EnvUnixSocketGroup specifies the owning, writable group to set for Unix
1212
// sockets created by _plugins_. Does not affect client behavior.
1313
EnvUnixSocketGroup = "PLUGIN_UNIX_SOCKET_GROUP"
14+
15+
envMultiplexGRPC = "PLUGIN_MULTIPLEX_GRPC"
1416
)

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ go 1.17
55
require (
66
github.com/golang/protobuf v1.5.0
77
github.com/hashicorp/go-hclog v0.14.1
8-
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb
8+
github.com/hashicorp/yamux v0.1.1
99
github.com/jhump/protoreflect v1.15.1
1010
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77
1111
github.com/oklog/run v1.0.0
1212
google.golang.org/grpc v1.38.0
13+
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743
1314
)
1415

1516
require (
@@ -22,5 +23,4 @@ require (
2223
golang.org/x/sys v0.13.0 // indirect
2324
golang.org/x/text v0.13.0 // indirect
2425
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
25-
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 // indirect
2626
)

0 commit comments

Comments
 (0)