github.com/hernad/nomad@v1.6.112/helper/pluginutils/loader/loader.go (about)

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