2022-06-15 11:08:03 -07:00

744 lines
18 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,
component.DownloaderType,
}
)
type PluginRegistration func(hclog.Logger) (*Plugin, error)
type PluginConfigurator func(*Instance, hclog.Logger) error
type PluginInitializer func(*Plugin, 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
initFuncs []PluginInitializer // Initializer functions applied to plugins at creation
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
rubyC *serverclient.RubyVagrantClient // Client to the Ruby runtime
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, // context for the manager
r *serverclient.RubyVagrantClient, // client to the ruby runtime
l hclog.Logger, // 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,
rubyC: r,
}
}
// Returns the client to the Ruby runtime
func (m *Manager) RubyClient() *serverclient.RubyVagrantClient {
if m.parent != nil {
return m.parent.RubyClient()
}
return m.rubyC
}
// 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), p.Options)); 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
}
// Add initializer to be applied to plugin when created
func (m *Manager) Initializer(fn PluginInitializer) error {
m.initFuncs = append(m.initFuncs, fn)
return nil
}
// Find a component instance by plugin 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)
}
// Get a plugin by name
func (m *Manager) Get(
n string, // Name of the plugin
t component.Type, // component type supported by plugin
) (*Plugin, error) {
for _, p := range m.Plugins {
if p.Name == n && p.HasType(t) {
return p, nil
}
}
if m.parent != nil {
return m.parent.Get(n, t)
}
return nil, fmt.Errorf("failed to locate plugin %s implementing component %s", n, t.String())
}
// 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("found typed plugin match",
"type", t.String(),
"name", 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. Note this returns a slice of core.NamedPlugin
// with only the Type and Name fields populated. To get a NamedPlugin with
// instance of the plugin you need to call GetPlugin.
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,
Options: c.Options,
}
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
// Run initializers on new plugin
for _, fn := range m.initFuncs {
if err = fn(plg, m.logger); err != nil {
return
}
}
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) {
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.Trace("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
}