github.com/tschmi5/nomad@v0.11.8/helper/pluginutils/loader/loader.go (about)

     1  package loader
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os/exec"
     7  
     8  	log "github.com/hashicorp/go-hclog"
     9  	plugin "github.com/hashicorp/go-plugin"
    10  	version "github.com/hashicorp/go-version"
    11  	"github.com/hashicorp/nomad/nomad/structs/config"
    12  	"github.com/hashicorp/nomad/plugins"
    13  	"github.com/hashicorp/nomad/plugins/base"
    14  	"github.com/hashicorp/nomad/plugins/device"
    15  	"github.com/hashicorp/nomad/plugins/drivers"
    16  	"github.com/hashicorp/nomad/plugins/shared/hclspec"
    17  )
    18  
    19  // PluginCatalog is used to retrieve plugins, either external or internal
    20  type PluginCatalog interface {
    21  	// Dispense returns the plugin given its name and type. This will also
    22  	// configure the plugin
    23  	Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error)
    24  
    25  	// Reattach is used to reattach to a previously launched external plugin.
    26  	Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error)
    27  
    28  	// Catalog returns the catalog of all plugins keyed by plugin type
    29  	Catalog() map[string][]*base.PluginInfoResponse
    30  }
    31  
    32  // InternalPluginConfig is used to configure launching an internal plugin.
    33  type InternalPluginConfig struct {
    34  	Config  map[string]interface{}
    35  	Factory plugins.PluginCtxFactory
    36  }
    37  
    38  // PluginID is a tuple identifying a plugin
    39  type PluginID struct {
    40  	// Name is the name of the plugin
    41  	Name string
    42  
    43  	// PluginType is the plugin's type
    44  	PluginType string
    45  }
    46  
    47  // String returns a friendly representation of the plugin.
    48  func (id PluginID) String() string {
    49  	return fmt.Sprintf("%q (%v)", id.Name, id.PluginType)
    50  }
    51  
    52  func PluginInfoID(resp *base.PluginInfoResponse) PluginID {
    53  	return PluginID{
    54  		Name:       resp.Name,
    55  		PluginType: resp.Type,
    56  	}
    57  }
    58  
    59  // PluginLoaderConfig configures a plugin loader.
    60  type PluginLoaderConfig struct {
    61  	// Logger is the logger used by the plugin loader
    62  	Logger log.Logger
    63  
    64  	// PluginDir is the directory scanned for loading plugins
    65  	PluginDir string
    66  
    67  	// Configs is an optional set of configs for plugins
    68  	Configs []*config.PluginConfig
    69  
    70  	// InternalPlugins allows registering internal plugins.
    71  	InternalPlugins map[PluginID]*InternalPluginConfig
    72  
    73  	// SupportedVersions is a mapping of plugin type to the supported versions
    74  	SupportedVersions map[string][]string
    75  }
    76  
    77  // PluginLoader is used to retrieve plugins either externally or from internal
    78  // factories.
    79  type PluginLoader struct {
    80  	// logger is the plugin loaders logger
    81  	logger log.Logger
    82  
    83  	// supportedVersions is a mapping of plugin type to the supported versions
    84  	supportedVersions map[string][]*version.Version
    85  
    86  	// pluginDir is the directory containing plugin binaries
    87  	pluginDir string
    88  
    89  	// plugins maps a plugin to information required to launch it
    90  	plugins map[PluginID]*pluginInfo
    91  }
    92  
    93  // pluginInfo captures the necessary information to launch and configure a
    94  // plugin.
    95  type pluginInfo struct {
    96  	factory plugins.PluginCtxFactory
    97  
    98  	exePath string
    99  	args    []string
   100  
   101  	baseInfo   *base.PluginInfoResponse
   102  	version    *version.Version
   103  	apiVersion string
   104  
   105  	configSchema  *hclspec.Spec
   106  	config        map[string]interface{}
   107  	msgpackConfig []byte
   108  }
   109  
   110  // NewPluginLoader returns an instance of a plugin loader or an error if the
   111  // plugins could not be loaded
   112  func NewPluginLoader(config *PluginLoaderConfig) (*PluginLoader, error) {
   113  	if err := validateConfig(config); err != nil {
   114  		return nil, fmt.Errorf("invalid plugin loader configuration passed: %v", err)
   115  	}
   116  
   117  	// Convert the versions
   118  	supportedVersions := make(map[string][]*version.Version, len(config.SupportedVersions))
   119  	for pType, versions := range config.SupportedVersions {
   120  		converted, err := convertVersions(versions)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  		supportedVersions[pType] = converted
   125  	}
   126  
   127  	logger := config.Logger.Named("plugin_loader").With("plugin_dir", config.PluginDir)
   128  	l := &PluginLoader{
   129  		logger:            logger,
   130  		supportedVersions: supportedVersions,
   131  		pluginDir:         config.PluginDir,
   132  		plugins:           make(map[PluginID]*pluginInfo),
   133  	}
   134  
   135  	if err := l.init(config); err != nil {
   136  		return nil, fmt.Errorf("failed to initialize plugin loader: %v", err)
   137  	}
   138  
   139  	return l, nil
   140  }
   141  
   142  // Dispense returns a plugin instance, loading it either internally or by
   143  // launching an external plugin.
   144  func (l *PluginLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error) {
   145  	id := PluginID{
   146  		Name:       name,
   147  		PluginType: pluginType,
   148  	}
   149  	pinfo, ok := l.plugins[id]
   150  	if !ok {
   151  		return nil, fmt.Errorf("unknown plugin with name %q and type %q", name, pluginType)
   152  	}
   153  
   154  	// If the plugin is internal, launch via the factory
   155  	var instance PluginInstance
   156  	if pinfo.factory != nil {
   157  		ctx, cancel := context.WithCancel(context.Background())
   158  		instance = &internalPluginInstance{
   159  			instance:   pinfo.factory(ctx, logger),
   160  			apiVersion: pinfo.apiVersion,
   161  			killFn:     cancel,
   162  		}
   163  	} else {
   164  		var err error
   165  		instance, err = l.dispensePlugin(pinfo.baseInfo.Type, pinfo.apiVersion, pinfo.exePath, pinfo.args, nil, logger)
   166  		if err != nil {
   167  			return nil, fmt.Errorf("failed to launch plugin: %v", err)
   168  		}
   169  	}
   170  
   171  	// Cast to the base type and set the config
   172  	b, ok := instance.Plugin().(base.BasePlugin)
   173  	if !ok {
   174  		return nil, fmt.Errorf("plugin %s doesn't implement base plugin interface", id)
   175  	}
   176  
   177  	c := &base.Config{
   178  		PluginConfig: pinfo.msgpackConfig,
   179  		AgentConfig:  config,
   180  		ApiVersion:   pinfo.apiVersion,
   181  	}
   182  
   183  	if err := b.SetConfig(c); err != nil {
   184  		return nil, fmt.Errorf("setting config for plugin %s failed: %v", id, err)
   185  	}
   186  
   187  	return instance, nil
   188  }
   189  
   190  // Reattach reattaches to a previously launched external plugin.
   191  func (l *PluginLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) {
   192  	return l.dispensePlugin(pluginType, "", "", nil, config, l.logger)
   193  }
   194  
   195  // dispensePlugin is used to launch or reattach to an external plugin.
   196  func (l *PluginLoader) dispensePlugin(
   197  	pluginType, apiVersion, cmd string, args []string, reattach *plugin.ReattachConfig,
   198  	logger log.Logger) (PluginInstance, error) {
   199  
   200  	var pluginCmd *exec.Cmd
   201  	if cmd != "" && reattach != nil {
   202  		return nil, fmt.Errorf("both launch command and reattach config specified")
   203  	} else if cmd == "" && reattach == nil {
   204  		return nil, fmt.Errorf("one of launch command or reattach config must be specified")
   205  	} else if cmd != "" {
   206  		pluginCmd = exec.Command(cmd, args...)
   207  	}
   208  
   209  	client := plugin.NewClient(&plugin.ClientConfig{
   210  		HandshakeConfig:  base.Handshake,
   211  		Plugins:          getPluginMap(pluginType, logger),
   212  		Cmd:              pluginCmd,
   213  		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   214  		Logger:           logger,
   215  		Reattach:         reattach,
   216  	})
   217  
   218  	// Connect via RPC
   219  	rpcClient, err := client.Client()
   220  	if err != nil {
   221  		client.Kill()
   222  		return nil, err
   223  	}
   224  
   225  	// Request the plugin
   226  	raw, err := rpcClient.Dispense(pluginType)
   227  	if err != nil {
   228  		client.Kill()
   229  		return nil, err
   230  	}
   231  
   232  	instance := &externalPluginInstance{
   233  		client:   client,
   234  		instance: raw,
   235  	}
   236  
   237  	if apiVersion != "" {
   238  		instance.apiVersion = apiVersion
   239  	} else {
   240  		// We do not know the API version since we are reattaching, so discover
   241  		// it
   242  		bplugin := raw.(base.BasePlugin)
   243  
   244  		// Retrieve base plugin information
   245  		i, err := bplugin.PluginInfo()
   246  		if err != nil {
   247  			return nil, fmt.Errorf("failed to get plugin info for plugin: %v", err)
   248  		}
   249  
   250  		apiVersion, err := l.selectApiVersion(i)
   251  		if err != nil {
   252  			return nil, fmt.Errorf("failed to validate API versions %v for plugin %s: %v", i.PluginApiVersions, i.Name, err)
   253  		}
   254  		if apiVersion == "" {
   255  			return nil, fmt.Errorf("failed to reattach to plugin because supported API versions for the plugin and Nomad do not overlap")
   256  		}
   257  
   258  		instance.apiVersion = apiVersion
   259  	}
   260  
   261  	return instance, nil
   262  }
   263  
   264  // getPluginMap returns a plugin map based on the type of plugin being launched.
   265  func getPluginMap(pluginType string, logger log.Logger) map[string]plugin.Plugin {
   266  	pmap := map[string]plugin.Plugin{
   267  		base.PluginTypeBase: &base.PluginBase{},
   268  	}
   269  
   270  	switch pluginType {
   271  	case base.PluginTypeDevice:
   272  		pmap[base.PluginTypeDevice] = &device.PluginDevice{}
   273  	case base.PluginTypeDriver:
   274  		pmap[base.PluginTypeDriver] = drivers.NewDriverPlugin(nil, logger)
   275  	}
   276  
   277  	return pmap
   278  }
   279  
   280  // Catalog returns the catalog of all plugins
   281  func (l *PluginLoader) Catalog() map[string][]*base.PluginInfoResponse {
   282  	c := make(map[string][]*base.PluginInfoResponse, 3)
   283  	for id, info := range l.plugins {
   284  		c[id.PluginType] = append(c[id.PluginType], info.baseInfo)
   285  	}
   286  	return c
   287  }