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 }