loader and singleton

This commit is contained in:
Alex Dadgar
2019-01-14 16:50:05 -08:00
committed by Michael Schurter
parent b9f36134dc
commit c19cd2e5cf
35 changed files with 31 additions and 31 deletions

View File

@@ -1,15 +0,0 @@
package loader
import (
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/device"
)
var (
// AgentSupportedApiVersions is the set of API versions supported by the
// Nomad agent by plugin type.
AgentSupportedApiVersions = map[string][]string{
base.PluginTypeDevice: {device.ApiVersion010},
base.PluginTypeDriver: {device.ApiVersion010},
}
)

View File

@@ -1,506 +0,0 @@
package loader
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
multierror "github.com/hashicorp/go-multierror"
plugin "github.com/hashicorp/go-plugin"
version "github.com/hashicorp/go-version"
hcl2 "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/shared/hclspec"
"github.com/hashicorp/nomad/plugins/shared/hclutils"
"github.com/zclconf/go-cty/cty/msgpack"
)
var (
// configParseCtx is the context used to parse a plugin's configuration
// stanza
configParseCtx = &hcl2.EvalContext{
Functions: hclutils.GetStdlibFuncs(),
}
)
// validateConfig returns whether or not the configuration is valid
func validateConfig(config *PluginLoaderConfig) error {
var mErr multierror.Error
if config == nil {
return fmt.Errorf("nil config passed")
} else if config.Logger == nil {
multierror.Append(&mErr, fmt.Errorf("nil logger passed"))
}
// Validate that all plugins have a binary name
for _, c := range config.Configs {
if c.Name == "" {
multierror.Append(&mErr, fmt.Errorf("plugin config passed without binary name"))
}
}
// Validate internal plugins
for k, config := range config.InternalPlugins {
// Validate config
if config == nil {
multierror.Append(&mErr, fmt.Errorf("nil config passed for internal plugin %s", k))
continue
} else if config.Factory == nil {
multierror.Append(&mErr, fmt.Errorf("nil factory passed for internal plugin %s", k))
continue
}
}
return mErr.ErrorOrNil()
}
// init initializes the plugin loader by compiling both internal and external
// plugins and selecting the highest versioned version of any given plugin.
func (l *PluginLoader) init(config *PluginLoaderConfig) error {
// Create a mapping of name to config
configMap := configMap(config.Configs)
// Initialize the internal plugins
internal, err := l.initInternal(config.InternalPlugins, configMap)
if err != nil {
return fmt.Errorf("failed to fingerprint internal plugins: %v", err)
}
// Scan for eligibile binaries
plugins, err := l.scan()
if err != nil {
return fmt.Errorf("failed to scan plugin directory %q: %v", l.pluginDir, err)
}
// Fingerprint the passed plugins
external, err := l.fingerprintPlugins(plugins, configMap)
if err != nil {
return fmt.Errorf("failed to fingerprint plugins: %v", err)
}
// Merge external and internal plugins
l.plugins = l.mergePlugins(internal, external)
// Validate that the configs are valid for the plugins
if err := l.validatePluginConfigs(); err != nil {
return fmt.Errorf("parsing plugin configurations failed: %v", err)
}
return nil
}
// initInternal initializes internal plugins.
func (l *PluginLoader) initInternal(plugins map[PluginID]*InternalPluginConfig, configs map[string]*config.PluginConfig) (map[PluginID]*pluginInfo, error) {
var mErr multierror.Error
fingerprinted := make(map[PluginID]*pluginInfo, len(plugins))
for k, config := range plugins {
// Create an instance
raw := config.Factory(l.logger)
base, ok := raw.(base.BasePlugin)
if !ok {
multierror.Append(&mErr, fmt.Errorf("internal plugin %s doesn't meet base plugin interface", k))
continue
}
info := &pluginInfo{
factory: config.Factory,
config: config.Config,
}
// Try to retrieve a user specified config
if userConfig, ok := configs[k.Name]; ok && userConfig.Config != nil {
info.config = userConfig.Config
}
// Fingerprint base info
i, err := base.PluginInfo()
if err != nil {
multierror.Append(&mErr, fmt.Errorf("PluginInfo info failed for internal plugin %s: %v", k, err))
continue
}
info.baseInfo = i
// Parse and set the plugin version
v, err := version.NewVersion(i.PluginVersion)
if err != nil {
multierror.Append(&mErr, fmt.Errorf("failed to parse version %q for internal plugin %s: %v", i.PluginVersion, k, err))
continue
}
info.version = v
// Detect the plugin API version to use
av, err := l.selectApiVersion(i)
if err != nil {
multierror.Append(&mErr, fmt.Errorf("failed to validate API versions %v for internal plugin %s: %v", i.PluginApiVersions, k, err))
continue
}
if av == "" {
l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", k)
continue
}
info.apiVersion = av
// Get the config schema
schema, err := base.ConfigSchema()
if err != nil {
multierror.Append(&mErr, fmt.Errorf("failed to retrieve config schema for internal plugin %s: %v", k, err))
continue
}
info.configSchema = schema
// Store the fingerprinted config
fingerprinted[k] = info
}
if err := mErr.ErrorOrNil(); err != nil {
return nil, err
}
return fingerprinted, nil
}
// selectApiVersion takes in PluginInfo and returns the highest compatable
// version or an error if the plugins response is malformed. If there is no
// overlap, an empty string is returned.
func (l *PluginLoader) selectApiVersion(i *base.PluginInfoResponse) (string, error) {
if i == nil {
return "", fmt.Errorf("nil plugin info given")
}
if len(i.PluginApiVersions) == 0 {
return "", fmt.Errorf("plugin provided no compatible API versions")
}
pluginVersions, err := convertVersions(i.PluginApiVersions)
if err != nil {
return "", fmt.Errorf("plugin provided invalid versions: %v", err)
}
// Lookup the supported versions. These will be sorted highest to lowest
supportedVersions, ok := l.supportedVersions[i.Type]
if !ok {
return "", fmt.Errorf("unsupported plugin type %q", i.Type)
}
for _, sv := range supportedVersions {
for _, pv := range pluginVersions {
if sv.Equal(pv) {
return pv.Original(), nil
}
}
}
return "", nil
}
// convertVersions takes a list of string versions and returns a sorted list of
// versions from highest to lowest.
func convertVersions(in []string) ([]*version.Version, error) {
converted := make([]*version.Version, len(in))
for i, v := range in {
vv, err := version.NewVersion(v)
if err != nil {
return nil, fmt.Errorf("failed to convert version %q : %v", v, err)
}
converted[i] = vv
}
sort.Slice(converted, func(i, j int) bool {
return converted[i].GreaterThan(converted[j])
})
return converted, nil
}
// scan scans the plugin directory and retrieves potentially eligible binaries
func (l *PluginLoader) scan() ([]os.FileInfo, error) {
if l.pluginDir == "" {
return nil, nil
}
// Capture the list of binaries in the plugins folder
f, err := os.Open(l.pluginDir)
if err != nil {
// There are no plugins to scan
if os.IsNotExist(err) {
l.logger.Warn("skipping external plugins since plugin_dir doesn't exist")
return nil, nil
}
return nil, fmt.Errorf("failed to open plugin directory %q: %v", l.pluginDir, err)
}
files, err := f.Readdirnames(-1)
f.Close()
if err != nil {
return nil, fmt.Errorf("failed to read plugin directory %q: %v", l.pluginDir, err)
}
var plugins []os.FileInfo
for _, f := range files {
f = filepath.Join(l.pluginDir, f)
s, err := os.Stat(f)
if err != nil {
return nil, fmt.Errorf("failed to stat file %q: %v", f, err)
}
if s.IsDir() {
l.logger.Warn("skipping subdir in plugin folder", "subdir", f)
continue
}
if !executable(f, s) {
l.logger.Warn("skipping un-executable file in plugin folder", "file", f)
continue
}
plugins = append(plugins, s)
}
return plugins, nil
}
// fingerprintPlugins fingerprints all external plugin binaries
func (l *PluginLoader) fingerprintPlugins(plugins []os.FileInfo, configs map[string]*config.PluginConfig) (map[PluginID]*pluginInfo, error) {
var mErr multierror.Error
fingerprinted := make(map[PluginID]*pluginInfo, len(plugins))
for _, p := range plugins {
name := cleanPluginExecutable(p.Name())
c := configs[name]
info, err := l.fingerprintPlugin(p, c)
if err != nil {
l.logger.Error("failed to fingerprint plugin", "plugin", name, "error", err)
multierror.Append(&mErr, err)
continue
}
if info == nil {
// Plugin was skipped for validation reasons
continue
}
id := PluginID{
Name: info.baseInfo.Name,
PluginType: info.baseInfo.Type,
}
// Detect if we already have seen a version of this plugin
if prev, ok := fingerprinted[id]; ok {
oldVersion := prev.version
selectedVersion := info.version
skip := false
// Determine if we should keep the previous version or override
if prev.version.GreaterThan(info.version) {
oldVersion = info.version
selectedVersion = prev.version
skip = true
}
l.logger.Info("multiple versions of plugin detected",
"plugin", info.baseInfo.Name, "older_version", oldVersion, "selected_version", selectedVersion)
if skip {
continue
}
}
// Add the plugin
fingerprinted[id] = info
}
if err := mErr.ErrorOrNil(); err != nil {
return nil, err
}
return fingerprinted, nil
}
// fingerprintPlugin fingerprints the passed external plugin
func (l *PluginLoader) fingerprintPlugin(pluginExe os.FileInfo, config *config.PluginConfig) (*pluginInfo, error) {
info := &pluginInfo{
exePath: filepath.Join(l.pluginDir, pluginExe.Name()),
}
// Build the command
cmd := exec.Command(info.exePath)
if config != nil {
cmd.Args = append(cmd.Args, config.Args...)
info.args = config.Args
info.config = config.Config
}
// Launch the plugin
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: base.Handshake,
Plugins: map[string]plugin.Plugin{
base.PluginTypeBase: &base.PluginBase{},
},
Cmd: cmd,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: l.logger,
})
defer client.Kill()
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
// Request the plugin
raw, err := rpcClient.Dispense(base.PluginTypeBase)
if err != nil {
return nil, err
}
// Cast the plugin to the base type
bplugin := raw.(base.BasePlugin)
// Retrieve base plugin information
i, err := bplugin.PluginInfo()
if err != nil {
return nil, fmt.Errorf("failed to get plugin info for plugin %q: %v", info.exePath, err)
}
info.baseInfo = i
// Parse and set the plugin version
v, err := version.NewVersion(i.PluginVersion)
if err != nil {
return nil, fmt.Errorf("failed to parse plugin %q (%v) version %q: %v",
i.Name, info.exePath, i.PluginVersion, err)
}
info.version = v
// Detect the plugin API version to use
av, err := l.selectApiVersion(i)
if err != nil {
return nil, fmt.Errorf("failed to validate API versions %v for plugin %s (%v): %v", i.PluginApiVersions, i.Name, info.exePath, err)
}
if av == "" {
l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", i.Name, "path", info.exePath)
return nil, nil
}
info.apiVersion = av
// Retrieve the schema
schema, err := bplugin.ConfigSchema()
if err != nil {
return nil, fmt.Errorf("failed to get plugin config schema for plugin %q: %v", info.exePath, err)
}
info.configSchema = schema
return info, nil
}
// mergePlugins merges internal and external plugins, preferring the highest
// version.
func (l *PluginLoader) mergePlugins(internal, external map[PluginID]*pluginInfo) map[PluginID]*pluginInfo {
finalized := make(map[PluginID]*pluginInfo, len(internal))
// Load the internal plugins
for k, v := range internal {
finalized[k] = v
}
for k, extPlugin := range external {
internal, ok := finalized[k]
if ok {
// We have overlapping plugins, determine if we should keep the
// internal version or override
if extPlugin.version.LessThan(internal.version) {
l.logger.Info("preferring internal version of plugin",
"plugin", extPlugin.baseInfo.Name, "internal_version", internal.version,
"external_version", extPlugin.version)
continue
}
}
// Add external plugin
finalized[k] = extPlugin
}
return finalized
}
// validatePluginConfigs is used to validate each plugins' configuration. If the
// plugin has a config, it is parsed with the plugins config schema and
// SetConfig is called to ensure the config is valid.
func (l *PluginLoader) validatePluginConfigs() error {
var mErr multierror.Error
for id, info := range l.plugins {
if err := l.validatePluginConfig(id, info); err != nil {
wrapped := multierror.Prefix(err, fmt.Sprintf("plugin %s:", id))
multierror.Append(&mErr, wrapped)
}
}
return mErr.ErrorOrNil()
}
// validatePluginConfig is used to validate the plugin's configuration. If the
// plugin has a config, it is parsed with the plugins config schema and
// SetConfig is called to ensure the config is valid.
func (l *PluginLoader) validatePluginConfig(id PluginID, info *pluginInfo) error {
var mErr multierror.Error
// Check if a config is allowed
if info.configSchema == nil {
if info.config != nil {
return fmt.Errorf("configuration not allowed but config passed")
}
// Nothing to do
return nil
}
// Convert the schema to hcl
spec, diag := hclspec.Convert(info.configSchema)
if diag.HasErrors() {
multierror.Append(&mErr, diag.Errs()...)
return multierror.Prefix(&mErr, "failed converting config schema:")
}
// If there is no config, initialize it to an empty map so we can still
// handle defaults
if info.config == nil {
info.config = map[string]interface{}{}
}
// Parse the config using the spec
val, diag := hclutils.ParseHclInterface(info.config, spec, configParseCtx)
if diag.HasErrors() {
multierror.Append(&mErr, diag.Errs()...)
return multierror.Prefix(&mErr, "failed parsing config:")
}
// Marshal the value
cdata, err := msgpack.Marshal(val, val.Type())
if err != nil {
return fmt.Errorf("failed to msgpack encode config: %v", err)
}
// Store the marshalled config
info.msgpackConfig = cdata
// Dispense the plugin and set its config and ensure it is error free
instance, err := l.Dispense(id.Name, id.PluginType, nil, l.logger)
if err != nil {
return fmt.Errorf("failed to dispense plugin: %v", err)
}
defer instance.Kill()
b, ok := instance.Plugin().(base.BasePlugin)
if !ok {
return fmt.Errorf("dispensed plugin %s doesn't meet base plugin interface", id)
}
c := &base.Config{
PluginConfig: cdata,
AgentConfig: nil,
ApiVersion: info.apiVersion,
}
if err := b.SetConfig(c); err != nil {
return fmt.Errorf("setting config on plugin failed: %v", err)
}
return nil
}

View File

@@ -1,61 +0,0 @@
package loader
import plugin "github.com/hashicorp/go-plugin"
// PluginInstance wraps an instance of a plugin. If the plugin is external, it
// provides methods to retrieve the ReattachConfig and to kill the plugin.
type PluginInstance interface {
// Internal returns if the plugin is internal
Internal() bool
// Kill kills the plugin if it is external. It is safe to call on internal
// plugins.
Kill()
// ReattachConfig returns the ReattachConfig and whether the plugin is internal
// or not. If the second return value is false, no ReattachConfig is
// possible to return.
ReattachConfig() (config *plugin.ReattachConfig, canReattach bool)
// Plugin returns the wrapped plugin instance.
Plugin() interface{}
// Exited returns whether the plugin has exited
Exited() bool
// ApiVersion returns the API version to be used with the plugin
ApiVersion() string
}
// internalPluginInstance wraps an internal plugin
type internalPluginInstance struct {
instance interface{}
apiVersion string
}
func (p *internalPluginInstance) Internal() bool { return true }
func (p *internalPluginInstance) Kill() {}
func (p *internalPluginInstance) ReattachConfig() (*plugin.ReattachConfig, bool) { return nil, false }
func (p *internalPluginInstance) Plugin() interface{} { return p.instance }
func (p *internalPluginInstance) Exited() bool { return false }
func (p *internalPluginInstance) ApiVersion() string { return p.apiVersion }
// externalPluginInstance wraps an external plugin
type externalPluginInstance struct {
client *plugin.Client
instance interface{}
apiVersion string
}
func (p *externalPluginInstance) Internal() bool { return false }
func (p *externalPluginInstance) Plugin() interface{} { return p.instance }
func (p *externalPluginInstance) Exited() bool { return p.client.Exited() }
func (p *externalPluginInstance) ApiVersion() string { return p.apiVersion }
func (p *externalPluginInstance) ReattachConfig() (*plugin.ReattachConfig, bool) {
return p.client.ReattachConfig(), true
}
func (p *externalPluginInstance) Kill() {
p.client.Kill()
}

View File

@@ -1,284 +0,0 @@
package loader
import (
"fmt"
"os/exec"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/plugins"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/device"
"github.com/hashicorp/nomad/plugins/drivers"
"github.com/hashicorp/nomad/plugins/shared/hclspec"
)
// PluginCatalog is used to retrieve plugins, either external or internal
type PluginCatalog interface {
// Dispense returns the plugin given its name and type. This will also
// configure the plugin
Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error)
// Reattach is used to reattach to a previously launched external plugin.
Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error)
// Catalog returns the catalog of all plugins keyed by plugin type
Catalog() map[string][]*base.PluginInfoResponse
}
// InternalPluginConfig is used to configure launching an internal plugin.
type InternalPluginConfig struct {
Config map[string]interface{}
Factory plugins.PluginFactory
}
// PluginID is a tuple identifying a plugin
type PluginID struct {
// Name is the name of the plugin
Name string
// PluginType is the plugin's type
PluginType string
}
// String returns a friendly representation of the plugin.
func (id PluginID) String() string {
return fmt.Sprintf("%q (%v)", id.Name, id.PluginType)
}
func PluginInfoID(resp *base.PluginInfoResponse) PluginID {
return PluginID{
Name: resp.Name,
PluginType: resp.Type,
}
}
// PluginLoaderConfig configures a plugin loader.
type PluginLoaderConfig struct {
// Logger is the logger used by the plugin loader
Logger log.Logger
// PluginDir is the directory scanned for loading plugins
PluginDir string
// Configs is an optional set of configs for plugins
Configs []*config.PluginConfig
// InternalPlugins allows registering internal plugins.
InternalPlugins map[PluginID]*InternalPluginConfig
// SupportedVersions is a mapping of plugin type to the supported versions
SupportedVersions map[string][]string
}
// PluginLoader is used to retrieve plugins either externally or from internal
// factories.
type PluginLoader struct {
// logger is the plugin loaders logger
logger log.Logger
// supportedVersions is a mapping of plugin type to the supported versions
supportedVersions map[string][]*version.Version
// pluginDir is the directory containing plugin binaries
pluginDir string
// plugins maps a plugin to information required to launch it
plugins map[PluginID]*pluginInfo
}
// pluginInfo captures the necessary information to launch and configure a
// plugin.
type pluginInfo struct {
factory plugins.PluginFactory
exePath string
args []string
baseInfo *base.PluginInfoResponse
version *version.Version
apiVersion string
configSchema *hclspec.Spec
config map[string]interface{}
msgpackConfig []byte
}
// NewPluginLoader returns an instance of a plugin loader or an error if the
// plugins could not be loaded
func NewPluginLoader(config *PluginLoaderConfig) (*PluginLoader, error) {
if err := validateConfig(config); err != nil {
return nil, fmt.Errorf("invalid plugin loader configuration passed: %v", err)
}
// Convert the versions
supportedVersions := make(map[string][]*version.Version, len(config.SupportedVersions))
for pType, versions := range config.SupportedVersions {
converted, err := convertVersions(versions)
if err != nil {
return nil, err
}
supportedVersions[pType] = converted
}
logger := config.Logger.Named("plugin_loader").With("plugin_dir", config.PluginDir)
l := &PluginLoader{
logger: logger,
supportedVersions: supportedVersions,
pluginDir: config.PluginDir,
plugins: make(map[PluginID]*pluginInfo),
}
if err := l.init(config); err != nil {
return nil, fmt.Errorf("failed to initialize plugin loader: %v", err)
}
return l, nil
}
// Dispense returns a plugin instance, loading it either internally or by
// launching an external plugin.
func (l *PluginLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error) {
id := PluginID{
Name: name,
PluginType: pluginType,
}
pinfo, ok := l.plugins[id]
if !ok {
return nil, fmt.Errorf("unknown plugin with name %q and type %q", name, pluginType)
}
// If the plugin is internal, launch via the factory
var instance PluginInstance
if pinfo.factory != nil {
instance = &internalPluginInstance{
instance: pinfo.factory(logger),
apiVersion: pinfo.apiVersion,
}
} else {
var err error
instance, err = l.dispensePlugin(pinfo.baseInfo.Type, pinfo.apiVersion, pinfo.exePath, pinfo.args, nil, logger)
if err != nil {
return nil, fmt.Errorf("failed to launch plugin: %v", err)
}
}
// Cast to the base type and set the config
b, ok := instance.Plugin().(base.BasePlugin)
if !ok {
return nil, fmt.Errorf("plugin %s doesn't implement base plugin interface", id)
}
c := &base.Config{
PluginConfig: pinfo.msgpackConfig,
AgentConfig: config,
ApiVersion: pinfo.apiVersion,
}
if err := b.SetConfig(c); err != nil {
return nil, fmt.Errorf("setting config for plugin %s failed: %v", id, err)
}
return instance, nil
}
// Reattach reattaches to a previously launched external plugin.
func (l *PluginLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) {
return l.dispensePlugin(pluginType, "", "", nil, config, l.logger)
}
// dispensePlugin is used to launch or reattach to an external plugin.
func (l *PluginLoader) dispensePlugin(
pluginType, apiVersion, cmd string, args []string, reattach *plugin.ReattachConfig,
logger log.Logger) (PluginInstance, error) {
var pluginCmd *exec.Cmd
if cmd != "" && reattach != nil {
return nil, fmt.Errorf("both launch command and reattach config specified")
} else if cmd == "" && reattach == nil {
return nil, fmt.Errorf("one of launch command or reattach config must be specified")
} else if cmd != "" {
pluginCmd = exec.Command(cmd, args...)
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: base.Handshake,
Plugins: getPluginMap(pluginType),
Cmd: pluginCmd,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: logger,
Reattach: reattach,
})
// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
client.Kill()
return nil, err
}
// Request the plugin
raw, err := rpcClient.Dispense(pluginType)
if err != nil {
client.Kill()
return nil, err
}
instance := &externalPluginInstance{
client: client,
instance: raw,
}
if apiVersion != "" {
instance.apiVersion = apiVersion
} else {
// We do not know the API version since we are reattaching, so discover
// it
bplugin := raw.(base.BasePlugin)
// Retrieve base plugin information
i, err := bplugin.PluginInfo()
if err != nil {
return nil, fmt.Errorf("failed to get plugin info for plugin: %v", err)
}
apiVersion, err := l.selectApiVersion(i)
if err != nil {
return nil, fmt.Errorf("failed to validate API versions %v for plugin %s: %v", i.PluginApiVersions, i.Name, err)
}
if apiVersion == "" {
return nil, fmt.Errorf("failed to reattach to plugin because supported API versions for the plugin and Nomad do not overlap")
}
instance.apiVersion = apiVersion
}
return instance, nil
}
// getPluginMap returns a plugin map based on the type of plugin being launched.
func getPluginMap(pluginType string) map[string]plugin.Plugin {
pmap := map[string]plugin.Plugin{
base.PluginTypeBase: &base.PluginBase{},
}
switch pluginType {
case base.PluginTypeDevice:
pmap[base.PluginTypeDevice] = &device.PluginDevice{}
case base.PluginTypeDriver:
pmap[base.PluginTypeDriver] = &drivers.PluginDriver{}
}
return pmap
}
// Catalog returns the catalog of all plugins
func (l *PluginLoader) Catalog() map[string][]*base.PluginInfoResponse {
c := make(map[string][]*base.PluginInfoResponse, 3)
for id, info := range l.plugins {
c[id.PluginType] = append(c[id.PluginType], info.baseInfo)
}
return c
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,195 +0,0 @@
package loader
import (
"context"
"flag"
"fmt"
"os"
"testing"
"time"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/device"
"github.com/hashicorp/nomad/plugins/shared/hclspec"
)
type stringSliceFlags []string
func (i *stringSliceFlags) String() string {
return "my string representation"
}
func (i *stringSliceFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
// TestMain runs either the tests or runs a mock plugin based on the passed
// flags
func TestMain(m *testing.M) {
var plugin, configSchema bool
var name, pluginType, pluginVersion string
var pluginApiVersions stringSliceFlags
flag.BoolVar(&plugin, "plugin", false, "run binary as a plugin")
flag.BoolVar(&configSchema, "config-schema", true, "return a config schema")
flag.StringVar(&name, "name", "", "plugin name")
flag.StringVar(&pluginType, "type", "", "plugin type")
flag.StringVar(&pluginVersion, "version", "", "plugin version")
flag.Var(&pluginApiVersions, "api-version", "supported plugin API version")
flag.Parse()
if plugin {
if err := pluginMain(name, pluginType, pluginVersion, pluginApiVersions, configSchema); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
} else {
os.Exit(m.Run())
}
}
// pluginMain starts a mock plugin using the passed parameters
func pluginMain(name, pluginType, version string, apiVersions []string, config bool) error {
// Validate passed parameters
if name == "" || pluginType == "" {
return fmt.Errorf("name and plugin type must be specified")
}
switch pluginType {
case base.PluginTypeDevice:
default:
return fmt.Errorf("unsupported plugin type %q", pluginType)
}
// Create the mock plugin
m := &mockPlugin{
name: name,
ptype: pluginType,
version: version,
apiVersions: apiVersions,
configSchema: config,
}
// Build the plugin map
pmap := map[string]plugin.Plugin{
base.PluginTypeBase: &base.PluginBase{Impl: m},
}
switch pluginType {
case base.PluginTypeDevice:
pmap[base.PluginTypeDevice] = &device.PluginDevice{Impl: m}
}
// Serve the plugin
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: base.Handshake,
Plugins: pmap,
GRPCServer: plugin.DefaultGRPCServer,
})
return nil
}
// mockFactory returns a PluginFactory method which creates the mock plugin with
// the passed parameters
func mockFactory(name, ptype, version string, apiVersions []string, configSchema bool) func(log log.Logger) interface{} {
return func(log log.Logger) interface{} {
return &mockPlugin{
name: name,
ptype: ptype,
version: version,
apiVersions: apiVersions,
configSchema: configSchema,
}
}
}
// mockPlugin is a plugin that meets various plugin interfaces but is only
// useful for testing.
type mockPlugin struct {
name string
ptype string
version string
apiVersions []string
configSchema bool
// config is built on SetConfig
config *mockPluginConfig
// nomadconfig is set on SetConfig
nomadConfig *base.AgentConfig
// negotiatedApiVersion is the version of the api to use and is set on
// SetConfig
negotiatedApiVersion string
}
// mockPluginConfig is the configuration for the mock plugin
type mockPluginConfig struct {
Foo string `codec:"foo"`
Bar int `codec:"bar"`
// ResKey is a key that is populated in the Env map when a device is
// reserved.
ResKey string `codec:"res_key"`
}
// PluginInfo returns the plugin information based on the passed fields when
// building the mock plugin
func (m *mockPlugin) PluginInfo() (*base.PluginInfoResponse, error) {
return &base.PluginInfoResponse{
Type: m.ptype,
PluginApiVersions: m.apiVersions,
PluginVersion: m.version,
Name: m.name,
}, nil
}
func (m *mockPlugin) ConfigSchema() (*hclspec.Spec, error) {
if !m.configSchema {
return nil, nil
}
// configSpec is the hclspec for parsing the mock's configuration
configSpec := hclspec.NewObject(map[string]*hclspec.Spec{
"foo": hclspec.NewAttr("foo", "string", false),
"bar": hclspec.NewAttr("bar", "number", false),
"res_key": hclspec.NewAttr("res_key", "string", false),
})
return configSpec, nil
}
// SetConfig decodes the configuration and stores it
func (m *mockPlugin) SetConfig(c *base.Config) error {
var config mockPluginConfig
if len(c.PluginConfig) != 0 {
if err := base.MsgPackDecode(c.PluginConfig, &config); err != nil {
return err
}
}
m.config = &config
m.nomadConfig = c.AgentConfig
m.negotiatedApiVersion = c.ApiVersion
return nil
}
func (m *mockPlugin) Fingerprint(ctx context.Context) (<-chan *device.FingerprintResponse, error) {
return make(chan *device.FingerprintResponse), nil
}
func (m *mockPlugin) Reserve(deviceIDs []string) (*device.ContainerReservation, error) {
if m.config == nil || m.config.ResKey == "" {
return nil, nil
}
return &device.ContainerReservation{
Envs: map[string]string{m.config.ResKey: "config-set"},
}, nil
}
func (m *mockPlugin) Stats(ctx context.Context, interval time.Duration) (<-chan *device.StatsResponse, error) {
return make(chan *device.StatsResponse), nil
}

View File

@@ -1,87 +0,0 @@
package loader
import (
"net"
"sync"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/plugins/base"
)
// MockCatalog provides a mock PluginCatalog to be used for testing
type MockCatalog struct {
DispenseF func(name, pluginType string, cfg *base.AgentConfig, logger log.Logger) (PluginInstance, error)
ReattachF func(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error)
CatalogF func() map[string][]*base.PluginInfoResponse
}
func (m *MockCatalog) Dispense(name, pluginType string, cfg *base.AgentConfig, logger log.Logger) (PluginInstance, error) {
return m.DispenseF(name, pluginType, cfg, logger)
}
func (m *MockCatalog) Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) {
return m.ReattachF(name, pluginType, config)
}
func (m *MockCatalog) Catalog() map[string][]*base.PluginInfoResponse {
return m.CatalogF()
}
// MockInstance provides a mock PluginInstance to be used for testing
type MockInstance struct {
InternalPlugin bool
KillF func()
ReattachConfigF func() (*plugin.ReattachConfig, bool)
PluginF func() interface{}
ExitedF func() bool
ApiVersionF func() string
}
func (m *MockInstance) Internal() bool { return m.InternalPlugin }
func (m *MockInstance) Kill() { m.KillF() }
func (m *MockInstance) ReattachConfig() (*plugin.ReattachConfig, bool) { return m.ReattachConfigF() }
func (m *MockInstance) Plugin() interface{} { return m.PluginF() }
func (m *MockInstance) Exited() bool { return m.ExitedF() }
func (m *MockInstance) ApiVersion() string { return m.ApiVersionF() }
// MockBasicExternalPlugin returns a MockInstance that simulates an external
// plugin returning it has been exited after kill is called. It returns the
// passed inst as the plugin
func MockBasicExternalPlugin(inst interface{}, apiVersion string) *MockInstance {
var killedLock sync.Mutex
killed := helper.BoolToPtr(false)
return &MockInstance{
InternalPlugin: false,
KillF: func() {
killedLock.Lock()
defer killedLock.Unlock()
*killed = true
},
ReattachConfigF: func() (*plugin.ReattachConfig, bool) {
return &plugin.ReattachConfig{
Protocol: "tcp",
Addr: &net.TCPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 3200,
Zone: "",
},
Pid: 1000,
}, true
},
PluginF: func() interface{} {
return inst
},
ExitedF: func() bool {
killedLock.Lock()
defer killedLock.Unlock()
return *killed
},
ApiVersionF: func() string { return apiVersion },
}
}

View File

@@ -1,26 +0,0 @@
package loader
import (
"strings"
"github.com/hashicorp/nomad/nomad/structs/config"
)
// configMap returns a mapping of plugin binary name to config.
func configMap(configs []*config.PluginConfig) map[string]*config.PluginConfig {
pluginMapping := make(map[string]*config.PluginConfig, len(configs))
for _, c := range configs {
pluginMapping[c.Name] = c
}
return pluginMapping
}
// cleanPluginExecutable strips the executable name of common suffixes
func cleanPluginExecutable(name string) string {
switch {
case strings.HasSuffix(name, ".exe"):
return strings.TrimSuffix(name, ".exe")
default:
return name
}
}

View File

@@ -1,54 +0,0 @@
package singleton
import (
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/plugins/shared/loader"
)
// future is a sharable future for retrieving a plugin instance or any error
// that may have occurred during the creation.
type future struct {
waitCh chan struct{}
id string
err error
instance loader.PluginInstance
}
// newFuture returns a new pull future
func newFuture() *future {
return &future{
waitCh: make(chan struct{}),
id: uuid.Generate(),
}
}
func (f *future) equal(o *future) bool {
if f == nil && o == nil {
return true
} else if f != nil && o != nil {
return f.id == o.id
} else {
return false
}
}
// wait waits till the future has a result
func (f *future) wait() *future {
<-f.waitCh
return f
}
// result returns the results of the future and should only ever be called after
// wait returns.
func (f *future) result() (loader.PluginInstance, error) {
return f.instance, f.err
}
// set is used to set the results and unblock any waiter. This may only be
// called once.
func (f *future) set(instance loader.PluginInstance, err error) {
f.instance = instance
f.err = err
close(f.waitCh)
}

View File

@@ -1,125 +0,0 @@
package singleton
import (
"fmt"
"sync"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/shared/loader"
)
var (
// SingletonPluginExited is returned when the dispense is called and the
// existing plugin has exited. The caller should retry, and this will issue
// a new plugin instance.
SingletonPluginExited = fmt.Errorf("singleton plugin exited")
)
// SingletonLoader is used to only load a single external plugin at a time.
type SingletonLoader struct {
// Loader is the underlying plugin loader that we wrap to give a singleton
// behavior.
loader loader.PluginCatalog
// instances is a mapping of the plugin to a future which holds a plugin
// instance
instances map[loader.PluginID]*future
instanceLock sync.Mutex
// logger is the logger used by the singleton
logger log.Logger
}
// NewSingletonLoader wraps a plugin catalog and provides singleton behavior on
// top by caching running instances.
func NewSingletonLoader(logger log.Logger, catalog loader.PluginCatalog) *SingletonLoader {
return &SingletonLoader{
loader: catalog,
logger: logger.Named("singleton_plugin_loader"),
instances: make(map[loader.PluginID]*future, 4),
}
}
// Catalog returns the catalog of all plugins keyed by plugin type
func (s *SingletonLoader) Catalog() map[string][]*base.PluginInfoResponse {
return s.loader.Catalog()
}
// Dispense returns the plugin given its name and type. This will also
// configure the plugin. If there is an instance of an already running plugin,
// this is used.
func (s *SingletonLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (loader.PluginInstance, error) {
return s.getPlugin(false, name, pluginType, logger, config, nil)
}
// Reattach is used to reattach to a previously launched external plugin.
func (s *SingletonLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (loader.PluginInstance, error) {
return s.getPlugin(true, name, pluginType, nil, nil, config)
}
// getPlugin is a helper that either dispenses or reattaches to a plugin using
// futures to ensure only a single instance is retrieved
func (s *SingletonLoader) getPlugin(reattach bool, name, pluginType string, logger log.Logger,
nomadConfig *base.AgentConfig, config *plugin.ReattachConfig) (loader.PluginInstance, error) {
// Lock the instance map to prevent races
s.instanceLock.Lock()
// Check if there is a future already
id := loader.PluginID{Name: name, PluginType: pluginType}
f, ok := s.instances[id]
// Create the future and go get a plugin
if !ok {
f = newFuture()
s.instances[id] = f
if reattach {
go s.reattach(f, name, pluginType, config)
} else {
go s.dispense(f, name, pluginType, nomadConfig, logger)
}
}
// Unlock so that the created future can be shared
s.instanceLock.Unlock()
i, err := f.wait().result()
if err != nil {
s.clearFuture(id, f)
return nil, err
}
if i.Exited() {
s.clearFuture(id, f)
return nil, SingletonPluginExited
}
return i, nil
}
// dispense should be called in a go routine to not block and creates the
// desired plugin, setting the results in the future.
func (s *SingletonLoader) dispense(f *future, name, pluginType string, config *base.AgentConfig, logger log.Logger) {
i, err := s.loader.Dispense(name, pluginType, config, logger)
f.set(i, err)
}
// reattach should be called in a go routine to not block and reattaches to the
// desired plugin, setting the results in the future.
func (s *SingletonLoader) reattach(f *future, name, pluginType string, config *plugin.ReattachConfig) {
i, err := s.loader.Reattach(name, pluginType, config)
f.set(i, err)
}
// clearFuture clears the future from the instances map only if the futures
// match. This prevents clearing the unintented instance.
func (s *SingletonLoader) clearFuture(id loader.PluginID, f *future) {
s.instanceLock.Lock()
defer s.instanceLock.Unlock()
if f.equal(s.instances[id]) {
delete(s.instances, id)
}
}

View File

@@ -1,249 +0,0 @@
package singleton
import (
"fmt"
"sync"
"testing"
"time"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/shared/loader"
"github.com/stretchr/testify/require"
)
func harness(t *testing.T) (*SingletonLoader, *loader.MockCatalog) {
c := &loader.MockCatalog{}
s := NewSingletonLoader(testlog.HCLogger(t), c)
return s, c
}
// Test that multiple dispenses return the same instance
func TestSingleton_Dispense(t *testing.T) {
t.Parallel()
require := require.New(t)
dispenseCalled := 0
s, c := harness(t)
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
p := &base.MockPlugin{}
i := &loader.MockInstance{
ExitedF: func() bool { return false },
PluginF: func() interface{} { return p },
}
dispenseCalled++
return i, nil
}
// Retrieve the plugin many times in parallel
const count = 128
var l sync.Mutex
var wg sync.WaitGroup
plugins := make(map[interface{}]struct{}, 1)
waitCh := make(chan struct{})
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
// Wait for unblock
<-waitCh
// Retrieve the plugin
p1, err := s.Dispense("foo", "bar", nil, testlog.HCLogger(t))
require.NotNil(p1)
require.NoError(err)
i1 := p1.Plugin()
require.NotNil(i1)
l.Lock()
plugins[i1] = struct{}{}
l.Unlock()
wg.Done()
}()
}
time.Sleep(10 * time.Millisecond)
close(waitCh)
wg.Wait()
require.Len(plugins, 1)
require.Equal(1, dispenseCalled)
}
// Test that after a plugin is dispensed, if it exits, an error is returned on
// the next dispense
func TestSingleton_Dispense_Exit_Dispense(t *testing.T) {
t.Parallel()
require := require.New(t)
exited := false
dispenseCalled := 0
s, c := harness(t)
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
p := &base.MockPlugin{}
i := &loader.MockInstance{
ExitedF: func() bool { return exited },
PluginF: func() interface{} { return p },
}
dispenseCalled++
return i, nil
}
// Retrieve the plugin
logger := testlog.HCLogger(t)
p1, err := s.Dispense("foo", "bar", nil, logger)
require.NotNil(p1)
require.NoError(err)
i1 := p1.Plugin()
require.NotNil(i1)
require.Equal(1, dispenseCalled)
// Mark the plugin as exited and retrieve again
exited = true
_, err = s.Dispense("foo", "bar", nil, logger)
require.Error(err)
require.Contains(err.Error(), "exited")
require.Equal(1, dispenseCalled)
// Mark the plugin as non-exited and retrieve again
exited = false
p2, err := s.Dispense("foo", "bar", nil, logger)
require.NotNil(p2)
require.NoError(err)
require.Equal(2, dispenseCalled)
i2 := p2.Plugin()
require.NotNil(i2)
if i1 == i2 {
t.Fatalf("i1 and i2 shouldn't be the same instance: %p vs %p", i1, i2)
}
}
// Test that if a plugin errors while being dispensed, the error is returned but
// not saved
func TestSingleton_DispenseError_Dispense(t *testing.T) {
t.Parallel()
require := require.New(t)
dispenseCalled := 0
good := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
p := &base.MockPlugin{}
i := &loader.MockInstance{
ExitedF: func() bool { return false },
PluginF: func() interface{} { return p },
}
dispenseCalled++
return i, nil
}
bad := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
dispenseCalled++
return nil, fmt.Errorf("bad")
}
s, c := harness(t)
c.DispenseF = bad
// Retrieve the plugin
logger := testlog.HCLogger(t)
p1, err := s.Dispense("foo", "bar", nil, logger)
require.Nil(p1)
require.Error(err)
require.Equal(1, dispenseCalled)
// Dispense again and ensure the same error isn't saved
c.DispenseF = good
p2, err := s.Dispense("foo", "bar", nil, logger)
require.NotNil(p2)
require.NoError(err)
require.Equal(2, dispenseCalled)
i2 := p2.Plugin()
require.NotNil(i2)
}
// Test that if a plugin errors while being reattached, the error is returned but
// not saved
func TestSingleton_ReattachError_Dispense(t *testing.T) {
t.Parallel()
require := require.New(t)
dispenseCalled, reattachCalled := 0, 0
s, c := harness(t)
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
p := &base.MockPlugin{}
i := &loader.MockInstance{
ExitedF: func() bool { return false },
PluginF: func() interface{} { return p },
}
dispenseCalled++
return i, nil
}
c.ReattachF = func(_, _ string, _ *plugin.ReattachConfig) (loader.PluginInstance, error) {
reattachCalled++
return nil, fmt.Errorf("bad")
}
// Retrieve the plugin
logger := testlog.HCLogger(t)
p1, err := s.Reattach("foo", "bar", nil)
require.Nil(p1)
require.Error(err)
require.Equal(0, dispenseCalled)
require.Equal(1, reattachCalled)
// Dispense and ensure the same error isn't saved
p2, err := s.Dispense("foo", "bar", nil, logger)
require.NotNil(p2)
require.NoError(err)
require.Equal(1, dispenseCalled)
require.Equal(1, reattachCalled)
i2 := p2.Plugin()
require.NotNil(i2)
}
// Test that after reattaching, dispense returns the same instance
func TestSingleton_Reattach_Dispense(t *testing.T) {
t.Parallel()
require := require.New(t)
dispenseCalled, reattachCalled := 0, 0
s, c := harness(t)
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
dispenseCalled++
return nil, fmt.Errorf("bad")
}
c.ReattachF = func(_, _ string, _ *plugin.ReattachConfig) (loader.PluginInstance, error) {
p := &base.MockPlugin{}
i := &loader.MockInstance{
ExitedF: func() bool { return false },
PluginF: func() interface{} { return p },
}
reattachCalled++
return i, nil
}
// Retrieve the plugin
logger := testlog.HCLogger(t)
p1, err := s.Reattach("foo", "bar", nil)
require.NotNil(p1)
require.NoError(err)
require.Equal(0, dispenseCalled)
require.Equal(1, reattachCalled)
i1 := p1.Plugin()
require.NotNil(i1)
// Dispense and ensure the same instance returned
p2, err := s.Dispense("foo", "bar", nil, logger)
require.NotNil(p2)
require.NoError(err)
require.Equal(0, dispenseCalled)
require.Equal(1, reattachCalled)
i2 := p2.Plugin()
require.NotNil(i2)
if i1 != i2 {
t.Fatalf("i1 and i2 should be the same instance: %p vs %p", i1, i2)
}
}