mirror of
https://github.com/kemko/nomad.git
synced 2026-01-02 16:35:44 +03:00
Introduce a device manager that manages the lifecycle of device plugins on the client. It fingerprints, collects stats, and forwards Reserve requests to the correct plugin. The manager, also handles device plugins failing and validates their output.
234 lines
6.6 KiB
Go
234 lines
6.6 KiB
Go
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/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.ClientAgentConfig, 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
|
|
}
|
|
|
|
// PluginLoader is used to retrieve plugins either externally or from internal
|
|
// factories.
|
|
type PluginLoader struct {
|
|
// logger is the plugin loaders logger
|
|
logger log.Logger
|
|
|
|
// pluginDir is the directory containing plugin binaries
|
|
pluginDir string
|
|
|
|
// plugins maps a plugin to information required to launch it
|
|
plugins map[PluginID]*pluginInfo
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// InternalPluginConfig is used to configure launching an internal plugin.
|
|
type InternalPluginConfig struct {
|
|
Config map[string]interface{}
|
|
Factory plugins.PluginFactory
|
|
}
|
|
|
|
// 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
|
|
|
|
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)
|
|
}
|
|
|
|
logger := config.Logger.Named("plugin_loader").With("plugin_dir", config.PluginDir)
|
|
l := &PluginLoader{
|
|
logger: logger,
|
|
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.ClientAgentConfig, 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),
|
|
}
|
|
} else {
|
|
var err error
|
|
instance, err = l.dispensePlugin(pinfo.baseInfo.Type, 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
|
|
base, ok := instance.Plugin().(base.BasePlugin)
|
|
if !ok {
|
|
return nil, fmt.Errorf("plugin %s doesn't implement base plugin interface", id)
|
|
}
|
|
|
|
if len(pinfo.msgpackConfig) != 0 {
|
|
if err := base.SetConfig(pinfo.msgpackConfig, config); 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, 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,
|
|
}
|
|
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{}
|
|
}
|
|
|
|
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
|
|
}
|