github.com/hernad/nomad@v1.6.112/helper/pluginutils/loader/init.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"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"sort"
    13  
    14  	multierror "github.com/hashicorp/go-multierror"
    15  	plugin "github.com/hashicorp/go-plugin"
    16  	version "github.com/hashicorp/go-version"
    17  	"github.com/hernad/nomad/helper/pluginutils/hclspecutils"
    18  	"github.com/hernad/nomad/helper/pluginutils/hclutils"
    19  	"github.com/hernad/nomad/nomad/structs/config"
    20  	"github.com/hernad/nomad/plugins/base"
    21  	"github.com/zclconf/go-cty/cty/msgpack"
    22  )
    23  
    24  // validateConfig returns whether or not the configuration is valid
    25  func validateConfig(config *PluginLoaderConfig) error {
    26  	var mErr multierror.Error
    27  	if config == nil {
    28  		return fmt.Errorf("nil config passed")
    29  	} else if config.Logger == nil {
    30  		_ = multierror.Append(&mErr, fmt.Errorf("nil logger passed"))
    31  	}
    32  
    33  	// Validate that all plugins have a binary name
    34  	for _, c := range config.Configs {
    35  		if c.Name == "" {
    36  			_ = multierror.Append(&mErr, fmt.Errorf("plugin config passed without binary name"))
    37  		}
    38  	}
    39  
    40  	// Validate internal plugins
    41  	for k, config := range config.InternalPlugins {
    42  		// Validate config
    43  		if config == nil {
    44  			_ = multierror.Append(&mErr, fmt.Errorf("nil config passed for internal plugin %s", k))
    45  			continue
    46  		} else if config.Factory == nil {
    47  			_ = multierror.Append(&mErr, fmt.Errorf("nil factory passed for internal plugin %s", k))
    48  			continue
    49  		}
    50  	}
    51  
    52  	return mErr.ErrorOrNil()
    53  }
    54  
    55  // init initializes the plugin loader by compiling both internal and external
    56  // plugins and selecting the highest versioned version of any given plugin.
    57  func (l *PluginLoader) init(config *PluginLoaderConfig) error {
    58  	// Create a mapping of name to config
    59  	configMap := configMap(config.Configs)
    60  
    61  	// Initialize the internal plugins
    62  	internal, err := l.initInternal(config.InternalPlugins, configMap)
    63  	if err != nil {
    64  		return fmt.Errorf("failed to fingerprint internal plugins: %v", err)
    65  	}
    66  
    67  	// Scan for eligibile binaries
    68  	plugins, err := l.scan()
    69  	if err != nil {
    70  		return fmt.Errorf("failed to scan plugin directory %q: %v", l.pluginDir, err)
    71  	}
    72  
    73  	// Fingerprint the passed plugins
    74  	external, err := l.fingerprintPlugins(plugins, configMap)
    75  	if err != nil {
    76  		return fmt.Errorf("failed to fingerprint plugins: %v", err)
    77  	}
    78  
    79  	// Merge external and internal plugins
    80  	l.plugins = l.mergePlugins(internal, external)
    81  
    82  	// Validate that the configs are valid for the plugins
    83  	if err := l.validatePluginConfigs(); err != nil {
    84  		return fmt.Errorf("parsing plugin configurations failed: %v", err)
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  // initInternal initializes internal plugins.
    91  func (l *PluginLoader) initInternal(plugins map[PluginID]*InternalPluginConfig, configs map[string]*config.PluginConfig) (map[PluginID]*pluginInfo, error) {
    92  	ctx, cancel := context.WithCancel(context.Background())
    93  	defer cancel()
    94  
    95  	var mErr multierror.Error
    96  	fingerprinted := make(map[PluginID]*pluginInfo, len(plugins))
    97  	for k, config := range plugins {
    98  		// Create an instance
    99  		raw := config.Factory(ctx, l.logger)
   100  		base, ok := raw.(base.BasePlugin)
   101  		if !ok {
   102  			_ = multierror.Append(&mErr, fmt.Errorf("internal plugin %s doesn't meet base plugin interface", k))
   103  			continue
   104  		}
   105  
   106  		info := &pluginInfo{
   107  			factory: config.Factory,
   108  			config:  config.Config,
   109  		}
   110  
   111  		// Try to retrieve a user specified config
   112  		if userConfig, ok := configs[k.Name]; ok && userConfig.Config != nil {
   113  			info.config = userConfig.Config
   114  		}
   115  
   116  		// Fingerprint base info
   117  		i, err := base.PluginInfo()
   118  		if err != nil {
   119  			_ = multierror.Append(&mErr, fmt.Errorf("PluginInfo info failed for internal plugin %s: %v", k, err))
   120  			continue
   121  		}
   122  		info.baseInfo = i
   123  
   124  		// Parse and set the plugin version
   125  		v, err := version.NewVersion(i.PluginVersion)
   126  		if err != nil {
   127  			_ = multierror.Append(&mErr, fmt.Errorf("failed to parse version %q for internal plugin %s: %v", i.PluginVersion, k, err))
   128  			continue
   129  		}
   130  		info.version = v
   131  
   132  		// Detect the plugin API version to use
   133  		av, err := l.selectApiVersion(i)
   134  		if err != nil {
   135  			_ = multierror.Append(&mErr, fmt.Errorf("failed to validate API versions %v for internal plugin %s: %v", i.PluginApiVersions, k, err))
   136  			continue
   137  		}
   138  		if av == "" {
   139  			l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", k)
   140  			continue
   141  		}
   142  		info.apiVersion = av
   143  
   144  		// Get the config schema
   145  		schema, err := base.ConfigSchema()
   146  		if err != nil {
   147  			_ = multierror.Append(&mErr, fmt.Errorf("failed to retrieve config schema for internal plugin %s: %v", k, err))
   148  			continue
   149  		}
   150  		info.configSchema = schema
   151  
   152  		// Store the fingerprinted config
   153  		fingerprinted[k] = info
   154  	}
   155  
   156  	if err := mErr.ErrorOrNil(); err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	return fingerprinted, nil
   161  }
   162  
   163  // selectApiVersion takes in PluginInfo and returns the highest compatable
   164  // version or an error if the plugins response is malformed. If there is no
   165  // overlap, an empty string is returned.
   166  func (l *PluginLoader) selectApiVersion(i *base.PluginInfoResponse) (string, error) {
   167  	if i == nil {
   168  		return "", fmt.Errorf("nil plugin info given")
   169  	}
   170  	if len(i.PluginApiVersions) == 0 {
   171  		return "", fmt.Errorf("plugin provided no compatible API versions")
   172  	}
   173  
   174  	pluginVersions, err := convertVersions(i.PluginApiVersions)
   175  	if err != nil {
   176  		return "", fmt.Errorf("plugin provided invalid versions: %v", err)
   177  	}
   178  
   179  	// Lookup the supported versions. These will be sorted highest to lowest
   180  	supportedVersions, ok := l.supportedVersions[i.Type]
   181  	if !ok {
   182  		return "", fmt.Errorf("unsupported plugin type %q", i.Type)
   183  	}
   184  
   185  	for _, sv := range supportedVersions {
   186  		for _, pv := range pluginVersions {
   187  			if sv.Equal(pv) {
   188  				return pv.Original(), nil
   189  			}
   190  		}
   191  	}
   192  
   193  	return "", nil
   194  }
   195  
   196  // convertVersions takes a list of string versions and returns a sorted list of
   197  // versions from highest to lowest.
   198  func convertVersions(in []string) ([]*version.Version, error) {
   199  	converted := make([]*version.Version, len(in))
   200  	for i, v := range in {
   201  		vv, err := version.NewVersion(v)
   202  		if err != nil {
   203  			return nil, fmt.Errorf("failed to convert version %q : %v", v, err)
   204  		}
   205  
   206  		converted[i] = vv
   207  	}
   208  
   209  	sort.Slice(converted, func(i, j int) bool {
   210  		return converted[i].GreaterThan(converted[j])
   211  	})
   212  
   213  	return converted, nil
   214  }
   215  
   216  // scan scans the plugin directory and retrieves potentially eligible binaries
   217  func (l *PluginLoader) scan() ([]os.FileInfo, error) {
   218  	if l.pluginDir == "" {
   219  		return nil, nil
   220  	}
   221  
   222  	// Capture the list of binaries in the plugins folder
   223  	f, err := os.Open(l.pluginDir)
   224  	if err != nil {
   225  		// There are no plugins to scan
   226  		if os.IsNotExist(err) {
   227  			l.logger.Warn("skipping external plugins since plugin_dir doesn't exist")
   228  			return nil, nil
   229  		}
   230  
   231  		return nil, fmt.Errorf("failed to open plugin directory %q: %v", l.pluginDir, err)
   232  	}
   233  	files, err := f.Readdirnames(-1)
   234  	f.Close()
   235  	if err != nil {
   236  		return nil, fmt.Errorf("failed to read plugin directory %q: %v", l.pluginDir, err)
   237  	}
   238  
   239  	var plugins []os.FileInfo
   240  	for _, f := range files {
   241  		f = filepath.Join(l.pluginDir, f)
   242  		s, err := os.Stat(f)
   243  		if err != nil {
   244  			return nil, fmt.Errorf("failed to stat file %q: %v", f, err)
   245  		}
   246  		if s.IsDir() {
   247  			l.logger.Warn("skipping subdir in plugin folder", "subdir", f)
   248  			continue
   249  		}
   250  
   251  		if !executable(f, s) {
   252  			l.logger.Warn("skipping un-executable file in plugin folder", "file", f)
   253  			continue
   254  		}
   255  		plugins = append(plugins, s)
   256  	}
   257  
   258  	return plugins, nil
   259  }
   260  
   261  // fingerprintPlugins fingerprints all external plugin binaries
   262  func (l *PluginLoader) fingerprintPlugins(plugins []os.FileInfo, configs map[string]*config.PluginConfig) (map[PluginID]*pluginInfo, error) {
   263  	var mErr multierror.Error
   264  	fingerprinted := make(map[PluginID]*pluginInfo, len(plugins))
   265  	for _, p := range plugins {
   266  		name := cleanPluginExecutable(p.Name())
   267  		c := configs[name]
   268  		info, err := l.fingerprintPlugin(p, c)
   269  		if err != nil {
   270  			l.logger.Error("failed to fingerprint plugin", "plugin", name, "error", err)
   271  			_ = multierror.Append(&mErr, err)
   272  			continue
   273  		}
   274  		if info == nil {
   275  			// Plugin was skipped for validation reasons
   276  			continue
   277  		}
   278  
   279  		id := PluginID{
   280  			Name:       info.baseInfo.Name,
   281  			PluginType: info.baseInfo.Type,
   282  		}
   283  
   284  		// Detect if we already have seen a version of this plugin
   285  		if prev, ok := fingerprinted[id]; ok {
   286  			oldVersion := prev.version
   287  			selectedVersion := info.version
   288  			skip := false
   289  
   290  			// Determine if we should keep the previous version or override
   291  			if prev.version.GreaterThan(info.version) {
   292  				oldVersion = info.version
   293  				selectedVersion = prev.version
   294  				skip = true
   295  			}
   296  			l.logger.Info("multiple versions of plugin detected",
   297  				"plugin", info.baseInfo.Name, "older_version", oldVersion, "selected_version", selectedVersion)
   298  
   299  			if skip {
   300  				continue
   301  			}
   302  		}
   303  
   304  		// Add the plugin
   305  		fingerprinted[id] = info
   306  	}
   307  
   308  	if err := mErr.ErrorOrNil(); err != nil {
   309  		return nil, err
   310  	}
   311  
   312  	return fingerprinted, nil
   313  }
   314  
   315  // fingerprintPlugin fingerprints the passed external plugin
   316  func (l *PluginLoader) fingerprintPlugin(pluginExe os.FileInfo, config *config.PluginConfig) (*pluginInfo, error) {
   317  	info := &pluginInfo{
   318  		exePath: filepath.Join(l.pluginDir, pluginExe.Name()),
   319  	}
   320  
   321  	// Build the command
   322  	cmd := exec.Command(info.exePath)
   323  	if config != nil {
   324  		cmd.Args = append(cmd.Args, config.Args...)
   325  		info.args = config.Args
   326  		info.config = config.Config
   327  	}
   328  
   329  	// Launch the plugin
   330  	client := plugin.NewClient(&plugin.ClientConfig{
   331  		HandshakeConfig: base.Handshake,
   332  		Plugins: map[string]plugin.Plugin{
   333  			base.PluginTypeBase: &base.PluginBase{},
   334  		},
   335  		Cmd:              cmd,
   336  		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   337  		Logger:           l.logger,
   338  	})
   339  	defer client.Kill()
   340  
   341  	// Connect via RPC
   342  	rpcClient, err := client.Client()
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  
   347  	// Request the plugin
   348  	raw, err := rpcClient.Dispense(base.PluginTypeBase)
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	// Cast the plugin to the base type
   354  	bplugin := raw.(base.BasePlugin)
   355  
   356  	// Retrieve base plugin information
   357  	i, err := bplugin.PluginInfo()
   358  	if err != nil {
   359  		return nil, fmt.Errorf("failed to get plugin info for plugin %q: %v", info.exePath, err)
   360  	}
   361  	info.baseInfo = i
   362  
   363  	// Parse and set the plugin version
   364  	v, err := version.NewVersion(i.PluginVersion)
   365  	if err != nil {
   366  		return nil, fmt.Errorf("failed to parse plugin %q (%v) version %q: %v",
   367  			i.Name, info.exePath, i.PluginVersion, err)
   368  	}
   369  	info.version = v
   370  
   371  	// Detect the plugin API version to use
   372  	av, err := l.selectApiVersion(i)
   373  	if err != nil {
   374  		return nil, fmt.Errorf("failed to validate API versions %v for plugin %s (%v): %v", i.PluginApiVersions, i.Name, info.exePath, err)
   375  	}
   376  	if av == "" {
   377  		l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", i.Name, "path", info.exePath)
   378  		return nil, nil
   379  	}
   380  	info.apiVersion = av
   381  
   382  	// Retrieve the schema
   383  	schema, err := bplugin.ConfigSchema()
   384  	if err != nil {
   385  		return nil, fmt.Errorf("failed to get plugin config schema for plugin %q: %v", info.exePath, err)
   386  	}
   387  	info.configSchema = schema
   388  
   389  	return info, nil
   390  }
   391  
   392  // mergePlugins merges internal and external plugins, preferring the highest
   393  // version.
   394  func (l *PluginLoader) mergePlugins(internal, external map[PluginID]*pluginInfo) map[PluginID]*pluginInfo {
   395  	finalized := make(map[PluginID]*pluginInfo, len(internal))
   396  
   397  	// Load the internal plugins
   398  	for k, v := range internal {
   399  		finalized[k] = v
   400  	}
   401  
   402  	for k, extPlugin := range external {
   403  		internal, ok := finalized[k]
   404  		if ok {
   405  			// We have overlapping plugins, determine if we should keep the
   406  			// internal version or override
   407  			if extPlugin.version.LessThan(internal.version) {
   408  				l.logger.Info("preferring internal version of plugin",
   409  					"plugin", extPlugin.baseInfo.Name, "internal_version", internal.version,
   410  					"external_version", extPlugin.version)
   411  				continue
   412  			}
   413  		}
   414  
   415  		// Add external plugin
   416  		finalized[k] = extPlugin
   417  	}
   418  
   419  	return finalized
   420  }
   421  
   422  // validatePluginConfigs is used to validate each plugins' configuration. If the
   423  // plugin has a config, it is parsed with the plugins config schema and
   424  // SetConfig is called to ensure the config is valid.
   425  func (l *PluginLoader) validatePluginConfigs() error {
   426  	var mErr multierror.Error
   427  	for id, info := range l.plugins {
   428  		if err := l.validatePluginConfig(id, info); err != nil {
   429  			wrapped := multierror.Prefix(err, fmt.Sprintf("plugin %s:", id))
   430  			_ = multierror.Append(&mErr, wrapped)
   431  		}
   432  	}
   433  
   434  	return mErr.ErrorOrNil()
   435  }
   436  
   437  // validatePluginConfig is used to validate the plugin's configuration. If the
   438  // plugin has a config, it is parsed with the plugins config schema and
   439  // SetConfig is called to ensure the config is valid.
   440  func (l *PluginLoader) validatePluginConfig(id PluginID, info *pluginInfo) error {
   441  	var mErr multierror.Error
   442  
   443  	// Check if a config is allowed
   444  	if info.configSchema == nil {
   445  		if info.config != nil {
   446  			return fmt.Errorf("configuration not allowed but config passed")
   447  		}
   448  
   449  		// Nothing to do
   450  		return nil
   451  	}
   452  
   453  	// Convert the schema to hcl
   454  	spec, diag := hclspecutils.Convert(info.configSchema)
   455  	if diag.HasErrors() {
   456  		_ = multierror.Append(&mErr, diag.Errs()...)
   457  		return multierror.Prefix(&mErr, "failed converting config schema:")
   458  	}
   459  
   460  	// If there is no config, initialize it to an empty map so we can still
   461  	// handle defaults
   462  	if info.config == nil {
   463  		info.config = map[string]interface{}{}
   464  	}
   465  
   466  	// Parse the config using the spec
   467  	val, diag, diagErrs := hclutils.ParseHclInterface(info.config, spec, nil)
   468  	if diag.HasErrors() {
   469  		_ = multierror.Append(&mErr, diagErrs...)
   470  		return multierror.Prefix(&mErr, "failed to parse config: ")
   471  
   472  	}
   473  
   474  	// Marshal the value
   475  	cdata, err := msgpack.Marshal(val, val.Type())
   476  	if err != nil {
   477  		return fmt.Errorf("failed to msgpack encode config: %v", err)
   478  	}
   479  
   480  	// Store the marshalled config
   481  	info.msgpackConfig = cdata
   482  
   483  	// Dispense the plugin and set its config and ensure it is error free
   484  	instance, err := l.Dispense(id.Name, id.PluginType, nil, l.logger)
   485  	if err != nil {
   486  		return fmt.Errorf("failed to dispense plugin: %v", err)
   487  	}
   488  	defer instance.Kill()
   489  
   490  	b, ok := instance.Plugin().(base.BasePlugin)
   491  	if !ok {
   492  		return fmt.Errorf("dispensed plugin %s doesn't meet base plugin interface", id)
   493  	}
   494  
   495  	c := &base.Config{
   496  		PluginConfig: cdata,
   497  		AgentConfig:  nil,
   498  		ApiVersion:   info.apiVersion,
   499  	}
   500  
   501  	if err := b.SetConfig(c); err != nil {
   502  		return fmt.Errorf("setting config on plugin failed: %v", err)
   503  	}
   504  	return nil
   505  }