vault/sdk/helper/pluginutil/run_config_test.go
John-Michael Faircloth 3565c90cf8
feature: multiplexing support for database plugins (#14033)
* feat: DB plugin multiplexing (#13734)

* WIP: start from main and get a plugin runner from core

* move MultiplexedClient map to plugin catalog
- call sys.NewPluginClient from PluginFactory
- updates to getPluginClient
- thread through isMetadataMode

* use go-plugin ClientProtocol interface
- call sys.NewPluginClient from dbplugin.NewPluginClient

* move PluginSets to dbplugin package
- export dbplugin HandshakeConfig
- small refactor of PluginCatalog.getPluginClient

* add removeMultiplexedClient; clean up on Close()
- call client.Kill from plugin catalog
- set rpcClient when muxed client exists

* add ID to dbplugin.DatabasePluginClient struct

* only create one plugin process per plugin type

* update NewPluginClient to return connection ID to sdk
- wrap grpc.ClientConn so we can inject the ID into context
- get ID from context on grpc server

* add v6 multiplexing  protocol version

* WIP: backwards compat for db plugins

* Ensure locking on plugin catalog access

- Create public GetPluginClient method for plugin catalog
- rename postgres db plugin

* use the New constructor for db plugins

* grpc server: use write lock for Close and rlock for CRUD

* cleanup MultiplexedClients on Close

* remove TODO

* fix multiplexing regression with grpc server connection

* cleanup grpc server instances on close

* embed ClientProtocol in Multiplexer interface

* use PluginClientConfig arg to make NewPluginClient plugin type agnostic

* create a new plugin process for non-muxed plugins

* feat: plugin multiplexing: handle plugin client cleanup (#13896)

* use closure for plugin client cleanup

* log and return errors; add comments

* move rpcClient wrapping to core for ID injection

* refactor core plugin client and sdk

* remove unused ID method

* refactor and only wrap clientConn on multiplexed plugins

* rename structs and do not export types

* Slight refactor of system view interface

* Revert "Slight refactor of system view interface"

This reverts commit 73d420e5cd2f0415e000c5a9284ea72a58016dd6.

* Revert "Revert "Slight refactor of system view interface""

This reverts commit f75527008a1db06d04a23e04c3059674be8adb5f.

* only provide pluginRunner arg to the internal newPluginClient method

* embed ClientProtocol in pluginClient and name logger

* Add back MLock support

* remove enableMlock arg from setupPluginCatalog

* rename plugin util interface to PluginClient

Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>

* feature: multiplexing: fix unit tests (#14007)

* fix grpc_server tests and add coverage

* update run_config tests

* add happy path test case for grpc_server ID from context

* update test helpers

* feat: multiplexing: handle v5 plugin compiled with new sdk

* add mux supported flag and increase test coverage

* set multiplexingSupport field in plugin server

* remove multiplexingSupport field in sdk

* revert postgres to non-multiplexed

* add comments on grpc server fields

* use pointer receiver on grpc server methods

* add changelog

* use pointer for grpcserver instance

* Use a gRPC server to determine if a plugin should be multiplexed

* Apply suggestions from code review

Co-authored-by: Brian Kassouf <briankassouf@users.noreply.github.com>

* add lock to removePluginClient

* add multiplexingSupport field to externalPlugin struct

* do not send nil to grpc MultiplexingSupport

* check err before logging

* handle locking scenario for cleanupFunc

* allow ServeConfigMultiplex to dispense v5 plugin

* reposition structs, add err check and comments

* add comment on locking for cleanupExternalPlugin

Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
Co-authored-by: Brian Kassouf <briankassouf@users.noreply.github.com>
2022-02-17 08:50:33 -06:00

354 lines
9.1 KiB
Go

package pluginutil
import (
"context"
"fmt"
"os/exec"
"reflect"
"testing"
"time"
"github.com/hashicorp/vault/sdk/version"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/sdk/helper/wrapping"
"github.com/stretchr/testify/mock"
)
func TestMakeConfig(t *testing.T) {
type testCase struct {
rc runConfig
responseWrapInfo *wrapping.ResponseWrapInfo
responseWrapInfoErr error
responseWrapInfoTimes int
mlockEnabled bool
mlockEnabledTimes int
expectedConfig *plugin.ClientConfig
expectTLSConfig bool
}
tests := map[string]testCase{
"metadata mode, not-AutoMTLS": {
rc: runConfig{
command: "echo",
args: []string{"foo", "bar"},
sha256: []byte("some_sha256"),
env: []string{"initial=true"},
PluginClientConfig: PluginClientConfig{
PluginSets: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
Logger: hclog.NewNullLogger(),
IsMetadataMode: true,
AutoMTLS: false,
},
},
responseWrapInfoTimes: 0,
mlockEnabled: false,
mlockEnabledTimes: 1,
expectedConfig: &plugin.ClientConfig{
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
VersionedPlugins: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
Cmd: commandWithEnv(
"echo",
[]string{"foo", "bar"},
[]string{
"initial=true",
fmt.Sprintf("%s=%s", PluginVaultVersionEnv, version.GetVersion().Version),
fmt.Sprintf("%s=%t", PluginMetadataModeEnv, true),
},
),
SecureConfig: &plugin.SecureConfig{
Checksum: []byte("some_sha256"),
// Hash is generated
},
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC,
plugin.ProtocolGRPC,
},
Logger: hclog.NewNullLogger(),
AutoMTLS: false,
},
expectTLSConfig: false,
},
"non-metadata mode, not-AutoMTLS": {
rc: runConfig{
command: "echo",
args: []string{"foo", "bar"},
sha256: []byte("some_sha256"),
env: []string{"initial=true"},
PluginClientConfig: PluginClientConfig{
PluginSets: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
Logger: hclog.NewNullLogger(),
IsMetadataMode: false,
AutoMTLS: false,
},
},
responseWrapInfo: &wrapping.ResponseWrapInfo{
Token: "testtoken",
},
responseWrapInfoTimes: 1,
mlockEnabled: true,
mlockEnabledTimes: 1,
expectedConfig: &plugin.ClientConfig{
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
VersionedPlugins: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
Cmd: commandWithEnv(
"echo",
[]string{"foo", "bar"},
[]string{
"initial=true",
fmt.Sprintf("%s=%t", PluginMlockEnabled, true),
fmt.Sprintf("%s=%s", PluginVaultVersionEnv, version.GetVersion().Version),
fmt.Sprintf("%s=%t", PluginMetadataModeEnv, false),
fmt.Sprintf("%s=%s", PluginUnwrapTokenEnv, "testtoken"),
},
),
SecureConfig: &plugin.SecureConfig{
Checksum: []byte("some_sha256"),
// Hash is generated
},
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC,
plugin.ProtocolGRPC,
},
Logger: hclog.NewNullLogger(),
AutoMTLS: false,
},
expectTLSConfig: true,
},
"metadata mode, AutoMTLS": {
rc: runConfig{
command: "echo",
args: []string{"foo", "bar"},
sha256: []byte("some_sha256"),
env: []string{"initial=true"},
PluginClientConfig: PluginClientConfig{
PluginSets: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
Logger: hclog.NewNullLogger(),
IsMetadataMode: true,
AutoMTLS: true,
},
},
responseWrapInfoTimes: 0,
mlockEnabled: false,
mlockEnabledTimes: 1,
expectedConfig: &plugin.ClientConfig{
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
VersionedPlugins: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
Cmd: commandWithEnv(
"echo",
[]string{"foo", "bar"},
[]string{
"initial=true",
fmt.Sprintf("%s=%s", PluginVaultVersionEnv, version.GetVersion().Version),
fmt.Sprintf("%s=%t", PluginMetadataModeEnv, true),
},
),
SecureConfig: &plugin.SecureConfig{
Checksum: []byte("some_sha256"),
// Hash is generated
},
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC,
plugin.ProtocolGRPC,
},
Logger: hclog.NewNullLogger(),
AutoMTLS: true,
},
expectTLSConfig: false,
},
"not-metadata mode, AutoMTLS": {
rc: runConfig{
command: "echo",
args: []string{"foo", "bar"},
sha256: []byte("some_sha256"),
env: []string{"initial=true"},
PluginClientConfig: PluginClientConfig{
PluginSets: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
Logger: hclog.NewNullLogger(),
IsMetadataMode: false,
AutoMTLS: true,
},
},
responseWrapInfoTimes: 0,
mlockEnabled: false,
mlockEnabledTimes: 1,
expectedConfig: &plugin.ClientConfig{
HandshakeConfig: plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "magic_cookie_key",
MagicCookieValue: "magic_cookie_value",
},
VersionedPlugins: map[int]plugin.PluginSet{
1: {
"bogus": nil,
},
},
Cmd: commandWithEnv(
"echo",
[]string{"foo", "bar"},
[]string{
"initial=true",
fmt.Sprintf("%s=%s", PluginVaultVersionEnv, version.GetVersion().Version),
fmt.Sprintf("%s=%t", PluginMetadataModeEnv, false),
},
),
SecureConfig: &plugin.SecureConfig{
Checksum: []byte("some_sha256"),
// Hash is generated
},
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC,
plugin.ProtocolGRPC,
},
Logger: hclog.NewNullLogger(),
AutoMTLS: true,
},
expectTLSConfig: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
mockWrapper := new(mockRunnerUtil)
mockWrapper.On("ResponseWrapData", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(test.responseWrapInfo, test.responseWrapInfoErr)
mockWrapper.On("MlockEnabled").
Return(test.mlockEnabled)
test.rc.wrapper = mockWrapper
defer mockWrapper.AssertNumberOfCalls(t, "ResponseWrapData", test.responseWrapInfoTimes)
defer mockWrapper.AssertNumberOfCalls(t, "MlockEnabled", test.mlockEnabledTimes)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
config, err := test.rc.makeConfig(ctx)
if err != nil {
t.Fatalf("no error expected, got: %s", err)
}
// The following fields are generated, so we just need to check for existence, not specific value
// The value must be nilled out before performing a DeepEqual check
hsh := config.SecureConfig.Hash
if hsh == nil {
t.Fatalf("Missing SecureConfig.Hash")
}
config.SecureConfig.Hash = nil
if test.expectTLSConfig && config.TLSConfig == nil {
t.Fatalf("TLS config expected, got nil")
}
if !test.expectTLSConfig && config.TLSConfig != nil {
t.Fatalf("no TLS config expected, got: %#v", config.TLSConfig)
}
config.TLSConfig = nil
if !reflect.DeepEqual(config, test.expectedConfig) {
t.Fatalf("Actual config: %#v\nExpected config: %#v", config, test.expectedConfig)
}
})
}
}
func commandWithEnv(cmd string, args []string, env []string) *exec.Cmd {
c := exec.Command(cmd, args...)
c.Env = env
return c
}
var _ RunnerUtil = &mockRunnerUtil{}
type mockRunnerUtil struct {
mock.Mock
}
func (m *mockRunnerUtil) NewPluginClient(ctx context.Context, config PluginClientConfig) (PluginClient, error) {
args := m.Called(ctx, config)
return args.Get(0).(PluginClient), args.Error(1)
}
func (m *mockRunnerUtil) ResponseWrapData(ctx context.Context, data map[string]interface{}, ttl time.Duration, jwt bool) (*wrapping.ResponseWrapInfo, error) {
args := m.Called(ctx, data, ttl, jwt)
return args.Get(0).(*wrapping.ResponseWrapInfo), args.Error(1)
}
func (m *mockRunnerUtil) MlockEnabled() bool {
args := m.Called()
return args.Bool(0)
}