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