Chris Roberts f39e5709f4 Make plugin manager usage consistent
Updated to properly handle sub managers. Configurators
  from parent managers properly applied to instances. Instances
  are cached within requesting manager. Parent loading is also
  handled in calling manager to properly support parent caching.
  Closers implementation replaced with cleanup.
2022-04-25 16:12:38 -07:00

693 lines
16 KiB
Go

package plugin
import (
"context"
"fmt"
"io/fs"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-plugin"
"google.golang.org/protobuf/encoding/protojson"
"github.com/hashicorp/vagrant-plugin-sdk/component"
"github.com/hashicorp/vagrant-plugin-sdk/core"
"github.com/hashicorp/vagrant-plugin-sdk/helper/path"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cleanup"
"github.com/hashicorp/vagrant-plugin-sdk/internal-shared/protomappers"
"github.com/hashicorp/vagrant/internal/serverclient"
)
var (
// This is the list of components which may be cached
// locally and re-used when requested
CacheableComponents = []component.Type{
component.CommandType,
component.ConfigType,
component.HostType,
component.MapperType,
component.PluginInfoType,
component.PushType,
}
)
type PluginRegistration func(hclog.Logger) (*Plugin, error)
type PluginConfigurator func(*Instance, hclog.Logger) error
type componentCache map[string]componentEntry
type componentEntry map[component.Type]*Instance
type Manager struct {
Plugins []*Plugin // Plugins managed by this manager
builtins *Builtin // Buitin plugins when using in process plugins
builtinsLoaded bool // Flag that builtin plugins are loaded
cache cacher.Cache // Cache used for named plugin requests
cleaner cleanup.Cleanup // Cleanup tasks to perform on closing
ctx context.Context // Context for the manager
discoveredPaths []path.Path // List of paths this manager has loaded
dispenseFuncs []PluginConfigurator // Configuration functions applied to instances
instances componentCache // Cache for prevlous generated components
legacyLoaded bool // Flag that legacy plugins have been loaded
legacyBroker *plugin.GRPCBroker // Broker for legacy runtime
logger hclog.Logger // Logger for the manager
m sync.Mutex
parent *Manager // Parent manager if this is a sub manager
srv []byte // Marshalled proto message for plugin manager
}
// Create a new plugin manager
func NewManager(ctx context.Context, l hclog.Logger) *Manager {
return &Manager{
Plugins: []*Plugin{},
builtins: NewBuiltins(ctx, l),
cache: cacher.New(),
cleaner: cleanup.New(),
ctx: ctx,
dispenseFuncs: []PluginConfigurator{},
instances: make(componentCache),
logger: l,
}
}
// Create a sub manager based off current manager
func (m *Manager) Sub(name string) *Manager {
if name == "" {
name = "submanager"
}
s := &Manager{
builtinsLoaded: true,
cache: cacher.New(),
cleaner: cleanup.New(),
ctx: m.ctx,
discoveredPaths: m.discoveredPaths,
legacyLoaded: true,
instances: make(componentCache),
logger: m.logger.Named(name),
parent: m,
}
m.closer(func() error { return s.Close() })
return s
}
// Returns the legacy broker if legacy is enabled. If
// manager is a sub manager, it will request from
// the parent
func (m *Manager) LegacyBroker() *plugin.GRPCBroker {
if m.legacyBroker != nil {
return m.legacyBroker
}
if m.parent != nil {
return m.parent.LegacyBroker()
}
return nil
}
// Returns true if legacy Vagrant (Ruby runtime) is enabled
func (m *Manager) LegacyEnabled() bool {
return m.LegacyBroker() != nil
}
// Load legacy Ruby based Vagrant plugins using a
// running Vagrant runtime
func (m *Manager) LoadLegacyPlugins(
c *serverclient.RubyVagrantClient, // Client connection to the Legacy Ruby Vagrant server
r plugin.ClientProtocol, // go-plugin client connection to Ruby plugin server
) (err error) {
m.m.Lock()
defer m.m.Unlock()
if m.legacyLoaded {
m.logger.Warn("ruby based legacy vagrant plugins already loaded, skipping")
return nil
}
m.logger.Trace("loading ruby based legacy vagrant plugins")
plugins, err := c.GetPlugins()
if err != nil {
m.logger.Trace("failed to fetch ruby based legacy vagrant plugin information",
"error", err,
)
return
}
for _, p := range plugins {
m.logger.Info("loading ruby based legacy vagrant plugin",
"name", p.Name,
"type", p.Type,
)
if err = m.register(RubyFactory(r, p.Name, component.Type(p.Type))); err != nil {
return
}
}
m.legacyLoaded = true
m.legacyBroker = c.GRPCBroker()
// Now add a configurator to set the plugin name on plugins
// when supported
err = m.Configure(
func(i *Instance, l hclog.Logger) error {
s, ok := i.Component.(HasPluginMetadata)
if !ok {
l.Warn("plugin does not support name metadata, skipping",
"component", i.Type.String(),
"name", i.Name,
)
return nil
}
s.SetRequestMetadata("plugin_name", i.Name)
return nil
},
)
return
}
// Load all known builtin plugins
func (m *Manager) LoadBuiltins() (err error) {
m.m.Lock()
defer m.m.Unlock()
if m.builtinsLoaded {
m.logger.Warn("builing plugins have already been loaded, skipping")
return nil
}
if IN_PROCESS_PLUGINS {
return m.loadInProcessBuiltins()
}
m.logger.Info("loading builtin plugins")
for name, _ := range Builtins {
if e := m.register(BuiltinFactory(name)); e != nil {
err = multierror.Append(err, e)
}
}
m.builtinsLoaded = true
return
}
// Finds any executable files in provided directory paths and
// registers them as plugins.
func (m *Manager) Discover(
paths ...path.Path, // List of paths to search for plugins
) error {
m.m.Lock()
defer m.m.Unlock()
for i := 0; i < len(paths); i++ {
dir := paths[i]
m.logger.Trace("starting plugin discovery process",
"path", dir.String())
for _, p := range m.discoveredPaths {
if p.String() == dir.String() {
m.logger.Warn("plugin discovery already processed, skipping",
"path", dir.String())
return nil
}
}
files, err := fs.ReadDir(os.DirFS(dir.String()), ".")
if err != nil {
m.logger.Warn("failed to read requested directory for discovery, skipping",
"path", dir.String(),
"error", err,
)
return nil
}
for _, entry := range files {
fullPath := dir.Join(entry.Name())
i, err := os.Stat(fullPath.String())
if err != nil {
m.logger.Error("failed to stat file",
"path", fullPath,
"error", err,
)
continue
}
m.logger.Trace("processing discovered path",
"path", fullPath,
"perms", i.Mode().Perm(),
)
if entry.Type().IsDir() {
m.logger.Trace("discovered path is directory, skipping",
"path", fullPath)
continue
}
if i.Mode().Perm()&0111 == 0 {
m.logger.Warn("discovered file is not executable, skipping",
"path", fullPath,
"perms", i.Mode().Perm(),
)
continue
}
if runtime.GOOS == "windows" &&
!strings.HasSuffix(entry.Name(), ".exe") &&
!strings.HasSuffix(entry.Name(), ".bat") {
m.logger.Warn("discovered file is not windows executable, skipping",
"path", fullPath)
continue
}
cmd := exec.Command(fullPath.String())
if err := m.register(Factory(cmd)); err != nil {
m.logger.Error("failed to register discovered plugin",
"path", fullPath,
"error", err,
)
return err
}
}
m.discoveredPaths = append(m.discoveredPaths, dir)
}
return nil
}
// Register a new plugin into the manager
func (m *Manager) Register(
factory PluginRegistration, // Function to generate plugin
) (err error) {
m.m.Lock()
defer m.m.Unlock()
return m.register(factory)
}
// List of plugin configurators that should be applied to instances
func (m *Manager) Configurators() (r []PluginConfigurator) {
if m.parent != nil {
r = m.parent.Configurators()
}
l := len(r) + len(m.dispenseFuncs)
rc := make([]PluginConfigurator, l)
copy(rc, r)
copy(rc[len(r):l], m.dispenseFuncs)
r = rc
return
}
// Add configuration to be applied to plugin instances when requested
func (m *Manager) Configure(fn PluginConfigurator) error {
m.dispenseFuncs = append(m.dispenseFuncs, fn)
return nil
}
// Find a specific plugin by name and component type
func (m *Manager) Find(
n string, // Name of the plugin
t component.Type, // component type of plugin
) (*Instance, error) {
m.m.Lock()
defer m.m.Unlock()
return m.find(n, t)
}
// Find all plugins which support a specific component type
func (m *Manager) Typed(
t component.Type, // Type of plugins
) ([]string, error) {
m.logger.Trace("searching for plugins",
"type", t.String())
result := []string{}
for _, p := range m.Plugins {
if p.HasType(t) {
result = append(result, p.Name)
}
}
m.logger.Trace("plugin search complete",
"type", t.String(),
"count", len(result))
if m.parent != nil {
pt, err := m.parent.Typed(t)
if err != nil {
return nil, err
}
result = append(result, pt...)
}
return result, nil
}
// Close the manager (and all managed plugins)
func (m *Manager) Close() (err error) {
m.m.Lock()
defer m.m.Unlock()
m.logger.Info("closing the plugin manager")
for _, p := range m.Plugins {
m.logger.Trace("closing plugin",
"plugin", p.Name,
)
if e := p.Close(); e != nil {
err = multierror.Append(err, e)
}
}
if cerr := m.cleaner.Close(); cerr != nil {
err = multierror.Append(err, cerr)
}
return
}
// Implements core.PluginManager
func (m *Manager) ListPlugins(typeNames ...string) ([]*core.NamedPlugin, error) {
result := []*core.NamedPlugin{}
for _, n := range typeNames {
t, err := component.FindType(n)
if err != nil {
return nil, err
}
list, err := m.Typed(t)
if err != nil {
return nil, err
}
for _, p := range list {
i := &core.NamedPlugin{
Type: t.String(),
Name: p,
}
result = append(result, i)
}
}
return result, nil
}
// Implements core.PluginManager
func (m *Manager) GetPlugin(name, typ string) (*core.NamedPlugin, error) {
t, err := component.FindType(typ)
if err != nil {
return nil, err
}
cid := t.String() + "-" + name
if c := m.cache.Get(cid); c != nil {
return c.(*core.NamedPlugin), nil
}
c, err := m.Find(name, t)
if err != nil {
return nil, err
}
v := &core.NamedPlugin{
Name: name,
Type: t.String(),
Plugin: c.Component,
}
m.cache.Register(cid, v)
return v, nil
}
// Get (and setup if needed) GRPC server connection information
func (m *Manager) Servinfo() ([]byte, error) {
if m.srv != nil {
return m.srv, nil
}
if m.LegacyBroker() == nil {
return nil, fmt.Errorf("legacy broker is unset, cannot create server")
}
i := &internal{
broker: m.LegacyBroker(),
cache: cacher.New(),
cleanup: m.cleaner,
logger: m.logger,
mappers: []*argmapper.Func{},
}
p, err := protomappers.PluginManagerProto(m, m.logger, i)
if err != nil {
m.logger.Warn("failed to create plugin manager grpc server",
"error", err,
)
return nil, err
}
m.logger.Info("new GRPC server instance started",
"address", p.Addr,
)
m.srv, err = protojson.Marshal(p)
return m.srv, err
}
// Loads builtin plugins using in process strategy
// instead of isolated processes
func (m *Manager) loadInProcessBuiltins() (err error) {
f := []PluginRegistration{}
m.logger.Warn("loading builtin plugins for in process execution")
for name, opts := range Builtins {
r, e := m.builtins.Add(name, opts...)
if e != nil {
err = multierror.Append(err, e)
continue
}
f = append(f, r)
}
if err != nil {
return
}
m.logger.Debug("starting in process builtin plugins")
m.builtins.Start()
m.logger.Trace("registering in process builtin plugins")
for _, b := range f {
if e := m.Register(b); e != nil {
err = multierror.Append(err, e)
}
}
return
}
// Registers plugin
// TODO(spox): Need to do a name check and error if
// name is already in use here or in parent
func (m *Manager) register(
factory PluginRegistration, // Function to generate plugin
) (err error) {
plg, err := factory(m.logger.ResetNamed("vagrant.plugin"))
if err != nil {
return
}
for _, t := range plg.Types {
m.logger.Info("registering plugin",
"type", t.String(),
"name", plg.Name,
)
}
plg.manager = m
m.Plugins = append(m.Plugins, plg)
return
}
// Returns an instance of the requested component. If
// the instance has already been found previously, it
// will return a cached value. If it has not previously
// been found, it will be generated and parent loaded
// if applicable. If the component type is allowed to
// be cached, it will be cached locally before being
// returned.
func (m *Manager) find(
n string, // name of plugin
t component.Type, // type of component
) (*Instance, error) {
// Ensure we have a valid entry in the cache map
if _, ok := m.instances[n]; !ok {
m.instances[n] = make(componentEntry)
}
// If we already have this instance cached, return it
if i, ok := m.instances[n][t]; ok {
m.logger.Debug("requested component found in local cache",
"name", n,
"type", t.String(),
)
return i, nil
}
// Try to fetch the instance
i, err := m.fetch(n, t, nil)
if err != nil {
return nil, err
}
// Attempt to load the parent if the component has one
if err := m.loadParent(i); err != nil {
return nil, err
}
// If we got it, store it in the cache and make sure
// it gets closed when we do
if m.isCacheable(t) {
m.instances[n][t] = i
}
m.closer(func() error {
m.logger.Trace("closing plugin instance",
"name", n,
"type", t.String(),
)
return i.Close()
})
return i, nil
}
// This handles fetching a component from this manager or
// the parent manager. It will prepend any PluginConfigurators
// defined on this manager to the list it is provided. The result
// is that components which are generated in a parent will have
// the parent's PluginConfigurators applied first, with the
// child PluginConfigurators applied after.
//
// It should be noted that this only handles generating the instance
// of a component. It does not cache it or load parents.
func (m *Manager) fetch(
n string, // name of plugin
t component.Type, // type of component
c []PluginConfigurator,
) (i *Instance, err error) {
m.logger.Info("configurators for use by fetch function",
"passed-count", len(c),
"local-count", len(m.dispenseFuncs),
)
var cfns []PluginConfigurator
if len(c) > 0 {
l := len(c) + len(m.dispenseFuncs)
cfns = make([]PluginConfigurator, l)
copy(cfns, m.dispenseFuncs)
copy(cfns[len(m.dispenseFuncs):l], c)
} else {
cfns = m.dispenseFuncs
}
// Find the plugin with the matching name and type
// and generate the component instance
for _, p := range m.Plugins {
if p.Name == n && p.HasType(t) {
return p.InstanceOf(t, cfns)
}
}
// If we have a parent, check if we can fetch it
// from the parent
if m.parent != nil {
return m.parent.fetch(n, t, cfns)
}
return nil, fmt.Errorf("failed to locate plugin `%s`", n)
}
// Add a cleanup function to be executed when this
// manager is closed
func (m *Manager) closer(f func() error) {
m.cleaner.Do(f)
}
// Check if component type can be cached
func (m *Manager) isCacheable(t component.Type) bool {
for _, v := range CacheableComponents {
if t == v {
return true
}
}
return false
}
// Check if an instance's component supports having a parent
// and, if so, loading that parent instance and setting it
// into the current instance.
func (m *Manager) loadParent(i *Instance) error {
c, ok := i.Component.(HasParent)
if !ok {
m.logger.Debug("component component does not support parents",
"type", i.Type.String(),
"name", i.Name,
)
return nil
}
parentName, err := c.Parent()
if err != nil {
m.logger.Error("component parent request failed",
"type", i.Type.String(),
"name", i.Name,
"error", err,
)
return err
}
// If the parent name is empty, there is no parent
if parentName == "" {
return nil
}
// Use find() here so the parent instance can be retrieved
// from the local cache (or can be cached if not yet created).
pi, err := m.find(parentName, i.Type)
if err != nil {
m.logger.Error("failed to find parent component",
"type", i.Type.String(),
"name", i.Name,
"parent_name", parentName,
"error", err,
)
return err
}
// Set the parent
i.Parent = pi
c.SetParentComponent(pi.Component)
return nil
}