github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/helper/pluginutils/loader/loader_test.go (about)

     1  package loader
     2  
     3  import (
     4  	"io"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"sort"
    10  	"strings"
    11  	"testing"
    12  
    13  	log "github.com/hashicorp/go-hclog"
    14  	version "github.com/hashicorp/go-version"
    15  	"github.com/hashicorp/nomad/helper/testlog"
    16  	"github.com/hashicorp/nomad/nomad/structs/config"
    17  	"github.com/hashicorp/nomad/plugins/base"
    18  	"github.com/hashicorp/nomad/plugins/device"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  var (
    23  	// supportedApiVersions is the set of api versions that the "client" can
    24  	// support
    25  	supportedApiVersions = map[string][]string{
    26  		base.PluginTypeDevice: {device.ApiVersion010},
    27  	}
    28  )
    29  
    30  // harness is used to build a temp directory and copy our own test executable
    31  // into it, allowing the plugin loader to scan for plugins.
    32  type harness struct {
    33  	t      *testing.T
    34  	tmpDir string
    35  }
    36  
    37  // newHarness returns a harness and copies our test binary to the temp directory
    38  // with the passed plugin names.
    39  func newHarness(t *testing.T, plugins []string) *harness {
    40  	t.Helper()
    41  
    42  	h := &harness{
    43  		t: t,
    44  	}
    45  
    46  	// Build a temp directory
    47  	path, err := ioutil.TempDir("", t.Name())
    48  	if err != nil {
    49  		t.Fatalf("failed to build tmp directory")
    50  	}
    51  	h.tmpDir = path
    52  
    53  	// Get our own executable path
    54  	selfExe, err := os.Executable()
    55  	if err != nil {
    56  		t.Fatalf("failed to get self executable path: %v", err)
    57  	}
    58  
    59  	exeSuffix := ""
    60  	if runtime.GOOS == "windows" {
    61  		exeSuffix = ".exe"
    62  	}
    63  	for _, p := range plugins {
    64  		dest := filepath.Join(h.tmpDir, p) + exeSuffix
    65  		if err := copyFile(selfExe, dest); err != nil {
    66  			t.Fatalf("failed to copy file: %v", err)
    67  		}
    68  	}
    69  
    70  	return h
    71  }
    72  
    73  // copyFile copies the src file to dst.
    74  func copyFile(src, dst string) error {
    75  	in, err := os.Open(src)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	defer in.Close()
    80  
    81  	out, err := os.Create(dst)
    82  	if err != nil {
    83  		return err
    84  	}
    85  	defer out.Close()
    86  
    87  	if _, err = io.Copy(out, in); err != nil {
    88  		return err
    89  	}
    90  	if err := out.Close(); err != nil {
    91  		return err
    92  	}
    93  
    94  	return os.Chmod(dst, 0777)
    95  }
    96  
    97  // pluginDir returns the plugin directory.
    98  func (h *harness) pluginDir() string {
    99  	return h.tmpDir
   100  }
   101  
   102  // cleanup removes the temp directory
   103  func (h *harness) cleanup() {
   104  	if err := os.RemoveAll(h.tmpDir); err != nil {
   105  		h.t.Fatalf("failed to remove tmp directory %q: %v", h.tmpDir, err)
   106  	}
   107  }
   108  
   109  func TestPluginLoader_External(t *testing.T) {
   110  	t.Parallel()
   111  	require := require.New(t)
   112  
   113  	// Create two plugins
   114  	plugins := []string{"mock-device", "mock-device-2"}
   115  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   116  	h := newHarness(t, plugins)
   117  	defer h.cleanup()
   118  
   119  	logger := testlog.HCLogger(t)
   120  	logger.SetLevel(log.Trace)
   121  	lconfig := &PluginLoaderConfig{
   122  		Logger:            logger,
   123  		PluginDir:         h.pluginDir(),
   124  		SupportedVersions: supportedApiVersions,
   125  		Configs: []*config.PluginConfig{
   126  			{
   127  				Name: plugins[0],
   128  				Args: []string{"-plugin", "-name", plugins[0],
   129  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0],
   130  					"-api-version", device.ApiVersion010},
   131  			},
   132  			{
   133  				Name: plugins[1],
   134  				Args: []string{"-plugin", "-name", plugins[1],
   135  					"-type", base.PluginTypeDevice, "-version", pluginVersions[1],
   136  					"-api-version", device.ApiVersion010, "-api-version", "v0.2.0"},
   137  			},
   138  		},
   139  	}
   140  
   141  	l, err := NewPluginLoader(lconfig)
   142  	require.NoError(err)
   143  
   144  	// Get the catalog and assert we have the two plugins
   145  	c := l.Catalog()
   146  	require.Len(c, 1)
   147  	require.Contains(c, base.PluginTypeDevice)
   148  	detected := c[base.PluginTypeDevice]
   149  	require.Len(detected, 2)
   150  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   151  
   152  	expected := []*base.PluginInfoResponse{
   153  		{
   154  			Name:              plugins[0],
   155  			Type:              base.PluginTypeDevice,
   156  			PluginVersion:     pluginVersions[0],
   157  			PluginApiVersions: []string{"v0.1.0"},
   158  		},
   159  		{
   160  			Name:              plugins[1],
   161  			Type:              base.PluginTypeDevice,
   162  			PluginVersion:     pluginVersions[1],
   163  			PluginApiVersions: []string{"v0.1.0", "v0.2.0"},
   164  		},
   165  	}
   166  	require.EqualValues(expected, detected)
   167  }
   168  
   169  func TestPluginLoader_External_ApiVersions(t *testing.T) {
   170  	t.Parallel()
   171  	require := require.New(t)
   172  
   173  	// Create two plugins
   174  	plugins := []string{"mock-device", "mock-device-2", "mock-device-3"}
   175  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   176  	h := newHarness(t, plugins)
   177  	defer h.cleanup()
   178  
   179  	logger := testlog.HCLogger(t)
   180  	logger.SetLevel(log.Trace)
   181  	lconfig := &PluginLoaderConfig{
   182  		Logger:    logger,
   183  		PluginDir: h.pluginDir(),
   184  		SupportedVersions: map[string][]string{
   185  			base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"},
   186  		},
   187  		Configs: []*config.PluginConfig{
   188  			{
   189  				// No supporting version
   190  				Name: plugins[0],
   191  				Args: []string{"-plugin", "-name", plugins[0],
   192  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0],
   193  					"-api-version", "v0.1.0"},
   194  			},
   195  			{
   196  				// Pick highest matching
   197  				Name: plugins[1],
   198  				Args: []string{"-plugin", "-name", plugins[1],
   199  					"-type", base.PluginTypeDevice, "-version", pluginVersions[1],
   200  					"-api-version", "v0.1.0",
   201  					"-api-version", "v0.2.0",
   202  					"-api-version", "v0.2.1",
   203  					"-api-version", "v0.2.2",
   204  				},
   205  			},
   206  			{
   207  				// Pick highest matching
   208  				Name: plugins[2],
   209  				Args: []string{"-plugin", "-name", plugins[2],
   210  					"-type", base.PluginTypeDevice, "-version", pluginVersions[1],
   211  					"-api-version", "v0.1.0",
   212  					"-api-version", "v0.2.0",
   213  					"-api-version", "v0.2.1",
   214  					"-api-version", "v0.3.0",
   215  				},
   216  			},
   217  		},
   218  	}
   219  
   220  	l, err := NewPluginLoader(lconfig)
   221  	require.NoError(err)
   222  
   223  	// Get the catalog and assert we have the two plugins
   224  	c := l.Catalog()
   225  	require.Len(c, 1)
   226  	require.Contains(c, base.PluginTypeDevice)
   227  	detected := c[base.PluginTypeDevice]
   228  	require.Len(detected, 2)
   229  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   230  
   231  	expected := []*base.PluginInfoResponse{
   232  		{
   233  			Name:              plugins[1],
   234  			Type:              base.PluginTypeDevice,
   235  			PluginVersion:     pluginVersions[1],
   236  			PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"},
   237  		},
   238  		{
   239  			Name:              plugins[2],
   240  			Type:              base.PluginTypeDevice,
   241  			PluginVersion:     pluginVersions[1],
   242  			PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"},
   243  		},
   244  	}
   245  	require.EqualValues(expected, detected)
   246  
   247  	// Test we chose the correct versions by dispensing and checking and then
   248  	// reattaching and checking
   249  	p1, err := l.Dispense(plugins[1], base.PluginTypeDevice, nil, logger)
   250  	require.NoError(err)
   251  	defer p1.Kill()
   252  	require.Equal("v0.2.1", p1.ApiVersion())
   253  
   254  	p2, err := l.Dispense(plugins[2], base.PluginTypeDevice, nil, logger)
   255  	require.NoError(err)
   256  	defer p2.Kill()
   257  	require.Equal("v0.3.0", p2.ApiVersion())
   258  
   259  	// Test reattach api versions
   260  	rc1, ok := p1.ReattachConfig()
   261  	require.True(ok)
   262  	r1, err := l.Reattach(plugins[1], base.PluginTypeDriver, rc1)
   263  	require.NoError(err)
   264  	require.Equal("v0.2.1", r1.ApiVersion())
   265  
   266  	rc2, ok := p2.ReattachConfig()
   267  	require.True(ok)
   268  	r2, err := l.Reattach(plugins[2], base.PluginTypeDriver, rc2)
   269  	require.NoError(err)
   270  	require.Equal("v0.3.0", r2.ApiVersion())
   271  }
   272  
   273  func TestPluginLoader_External_NoApiVersion(t *testing.T) {
   274  	t.Parallel()
   275  	require := require.New(t)
   276  
   277  	// Create two plugins
   278  	plugins := []string{"mock-device"}
   279  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   280  	h := newHarness(t, plugins)
   281  	defer h.cleanup()
   282  
   283  	logger := testlog.HCLogger(t)
   284  	logger.SetLevel(log.Trace)
   285  	lconfig := &PluginLoaderConfig{
   286  		Logger:            logger,
   287  		PluginDir:         h.pluginDir(),
   288  		SupportedVersions: supportedApiVersions,
   289  		Configs: []*config.PluginConfig{
   290  			{
   291  				Name: plugins[0],
   292  				Args: []string{"-plugin", "-name", plugins[0],
   293  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0]},
   294  			},
   295  		},
   296  	}
   297  
   298  	_, err := NewPluginLoader(lconfig)
   299  	require.Error(err)
   300  	require.Contains(err.Error(), "no compatible API versions")
   301  }
   302  
   303  func TestPluginLoader_External_Config(t *testing.T) {
   304  	t.Parallel()
   305  	require := require.New(t)
   306  
   307  	// Create two plugins
   308  	plugins := []string{"mock-device", "mock-device-2"}
   309  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   310  	h := newHarness(t, plugins)
   311  	defer h.cleanup()
   312  
   313  	logger := testlog.HCLogger(t)
   314  	logger.SetLevel(log.Trace)
   315  	lconfig := &PluginLoaderConfig{
   316  		Logger:            logger,
   317  		PluginDir:         h.pluginDir(),
   318  		SupportedVersions: supportedApiVersions,
   319  		Configs: []*config.PluginConfig{
   320  			{
   321  				Name: plugins[0],
   322  				Args: []string{"-plugin", "-name", plugins[0],
   323  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
   324  				Config: map[string]interface{}{
   325  					"foo": "1",
   326  					"bar": "2",
   327  				},
   328  			},
   329  			{
   330  				Name: plugins[1],
   331  				Args: []string{"-plugin", "-name", plugins[1],
   332  					"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010},
   333  				Config: map[string]interface{}{
   334  					"foo": "3",
   335  					"bar": "4",
   336  				},
   337  			},
   338  		},
   339  	}
   340  
   341  	l, err := NewPluginLoader(lconfig)
   342  	require.NoError(err)
   343  
   344  	// Get the catalog and assert we have the two plugins
   345  	c := l.Catalog()
   346  	require.Len(c, 1)
   347  	require.Contains(c, base.PluginTypeDevice)
   348  	detected := c[base.PluginTypeDevice]
   349  	require.Len(detected, 2)
   350  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   351  
   352  	expected := []*base.PluginInfoResponse{
   353  		{
   354  			Name:              plugins[0],
   355  			Type:              base.PluginTypeDevice,
   356  			PluginVersion:     pluginVersions[0],
   357  			PluginApiVersions: []string{device.ApiVersion010},
   358  		},
   359  		{
   360  			Name:              plugins[1],
   361  			Type:              base.PluginTypeDevice,
   362  			PluginVersion:     pluginVersions[1],
   363  			PluginApiVersions: []string{device.ApiVersion010},
   364  		},
   365  	}
   366  	require.EqualValues(expected, detected)
   367  }
   368  
   369  // Pass a config but make sure it is fatal
   370  func TestPluginLoader_External_Config_Bad(t *testing.T) {
   371  	t.Parallel()
   372  	require := require.New(t)
   373  
   374  	// Create a plugin
   375  	plugins := []string{"mock-device"}
   376  	pluginVersions := []string{"v0.0.1"}
   377  	h := newHarness(t, plugins)
   378  	defer h.cleanup()
   379  
   380  	logger := testlog.HCLogger(t)
   381  	logger.SetLevel(log.Trace)
   382  	lconfig := &PluginLoaderConfig{
   383  		Logger:            logger,
   384  		PluginDir:         h.pluginDir(),
   385  		SupportedVersions: supportedApiVersions,
   386  		Configs: []*config.PluginConfig{
   387  			{
   388  				Name: plugins[0],
   389  				Args: []string{"-plugin", "-name", plugins[0],
   390  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
   391  				Config: map[string]interface{}{
   392  					"foo":          "1",
   393  					"bar":          "2",
   394  					"non-existent": "3",
   395  				},
   396  			},
   397  		},
   398  	}
   399  
   400  	_, err := NewPluginLoader(lconfig)
   401  	require.Error(err)
   402  	require.Contains(err.Error(), "No argument or block type is named \"non-existent\"")
   403  }
   404  
   405  func TestPluginLoader_External_VersionOverlap(t *testing.T) {
   406  	t.Parallel()
   407  	require := require.New(t)
   408  
   409  	// Create two plugins
   410  	plugins := []string{"mock-device", "mock-device-2"}
   411  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   412  	h := newHarness(t, plugins)
   413  	defer h.cleanup()
   414  
   415  	logger := testlog.HCLogger(t)
   416  	logger.SetLevel(log.Trace)
   417  	lconfig := &PluginLoaderConfig{
   418  		Logger:            logger,
   419  		PluginDir:         h.pluginDir(),
   420  		SupportedVersions: supportedApiVersions,
   421  		Configs: []*config.PluginConfig{
   422  			{
   423  				Name: plugins[0],
   424  				Args: []string{"-plugin", "-name", plugins[0],
   425  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
   426  			},
   427  			{
   428  				Name: plugins[1],
   429  				Args: []string{"-plugin", "-name", plugins[0],
   430  					"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010},
   431  			},
   432  		},
   433  	}
   434  
   435  	l, err := NewPluginLoader(lconfig)
   436  	require.NoError(err)
   437  
   438  	// Get the catalog and assert we have the two plugins
   439  	c := l.Catalog()
   440  	require.Len(c, 1)
   441  	require.Contains(c, base.PluginTypeDevice)
   442  	detected := c[base.PluginTypeDevice]
   443  	require.Len(detected, 1)
   444  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   445  
   446  	expected := []*base.PluginInfoResponse{
   447  		{
   448  			Name:              plugins[0],
   449  			Type:              base.PluginTypeDevice,
   450  			PluginVersion:     pluginVersions[1],
   451  			PluginApiVersions: []string{device.ApiVersion010},
   452  		},
   453  	}
   454  	require.EqualValues(expected, detected)
   455  }
   456  
   457  func TestPluginLoader_Internal(t *testing.T) {
   458  	t.Parallel()
   459  	require := require.New(t)
   460  
   461  	// Create the harness
   462  	h := newHarness(t, nil)
   463  	defer h.cleanup()
   464  
   465  	plugins := []string{"mock-device", "mock-device-2"}
   466  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   467  	pluginApiVersions := []string{device.ApiVersion010}
   468  
   469  	logger := testlog.HCLogger(t)
   470  	logger.SetLevel(log.Trace)
   471  	lconfig := &PluginLoaderConfig{
   472  		Logger:            logger,
   473  		PluginDir:         h.pluginDir(),
   474  		SupportedVersions: supportedApiVersions,
   475  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   476  			{
   477  				Name:       plugins[0],
   478  				PluginType: base.PluginTypeDevice,
   479  			}: {
   480  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
   481  			},
   482  			{
   483  				Name:       plugins[1],
   484  				PluginType: base.PluginTypeDevice,
   485  			}: {
   486  				Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
   487  			},
   488  		},
   489  	}
   490  
   491  	l, err := NewPluginLoader(lconfig)
   492  	require.NoError(err)
   493  
   494  	// Get the catalog and assert we have the two plugins
   495  	c := l.Catalog()
   496  	require.Len(c, 1)
   497  	require.Contains(c, base.PluginTypeDevice)
   498  	detected := c[base.PluginTypeDevice]
   499  	require.Len(detected, 2)
   500  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   501  
   502  	expected := []*base.PluginInfoResponse{
   503  		{
   504  			Name:              plugins[0],
   505  			Type:              base.PluginTypeDevice,
   506  			PluginVersion:     pluginVersions[0],
   507  			PluginApiVersions: []string{device.ApiVersion010},
   508  		},
   509  		{
   510  			Name:              plugins[1],
   511  			Type:              base.PluginTypeDevice,
   512  			PluginVersion:     pluginVersions[1],
   513  			PluginApiVersions: []string{device.ApiVersion010},
   514  		},
   515  	}
   516  	require.EqualValues(expected, detected)
   517  }
   518  
   519  func TestPluginLoader_Internal_ApiVersions(t *testing.T) {
   520  	t.Parallel()
   521  	require := require.New(t)
   522  
   523  	// Create two plugins
   524  	plugins := []string{"mock-device", "mock-device-2", "mock-device-3"}
   525  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   526  	h := newHarness(t, nil)
   527  	defer h.cleanup()
   528  
   529  	logger := testlog.HCLogger(t)
   530  	logger.SetLevel(log.Trace)
   531  	lconfig := &PluginLoaderConfig{
   532  		Logger:    logger,
   533  		PluginDir: h.pluginDir(),
   534  		SupportedVersions: map[string][]string{
   535  			base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"},
   536  		},
   537  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   538  			{
   539  				Name:       plugins[0],
   540  				PluginType: base.PluginTypeDevice,
   541  			}: {
   542  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], []string{"v0.1.0"}, true),
   543  			},
   544  			{
   545  				Name:       plugins[1],
   546  				PluginType: base.PluginTypeDevice,
   547  			}: {
   548  				Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1],
   549  					[]string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"}, true),
   550  			},
   551  			{
   552  				Name:       plugins[2],
   553  				PluginType: base.PluginTypeDevice,
   554  			}: {
   555  				Factory: mockFactory(plugins[2], base.PluginTypeDevice, pluginVersions[1],
   556  					[]string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"}, true),
   557  			},
   558  		},
   559  	}
   560  
   561  	l, err := NewPluginLoader(lconfig)
   562  	require.NoError(err)
   563  
   564  	// Get the catalog and assert we have the two plugins
   565  	c := l.Catalog()
   566  	require.Len(c, 1)
   567  	require.Contains(c, base.PluginTypeDevice)
   568  	detected := c[base.PluginTypeDevice]
   569  	require.Len(detected, 2)
   570  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   571  
   572  	expected := []*base.PluginInfoResponse{
   573  		{
   574  			Name:              plugins[1],
   575  			Type:              base.PluginTypeDevice,
   576  			PluginVersion:     pluginVersions[1],
   577  			PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"},
   578  		},
   579  		{
   580  			Name:              plugins[2],
   581  			Type:              base.PluginTypeDevice,
   582  			PluginVersion:     pluginVersions[1],
   583  			PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"},
   584  		},
   585  	}
   586  	require.EqualValues(expected, detected)
   587  
   588  	// Test we chose the correct versions by dispensing and checking and then
   589  	// reattaching and checking
   590  	p1, err := l.Dispense(plugins[1], base.PluginTypeDevice, nil, logger)
   591  	require.NoError(err)
   592  	defer p1.Kill()
   593  	require.Equal("v0.2.1", p1.ApiVersion())
   594  
   595  	p2, err := l.Dispense(plugins[2], base.PluginTypeDevice, nil, logger)
   596  	require.NoError(err)
   597  	defer p2.Kill()
   598  	require.Equal("v0.3.0", p2.ApiVersion())
   599  }
   600  
   601  func TestPluginLoader_Internal_NoApiVersion(t *testing.T) {
   602  	t.Parallel()
   603  	require := require.New(t)
   604  
   605  	// Create two plugins
   606  	plugins := []string{"mock-device"}
   607  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   608  	h := newHarness(t, nil)
   609  	defer h.cleanup()
   610  
   611  	logger := testlog.HCLogger(t)
   612  	logger.SetLevel(log.Trace)
   613  	lconfig := &PluginLoaderConfig{
   614  		Logger:            logger,
   615  		PluginDir:         h.pluginDir(),
   616  		SupportedVersions: supportedApiVersions,
   617  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   618  			{
   619  				Name:       plugins[0],
   620  				PluginType: base.PluginTypeDevice,
   621  			}: {
   622  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], nil, true),
   623  			},
   624  		},
   625  	}
   626  
   627  	_, err := NewPluginLoader(lconfig)
   628  	require.Error(err)
   629  	require.Contains(err.Error(), "no compatible API versions")
   630  }
   631  
   632  func TestPluginLoader_Internal_Config(t *testing.T) {
   633  	t.Parallel()
   634  	require := require.New(t)
   635  
   636  	// Create the harness
   637  	h := newHarness(t, nil)
   638  	defer h.cleanup()
   639  
   640  	plugins := []string{"mock-device", "mock-device-2"}
   641  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   642  	pluginApiVersions := []string{device.ApiVersion010}
   643  
   644  	logger := testlog.HCLogger(t)
   645  	logger.SetLevel(log.Trace)
   646  	lconfig := &PluginLoaderConfig{
   647  		Logger:            logger,
   648  		PluginDir:         h.pluginDir(),
   649  		SupportedVersions: supportedApiVersions,
   650  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   651  			{
   652  				Name:       plugins[0],
   653  				PluginType: base.PluginTypeDevice,
   654  			}: {
   655  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
   656  				Config: map[string]interface{}{
   657  					"foo": "1",
   658  					"bar": "2",
   659  				},
   660  			},
   661  			{
   662  				Name:       plugins[1],
   663  				PluginType: base.PluginTypeDevice,
   664  			}: {
   665  				Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
   666  				Config: map[string]interface{}{
   667  					"foo": "3",
   668  					"bar": "4",
   669  				},
   670  			},
   671  		},
   672  	}
   673  
   674  	l, err := NewPluginLoader(lconfig)
   675  	require.NoError(err)
   676  
   677  	// Get the catalog and assert we have the two plugins
   678  	c := l.Catalog()
   679  	require.Len(c, 1)
   680  	require.Contains(c, base.PluginTypeDevice)
   681  	detected := c[base.PluginTypeDevice]
   682  	require.Len(detected, 2)
   683  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   684  
   685  	expected := []*base.PluginInfoResponse{
   686  		{
   687  			Name:              plugins[0],
   688  			Type:              base.PluginTypeDevice,
   689  			PluginVersion:     pluginVersions[0],
   690  			PluginApiVersions: []string{device.ApiVersion010},
   691  		},
   692  		{
   693  			Name:              plugins[1],
   694  			Type:              base.PluginTypeDevice,
   695  			PluginVersion:     pluginVersions[1],
   696  			PluginApiVersions: []string{device.ApiVersion010},
   697  		},
   698  	}
   699  	require.EqualValues(expected, detected)
   700  }
   701  
   702  // Tests that an external config can override the config of an internal plugin
   703  func TestPluginLoader_Internal_ExternalConfig(t *testing.T) {
   704  	t.Parallel()
   705  	require := require.New(t)
   706  
   707  	// Create the harness
   708  	h := newHarness(t, nil)
   709  	defer h.cleanup()
   710  
   711  	plugin := "mock-device"
   712  	pluginVersion := "v0.0.1"
   713  	pluginApiVersions := []string{device.ApiVersion010}
   714  
   715  	id := PluginID{
   716  		Name:       plugin,
   717  		PluginType: base.PluginTypeDevice,
   718  	}
   719  	expectedConfig := map[string]interface{}{
   720  		"foo": "2",
   721  		"bar": "3",
   722  	}
   723  
   724  	logger := testlog.HCLogger(t)
   725  	logger.SetLevel(log.Trace)
   726  	lconfig := &PluginLoaderConfig{
   727  		Logger:            logger,
   728  		PluginDir:         h.pluginDir(),
   729  		SupportedVersions: supportedApiVersions,
   730  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   731  			id: {
   732  				Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true),
   733  				Config: map[string]interface{}{
   734  					"foo": "1",
   735  					"bar": "2",
   736  				},
   737  			},
   738  		},
   739  		Configs: []*config.PluginConfig{
   740  			{
   741  				Name:   plugin,
   742  				Config: expectedConfig,
   743  			},
   744  		},
   745  	}
   746  
   747  	l, err := NewPluginLoader(lconfig)
   748  	require.NoError(err)
   749  
   750  	// Get the catalog and assert we have the two plugins
   751  	c := l.Catalog()
   752  	require.Len(c, 1)
   753  	require.Contains(c, base.PluginTypeDevice)
   754  	detected := c[base.PluginTypeDevice]
   755  	require.Len(detected, 1)
   756  
   757  	expected := []*base.PluginInfoResponse{
   758  		{
   759  			Name:              plugin,
   760  			Type:              base.PluginTypeDevice,
   761  			PluginVersion:     pluginVersion,
   762  			PluginApiVersions: []string{device.ApiVersion010},
   763  		},
   764  	}
   765  	require.EqualValues(expected, detected)
   766  
   767  	// Check the config
   768  	loaded, ok := l.plugins[id]
   769  	require.True(ok)
   770  	require.EqualValues(expectedConfig, loaded.config)
   771  }
   772  
   773  // Pass a config but make sure it is fatal
   774  func TestPluginLoader_Internal_Config_Bad(t *testing.T) {
   775  	t.Parallel()
   776  	require := require.New(t)
   777  
   778  	// Create the harness
   779  	h := newHarness(t, nil)
   780  	defer h.cleanup()
   781  
   782  	plugins := []string{"mock-device"}
   783  	pluginVersions := []string{"v0.0.1"}
   784  	pluginApiVersions := []string{device.ApiVersion010}
   785  
   786  	logger := testlog.HCLogger(t)
   787  	logger.SetLevel(log.Trace)
   788  	lconfig := &PluginLoaderConfig{
   789  		Logger:            logger,
   790  		PluginDir:         h.pluginDir(),
   791  		SupportedVersions: supportedApiVersions,
   792  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   793  			{
   794  				Name:       plugins[0],
   795  				PluginType: base.PluginTypeDevice,
   796  			}: {
   797  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
   798  				Config: map[string]interface{}{
   799  					"foo":          "1",
   800  					"bar":          "2",
   801  					"non-existent": "3",
   802  				},
   803  			},
   804  		},
   805  	}
   806  
   807  	_, err := NewPluginLoader(lconfig)
   808  	require.Error(err)
   809  	require.Contains(err.Error(), "No argument or block type is named \"non-existent\"")
   810  }
   811  
   812  func TestPluginLoader_InternalOverrideExternal(t *testing.T) {
   813  	t.Parallel()
   814  	require := require.New(t)
   815  
   816  	// Create two plugins
   817  	plugins := []string{"mock-device"}
   818  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   819  	pluginApiVersions := []string{device.ApiVersion010}
   820  
   821  	h := newHarness(t, plugins)
   822  	defer h.cleanup()
   823  
   824  	logger := testlog.HCLogger(t)
   825  	logger.SetLevel(log.Trace)
   826  	lconfig := &PluginLoaderConfig{
   827  		Logger:            logger,
   828  		PluginDir:         h.pluginDir(),
   829  		SupportedVersions: supportedApiVersions,
   830  		Configs: []*config.PluginConfig{
   831  			{
   832  				Name: plugins[0],
   833  				Args: []string{"-plugin", "-name", plugins[0],
   834  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", pluginApiVersions[0]},
   835  			},
   836  		},
   837  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   838  			{
   839  				Name:       plugins[0],
   840  				PluginType: base.PluginTypeDevice,
   841  			}: {
   842  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
   843  			},
   844  		},
   845  	}
   846  
   847  	l, err := NewPluginLoader(lconfig)
   848  	require.NoError(err)
   849  
   850  	// Get the catalog and assert we have the two plugins
   851  	c := l.Catalog()
   852  	require.Len(c, 1)
   853  	require.Contains(c, base.PluginTypeDevice)
   854  	detected := c[base.PluginTypeDevice]
   855  	require.Len(detected, 1)
   856  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   857  
   858  	expected := []*base.PluginInfoResponse{
   859  		{
   860  			Name:              plugins[0],
   861  			Type:              base.PluginTypeDevice,
   862  			PluginVersion:     pluginVersions[1],
   863  			PluginApiVersions: []string{device.ApiVersion010},
   864  		},
   865  	}
   866  	require.EqualValues(expected, detected)
   867  }
   868  
   869  func TestPluginLoader_ExternalOverrideInternal(t *testing.T) {
   870  	t.Parallel()
   871  	require := require.New(t)
   872  
   873  	// Create two plugins
   874  	plugins := []string{"mock-device"}
   875  	pluginVersions := []string{"v0.0.1", "v0.0.2"}
   876  	pluginApiVersions := []string{device.ApiVersion010}
   877  
   878  	h := newHarness(t, plugins)
   879  	defer h.cleanup()
   880  
   881  	logger := testlog.HCLogger(t)
   882  	logger.SetLevel(log.Trace)
   883  	lconfig := &PluginLoaderConfig{
   884  		Logger:            logger,
   885  		PluginDir:         h.pluginDir(),
   886  		SupportedVersions: supportedApiVersions,
   887  		Configs: []*config.PluginConfig{
   888  			{
   889  				Name: plugins[0],
   890  				Args: []string{"-plugin", "-name", plugins[0],
   891  					"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", pluginApiVersions[0]},
   892  			},
   893  		},
   894  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   895  			{
   896  				Name:       plugins[0],
   897  				PluginType: base.PluginTypeDevice,
   898  			}: {
   899  				Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
   900  			},
   901  		},
   902  	}
   903  
   904  	l, err := NewPluginLoader(lconfig)
   905  	require.NoError(err)
   906  
   907  	// Get the catalog and assert we have the two plugins
   908  	c := l.Catalog()
   909  	require.Len(c, 1)
   910  	require.Contains(c, base.PluginTypeDevice)
   911  	detected := c[base.PluginTypeDevice]
   912  	require.Len(detected, 1)
   913  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
   914  
   915  	expected := []*base.PluginInfoResponse{
   916  		{
   917  			Name:              plugins[0],
   918  			Type:              base.PluginTypeDevice,
   919  			PluginVersion:     pluginVersions[1],
   920  			PluginApiVersions: []string{device.ApiVersion010},
   921  		},
   922  	}
   923  	require.EqualValues(expected, detected)
   924  }
   925  
   926  func TestPluginLoader_Dispense_External(t *testing.T) {
   927  	t.Parallel()
   928  	require := require.New(t)
   929  
   930  	// Create two plugins
   931  	plugin := "mock-device"
   932  	pluginVersion := "v0.0.1"
   933  	h := newHarness(t, []string{plugin})
   934  	defer h.cleanup()
   935  
   936  	expKey := "set_config_worked"
   937  
   938  	logger := testlog.HCLogger(t)
   939  	logger.SetLevel(log.Trace)
   940  	lconfig := &PluginLoaderConfig{
   941  		Logger:            logger,
   942  		PluginDir:         h.pluginDir(),
   943  		SupportedVersions: supportedApiVersions,
   944  		Configs: []*config.PluginConfig{
   945  			{
   946  				Name: plugin,
   947  				Args: []string{"-plugin", "-name", plugin,
   948  					"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
   949  				Config: map[string]interface{}{
   950  					"res_key": expKey,
   951  				},
   952  			},
   953  		},
   954  	}
   955  
   956  	l, err := NewPluginLoader(lconfig)
   957  	require.NoError(err)
   958  
   959  	// Dispense a device plugin
   960  	p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
   961  	require.NoError(err)
   962  	defer p.Kill()
   963  
   964  	instance, ok := p.Plugin().(device.DevicePlugin)
   965  	require.True(ok)
   966  
   967  	res, err := instance.Reserve([]string{"fake"})
   968  	require.NoError(err)
   969  	require.NotNil(res)
   970  	require.Contains(res.Envs, expKey)
   971  }
   972  
   973  func TestPluginLoader_Dispense_Internal(t *testing.T) {
   974  	t.Parallel()
   975  	require := require.New(t)
   976  
   977  	// Create two plugins
   978  	plugin := "mock-device"
   979  	pluginVersion := "v0.0.1"
   980  	pluginApiVersions := []string{device.ApiVersion010}
   981  	h := newHarness(t, nil)
   982  	defer h.cleanup()
   983  
   984  	expKey := "set_config_worked"
   985  	expNomadConfig := &base.AgentConfig{
   986  		Driver: &base.ClientDriverConfig{
   987  			ClientMinPort: 100,
   988  		},
   989  	}
   990  
   991  	logger := testlog.HCLogger(t)
   992  	logger.SetLevel(log.Trace)
   993  	lconfig := &PluginLoaderConfig{
   994  		Logger:            logger,
   995  		PluginDir:         h.pluginDir(),
   996  		SupportedVersions: supportedApiVersions,
   997  		InternalPlugins: map[PluginID]*InternalPluginConfig{
   998  			{
   999  				Name:       plugin,
  1000  				PluginType: base.PluginTypeDevice,
  1001  			}: {
  1002  				Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true),
  1003  				Config: map[string]interface{}{
  1004  					"res_key": expKey,
  1005  				},
  1006  			},
  1007  		},
  1008  	}
  1009  
  1010  	l, err := NewPluginLoader(lconfig)
  1011  	require.NoError(err)
  1012  
  1013  	// Dispense a device plugin
  1014  	p, err := l.Dispense(plugin, base.PluginTypeDevice, expNomadConfig, logger)
  1015  	require.NoError(err)
  1016  	defer p.Kill()
  1017  
  1018  	instance, ok := p.Plugin().(device.DevicePlugin)
  1019  	require.True(ok)
  1020  
  1021  	res, err := instance.Reserve([]string{"fake"})
  1022  	require.NoError(err)
  1023  	require.NotNil(res)
  1024  	require.Contains(res.Envs, expKey)
  1025  
  1026  	mock, ok := p.Plugin().(*mockPlugin)
  1027  	require.True(ok)
  1028  	require.Exactly(expNomadConfig, mock.nomadConfig)
  1029  	require.Equal(device.ApiVersion010, mock.negotiatedApiVersion)
  1030  }
  1031  
  1032  func TestPluginLoader_Dispense_NoConfigSchema_External(t *testing.T) {
  1033  	t.Parallel()
  1034  	require := require.New(t)
  1035  
  1036  	// Create two plugins
  1037  	plugin := "mock-device"
  1038  	pluginVersion := "v0.0.1"
  1039  	h := newHarness(t, []string{plugin})
  1040  	defer h.cleanup()
  1041  
  1042  	expKey := "set_config_worked"
  1043  
  1044  	logger := testlog.HCLogger(t)
  1045  	logger.SetLevel(log.Trace)
  1046  	lconfig := &PluginLoaderConfig{
  1047  		Logger:            logger,
  1048  		PluginDir:         h.pluginDir(),
  1049  		SupportedVersions: supportedApiVersions,
  1050  		Configs: []*config.PluginConfig{
  1051  			{
  1052  				Name: plugin,
  1053  				Args: []string{"-plugin", "-config-schema=false", "-name", plugin,
  1054  					"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
  1055  				Config: map[string]interface{}{
  1056  					"res_key": expKey,
  1057  				},
  1058  			},
  1059  		},
  1060  	}
  1061  
  1062  	_, err := NewPluginLoader(lconfig)
  1063  	require.Error(err)
  1064  	require.Contains(err.Error(), "configuration not allowed")
  1065  
  1066  	// Remove the config and try again
  1067  	lconfig.Configs[0].Config = nil
  1068  	l, err := NewPluginLoader(lconfig)
  1069  	require.NoError(err)
  1070  
  1071  	// Dispense a device plugin
  1072  	p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
  1073  	require.NoError(err)
  1074  	defer p.Kill()
  1075  
  1076  	_, ok := p.Plugin().(device.DevicePlugin)
  1077  	require.True(ok)
  1078  }
  1079  
  1080  func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) {
  1081  	t.Parallel()
  1082  	require := require.New(t)
  1083  
  1084  	// Create two plugins
  1085  	plugin := "mock-device"
  1086  	pluginVersion := "v0.0.1"
  1087  	pluginApiVersions := []string{device.ApiVersion010}
  1088  	h := newHarness(t, nil)
  1089  	defer h.cleanup()
  1090  
  1091  	expKey := "set_config_worked"
  1092  
  1093  	logger := testlog.HCLogger(t)
  1094  	logger.SetLevel(log.Trace)
  1095  	pid := PluginID{
  1096  		Name:       plugin,
  1097  		PluginType: base.PluginTypeDevice,
  1098  	}
  1099  	lconfig := &PluginLoaderConfig{
  1100  		Logger:            logger,
  1101  		PluginDir:         h.pluginDir(),
  1102  		SupportedVersions: supportedApiVersions,
  1103  		InternalPlugins: map[PluginID]*InternalPluginConfig{
  1104  			pid: {
  1105  				Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, false),
  1106  				Config: map[string]interface{}{
  1107  					"res_key": expKey,
  1108  				},
  1109  			},
  1110  		},
  1111  	}
  1112  
  1113  	_, err := NewPluginLoader(lconfig)
  1114  	require.Error(err)
  1115  	require.Contains(err.Error(), "configuration not allowed")
  1116  
  1117  	// Remove the config and try again
  1118  	lconfig.InternalPlugins[pid].Factory = mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true)
  1119  	l, err := NewPluginLoader(lconfig)
  1120  	require.NoError(err)
  1121  
  1122  	// Dispense a device plugin
  1123  	p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
  1124  	require.NoError(err)
  1125  	defer p.Kill()
  1126  
  1127  	_, ok := p.Plugin().(device.DevicePlugin)
  1128  	require.True(ok)
  1129  }
  1130  
  1131  func TestPluginLoader_Reattach_External(t *testing.T) {
  1132  	t.Parallel()
  1133  	require := require.New(t)
  1134  
  1135  	// Create a plugin
  1136  	plugin := "mock-device"
  1137  	pluginVersion := "v0.0.1"
  1138  	h := newHarness(t, []string{plugin})
  1139  	defer h.cleanup()
  1140  
  1141  	expKey := "set_config_worked"
  1142  
  1143  	logger := testlog.HCLogger(t)
  1144  	logger.SetLevel(log.Trace)
  1145  	lconfig := &PluginLoaderConfig{
  1146  		Logger:            logger,
  1147  		PluginDir:         h.pluginDir(),
  1148  		SupportedVersions: supportedApiVersions,
  1149  		Configs: []*config.PluginConfig{
  1150  			{
  1151  				Name: plugin,
  1152  				Args: []string{"-plugin", "-name", plugin,
  1153  					"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
  1154  				Config: map[string]interface{}{
  1155  					"res_key": expKey,
  1156  				},
  1157  			},
  1158  		},
  1159  	}
  1160  
  1161  	l, err := NewPluginLoader(lconfig)
  1162  	require.NoError(err)
  1163  
  1164  	// Dispense a device plugin
  1165  	p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
  1166  	require.NoError(err)
  1167  	defer p.Kill()
  1168  
  1169  	instance, ok := p.Plugin().(device.DevicePlugin)
  1170  	require.True(ok)
  1171  
  1172  	res, err := instance.Reserve([]string{"fake"})
  1173  	require.NoError(err)
  1174  	require.NotNil(res)
  1175  	require.Contains(res.Envs, expKey)
  1176  
  1177  	// Reattach to the plugin
  1178  	reattach, ok := p.ReattachConfig()
  1179  	require.True(ok)
  1180  
  1181  	p2, err := l.Reattach(plugin, base.PluginTypeDevice, reattach)
  1182  	require.NoError(err)
  1183  
  1184  	// Get the reattached plugin and ensure its the same
  1185  	instance2, ok := p2.Plugin().(device.DevicePlugin)
  1186  	require.True(ok)
  1187  
  1188  	res2, err := instance2.Reserve([]string{"fake"})
  1189  	require.NoError(err)
  1190  	require.NotNil(res2)
  1191  	require.Contains(res2.Envs, expKey)
  1192  }
  1193  
  1194  // Test the loader trying to launch a non-plugin binary
  1195  func TestPluginLoader_Bad_Executable(t *testing.T) {
  1196  	t.Parallel()
  1197  	require := require.New(t)
  1198  
  1199  	// Create a plugin
  1200  	plugin := "mock-device"
  1201  	h := newHarness(t, []string{plugin})
  1202  	defer h.cleanup()
  1203  
  1204  	logger := testlog.HCLogger(t)
  1205  	logger.SetLevel(log.Trace)
  1206  	lconfig := &PluginLoaderConfig{
  1207  		Logger:            logger,
  1208  		PluginDir:         h.pluginDir(),
  1209  		SupportedVersions: supportedApiVersions,
  1210  		Configs: []*config.PluginConfig{
  1211  			{
  1212  				Name: plugin,
  1213  				Args: []string{"-bad-flag"},
  1214  			},
  1215  		},
  1216  	}
  1217  
  1218  	_, err := NewPluginLoader(lconfig)
  1219  	require.Error(err)
  1220  	require.Contains(err.Error(), "failed to fingerprint plugin")
  1221  }
  1222  
  1223  // Test that we skip directories, non-executables and follow symlinks
  1224  func TestPluginLoader_External_SkipBadFiles(t *testing.T) {
  1225  	if runtime.GOOS == "windows" {
  1226  		t.Skip("Windows currently does not skip non exe files")
  1227  	}
  1228  	t.Parallel()
  1229  	require := require.New(t)
  1230  
  1231  	// Create two plugins
  1232  	plugins := []string{"mock-device"}
  1233  	pluginVersions := []string{"v0.0.1"}
  1234  	h := newHarness(t, nil)
  1235  	defer h.cleanup()
  1236  
  1237  	// Create a folder inside our plugin dir
  1238  	require.NoError(os.Mkdir(filepath.Join(h.pluginDir(), "folder"), 0666))
  1239  
  1240  	// Get our own executable path
  1241  	selfExe, err := os.Executable()
  1242  	require.NoError(err)
  1243  
  1244  	// Create a symlink from our own binary to the directory
  1245  	require.NoError(os.Symlink(selfExe, filepath.Join(h.pluginDir(), plugins[0])))
  1246  
  1247  	// Create a non-executable file
  1248  	require.NoError(ioutil.WriteFile(filepath.Join(h.pluginDir(), "some.yaml"), []byte("hcl > yaml"), 0666))
  1249  
  1250  	logger := testlog.HCLogger(t)
  1251  	logger.SetLevel(log.Trace)
  1252  	lconfig := &PluginLoaderConfig{
  1253  		Logger:            logger,
  1254  		PluginDir:         h.pluginDir(),
  1255  		SupportedVersions: supportedApiVersions,
  1256  		Configs: []*config.PluginConfig{
  1257  			{
  1258  				Name: plugins[0],
  1259  				Args: []string{"-plugin", "-name", plugins[0],
  1260  					"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
  1261  			},
  1262  		},
  1263  	}
  1264  
  1265  	l, err := NewPluginLoader(lconfig)
  1266  	require.NoError(err)
  1267  
  1268  	// Get the catalog and assert we have the two plugins
  1269  	c := l.Catalog()
  1270  	require.Len(c, 1)
  1271  	require.Contains(c, base.PluginTypeDevice)
  1272  	detected := c[base.PluginTypeDevice]
  1273  	require.Len(detected, 1)
  1274  	sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
  1275  
  1276  	expected := []*base.PluginInfoResponse{
  1277  		{
  1278  			Name:              plugins[0],
  1279  			Type:              base.PluginTypeDevice,
  1280  			PluginVersion:     pluginVersions[0],
  1281  			PluginApiVersions: []string{device.ApiVersion010},
  1282  		},
  1283  	}
  1284  	require.EqualValues(expected, detected)
  1285  }
  1286  
  1287  func TestPluginLoader_ConvertVersions(t *testing.T) {
  1288  	v010 := version.Must(version.NewVersion("v0.1.0"))
  1289  	v020 := version.Must(version.NewVersion("v0.2.0"))
  1290  	v021 := version.Must(version.NewVersion("v0.2.1"))
  1291  	v030 := version.Must(version.NewVersion("v0.3.0"))
  1292  
  1293  	cases := []struct {
  1294  		in  []string
  1295  		out []*version.Version
  1296  		err bool
  1297  	}{
  1298  		{
  1299  			in:  []string{"v0.1.0", "0.2.0", "v0.2.1"},
  1300  			out: []*version.Version{v021, v020, v010},
  1301  		},
  1302  		{
  1303  			in:  []string{"0.3.0", "v0.1.0", "0.2.0", "v0.2.1"},
  1304  			out: []*version.Version{v030, v021, v020, v010},
  1305  		},
  1306  		{
  1307  			in:  []string{"foo", "v0.1.0", "0.2.0", "v0.2.1"},
  1308  			err: true,
  1309  		},
  1310  	}
  1311  
  1312  	for _, c := range cases {
  1313  		t.Run(strings.Join(c.in, ","), func(t *testing.T) {
  1314  			act, err := convertVersions(c.in)
  1315  			if err != nil {
  1316  				if c.err {
  1317  					return
  1318  				}
  1319  				t.Fatalf("unexpected err: %v", err)
  1320  			}
  1321  			require.Len(t, act, len(c.out))
  1322  			for i, v := range act {
  1323  				if !v.Equal(c.out[i]) {
  1324  					t.Fatalf("parsed version[%d] not equal: %v != %v", i, v, c.out[i])
  1325  				}
  1326  			}
  1327  		})
  1328  	}
  1329  }