2017-04-04 00:52:29 +00:00
package vault
import (
2018-01-19 06:44:44 +00:00
"context"
2022-08-25 20:31:42 +00:00
"encoding/hex"
2017-04-04 00:52:29 +00:00
"encoding/json"
"errors"
"fmt"
2022-08-25 20:31:42 +00:00
"path"
2017-04-04 00:52:29 +00:00
"path/filepath"
"strings"
"sync"
2018-11-07 01:21:24 +00:00
log "github.com/hashicorp/go-hclog"
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface
Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.
I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:
If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.
* Placeholder backend should be external
We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.
So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 23:37:59 +00:00
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-plugin"
2022-02-17 14:50:33 +00:00
"github.com/hashicorp/go-secure-stdlib/base62"
2022-08-25 20:31:42 +00:00
semver "github.com/hashicorp/go-version"
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface
Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.
I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:
If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.
* Placeholder backend should be external
We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.
So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 23:37:59 +00:00
"github.com/hashicorp/vault/helper/versions"
2020-10-15 19:20:12 +00:00
v4 "github.com/hashicorp/vault/sdk/database/dbplugin"
v5 "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
2019-04-12 21:54:35 +00:00
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/logical"
backendplugin "github.com/hashicorp/vault/sdk/plugin"
2022-02-17 14:50:33 +00:00
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
2017-04-04 00:52:29 +00:00
)
var (
2022-08-30 02:42:26 +00:00
pluginCatalogPath = "core/plugin-catalog/"
ErrDirectoryNotConfigured = errors . New ( "could not set plugin, plugin directory is not configured" )
ErrPluginNotFound = errors . New ( "plugin not found in the catalog" )
ErrPluginConnectionNotFound = errors . New ( "plugin connection not found for client" )
ErrPluginBadType = errors . New ( "unable to determine plugin type" )
2017-04-04 00:52:29 +00:00
)
2017-04-12 17:01:36 +00:00
// PluginCatalog keeps a record of plugins known to vault. External plugins need
// to be registered to the catalog before they can be used in backends. Builtin
// plugins are automatically detected and included in the catalog.
2017-04-04 00:52:29 +00:00
type PluginCatalog struct {
2018-11-07 01:21:24 +00:00
builtinRegistry BuiltinRegistry
catalogView * BarrierView
directory string
2022-02-17 14:50:33 +00:00
logger log . Logger
// externalPlugins holds plugin process connections by plugin name
//
// This allows plugins that suppport multiplexing to use a single grpc
// connection to communicate with multiple "backends". Each backend
// configuration using the same plugin will be routed to the existing
// plugin process.
externalPlugins map [ string ] * externalPlugin
mlockPlugins bool
2017-04-04 00:52:29 +00:00
2017-04-05 00:12:02 +00:00
lock sync . RWMutex
2017-04-04 00:52:29 +00:00
}
2022-02-17 14:50:33 +00:00
// externalPlugin holds client connections for multiplexed and
// non-multiplexed plugin processes
type externalPlugin struct {
// name is the plugin name
name string
// connections holds client connections by ID
connections map [ string ] * pluginClient
multiplexingSupport bool
}
// pluginClient represents a connection to a plugin process
type pluginClient struct {
logger log . Logger
// id is the connection ID
2022-08-30 02:42:26 +00:00
id string
pid int
2022-02-17 14:50:33 +00:00
// client handles the lifecycle of a plugin process
// multiplexed plugins share the same client
client * plugin . Client
clientConn grpc . ClientConnInterface
cleanupFunc func ( ) error
2022-08-30 02:42:26 +00:00
reloadFunc func ( ) error
2022-02-17 14:50:33 +00:00
plugin . ClientProtocol
}
2022-04-04 16:45:41 +00:00
func wrapFactoryCheckPerms ( core * Core , f logical . Factory ) logical . Factory {
return func ( ctx context . Context , conf * logical . BackendConfig ) ( logical . Backend , error ) {
if err := core . CheckPluginPerms ( conf . Config [ "plugin_name" ] ) ; err != nil {
return nil , err
}
return f ( ctx , conf )
}
}
2018-11-07 01:21:24 +00:00
func ( c * Core ) setupPluginCatalog ( ctx context . Context ) error {
2017-04-04 21:43:39 +00:00
c . pluginCatalog = & PluginCatalog {
2018-11-07 01:21:24 +00:00
builtinRegistry : c . builtinRegistry ,
catalogView : NewBarrierView ( c . barrier , pluginCatalogPath ) ,
directory : c . pluginDirectory ,
2022-02-17 14:50:33 +00:00
logger : c . logger ,
mlockPlugins : c . enableMlock ,
2018-11-07 01:21:24 +00:00
}
// Run upgrade if untyped plugins exist
err := c . pluginCatalog . UpgradePlugins ( ctx , c . logger )
if err != nil {
c . logger . Error ( "error while upgrading plugin storage" , "error" , err )
2022-02-17 14:50:33 +00:00
return err
2017-04-04 21:43:39 +00:00
}
2017-04-04 00:52:29 +00:00
2017-09-01 05:02:03 +00:00
if c . logger . IsInfo ( ) {
2018-04-03 00:46:59 +00:00
c . logger . Info ( "successfully setup plugin catalog" , "plugin-directory" , c . pluginDirectory )
2017-09-01 05:02:03 +00:00
}
2017-04-04 00:52:29 +00:00
return nil
}
2022-02-17 14:50:33 +00:00
type pluginClientConn struct {
* grpc . ClientConn
id string
}
var _ grpc . ClientConnInterface = & pluginClientConn { }
func ( d * pluginClientConn ) Invoke ( ctx context . Context , method string , args interface { } , reply interface { } , opts ... grpc . CallOption ) error {
// Inject ID to the context
md := metadata . Pairs ( pluginutil . MultiplexingCtxKey , d . id )
idCtx := metadata . NewOutgoingContext ( ctx , md )
return d . ClientConn . Invoke ( idCtx , method , args , reply , opts ... )
}
func ( d * pluginClientConn ) NewStream ( ctx context . Context , desc * grpc . StreamDesc , method string , opts ... grpc . CallOption ) ( grpc . ClientStream , error ) {
// Inject ID to the context
md := metadata . Pairs ( pluginutil . MultiplexingCtxKey , d . id )
idCtx := metadata . NewOutgoingContext ( ctx , md )
return d . ClientConn . NewStream ( idCtx , desc , method , opts ... )
}
func ( p * pluginClient ) Conn ( ) grpc . ClientConnInterface {
return p . clientConn
}
2022-08-30 02:42:26 +00:00
func ( p * pluginClient ) Reload ( ) error {
p . logger . Debug ( "reload external plugin process" )
return p . reloadFunc ( )
}
// reloadExternalPlugin
// This should be called with the write lock held.
func ( c * PluginCatalog ) reloadExternalPlugin ( name , id string ) error {
extPlugin , ok := c . externalPlugins [ name ]
if ! ok {
return fmt . Errorf ( "plugin client not found" )
}
if ! extPlugin . multiplexingSupport {
err := c . cleanupExternalPlugin ( name , id )
if err != nil {
return err
}
return nil
}
pc , ok := extPlugin . connections [ id ]
if ! ok {
return fmt . Errorf ( "%w id: %s" , ErrPluginConnectionNotFound , id )
}
delete ( c . externalPlugins , name )
pc . client . Kill ( )
c . logger . Debug ( "killed external plugin process for reload" , "name" , name , "pid" , pc . pid )
return nil
}
2022-02-17 14:50:33 +00:00
// Close calls the plugin client's cleanupFunc to do any necessary cleanup on
// the plugin client and the PluginCatalog. This implements the
// plugin.ClientProtocol interface.
func ( p * pluginClient ) Close ( ) error {
p . logger . Debug ( "cleaning up plugin client connection" , "id" , p . id )
return p . cleanupFunc ( )
}
// cleanupExternalPlugin will kill plugin processes and perform any necessary
// cleanup on the externalPlugins map for multiplexed and non-multiplexed
// plugins. This should be called with the write lock held.
func ( c * PluginCatalog ) cleanupExternalPlugin ( name , id string ) error {
extPlugin , ok := c . externalPlugins [ name ]
if ! ok {
return fmt . Errorf ( "plugin client not found" )
}
2022-06-22 18:29:25 +00:00
pc , ok := extPlugin . connections [ id ]
if ! ok {
2022-08-30 02:42:26 +00:00
// this can happen if the backend is reloaded due to a plugin process
// being killed out of band
c . logger . Warn ( ErrPluginConnectionNotFound . Error ( ) , "id" , id )
return fmt . Errorf ( "%w id: %s" , ErrPluginConnectionNotFound , id )
2022-06-22 18:29:25 +00:00
}
2022-02-17 14:50:33 +00:00
delete ( extPlugin . connections , id )
2022-08-30 02:42:26 +00:00
c . logger . Debug ( "removed plugin client connection" , "id" , id )
2022-02-17 14:50:33 +00:00
if ! extPlugin . multiplexingSupport {
2022-02-17 20:32:31 +00:00
pc . client . Kill ( )
2022-02-17 14:50:33 +00:00
if len ( extPlugin . connections ) == 0 {
delete ( c . externalPlugins , name )
}
2022-08-30 02:42:26 +00:00
c . logger . Debug ( "killed external plugin process" , "name" , name , "pid" , pc . pid )
2022-02-17 20:32:31 +00:00
} else if len ( extPlugin . connections ) == 0 || pc . client . Exited ( ) {
pc . client . Kill ( )
2022-02-17 14:50:33 +00:00
delete ( c . externalPlugins , name )
2022-08-30 02:42:26 +00:00
c . logger . Debug ( "killed external multiplexed plugin process" , "name" , name , "pid" , pc . pid )
2022-02-17 14:50:33 +00:00
}
return nil
}
func ( c * PluginCatalog ) getExternalPlugin ( pluginName string ) * externalPlugin {
if extPlugin , ok := c . externalPlugins [ pluginName ] ; ok {
return extPlugin
}
return c . newExternalPlugin ( pluginName )
}
func ( c * PluginCatalog ) newExternalPlugin ( pluginName string ) * externalPlugin {
if c . externalPlugins == nil {
c . externalPlugins = make ( map [ string ] * externalPlugin )
}
extPlugin := & externalPlugin {
connections : make ( map [ string ] * pluginClient ) ,
name : pluginName ,
}
c . externalPlugins [ pluginName ] = extPlugin
return extPlugin
}
// NewPluginClient returns a client for managing the lifecycle of a plugin
// process
func ( c * PluginCatalog ) NewPluginClient ( ctx context . Context , config pluginutil . PluginClientConfig ) ( * pluginClient , error ) {
c . lock . Lock ( )
defer c . lock . Unlock ( )
if config . Name == "" {
return nil , fmt . Errorf ( "no name provided for plugin" )
}
if config . PluginType == consts . PluginTypeUnknown {
return nil , fmt . Errorf ( "no plugin type provided" )
}
2022-08-25 20:31:42 +00:00
pluginRunner , err := c . get ( ctx , config . Name , config . PluginType , config . Version )
2022-02-17 14:50:33 +00:00
if err != nil {
return nil , fmt . Errorf ( "failed to lookup plugin: %w" , err )
}
if pluginRunner == nil {
return nil , fmt . Errorf ( "no plugin found" )
}
pc , err := c . newPluginClient ( ctx , pluginRunner , config )
return pc , err
}
// newPluginClient returns a client for managing the lifecycle of a plugin
// process. Callers should have the write lock held.
func ( c * PluginCatalog ) newPluginClient ( ctx context . Context , pluginRunner * pluginutil . PluginRunner , config pluginutil . PluginClientConfig ) ( * pluginClient , error ) {
if pluginRunner == nil {
return nil , fmt . Errorf ( "no plugin found" )
}
extPlugin := c . getExternalPlugin ( pluginRunner . Name )
id , err := base62 . Random ( 10 )
if err != nil {
return nil , err
}
pc := & pluginClient {
id : id ,
logger : c . logger . Named ( pluginRunner . Name ) ,
cleanupFunc : func ( ) error {
c . lock . Lock ( )
defer c . lock . Unlock ( )
return c . cleanupExternalPlugin ( pluginRunner . Name , id )
} ,
2022-08-30 02:42:26 +00:00
reloadFunc : func ( ) error {
c . lock . Lock ( )
defer c . lock . Unlock ( )
return c . reloadExternalPlugin ( pluginRunner . Name , id )
} ,
2022-02-17 14:50:33 +00:00
}
2022-02-18 02:40:33 +00:00
// Multiplexing support will always be false initially, but will be
// adjusted once we query from the plugin whether it can multiplex or not
if ! extPlugin . multiplexingSupport || len ( extPlugin . connections ) == 0 {
2022-02-17 14:50:33 +00:00
c . logger . Debug ( "spawning a new plugin process" , "plugin_name" , pluginRunner . Name , "id" , id )
client , err := pluginRunner . RunConfig ( ctx ,
pluginutil . PluginSets ( config . PluginSets ) ,
pluginutil . HandshakeConfig ( config . HandshakeConfig ) ,
pluginutil . Logger ( config . Logger ) ,
pluginutil . MetadataMode ( config . IsMetadataMode ) ,
pluginutil . MLock ( c . mlockPlugins ) ,
2022-08-30 02:42:26 +00:00
pluginutil . AutoMTLS ( config . AutoMTLS ) ,
pluginutil . Runner ( config . Wrapper ) ,
2022-02-17 14:50:33 +00:00
)
if err != nil {
return nil , err
}
pc . client = client
} else {
c . logger . Debug ( "returning existing plugin client for multiplexed plugin" , "id" , id )
// get the first client, since they are all the same
for k := range extPlugin . connections {
pc . client = extPlugin . connections [ k ] . client
break
}
if pc . client == nil {
return nil , fmt . Errorf ( "plugin client is nil" )
}
}
// Get the protocol client for this connection.
// Subsequent calls to this will return the same client.
rpcClient , err := pc . client . Client ( )
if err != nil {
return nil , err
}
2022-08-30 02:42:26 +00:00
// get the external plugin pid
conf := pc . client . ReattachConfig ( )
if conf != nil {
pc . pid = conf . Pid
}
2022-02-17 14:50:33 +00:00
clientConn := rpcClient . ( * plugin . GRPCClient ) . Conn
2022-09-08 15:32:46 +00:00
muxed , err := pluginutil . MultiplexingSupported ( ctx , clientConn , config . Name )
2022-02-18 02:40:33 +00:00
if err != nil {
return nil , err
}
2022-09-08 15:32:46 +00:00
pc . clientConn = & pluginClientConn {
ClientConn : clientConn ,
id : id ,
2022-02-17 14:50:33 +00:00
}
pc . ClientProtocol = rpcClient
extPlugin . connections [ id ] = pc
extPlugin . name = pluginRunner . Name
2022-02-18 02:40:33 +00:00
extPlugin . multiplexingSupport = muxed
2022-02-17 14:50:33 +00:00
return extPlugin . connections [ id ] , nil
}
2018-11-07 01:21:24 +00:00
// getPluginTypeFromUnknown will attempt to run the plugin to determine the
2022-08-30 02:42:26 +00:00
// type. It will first attempt to run as a database plugin then a backend
// plugin.
2022-09-21 19:25:04 +00:00
func ( c * PluginCatalog ) getPluginTypeFromUnknown ( ctx context . Context , plugin * pluginutil . PluginRunner ) ( consts . PluginType , error ) {
2020-09-18 21:10:54 +00:00
merr := & multierror . Error { }
2022-02-18 02:40:33 +00:00
err := c . isDatabasePlugin ( ctx , plugin )
2020-09-18 21:10:54 +00:00
if err == nil {
2022-02-18 02:40:33 +00:00
return consts . PluginTypeDatabase , nil
2020-09-18 21:10:54 +00:00
}
merr = multierror . Append ( merr , err )
2022-08-30 02:42:26 +00:00
pluginType , err := c . getBackendPluginType ( ctx , plugin )
if err == nil {
return pluginType , nil
}
merr = multierror . Append ( merr , err )
return consts . PluginTypeUnknown , merr
}
// getBackendPluginType returns an error if the plugin is not a backend plugin.
func ( c * PluginCatalog ) getBackendPluginType ( ctx context . Context , pluginRunner * pluginutil . PluginRunner ) ( consts . PluginType , error ) {
merr := & multierror . Error { }
2022-07-20 15:36:23 +00:00
// Attempt to run as backend plugin
2022-08-30 02:42:26 +00:00
config := pluginutil . PluginClientConfig {
Name : pluginRunner . Name ,
PluginSets : backendplugin . PluginSet ,
HandshakeConfig : backendplugin . HandshakeConfig ,
Logger : log . NewNullLogger ( ) ,
IsMetadataMode : false ,
AutoMTLS : true ,
}
var client logical . Backend
var attemptV4 bool
// First, attempt to run as backend V5 plugin
c . logger . Debug ( "attempting to load backend plugin" , "name" , pluginRunner . Name )
pc , err := c . newPluginClient ( ctx , pluginRunner , config )
2020-09-18 21:10:54 +00:00
if err == nil {
2022-08-30 02:42:26 +00:00
// we spawned a subprocess, so make sure to clean it up
defer c . cleanupExternalPlugin ( pluginRunner . Name , pc . id )
// dispense the plugin so we can get its type
client , err = backendplugin . Dispense ( pc . ClientProtocol , pc )
2022-07-18 21:25:18 +00:00
if err != nil {
2022-08-30 02:42:26 +00:00
merr = multierror . Append ( merr , fmt . Errorf ( "failed to dispense plugin as backend v5: %w" , err ) )
c . logger . Debug ( "failed to dispense v5 backend plugin" , "name" , pluginRunner . Name )
attemptV4 = true
} else {
c . logger . Debug ( "successfully dispensed v5 backend plugin" , "name" , pluginRunner . Name )
2022-07-18 21:25:18 +00:00
}
2022-08-30 02:42:26 +00:00
} else {
attemptV4 = true
}
2022-07-18 21:25:18 +00:00
2022-08-30 02:42:26 +00:00
if attemptV4 {
2022-09-06 16:56:10 +00:00
c . logger . Debug ( "failed to dispense v5 backend plugin" , "name" , pluginRunner . Name )
2022-08-30 02:42:26 +00:00
config . AutoMTLS = false
config . IsMetadataMode = true
// attempt to run as a v4 backend plugin
client , err = backendplugin . NewPluginClient ( ctx , nil , pluginRunner , log . NewNullLogger ( ) , true )
if err != nil {
merr = multierror . Append ( merr , fmt . Errorf ( "failed to dispense v4 backend plugin: %w" , err ) )
2022-09-06 16:56:10 +00:00
c . logger . Debug ( "failed to dispense v4 backend plugin" , "name" , pluginRunner . Name , "error" , merr )
2022-08-30 02:42:26 +00:00
return consts . PluginTypeUnknown , merr . ErrorOrNil ( )
2022-07-20 15:36:23 +00:00
}
2022-08-30 02:42:26 +00:00
c . logger . Debug ( "successfully dispensed v4 backend plugin" , "name" , pluginRunner . Name )
defer client . Cleanup ( ctx )
}
err = client . Setup ( ctx , & logical . BackendConfig { } )
if err != nil {
return consts . PluginTypeUnknown , err
}
backendType := client . Type ( )
switch backendType {
case logical . TypeCredential :
return consts . PluginTypeCredential , nil
case logical . TypeLogical :
return consts . PluginTypeSecrets , nil
2018-11-07 01:21:24 +00:00
}
2020-09-18 21:10:54 +00:00
if client == nil || client . Type ( ) == logical . TypeUnknown {
2022-08-30 02:42:26 +00:00
c . logger . Warn ( "unknown plugin type" ,
"plugin name" , pluginRunner . Name ,
2020-09-18 21:10:54 +00:00
"error" , merr . Error ( ) )
} else {
2022-08-30 02:42:26 +00:00
c . logger . Warn ( "unsupported plugin type" ,
"plugin name" , pluginRunner . Name ,
2020-09-18 21:10:54 +00:00
"plugin type" , client . Type ( ) . String ( ) ,
"error" , merr . Error ( ) )
2018-11-07 01:21:24 +00:00
}
2022-08-30 02:42:26 +00:00
merr = multierror . Append ( merr , fmt . Errorf ( "failed to load plugin as backend plugin: %w" , err ) )
return consts . PluginTypeUnknown , merr . ErrorOrNil ( )
2018-11-07 01:21:24 +00:00
}
2022-09-21 19:25:04 +00:00
// getBackendRunningVersion attempts to get the plugin version
func ( c * PluginCatalog ) getBackendRunningVersion ( ctx context . Context , pluginRunner * pluginutil . PluginRunner ) ( logical . PluginVersion , error ) {
merr := & multierror . Error { }
// Attempt to run as backend plugin
config := pluginutil . PluginClientConfig {
Name : pluginRunner . Name ,
PluginSets : backendplugin . PluginSet ,
HandshakeConfig : backendplugin . HandshakeConfig ,
Logger : log . NewNullLogger ( ) ,
IsMetadataMode : false ,
AutoMTLS : true ,
}
var client logical . Backend
// First, attempt to run as backend V5 plugin
c . logger . Debug ( "attempting to load backend plugin" , "name" , pluginRunner . Name )
pc , err := c . newPluginClient ( ctx , pluginRunner , config )
if err == nil {
// we spawned a subprocess, so make sure to clean it up
defer c . cleanupExternalPlugin ( pluginRunner . Name , pc . id )
// dispense the plugin so we can get its version
client , err = backendplugin . Dispense ( pc . ClientProtocol , pc )
if err == nil {
c . logger . Debug ( "successfully dispensed v5 backend plugin" , "name" , pluginRunner . Name )
err = client . Setup ( ctx , & logical . BackendConfig { } )
if err != nil {
return logical . EmptyPluginVersion , nil
}
if versioner , ok := client . ( logical . PluginVersioner ) ; ok {
return versioner . PluginVersion ( ) , nil
}
return logical . EmptyPluginVersion , nil
}
merr = multierror . Append ( merr , fmt . Errorf ( "failed to dispense plugin as backend v5: %w" , err ) )
}
c . logger . Debug ( "failed to dispense v5 backend plugin" , "name" , pluginRunner . Name , "error" , err )
config . AutoMTLS = false
config . IsMetadataMode = true
// attempt to run as a v4 backend plugin
client , err = backendplugin . NewPluginClient ( ctx , nil , pluginRunner , log . NewNullLogger ( ) , true )
if err != nil {
merr = multierror . Append ( merr , fmt . Errorf ( "failed to dispense v4 backend plugin: %w" , err ) )
c . logger . Debug ( "failed to dispense v4 backend plugin" , "name" , pluginRunner . Name , "error" , merr )
return logical . EmptyPluginVersion , merr . ErrorOrNil ( )
}
c . logger . Debug ( "successfully dispensed v4 backend plugin" , "name" , pluginRunner . Name )
defer client . Cleanup ( ctx )
err = client . Setup ( ctx , & logical . BackendConfig { } )
if err != nil {
return logical . EmptyPluginVersion , err
}
if versioner , ok := client . ( logical . PluginVersioner ) ; ok {
return versioner . PluginVersion ( ) , nil
}
return logical . EmptyPluginVersion , nil
}
// getDatabaseRunningVersion returns the version reported by a database plugin
func ( c * PluginCatalog ) getDatabaseRunningVersion ( ctx context . Context , pluginRunner * pluginutil . PluginRunner ) ( logical . PluginVersion , error ) {
merr := & multierror . Error { }
config := pluginutil . PluginClientConfig {
Name : pluginRunner . Name ,
PluginSets : v5 . PluginSets ,
PluginType : consts . PluginTypeDatabase ,
Version : pluginRunner . Version ,
HandshakeConfig : v5 . HandshakeConfig ,
Logger : log . Default ( ) ,
IsMetadataMode : true ,
AutoMTLS : true ,
}
// Attempt to run as database V5+ multiplexed plugin
c . logger . Debug ( "attempting to load database plugin as v5" , "name" , pluginRunner . Name )
v5Client , err := c . newPluginClient ( ctx , pluginRunner , config )
if err == nil {
defer func ( ) {
// Close the client and cleanup the plugin process
err = c . cleanupExternalPlugin ( pluginRunner . Name , v5Client . id )
if err != nil {
c . logger . Error ( "error closing plugin client" , "error" , err )
}
} ( )
raw , err := v5Client . Dispense ( "database" )
if err != nil {
return logical . EmptyPluginVersion , err
}
if versioner , ok := raw . ( logical . PluginVersioner ) ; ok {
return versioner . PluginVersion ( ) , nil
}
return logical . EmptyPluginVersion , nil
}
merr = multierror . Append ( merr , fmt . Errorf ( "failed to load plugin as database v5: %w" , err ) )
c . logger . Debug ( "attempting to load database plugin as v4" , "name" , pluginRunner . Name )
v4Client , err := v4 . NewPluginClient ( ctx , nil , pluginRunner , log . NewNullLogger ( ) , true )
if err == nil {
// Close the client and cleanup the plugin process
defer func ( ) {
err = v4Client . Close ( )
if err != nil {
c . logger . Error ( "error closing plugin client" , "error" , err )
}
} ( )
if versioner , ok := v4Client . ( logical . PluginVersioner ) ; ok {
return versioner . PluginVersion ( ) , nil
}
return logical . EmptyPluginVersion , nil
}
merr = multierror . Append ( merr , fmt . Errorf ( "failed to load plugin as database v4: %w" , err ) )
return logical . EmptyPluginVersion , merr
}
2022-08-30 02:42:26 +00:00
// isDatabasePlugin returns an error if the plugin is not a database plugin.
2022-02-18 02:40:33 +00:00
func ( c * PluginCatalog ) isDatabasePlugin ( ctx context . Context , pluginRunner * pluginutil . PluginRunner ) error {
2020-09-18 21:10:54 +00:00
merr := & multierror . Error { }
2022-02-17 14:50:33 +00:00
config := pluginutil . PluginClientConfig {
Name : pluginRunner . Name ,
PluginSets : v5 . PluginSets ,
PluginType : consts . PluginTypeDatabase ,
2022-08-25 20:31:42 +00:00
Version : pluginRunner . Version ,
2022-02-17 14:50:33 +00:00
HandshakeConfig : v5 . HandshakeConfig ,
Logger : log . NewNullLogger ( ) ,
IsMetadataMode : true ,
AutoMTLS : true ,
}
2022-02-18 02:40:33 +00:00
2022-09-21 19:25:04 +00:00
// Attempt to run as database V5+ multiplexed plugin
2022-02-18 02:40:33 +00:00
c . logger . Debug ( "attempting to load database plugin as v5" , "name" , pluginRunner . Name )
2022-02-17 14:50:33 +00:00
v5Client , err := c . newPluginClient ( ctx , pluginRunner , config )
2020-09-18 21:10:54 +00:00
if err == nil {
// Close the client and cleanup the plugin process
2022-02-17 14:50:33 +00:00
err = c . cleanupExternalPlugin ( pluginRunner . Name , v5Client . id )
if err != nil {
c . logger . Error ( "error closing plugin client" , "error" , err )
}
2022-02-18 02:40:33 +00:00
return nil
2020-09-18 21:10:54 +00:00
}
merr = multierror . Append ( merr , fmt . Errorf ( "failed to load plugin as database v5: %w" , err ) )
2022-02-18 02:40:33 +00:00
c . logger . Debug ( "attempting to load database plugin as v4" , "name" , pluginRunner . Name )
2022-02-17 14:50:33 +00:00
v4Client , err := v4 . NewPluginClient ( ctx , nil , pluginRunner , log . NewNullLogger ( ) , true )
2020-09-18 21:10:54 +00:00
if err == nil {
// Close the client and cleanup the plugin process
2022-02-17 14:50:33 +00:00
err = v4Client . Close ( )
if err != nil {
c . logger . Error ( "error closing plugin client" , "error" , err )
}
2022-02-18 02:40:33 +00:00
return nil
2020-09-18 21:10:54 +00:00
}
merr = multierror . Append ( merr , fmt . Errorf ( "failed to load plugin as database v4: %w" , err ) )
2022-02-18 02:40:33 +00:00
return merr . ErrorOrNil ( )
2020-09-18 21:10:54 +00:00
}
2018-11-07 01:21:24 +00:00
// UpdatePlugins will loop over all the plugins of unknown type and attempt to
// upgrade them to typed plugins
func ( c * PluginCatalog ) UpgradePlugins ( ctx context . Context , logger log . Logger ) error {
c . lock . Lock ( )
defer c . lock . Unlock ( )
// If the directory isn't set we can skip the upgrade attempt
if c . directory == "" {
return nil
}
// List plugins from old location
pluginsRaw , err := c . catalogView . List ( ctx , "" )
if err != nil {
return err
}
plugins := make ( [ ] string , 0 , len ( pluginsRaw ) )
for _ , p := range pluginsRaw {
if ! strings . HasSuffix ( p , "/" ) {
plugins = append ( plugins , p )
}
}
logger . Info ( "upgrading plugin information" , "plugins" , plugins )
var retErr error
for _ , pluginName := range plugins {
pluginRaw , err := c . catalogView . Get ( ctx , pluginName )
if err != nil {
2021-05-11 17:12:54 +00:00
retErr = multierror . Append ( fmt . Errorf ( "failed to load plugin entry: %w" , err ) )
2018-11-07 01:21:24 +00:00
continue
}
plugin := new ( pluginutil . PluginRunner )
if err := jsonutil . DecodeJSON ( pluginRaw . Value , plugin ) ; err != nil {
2021-05-11 17:12:54 +00:00
retErr = multierror . Append ( fmt . Errorf ( "failed to decode plugin entry: %w" , err ) )
2018-11-07 01:21:24 +00:00
continue
}
// prepend the plugin directory to the command
cmdOld := plugin . Command
plugin . Command = filepath . Join ( c . directory , plugin . Command )
2022-08-25 20:31:42 +00:00
// Upgrade the storage. At this point we don't know what type of plugin this is so pass in the unknown type.
runner , err := c . setInternal ( ctx , pluginName , consts . PluginTypeUnknown , plugin . Version , cmdOld , plugin . Args , plugin . Env , plugin . Sha256 )
2018-11-07 01:21:24 +00:00
if err != nil {
2022-02-18 02:40:33 +00:00
if errors . Is ( err , ErrPluginBadType ) {
retErr = multierror . Append ( retErr , fmt . Errorf ( "could not upgrade plugin %s: plugin of unknown type" , pluginName ) )
continue
}
2018-11-07 01:21:24 +00:00
retErr = multierror . Append ( retErr , fmt . Errorf ( "could not upgrade plugin %s: %s" , pluginName , err ) )
continue
}
err = c . catalogView . Delete ( ctx , pluginName )
if err != nil {
logger . Error ( "could not remove plugin" , "plugin" , pluginName , "error" , err )
}
2022-02-18 02:40:33 +00:00
logger . Info ( "upgraded plugin type" , "plugin" , pluginName , "type" , runner . Type . String ( ) )
2018-11-07 01:21:24 +00:00
}
return retErr
}
2017-04-12 17:01:36 +00:00
// Get retrieves a plugin with the specified name from the catalog. It first
// looks for external plugins with this name and then looks for builtin plugins.
// It returns a PluginRunner or an error if no plugin was found.
2022-08-25 20:31:42 +00:00
func ( c * PluginCatalog ) Get ( ctx context . Context , name string , pluginType consts . PluginType , version string ) ( * pluginutil . PluginRunner , error ) {
2017-04-12 16:40:54 +00:00
c . lock . RLock ( )
2022-08-25 20:31:42 +00:00
runner , err := c . get ( ctx , name , pluginType , version )
2019-04-08 17:40:54 +00:00
c . lock . RUnlock ( )
return runner , err
}
2017-04-12 16:40:54 +00:00
2022-08-25 20:31:42 +00:00
func ( c * PluginCatalog ) get ( ctx context . Context , name string , pluginType consts . PluginType , version string ) ( * pluginutil . PluginRunner , error ) {
2017-04-24 17:30:33 +00:00
// If the directory isn't set only look for builtin plugins.
if c . directory != "" {
// Look for external plugins in the barrier
2022-08-25 20:31:42 +00:00
storageKey := path . Join ( pluginType . String ( ) , name )
if version != "" {
storageKey = path . Join ( storageKey , version )
}
out , err := c . catalogView . Get ( ctx , storageKey )
2017-04-24 17:30:33 +00:00
if err != nil {
2021-05-11 17:12:54 +00:00
return nil , fmt . Errorf ( "failed to retrieve plugin %q: %w" , name , err )
2017-04-04 21:43:39 +00:00
}
2022-08-25 20:31:42 +00:00
if out == nil && version == "" {
2018-11-07 01:21:24 +00:00
// Also look for external plugins under what their name would have been if they
// were registered before plugin types existed.
out , err = c . catalogView . Get ( ctx , name )
if err != nil {
2021-05-11 17:12:54 +00:00
return nil , fmt . Errorf ( "failed to retrieve plugin %q: %w" , name , err )
2018-11-07 01:21:24 +00:00
}
}
2017-04-24 17:30:33 +00:00
if out != nil {
entry := new ( pluginutil . PluginRunner )
if err := jsonutil . DecodeJSON ( out . Value , entry ) ; err != nil {
2021-05-11 17:12:54 +00:00
return nil , fmt . Errorf ( "failed to decode plugin entry: %w" , err )
2017-04-24 17:30:33 +00:00
}
2018-11-07 01:21:24 +00:00
if entry . Type != pluginType && entry . Type != consts . PluginTypeUnknown {
return nil , nil
}
2017-04-04 21:43:39 +00:00
2017-05-04 19:36:06 +00:00
// prepend the plugin directory to the command
entry . Command = filepath . Join ( c . directory , entry . Command )
2017-04-24 17:30:33 +00:00
return entry , nil
}
2017-04-04 00:52:29 +00:00
}
2022-08-25 20:31:42 +00:00
2022-09-22 22:15:46 +00:00
builtinVersion := versions . GetBuiltinVersion ( pluginType , name )
if version == "" || version == builtinVersion {
2022-08-25 20:31:42 +00:00
// Look for builtin plugins
if factory , ok := c . builtinRegistry . Get ( name , pluginType ) ; ok {
return & pluginutil . PluginRunner {
Name : name ,
Type : pluginType ,
Builtin : true ,
BuiltinFactory : factory ,
2022-09-22 22:15:46 +00:00
Version : builtinVersion ,
2022-08-25 20:31:42 +00:00
} , nil
}
2017-04-04 00:52:29 +00:00
}
2017-04-25 01:31:27 +00:00
return nil , nil
2017-04-04 00:52:29 +00:00
}
2017-04-12 17:01:36 +00:00
// Set registers a new external plugin with the catalog, or updates an existing
// external plugin. It takes the name, command and SHA256 of the plugin.
2022-08-25 20:31:42 +00:00
func ( c * PluginCatalog ) Set ( ctx context . Context , name string , pluginType consts . PluginType , version string , command string , args [ ] string , env [ ] string , sha256 [ ] byte ) error {
2017-04-24 17:30:33 +00:00
if c . directory == "" {
return ErrDirectoryNotConfigured
}
2017-05-12 17:52:33 +00:00
switch {
case strings . Contains ( name , ".." ) :
fallthrough
case strings . Contains ( command , ".." ) :
return consts . ErrPathContainsParentReferences
}
2017-04-12 16:40:54 +00:00
c . lock . Lock ( )
defer c . lock . Unlock ( )
2022-08-25 20:31:42 +00:00
_ , err := c . setInternal ( ctx , name , pluginType , version , command , args , env , sha256 )
2022-02-18 02:40:33 +00:00
return err
2018-11-07 01:21:24 +00:00
}
2022-08-25 20:31:42 +00:00
func ( c * PluginCatalog ) setInternal ( ctx context . Context , name string , pluginType consts . PluginType , version string , command string , args [ ] string , env [ ] string , sha256 [ ] byte ) ( * pluginutil . PluginRunner , error ) {
2017-04-04 00:52:29 +00:00
// Best effort check to make sure the command isn't breaking out of the
// configured plugin directory.
2018-01-18 00:19:28 +00:00
commandFull := filepath . Join ( c . directory , command )
2017-05-04 19:36:06 +00:00
sym , err := filepath . EvalSymlinks ( commandFull )
2017-04-04 00:52:29 +00:00
if err != nil {
2022-02-18 02:40:33 +00:00
return nil , fmt . Errorf ( "error while validating the command path: %w" , err )
2017-04-04 00:52:29 +00:00
}
symAbs , err := filepath . Abs ( filepath . Dir ( sym ) )
if err != nil {
2022-02-18 02:40:33 +00:00
return nil , fmt . Errorf ( "error while validating the command path: %w" , err )
2017-04-04 00:52:29 +00:00
}
if symAbs != c . directory {
2022-02-18 02:40:33 +00:00
return nil , errors . New ( "cannot execute files outside of configured plugin directory" )
2017-04-04 00:52:29 +00:00
}
2022-09-21 19:25:04 +00:00
// entryTmp should only be used for the below type and version checks, it uses the
// full command instead of the relative command.
entryTmp := & pluginutil . PluginRunner {
Name : name ,
Command : commandFull ,
Args : args ,
Env : env ,
Sha256 : sha256 ,
Builtin : false ,
}
2018-11-07 01:21:24 +00:00
// If the plugin type is unknown, we want to attempt to determine the type
if pluginType == consts . PluginTypeUnknown {
2022-09-21 19:25:04 +00:00
pluginType , err = c . getPluginTypeFromUnknown ( ctx , entryTmp )
2019-01-15 15:51:55 +00:00
if err != nil {
2022-02-18 02:40:33 +00:00
return nil , err
2019-01-15 15:51:55 +00:00
}
if pluginType == consts . PluginTypeUnknown {
2022-02-18 02:40:33 +00:00
return nil , ErrPluginBadType
2018-11-07 01:21:24 +00:00
}
}
2022-09-21 19:25:04 +00:00
// getting the plugin version is best-effort, so errors are not fatal
runningVersion := logical . EmptyPluginVersion
var versionErr error
switch pluginType {
case consts . PluginTypeSecrets , consts . PluginTypeCredential :
runningVersion , versionErr = c . getBackendRunningVersion ( ctx , entryTmp )
case consts . PluginTypeDatabase :
runningVersion , versionErr = c . getDatabaseRunningVersion ( ctx , entryTmp )
default :
return nil , fmt . Errorf ( "unknown plugin type: %v" , pluginType )
}
if versionErr != nil {
c . logger . Warn ( "Error determining plugin version" , "error" , versionErr )
} else if version != "" && runningVersion . Version != "" && version != runningVersion . Version {
c . logger . Warn ( "Plugin self-reported version did not match requested version" , "plugin" , name , "requestedVersion" , version , "reportedVersion" , runningVersion . Version )
return nil , fmt . Errorf ( "plugin version mismatch: %s reported version (%s) did not match requested version (%s)" , name , runningVersion . Version , version )
}
2017-04-04 00:52:29 +00:00
entry := & pluginutil . PluginRunner {
2022-02-18 02:40:33 +00:00
Name : name ,
Type : pluginType ,
2022-08-25 20:31:42 +00:00
Version : version ,
2022-02-18 02:40:33 +00:00
Command : command ,
Args : args ,
Env : env ,
Sha256 : sha256 ,
Builtin : false ,
2017-04-04 00:52:29 +00:00
}
buf , err := json . Marshal ( entry )
if err != nil {
2022-02-18 02:40:33 +00:00
return nil , fmt . Errorf ( "failed to encode plugin entry: %w" , err )
2017-04-04 00:52:29 +00:00
}
2022-08-25 20:31:42 +00:00
storageKey := path . Join ( pluginType . String ( ) , name )
if version != "" {
storageKey = path . Join ( storageKey , version )
}
2017-04-04 00:52:29 +00:00
logicalEntry := logical . StorageEntry {
2022-08-25 20:31:42 +00:00
Key : storageKey ,
2017-04-04 00:52:29 +00:00
Value : buf ,
}
2018-01-19 06:44:44 +00:00
if err := c . catalogView . Put ( ctx , & logicalEntry ) ; err != nil {
2022-02-18 02:40:33 +00:00
return nil , fmt . Errorf ( "failed to persist plugin entry: %w" , err )
2017-04-04 00:52:29 +00:00
}
2022-02-18 02:40:33 +00:00
return entry , nil
2017-04-04 00:52:29 +00:00
}
2017-04-12 16:40:54 +00:00
2017-04-12 17:01:36 +00:00
// Delete is used to remove an external plugin from the catalog. Builtin plugins
// can not be deleted.
2022-08-25 20:31:42 +00:00
func ( c * PluginCatalog ) Delete ( ctx context . Context , name string , pluginType consts . PluginType , pluginVersion string ) error {
2017-04-12 16:40:54 +00:00
c . lock . Lock ( )
defer c . lock . Unlock ( )
2018-11-07 01:21:24 +00:00
// Check the name under which the plugin exists, but if it's unfound, don't return any error.
2022-08-25 20:31:42 +00:00
pluginKey := path . Join ( pluginType . String ( ) , name )
if pluginVersion != "" {
pluginKey = path . Join ( pluginKey , pluginVersion )
}
2018-11-07 01:21:24 +00:00
out , err := c . catalogView . Get ( ctx , pluginKey )
if err != nil || out == nil {
pluginKey = name
}
return c . catalogView . Delete ( ctx , pluginKey )
2017-04-12 16:40:54 +00:00
}
2017-04-12 17:01:36 +00:00
// List returns a list of all the known plugin names. If an external and builtin
// plugin share the same name, only one instance of the name will be returned.
2018-11-07 01:21:24 +00:00
func ( c * PluginCatalog ) List ( ctx context . Context , pluginType consts . PluginType ) ( [ ] string , error ) {
2022-08-25 20:31:42 +00:00
plugins , err := c . listInternal ( ctx , pluginType , false )
if err != nil {
return nil , err
}
// Use a set to de-dupe between builtin and unversioned external plugins.
// External plugins with the same name as a builtin override the builtin.
uniquePluginNames := make ( map [ string ] struct { } )
for _ , plugin := range plugins {
uniquePluginNames [ plugin . Name ] = struct { } { }
}
retList := make ( [ ] string , 0 , len ( uniquePluginNames ) )
for plugin := range uniquePluginNames {
retList = append ( retList , plugin )
}
return retList , nil
}
func ( c * PluginCatalog ) ListVersionedPlugins ( ctx context . Context , pluginType consts . PluginType ) ( [ ] pluginutil . VersionedPlugin , error ) {
return c . listInternal ( ctx , pluginType , true )
}
func ( c * PluginCatalog ) listInternal ( ctx context . Context , pluginType consts . PluginType , includeVersioned bool ) ( [ ] pluginutil . VersionedPlugin , error ) {
2017-04-12 16:40:54 +00:00
c . lock . RLock ( )
defer c . lock . RUnlock ( )
2022-08-25 20:31:42 +00:00
var result [ ] pluginutil . VersionedPlugin
2017-04-12 17:01:36 +00:00
// Collect keys for external plugins in the barrier.
2022-08-25 20:31:42 +00:00
plugins , err := logical . CollectKeys ( ctx , c . catalogView )
2017-04-12 16:40:54 +00:00
if err != nil {
return nil , err
}
2022-08-25 20:31:42 +00:00
unversionedPlugins := make ( map [ string ] struct { } )
for _ , plugin := range plugins {
// Some keys will be prepended with the plugin type, but other ones won't.
// Users don't expect to see the plugin type, so we need to strip that here.
var normalizedName , version string
var semanticVersion * semver . Version
2022-09-22 19:55:46 +00:00
storedType := consts . PluginTypeUnknown
2022-08-25 20:31:42 +00:00
parts := strings . Split ( plugin , "/" )
2017-04-12 16:40:54 +00:00
2022-08-25 20:31:42 +00:00
switch len ( parts ) {
case 1 : // Unversioned, no type (legacy)
normalizedName = parts [ 0 ]
// Use 0.0.0 to ensure unversioned is sorted as the oldest version.
semanticVersion , err = semver . NewVersion ( "0.0.0" )
if err != nil {
return nil , err
}
case 2 : // Unversioned
2022-09-22 19:55:46 +00:00
if storedType , err = consts . ParsePluginType ( parts [ 0 ] ) ; err == nil {
2022-08-25 20:31:42 +00:00
normalizedName = parts [ 1 ]
// Use 0.0.0 to ensure unversioned is sorted as the oldest version.
semanticVersion , err = semver . NewVersion ( "0.0.0" )
if err != nil {
return nil , err
}
} else {
2022-09-22 19:55:46 +00:00
return nil , fmt . Errorf ( "unknown plugin type in plugin catalog: %s: %w" , plugin , err )
2022-08-25 20:31:42 +00:00
}
case 3 : // Versioned, with type
if ! includeVersioned {
continue
}
2017-04-12 16:40:54 +00:00
2022-09-22 19:55:46 +00:00
storedType , err = consts . ParsePluginType ( parts [ 0 ] )
if err != nil {
return nil , fmt . Errorf ( "unexpected error parsing plugin type from plugin catalog entry %q: %w" , plugin , err )
}
2022-08-25 20:31:42 +00:00
normalizedName , version = parts [ 1 ] , parts [ 2 ]
semanticVersion , err = semver . NewVersion ( version )
if err != nil {
return nil , fmt . Errorf ( "unexpected error parsing version from plugin catalog entry %q: %w" , plugin , err )
}
default :
return nil , fmt . Errorf ( "unexpected entry in plugin catalog: %s" , plugin )
}
2018-11-07 01:21:24 +00:00
// Only list user-added plugins if they're of the given type.
2022-09-22 19:55:46 +00:00
if storedType != consts . PluginTypeUnknown && storedType != pluginType {
continue
}
entry , err := c . get ( ctx , normalizedName , pluginType , version )
if err != nil || entry == nil {
continue
}
result = append ( result , pluginutil . VersionedPlugin {
Name : normalizedName ,
Type : pluginType . String ( ) ,
Version : version ,
SHA256 : hex . EncodeToString ( entry . Sha256 ) ,
SemanticVersion : semanticVersion ,
} )
if version == "" {
unversionedPlugins [ normalizedName ] = struct { } { }
2018-11-07 01:21:24 +00:00
}
2017-04-12 16:40:54 +00:00
}
2022-08-25 20:31:42 +00:00
// Get the builtin plugins.
builtinPlugins := c . builtinRegistry . Keys ( pluginType )
for _ , plugin := range builtinPlugins {
// Unversioned plugins fully replace builtins of the same name.
if _ , ok := unversionedPlugins [ plugin ] ; ok {
continue
}
Add plugin version to GRPC interface (#17088)
Add plugin version to GRPC interface
Added a version interface in the sdk/logical so that it can be shared between all plugin types, and then wired it up to RunningVersion in the mounts, auth list, and database systems.
I've tested that this works with auth, database, and secrets plugin types, with the following logic to populate RunningVersion:
If a plugin has a PluginVersion() method implemented, then that is used
If not, and the plugin is built into the Vault binary, then the go.mod version is used
Otherwise, the it will be the empty string.
My apologies for the length of this PR.
* Placeholder backend should be external
We use a placeholder backend (previously a framework.Backend) before a
GRPC plugin is lazy-loaded. This makes us later think the plugin is a
builtin plugin.
So we added a `placeholderBackend` type that overrides the
`IsExternal()` method so that later we know that the plugin is external,
and don't give it a default builtin version.
2022-09-15 23:37:59 +00:00
version := versions . GetBuiltinVersion ( pluginType , plugin )
2022-08-25 20:31:42 +00:00
semanticVersion , err := semver . NewVersion ( version )
2022-09-09 20:03:07 +00:00
deprecationStatus , _ := c . builtinRegistry . DeprecationStatus ( plugin , pluginType )
2022-08-25 20:31:42 +00:00
if err != nil {
return nil , err
}
result = append ( result , pluginutil . VersionedPlugin {
2022-09-09 20:03:07 +00:00
Name : plugin ,
Type : pluginType . String ( ) ,
Version : version ,
Builtin : true ,
SemanticVersion : semanticVersion ,
DeprecationStatus : deprecationStatus . String ( ) ,
2022-08-25 20:31:42 +00:00
} )
}
return result , nil
}