Plugin version negotiation (#5434)
* Plugin version updates * Update datatbase plugins * Revert netRPC deletions * Revert netRPC deletions * Update plugins to serve both versions * Update database plugins * Add Initialize back in * revert pointer changes * Add deprecation warning * Update tests * Update go-plugin * Review Feedback
This commit is contained in:
parent
51a240ec74
commit
e943a60041
|
@ -32,12 +32,21 @@ func (dc *DatabasePluginClient) Close() error {
|
|||
// plugin. The client is wrapped in a DatabasePluginClient object to ensure the
|
||||
// plugin is killed on call of Close().
|
||||
func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger) (Database, error) {
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
"database": new(DatabasePlugin),
|
||||
// pluginSets is the map of plugins we can dispense.
|
||||
pluginSets := map[int]plugin.PluginSet{
|
||||
// Version 3 supports both protocols
|
||||
3: plugin.PluginSet{
|
||||
"database": &DatabasePlugin{
|
||||
GRPCDatabasePlugin: new(GRPCDatabasePlugin),
|
||||
},
|
||||
},
|
||||
// Version 4 only supports gRPC
|
||||
4: plugin.PluginSet{
|
||||
"database": new(GRPCDatabasePlugin),
|
||||
},
|
||||
}
|
||||
|
||||
client, err := pluginRunner.Run(ctx, sys, pluginMap, handshakeConfig, []string{}, logger)
|
||||
client, err := pluginRunner.Run(ctx, sys, pluginSets, handshakeConfig, []string{}, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ type Database interface {
|
|||
Init(ctx context.Context, config map[string]interface{}, verifyConnection bool) (saveConfig map[string]interface{}, err error)
|
||||
Close() error
|
||||
|
||||
// DEPRECATED, will be removed in 0.12
|
||||
// DEPRECATED, will be removed in 0.13
|
||||
Initialize(ctx context.Context, config map[string]interface{}, verifyConnection bool) (err error)
|
||||
}
|
||||
|
||||
|
@ -104,25 +104,35 @@ func PluginFactory(ctx context.Context, pluginName string, sys pluginutil.LookRu
|
|||
// This prevents users from executing bad plugins or executing a plugin
|
||||
// directory. It is a UX feature, not a security feature.
|
||||
var handshakeConfig = plugin.HandshakeConfig{
|
||||
ProtocolVersion: 3,
|
||||
ProtocolVersion: 4,
|
||||
MagicCookieKey: "VAULT_DATABASE_PLUGIN",
|
||||
MagicCookieValue: "926a0820-aea2-be28-51d6-83cdf00e8edb",
|
||||
}
|
||||
|
||||
var _ plugin.Plugin = &DatabasePlugin{}
|
||||
var _ plugin.GRPCPlugin = &DatabasePlugin{}
|
||||
var _ plugin.Plugin = &GRPCDatabasePlugin{}
|
||||
var _ plugin.GRPCPlugin = &GRPCDatabasePlugin{}
|
||||
|
||||
// DatabasePlugin implements go-plugin's Plugin interface. It has methods for
|
||||
// retrieving a server and a client instance of the plugin.
|
||||
type DatabasePlugin struct {
|
||||
impl Database
|
||||
*GRPCDatabasePlugin
|
||||
}
|
||||
|
||||
// GRPCDatabasePlugin is the plugin.Plugin implementation that only supports GRPC
|
||||
// transport
|
||||
type GRPCDatabasePlugin struct {
|
||||
Impl Database
|
||||
|
||||
// Embeding this will disable the netRPC protocol
|
||||
plugin.NetRPCUnsupportedPlugin
|
||||
}
|
||||
|
||||
func (d DatabasePlugin) Server(*plugin.MuxBroker) (interface{}, error) {
|
||||
impl := &DatabaseErrorSanitizerMiddleware{
|
||||
next: d.impl,
|
||||
next: d.Impl,
|
||||
}
|
||||
|
||||
return &databasePluginRPCServer{impl: impl}, nil
|
||||
}
|
||||
|
||||
|
@ -130,16 +140,16 @@ func (DatabasePlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, e
|
|||
return &databasePluginRPCClient{client: c}, nil
|
||||
}
|
||||
|
||||
func (d DatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
func (d GRPCDatabasePlugin) GRPCServer(_ *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
impl := &DatabaseErrorSanitizerMiddleware{
|
||||
next: d.impl,
|
||||
next: d.Impl,
|
||||
}
|
||||
|
||||
RegisterDatabaseServer(s, &gRPCServer{impl: impl})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (DatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||
func (GRPCDatabasePlugin) GRPCClient(doneCtx context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||
return &gRPCClient{
|
||||
client: NewDatabaseClient(c),
|
||||
clientConn: c,
|
||||
|
|
|
@ -127,6 +127,7 @@ func TestPlugin_NetRPC_Main(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
os.Unsetenv(pluginutil.PluginVaultVersionEnv)
|
||||
p := &mockPlugin{
|
||||
users: make(map[string][]string),
|
||||
}
|
||||
|
|
|
@ -15,24 +15,34 @@ func Serve(db Database, tlsProvider func() (*tls.Config, error)) {
|
|||
}
|
||||
|
||||
func ServeConfig(db Database, tlsProvider func() (*tls.Config, error)) *plugin.ServeConfig {
|
||||
dbPlugin := &DatabasePlugin{
|
||||
impl: db,
|
||||
}
|
||||
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
"database": dbPlugin,
|
||||
// pluginSets is the map of plugins we can dispense.
|
||||
pluginSets := map[int]plugin.PluginSet{
|
||||
3: plugin.PluginSet{
|
||||
"database": &DatabasePlugin{
|
||||
GRPCDatabasePlugin: &GRPCDatabasePlugin{
|
||||
Impl: db,
|
||||
},
|
||||
},
|
||||
},
|
||||
4: plugin.PluginSet{
|
||||
"database": &GRPCDatabasePlugin{
|
||||
Impl: db,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conf := &plugin.ServeConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: pluginMap,
|
||||
VersionedPlugins: pluginSets,
|
||||
TLSProvider: tlsProvider,
|
||||
GRPCServer: plugin.DefaultGRPCServer,
|
||||
}
|
||||
|
||||
// If we do not have gRPC support fallback to version 3
|
||||
// Remove this block in 0.13
|
||||
if !pluginutil.GRPCSupport() {
|
||||
conf.GRPCServer = nil
|
||||
delete(conf.VersionedPlugins, 4)
|
||||
}
|
||||
|
||||
return conf
|
||||
|
|
|
@ -35,32 +35,27 @@ func OptionallyEnableMlock() error {
|
|||
// it fails to meet the version constraint.
|
||||
func GRPCSupport() bool {
|
||||
verString := os.Getenv(PluginVaultVersionEnv)
|
||||
|
||||
// If the env var is empty, we fall back to netrpc for backward compatibility.
|
||||
if verString == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if verString != "unknown" {
|
||||
ver, err := version.NewVersion(verString)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Due to some regressions on 0.9.2 & 0.9.3 we now require version 0.9.4
|
||||
// to allow the plugin framework to default to gRPC.
|
||||
constraint, err := version.NewConstraint(">= 0.9.4")
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return constraint.Check(ver)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns true if the plugin calling this function is running in metadata mode.
|
||||
// InMetadataMode returns true if the plugin calling this function is running in metadata mode.
|
||||
func InMetadataMode() bool {
|
||||
return os.Getenv(PluginMetadataModeEnv) == "true"
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ type Looker interface {
|
|||
LookupPlugin(context.Context, string) (*PluginRunner, error)
|
||||
}
|
||||
|
||||
// Wrapper interface defines the functions needed by the runner to wrap the
|
||||
// RunnerUtil interface defines the functions needed by the runner to wrap the
|
||||
// metadata needed to run a plugin process. This includes looking up Mlock
|
||||
// configuration and wrapping data in a response wrapped token.
|
||||
// logical.SystemView implementations satisfy this interface.
|
||||
|
@ -31,7 +31,7 @@ type RunnerUtil interface {
|
|||
MlockEnabled() bool
|
||||
}
|
||||
|
||||
// LookWrapper defines the functions for both Looker and Wrapper
|
||||
// LookRunnerUtil defines the functions for both Looker and Wrapper
|
||||
type LookRunnerUtil interface {
|
||||
Looker
|
||||
RunnerUtil
|
||||
|
@ -52,19 +52,19 @@ type PluginRunner struct {
|
|||
// Run takes a wrapper RunnerUtil instance along with the go-plugin parameters and
|
||||
// returns a configured plugin.Client with TLS Configured and a wrapping token set
|
||||
// on PluginUnwrapTokenEnv for plugin process consumption.
|
||||
func (r *PluginRunner) Run(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) {
|
||||
return r.runCommon(ctx, wrapper, pluginMap, hs, env, logger, false)
|
||||
func (r *PluginRunner) Run(ctx context.Context, wrapper RunnerUtil, pluginSets map[int]plugin.PluginSet, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) {
|
||||
return r.runCommon(ctx, wrapper, pluginSets, hs, env, logger, false)
|
||||
}
|
||||
|
||||
// RunMetadataMode returns a configured plugin.Client that will dispense a plugin
|
||||
// in metadata mode. The PluginMetadataModeEnv is passed in as part of the Cmd to
|
||||
// plugin.Client, and consumed by the plugin process on pluginutil.VaultPluginTLSProvider.
|
||||
func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) {
|
||||
return r.runCommon(ctx, wrapper, pluginMap, hs, env, logger, true)
|
||||
func (r *PluginRunner) RunMetadataMode(ctx context.Context, wrapper RunnerUtil, pluginSets map[int]plugin.PluginSet, hs plugin.HandshakeConfig, env []string, logger log.Logger) (*plugin.Client, error) {
|
||||
return r.runCommon(ctx, wrapper, pluginSets, hs, env, logger, true)
|
||||
|
||||
}
|
||||
|
||||
func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, pluginMap map[string]plugin.Plugin, hs plugin.HandshakeConfig, env []string, logger log.Logger, isMetadataMode bool) (*plugin.Client, error) {
|
||||
func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, pluginSets map[int]plugin.PluginSet, hs plugin.HandshakeConfig, env []string, logger log.Logger, isMetadataMode bool) (*plugin.Client, error) {
|
||||
cmd := exec.Command(r.Command, r.Args...)
|
||||
|
||||
// `env` should always go last to avoid overwriting internal values that might
|
||||
|
@ -116,7 +116,7 @@ func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, plugin
|
|||
|
||||
clientConfig := &plugin.ClientConfig{
|
||||
HandshakeConfig: hs,
|
||||
Plugins: pluginMap,
|
||||
VersionedPlugins: pluginSets,
|
||||
Cmd: cmd,
|
||||
SecureConfig: secureConfig,
|
||||
TLSConfig: clientTLSConfig,
|
||||
|
@ -132,6 +132,8 @@ func (r *PluginRunner) runCommon(ctx context.Context, wrapper RunnerUtil, plugin
|
|||
return client, nil
|
||||
}
|
||||
|
||||
// APIClientMeta is a helper that plugins can use to configure TLS connections
|
||||
// back to Vault.
|
||||
type APIClientMeta struct {
|
||||
// These are set by the command line flags.
|
||||
flagCACert string
|
||||
|
@ -141,6 +143,7 @@ type APIClientMeta struct {
|
|||
flagInsecure bool
|
||||
}
|
||||
|
||||
// FlagSet returns the flag set for configuring the TLS connection
|
||||
func (f *APIClientMeta) FlagSet() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("vault plugin settings", flag.ContinueOnError)
|
||||
|
||||
|
@ -153,6 +156,7 @@ func (f *APIClientMeta) FlagSet() *flag.FlagSet {
|
|||
return fs
|
||||
}
|
||||
|
||||
// GetTLSConfig will return a TLSConfig based off the values from the flags
|
||||
func (f *APIClientMeta) GetTLSConfig() *api.TLSConfig {
|
||||
// If we need custom TLS configuration, then set it
|
||||
if f.flagCACert != "" || f.flagCAPath != "" || f.flagClientCert != "" || f.flagClientKey != "" || f.flagInsecure {
|
||||
|
@ -171,7 +175,7 @@ func (f *APIClientMeta) GetTLSConfig() *api.TLSConfig {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CancelIfCanceled takes a context cancel func and a context. If the context is
|
||||
// CtxCancelIfCanceled takes a context cancel func and a context. If the context is
|
||||
// shutdown the cancelfunc is called. This is useful for merging two cancel
|
||||
// functions.
|
||||
func CtxCancelIfCanceled(f context.CancelFunc, ctxCanceler context.Context) chan struct{} {
|
||||
|
|
|
@ -13,11 +13,25 @@ import (
|
|||
"github.com/hashicorp/vault/logical/plugin/pb"
|
||||
)
|
||||
|
||||
var _ plugin.Plugin = (*BackendPlugin)(nil)
|
||||
var _ plugin.GRPCPlugin = (*BackendPlugin)(nil)
|
||||
var _ plugin.Plugin = (*GRPCBackendPlugin)(nil)
|
||||
var _ plugin.GRPCPlugin = (*GRPCBackendPlugin)(nil)
|
||||
|
||||
// BackendPlugin is the plugin.Plugin implementation
|
||||
type BackendPlugin struct {
|
||||
*GRPCBackendPlugin
|
||||
}
|
||||
|
||||
// GRPCBackendPlugin is the plugin.Plugin implementation that only supports GRPC
|
||||
// transport
|
||||
type GRPCBackendPlugin struct {
|
||||
Factory logical.Factory
|
||||
metadataMode bool
|
||||
MetadataMode bool
|
||||
Logger log.Logger
|
||||
|
||||
// Embeding this will disable the netRPC protocol
|
||||
plugin.NetRPCUnsupportedPlugin
|
||||
}
|
||||
|
||||
// Server gets called when on plugin.Serve()
|
||||
|
@ -33,10 +47,14 @@ func (b *BackendPlugin) Server(broker *plugin.MuxBroker) (interface{}, error) {
|
|||
|
||||
// Client gets called on plugin.NewClient()
|
||||
func (b BackendPlugin) Client(broker *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
|
||||
return &backendPluginClient{client: c, broker: broker, metadataMode: b.metadataMode}, nil
|
||||
return &backendPluginClient{
|
||||
client: c,
|
||||
broker: broker,
|
||||
metadataMode: b.MetadataMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b BackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
func (b GRPCBackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
pb.RegisterBackendServer(s, &backendGRPCPluginServer{
|
||||
broker: broker,
|
||||
factory: b.Factory,
|
||||
|
@ -47,13 +65,14 @@ func (b BackendPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *BackendPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||
func (b *GRPCBackendPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||
ret := &backendGRPCPluginClient{
|
||||
client: pb.NewBackendClient(c),
|
||||
clientConn: c,
|
||||
broker: broker,
|
||||
cleanupCh: make(chan struct{}),
|
||||
doneCtx: ctx,
|
||||
metadataMode: b.MetadataMode,
|
||||
}
|
||||
|
||||
// Create the value and set the type
|
||||
|
|
|
@ -140,8 +140,10 @@ func testBackend(t *testing.T) (logical.Backend, func()) {
|
|||
// Create a mock provider
|
||||
pluginMap := map[string]gplugin.Plugin{
|
||||
"backend": &BackendPlugin{
|
||||
GRPCBackendPlugin: &GRPCBackendPlugin{
|
||||
Factory: mock.Factory,
|
||||
},
|
||||
},
|
||||
}
|
||||
client, _ := gplugin.TestPluginRPCConn(t, pluginMap, nil)
|
||||
cleanup := func() {
|
||||
|
|
|
@ -141,6 +141,7 @@ func testGRPCBackend(t *testing.T) (logical.Backend, func()) {
|
|||
// Create a mock provider
|
||||
pluginMap := map[string]gplugin.Plugin{
|
||||
"backend": &BackendPlugin{
|
||||
GRPCBackendPlugin: &GRPCBackendPlugin{
|
||||
Factory: mock.Factory,
|
||||
Logger: log.New(&log.LoggerOptions{
|
||||
Level: log.Debug,
|
||||
|
@ -148,6 +149,7 @@ func testGRPCBackend(t *testing.T) (logical.Backend, func()) {
|
|||
JSONFormat: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
client, _ := gplugin.TestPluginGRPCConn(t, pluginMap)
|
||||
cleanup := func() {
|
||||
|
|
|
@ -96,9 +96,18 @@ func NewBackend(ctx context.Context, pluginName string, sys pluginutil.LookRunne
|
|||
|
||||
func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunner *pluginutil.PluginRunner, logger log.Logger, isMetadataMode bool) (logical.Backend, error) {
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
pluginMap := map[string]plugin.Plugin{
|
||||
pluginSet := map[int]plugin.PluginSet{
|
||||
3: plugin.PluginSet{
|
||||
"backend": &BackendPlugin{
|
||||
metadataMode: isMetadataMode,
|
||||
GRPCBackendPlugin: &GRPCBackendPlugin{
|
||||
MetadataMode: isMetadataMode,
|
||||
},
|
||||
},
|
||||
},
|
||||
4: plugin.PluginSet{
|
||||
"backend": &GRPCBackendPlugin{
|
||||
MetadataMode: isMetadataMode,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -107,9 +116,9 @@ func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunne
|
|||
var client *plugin.Client
|
||||
var err error
|
||||
if isMetadataMode {
|
||||
client, err = pluginRunner.RunMetadataMode(ctx, sys, pluginMap, handshakeConfig, []string{}, namedLogger)
|
||||
client, err = pluginRunner.RunMetadataMode(ctx, sys, pluginSet, handshakeConfig, []string{}, namedLogger)
|
||||
} else {
|
||||
client, err = pluginRunner.Run(ctx, sys, pluginMap, handshakeConfig, []string{}, namedLogger)
|
||||
client, err = pluginRunner.Run(ctx, sys, pluginSet, handshakeConfig, []string{}, namedLogger)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -133,6 +142,7 @@ func newPluginClient(ctx context.Context, sys pluginutil.RunnerUtil, pluginRunne
|
|||
// implementation but is in fact over an RPC connection.
|
||||
switch raw.(type) {
|
||||
case *backendPluginClient:
|
||||
logger.Warn("plugin is using deprecated netRPC transport, recompile plugin to upgrade to gRPC", "plugin", pluginRunner.Name)
|
||||
backend = raw.(*backendPluginClient)
|
||||
transport = "netRPC"
|
||||
case *backendGRPCPluginClient:
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
// BackendPluginName is the name of the plugin that can be
|
||||
// dispensed rom the plugin server.
|
||||
// dispensed from the plugin server.
|
||||
const BackendPluginName = "backend"
|
||||
|
||||
type TLSProviderFunc func() (*tls.Config, error)
|
||||
|
@ -38,11 +38,21 @@ func Serve(opts *ServeOpts) error {
|
|||
}
|
||||
|
||||
// pluginMap is the map of plugins we can dispense.
|
||||
var pluginMap = map[string]plugin.Plugin{
|
||||
pluginSets := map[int]plugin.PluginSet{
|
||||
3: plugin.PluginSet{
|
||||
"backend": &BackendPlugin{
|
||||
GRPCBackendPlugin: &GRPCBackendPlugin{
|
||||
Factory: opts.BackendFactoryFunc,
|
||||
Logger: logger,
|
||||
},
|
||||
},
|
||||
},
|
||||
4: plugin.PluginSet{
|
||||
"backend": &GRPCBackendPlugin{
|
||||
Factory: opts.BackendFactoryFunc,
|
||||
Logger: logger,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := pluginutil.OptionallyEnableMlock()
|
||||
|
@ -52,7 +62,7 @@ func Serve(opts *ServeOpts) error {
|
|||
|
||||
serveOpts := &plugin.ServeConfig{
|
||||
HandshakeConfig: handshakeConfig,
|
||||
Plugins: pluginMap,
|
||||
VersionedPlugins: pluginSets,
|
||||
TLSProvider: opts.TLSProviderFunc,
|
||||
Logger: logger,
|
||||
|
||||
|
@ -64,11 +74,13 @@ func Serve(opts *ServeOpts) error {
|
|||
},
|
||||
}
|
||||
|
||||
// If we do not have gRPC support fallback to version 3
|
||||
// Remove this block in 0.13
|
||||
if !pluginutil.GRPCSupport() {
|
||||
serveOpts.GRPCServer = nil
|
||||
delete(pluginSets, 4)
|
||||
}
|
||||
|
||||
// If FetchMetadata is true, run without TLSProvider
|
||||
plugin.Serve(serveOpts)
|
||||
|
||||
return nil
|
||||
|
@ -79,7 +91,7 @@ func Serve(opts *ServeOpts) error {
|
|||
// This prevents users from executing bad plugins or executing a plugin
|
||||
// directory. It is a UX feature, not a security feature.
|
||||
var handshakeConfig = plugin.HandshakeConfig{
|
||||
ProtocolVersion: 3,
|
||||
ProtocolVersion: 4,
|
||||
MagicCookieKey: "VAULT_BACKEND_PLUGIN",
|
||||
MagicCookieValue: "6669da05-b1c8-4f49-97d9-c8e5bed98e20",
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue