open-nomad/helper/pluginutils/loader/loader_test.go

1333 lines
35 KiB
Go

package loader
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
log "github.com/hashicorp/go-hclog"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/device"
"github.com/stretchr/testify/require"
)
var (
// supportedApiVersions is the set of api versions that the "client" can
// support
supportedApiVersions = map[string][]string{
base.PluginTypeDevice: {device.ApiVersion010},
}
)
// harness is used to build a temp directory and copy our own test executable
// into it, allowing the plugin loader to scan for plugins.
type harness struct {
t *testing.T
tmpDir string
}
// newHarness returns a harness and copies our test binary to the temp directory
// with the passed plugin names.
func newHarness(t *testing.T, plugins []string) *harness {
t.Helper()
h := &harness{
t: t,
}
// Build a temp directory
path, err := ioutil.TempDir("", t.Name())
if err != nil {
t.Fatalf("failed to build tmp directory")
}
h.tmpDir = path
// Get our own executable path
selfExe, err := os.Executable()
if err != nil {
t.Fatalf("failed to get self executable path: %v", err)
}
exeSuffix := ""
if runtime.GOOS == "windows" {
exeSuffix = ".exe"
}
for _, p := range plugins {
dest := filepath.Join(h.tmpDir, p) + exeSuffix
if err := copyFile(selfExe, dest); err != nil {
t.Fatalf("failed to copy file: %v", err)
}
}
return h
}
// copyFile copies the src file to dst.
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err = io.Copy(out, in); err != nil {
return err
}
if err := out.Close(); err != nil {
return err
}
return os.Chmod(dst, 0777)
}
// pluginDir returns the plugin directory.
func (h *harness) pluginDir() string {
return h.tmpDir
}
// cleanup removes the temp directory
func (h *harness) cleanup() {
if err := os.RemoveAll(h.tmpDir); err != nil {
h.t.Fatalf("failed to remove tmp directory %q: %v", h.tmpDir, err)
}
}
func TestPluginLoader_External(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device", "mock-device-2"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0],
"-api-version", device.ApiVersion010},
},
{
Name: plugins[1],
Args: []string{"-plugin", "-name", plugins[1],
"-type", base.PluginTypeDevice, "-version", pluginVersions[1],
"-api-version", device.ApiVersion010, "-api-version", "v0.2.0"},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 2)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[0],
PluginApiVersions: []string{"v0.1.0"},
},
{
Name: plugins[1],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{"v0.1.0", "v0.2.0"},
},
}
require.EqualValues(expected, detected)
}
func TestPluginLoader_External_ApiVersions(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device", "mock-device-2", "mock-device-3"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: map[string][]string{
base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"},
},
Configs: []*config.PluginConfig{
{
// No supporting version
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0],
"-api-version", "v0.1.0"},
},
{
// Pick highest matching
Name: plugins[1],
Args: []string{"-plugin", "-name", plugins[1],
"-type", base.PluginTypeDevice, "-version", pluginVersions[1],
"-api-version", "v0.1.0",
"-api-version", "v0.2.0",
"-api-version", "v0.2.1",
"-api-version", "v0.2.2",
},
},
{
// Pick highest matching
Name: plugins[2],
Args: []string{"-plugin", "-name", plugins[2],
"-type", base.PluginTypeDevice, "-version", pluginVersions[1],
"-api-version", "v0.1.0",
"-api-version", "v0.2.0",
"-api-version", "v0.2.1",
"-api-version", "v0.3.0",
},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 2)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[1],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"},
},
{
Name: plugins[2],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"},
},
}
require.EqualValues(expected, detected)
// Test we chose the correct versions by dispensing and checking and then
// reattaching and checking
p1, err := l.Dispense(plugins[1], base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p1.Kill()
require.Equal("v0.2.1", p1.ApiVersion())
p2, err := l.Dispense(plugins[2], base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p2.Kill()
require.Equal("v0.3.0", p2.ApiVersion())
// Test reattach api versions
rc1, ok := p1.ReattachConfig()
require.True(ok)
r1, err := l.Reattach(plugins[1], base.PluginTypeDriver, rc1)
require.NoError(err)
require.Equal("v0.2.1", r1.ApiVersion())
rc2, ok := p2.ReattachConfig()
require.True(ok)
r2, err := l.Reattach(plugins[2], base.PluginTypeDriver, rc2)
require.NoError(err)
require.Equal("v0.3.0", r2.ApiVersion())
}
func TestPluginLoader_External_NoApiVersion(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0]},
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "no compatible API versions")
}
func TestPluginLoader_External_Config(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device", "mock-device-2"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
Config: map[string]interface{}{
"foo": "1",
"bar": "2",
},
},
{
Name: plugins[1],
Args: []string{"-plugin", "-name", plugins[1],
"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010},
Config: map[string]interface{}{
"foo": "3",
"bar": "4",
},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 2)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[0],
PluginApiVersions: []string{device.ApiVersion010},
},
{
Name: plugins[1],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
// Pass a config but make sure it is fatal
func TestPluginLoader_External_Config_Bad(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create a plugin
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1"}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
Config: map[string]interface{}{
"foo": "1",
"bar": "2",
"non-existent": "3",
},
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "No argument or block type is named \"non-existent\"")
}
func TestPluginLoader_External_VersionOverlap(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device", "mock-device-2"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
},
{
Name: plugins[1],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 1)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
func TestPluginLoader_Internal(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create the harness
h := newHarness(t, nil)
defer h.cleanup()
plugins := []string{"mock-device", "mock-device-2"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
pluginApiVersions := []string{device.ApiVersion010}
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
},
{
Name: plugins[1],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 2)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[0],
PluginApiVersions: []string{device.ApiVersion010},
},
{
Name: plugins[1],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
func TestPluginLoader_Internal_ApiVersions(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device", "mock-device-2", "mock-device-3"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, nil)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: map[string][]string{
base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"},
},
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], []string{"v0.1.0"}, true),
},
{
Name: plugins[1],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1],
[]string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"}, true),
},
{
Name: plugins[2],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[2], base.PluginTypeDevice, pluginVersions[1],
[]string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"}, true),
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 2)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[1],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"},
},
{
Name: plugins[2],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"},
},
}
require.EqualValues(expected, detected)
// Test we chose the correct versions by dispensing and checking and then
// reattaching and checking
p1, err := l.Dispense(plugins[1], base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p1.Kill()
require.Equal("v0.2.1", p1.ApiVersion())
p2, err := l.Dispense(plugins[2], base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p2.Kill()
require.Equal("v0.3.0", p2.ApiVersion())
}
func TestPluginLoader_Internal_NoApiVersion(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
h := newHarness(t, nil)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], nil, true),
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "no compatible API versions")
}
func TestPluginLoader_Internal_Config(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create the harness
h := newHarness(t, nil)
defer h.cleanup()
plugins := []string{"mock-device", "mock-device-2"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
pluginApiVersions := []string{device.ApiVersion010}
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
Config: map[string]interface{}{
"foo": "1",
"bar": "2",
},
},
{
Name: plugins[1],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
Config: map[string]interface{}{
"foo": "3",
"bar": "4",
},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 2)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[0],
PluginApiVersions: []string{device.ApiVersion010},
},
{
Name: plugins[1],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
// Tests that an external config can override the config of an internal plugin
func TestPluginLoader_Internal_ExternalConfig(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create the harness
h := newHarness(t, nil)
defer h.cleanup()
plugin := "mock-device"
pluginVersion := "v0.0.1"
pluginApiVersions := []string{device.ApiVersion010}
id := PluginID{
Name: plugin,
PluginType: base.PluginTypeDevice,
}
expectedConfig := map[string]interface{}{
"foo": "2",
"bar": "3",
}
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
id: {
Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true),
Config: map[string]interface{}{
"foo": "1",
"bar": "2",
},
},
},
Configs: []*config.PluginConfig{
{
Name: plugin,
Config: expectedConfig,
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 1)
expected := []*base.PluginInfoResponse{
{
Name: plugin,
Type: base.PluginTypeDevice,
PluginVersion: pluginVersion,
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
// Check the config
loaded, ok := l.plugins[id]
require.True(ok)
require.EqualValues(expectedConfig, loaded.config)
}
// Pass a config but make sure it is fatal
func TestPluginLoader_Internal_Config_Bad(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create the harness
h := newHarness(t, nil)
defer h.cleanup()
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1"}
pluginApiVersions := []string{device.ApiVersion010}
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
Config: map[string]interface{}{
"foo": "1",
"bar": "2",
"non-existent": "3",
},
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "No argument or block type is named \"non-existent\"")
}
func TestPluginLoader_InternalOverrideExternal(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
pluginApiVersions := []string{device.ApiVersion010}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", pluginApiVersions[0]},
},
},
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 1)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
func TestPluginLoader_ExternalOverrideInternal(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1", "v0.0.2"}
pluginApiVersions := []string{device.ApiVersion010}
h := newHarness(t, plugins)
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", pluginApiVersions[0]},
},
},
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugins[0],
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 1)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[1],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
func TestPluginLoader_Dispense_External(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugin := "mock-device"
pluginVersion := "v0.0.1"
h := newHarness(t, []string{plugin})
defer h.cleanup()
expKey := "set_config_worked"
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugin,
Args: []string{"-plugin", "-name", plugin,
"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
Config: map[string]interface{}{
"res_key": expKey,
},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Dispense a device plugin
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p.Kill()
instance, ok := p.Plugin().(device.DevicePlugin)
require.True(ok)
res, err := instance.Reserve([]string{"fake"})
require.NoError(err)
require.NotNil(res)
require.Contains(res.Envs, expKey)
}
func TestPluginLoader_Dispense_Internal(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugin := "mock-device"
pluginVersion := "v0.0.1"
pluginApiVersions := []string{device.ApiVersion010}
h := newHarness(t, nil)
defer h.cleanup()
expKey := "set_config_worked"
expNomadConfig := &base.AgentConfig{
Driver: &base.ClientDriverConfig{
ClientMinPort: 100,
},
}
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
{
Name: plugin,
PluginType: base.PluginTypeDevice,
}: {
Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true),
Config: map[string]interface{}{
"res_key": expKey,
},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Dispense a device plugin
p, err := l.Dispense(plugin, base.PluginTypeDevice, expNomadConfig, logger)
require.NoError(err)
defer p.Kill()
instance, ok := p.Plugin().(device.DevicePlugin)
require.True(ok)
res, err := instance.Reserve([]string{"fake"})
require.NoError(err)
require.NotNil(res)
require.Contains(res.Envs, expKey)
mock, ok := p.Plugin().(*mockPlugin)
require.True(ok)
require.Exactly(expNomadConfig, mock.nomadConfig)
require.Equal(device.ApiVersion010, mock.negotiatedApiVersion)
}
func TestPluginLoader_Dispense_NoConfigSchema_External(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugin := "mock-device"
pluginVersion := "v0.0.1"
h := newHarness(t, []string{plugin})
defer h.cleanup()
expKey := "set_config_worked"
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugin,
Args: []string{"-plugin", "-config-schema=false", "-name", plugin,
"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
Config: map[string]interface{}{
"res_key": expKey,
},
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "configuration not allowed")
// Remove the config and try again
lconfig.Configs[0].Config = nil
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Dispense a device plugin
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p.Kill()
_, ok := p.Plugin().(device.DevicePlugin)
require.True(ok)
}
func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create two plugins
plugin := "mock-device"
pluginVersion := "v0.0.1"
pluginApiVersions := []string{device.ApiVersion010}
h := newHarness(t, nil)
defer h.cleanup()
expKey := "set_config_worked"
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
pid := PluginID{
Name: plugin,
PluginType: base.PluginTypeDevice,
}
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
InternalPlugins: map[PluginID]*InternalPluginConfig{
pid: {
Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, false),
Config: map[string]interface{}{
"res_key": expKey,
},
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "configuration not allowed")
// Remove the config and try again
lconfig.InternalPlugins[pid].Factory = mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true)
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Dispense a device plugin
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p.Kill()
_, ok := p.Plugin().(device.DevicePlugin)
require.True(ok)
}
func TestPluginLoader_Reattach_External(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create a plugin
plugin := "mock-device"
pluginVersion := "v0.0.1"
h := newHarness(t, []string{plugin})
defer h.cleanup()
expKey := "set_config_worked"
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugin,
Args: []string{"-plugin", "-name", plugin,
"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
Config: map[string]interface{}{
"res_key": expKey,
},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Dispense a device plugin
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
require.NoError(err)
defer p.Kill()
instance, ok := p.Plugin().(device.DevicePlugin)
require.True(ok)
res, err := instance.Reserve([]string{"fake"})
require.NoError(err)
require.NotNil(res)
require.Contains(res.Envs, expKey)
// Reattach to the plugin
reattach, ok := p.ReattachConfig()
require.True(ok)
p2, err := l.Reattach(plugin, base.PluginTypeDevice, reattach)
require.NoError(err)
// Get the reattached plugin and ensure its the same
instance2, ok := p2.Plugin().(device.DevicePlugin)
require.True(ok)
res2, err := instance2.Reserve([]string{"fake"})
require.NoError(err)
require.NotNil(res2)
require.Contains(res2.Envs, expKey)
}
// Test the loader trying to launch a non-plugin binary
func TestPluginLoader_Bad_Executable(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
// Create a plugin
plugin := "mock-device"
h := newHarness(t, []string{plugin})
defer h.cleanup()
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugin,
Args: []string{"-bad-flag"},
},
},
}
_, err := NewPluginLoader(lconfig)
require.Error(err)
require.Contains(err.Error(), "failed to fingerprint plugin")
}
// Test that we skip directories, non-executables and follow symlinks
func TestPluginLoader_External_SkipBadFiles(t *testing.T) {
ci.Parallel(t)
if runtime.GOOS == "windows" {
t.Skip("Windows currently does not skip non exe files")
}
require := require.New(t)
// Create two plugins
plugins := []string{"mock-device"}
pluginVersions := []string{"v0.0.1"}
h := newHarness(t, nil)
defer h.cleanup()
// Create a folder inside our plugin dir
require.NoError(os.Mkdir(filepath.Join(h.pluginDir(), "folder"), 0666))
// Get our own executable path
selfExe, err := os.Executable()
require.NoError(err)
// Create a symlink from our own binary to the directory
require.NoError(os.Symlink(selfExe, filepath.Join(h.pluginDir(), plugins[0])))
// Create a non-executable file
require.NoError(ioutil.WriteFile(filepath.Join(h.pluginDir(), "some.yaml"), []byte("hcl > yaml"), 0666))
logger := testlog.HCLogger(t)
logger.SetLevel(log.Trace)
lconfig := &PluginLoaderConfig{
Logger: logger,
PluginDir: h.pluginDir(),
SupportedVersions: supportedApiVersions,
Configs: []*config.PluginConfig{
{
Name: plugins[0],
Args: []string{"-plugin", "-name", plugins[0],
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
},
},
}
l, err := NewPluginLoader(lconfig)
require.NoError(err)
// Get the catalog and assert we have the two plugins
c := l.Catalog()
require.Len(c, 1)
require.Contains(c, base.PluginTypeDevice)
detected := c[base.PluginTypeDevice]
require.Len(detected, 1)
sort.Slice(detected, func(i, j int) bool { return detected[i].Name < detected[j].Name })
expected := []*base.PluginInfoResponse{
{
Name: plugins[0],
Type: base.PluginTypeDevice,
PluginVersion: pluginVersions[0],
PluginApiVersions: []string{device.ApiVersion010},
},
}
require.EqualValues(expected, detected)
}
func TestPluginLoader_ConvertVersions(t *testing.T) {
ci.Parallel(t)
v010 := version.Must(version.NewVersion("v0.1.0"))
v020 := version.Must(version.NewVersion("v0.2.0"))
v021 := version.Must(version.NewVersion("v0.2.1"))
v030 := version.Must(version.NewVersion("v0.3.0"))
cases := []struct {
in []string
out []*version.Version
err bool
}{
{
in: []string{"v0.1.0", "0.2.0", "v0.2.1"},
out: []*version.Version{v021, v020, v010},
},
{
in: []string{"0.3.0", "v0.1.0", "0.2.0", "v0.2.1"},
out: []*version.Version{v030, v021, v020, v010},
},
{
in: []string{"foo", "v0.1.0", "0.2.0", "v0.2.1"},
err: true,
},
}
for _, c := range cases {
t.Run(strings.Join(c.in, ","), func(t *testing.T) {
act, err := convertVersions(c.in)
if err != nil {
if c.err {
return
}
t.Fatalf("unexpected err: %v", err)
}
require.Len(t, act, len(c.out))
for i, v := range act {
if !v.Equal(c.out[i]) {
t.Fatalf("parsed version[%d] not equal: %v != %v", i, v, c.out[i])
}
}
})
}
}