package plugin import ( "context" "fmt" "io/fs" "os" "os/exec" "runtime" "strings" "sync" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-plugin" "github.com/hashicorp/vagrant-plugin-sdk/component" "github.com/hashicorp/vagrant-plugin-sdk/helper/path" "github.com/hashicorp/vagrant-plugin-sdk/internal-shared/cacher" "github.com/hashicorp/vagrant/internal/serverclient" ) type PluginRegistration func(hclog.Logger) (*Plugin, error) type Manager struct { Plugins []*Plugin builtins *Builtin builtinsLoaded bool closers []func() error ctx context.Context discoveredPaths []path.Path legacyLoaded bool logger hclog.Logger m sync.Mutex parent *Manager cache cacher.Cache } // Create a new plugin manager func NewManager(ctx context.Context, l hclog.Logger) *Manager { return &Manager{ Plugins: []*Plugin{}, builtins: NewBuiltins(ctx, l), ctx: ctx, logger: l, cache: cacher.New(), } } // Create a sub manager based off current manager func (m *Manager) Sub(name string) *Manager { if name == "" { name = "submanager" } s := &Manager{ builtinsLoaded: true, closers: []func() error{}, ctx: m.ctx, discoveredPaths: m.discoveredPaths, legacyLoaded: true, logger: m.logger.Named(name), parent: m, cache: m.cache, } m.closer(func() error { return s.Close() }) return m } // 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 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) } // 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 ) (p *Plugin, err error) { for _, p = range m.Plugins { if p.Name == n && p.HasType(t) { return } } if m.parent != nil { return m.parent.Find(n, t) } return nil, fmt.Errorf("failed to locate plugin `%s`", n) } // Find all plugins which support a specific component type func (m *Manager) Typed( t component.Type, // Type of plugins ) ([]*Plugin, error) { m.logger.Trace("searching for plugins", "type", t.String()) result := []*Plugin{} for _, p := range m.Plugins { if p.HasType(t) { result = append(result, p) } } 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() for _, c := range m.closers { if e := c(); err != nil { err = multierror.Append(err, e) } } for _, p := range m.Plugins { if e := p.Close(); e != nil { err = multierror.Append(err, e) } } return } // 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 func (m *Manager) register( factory PluginRegistration, // Function to generate plugin ) (err error) { plg, err := factory(m.logger.ResetNamed("vagrant.plugin")) if err != nil { return } plg.Cache = m.cache for _, t := range plg.Types { m.logger.Info("registering plugin", "type", t.String(), "name", plg.Name, ) } m.Plugins = append(m.Plugins, plg) return } func (m *Manager) closer(f func() error) { m.closers = append(m.closers, f) }