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

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