2018-09-07 23:58:40 +00:00
|
|
|
package loader
|
|
|
|
|
|
|
|
import (
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2019-01-10 15:24:29 +00:00
|
|
|
"runtime"
|
2018-09-07 23:58:40 +00:00
|
|
|
"sort"
|
2018-12-18 00:40:58 +00:00
|
|
|
"strings"
|
2018-09-07 23:58:40 +00:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
2018-12-18 00:40:58 +00:00
|
|
|
version "github.com/hashicorp/go-version"
|
2018-09-18 17:48:37 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
2018-09-07 23:58:40 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs/config"
|
|
|
|
"github.com/hashicorp/nomad/plugins/base"
|
|
|
|
"github.com/hashicorp/nomad/plugins/device"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
2018-12-18 00:40:58 +00:00
|
|
|
var (
|
|
|
|
// supportedApiVersions is the set of api versions that the "client" can
|
|
|
|
// support
|
|
|
|
supportedApiVersions = map[string][]string{
|
2018-12-18 23:07:30 +00:00
|
|
|
base.PluginTypeDevice: {device.ApiVersion010},
|
2018-12-18 00:40:58 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2018-09-07 23:58:40 +00:00
|
|
|
// 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
|
2018-09-11 00:29:28 +00:00
|
|
|
// with the passed plugin names.
|
2018-09-07 23:58:40 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-01-10 09:59:04 +00:00
|
|
|
exeSuffix := ""
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
exeSuffix = ".exe"
|
|
|
|
}
|
2018-09-07 23:58:40 +00:00
|
|
|
for _, p := range plugins {
|
2019-01-16 14:06:39 +00:00
|
|
|
dest := filepath.Join(h.tmpDir, p) + exeSuffix
|
2018-09-07 23:58:40 +00:00
|
|
|
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) {
|
|
|
|
t.Parallel()
|
|
|
|
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()
|
|
|
|
|
2018-12-18 00:40:58 +00:00
|
|
|
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) {
|
|
|
|
t.Parallel()
|
|
|
|
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()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-09-18 17:48:37 +00:00
|
|
|
Logger: logger,
|
2018-09-07 23:58:40 +00:00
|
|
|
PluginDir: h.pluginDir(),
|
2018-12-18 00:40:58 +00:00
|
|
|
SupportedVersions: map[string][]string{
|
2018-12-18 23:07:30 +00:00
|
|
|
base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"},
|
2018-12-18 00:40:58 +00:00
|
|
|
},
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
// No supporting version
|
2018-09-07 23:58:40 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[0],
|
|
|
|
"-api-version", "v0.1.0"},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
// Pick highest matching
|
2018-09-07 23:58:40 +00:00
|
|
|
Name: plugins[1],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[1],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-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",
|
|
|
|
},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[1],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[2],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
2018-12-18 00:40:58 +00:00
|
|
|
|
|
|
|
// 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) {
|
|
|
|
t.Parallel()
|
|
|
|
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")
|
2018-09-07 23:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_External_Config(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
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()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"foo": "1",
|
|
|
|
"bar": "2",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: plugins[1],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[1],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[0],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[1],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pass a config but make sure it is fatal
|
|
|
|
func TestPluginLoader_External_Config_Bad(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
2019-01-18 19:58:26 +00:00
|
|
|
// Create a plugin
|
2018-09-07 23:58:40 +00:00
|
|
|
plugins := []string{"mock-device"}
|
|
|
|
pluginVersions := []string{"v0.0.1"}
|
|
|
|
h := newHarness(t, plugins)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"foo": "1",
|
|
|
|
"bar": "2",
|
2018-09-15 23:42:38 +00:00
|
|
|
"non-existent": "3",
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := NewPluginLoader(lconfig)
|
|
|
|
require.Error(err)
|
2018-09-15 23:42:38 +00:00
|
|
|
require.Contains(err.Error(), "No argument or block type is named \"non-existent\"")
|
2018-09-07 23:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_External_VersionOverlap(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create two plugins
|
2018-09-11 00:29:28 +00:00
|
|
|
plugins := []string{"mock-device", "mock-device-2"}
|
2018-09-07 23:58:40 +00:00
|
|
|
pluginVersions := []string{"v0.0.1", "v0.0.2"}
|
|
|
|
h := newHarness(t, plugins)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: plugins[1],
|
2018-09-11 00:29:28 +00:00
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_Internal(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
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"}
|
2018-12-18 00:40:58 +00:00
|
|
|
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) {
|
|
|
|
t.Parallel()
|
|
|
|
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()
|
2018-09-07 23:58:40 +00:00
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-09-18 17:48:37 +00:00
|
|
|
Logger: logger,
|
2018-09-07 23:58:40 +00:00
|
|
|
PluginDir: h.pluginDir(),
|
2018-12-18 00:40:58 +00:00
|
|
|
SupportedVersions: map[string][]string{
|
2018-12-18 23:07:30 +00:00
|
|
|
base.PluginTypeDevice: {"0.2.0", "0.2.1", "0.3.0"},
|
2018-12-18 00:40:58 +00:00
|
|
|
},
|
2018-09-07 23:58:40 +00:00
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], []string{"v0.1.0"}, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: plugins[1],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
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),
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[1],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.2.2"},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[2],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{"v0.1.0", "v0.2.0", "v0.2.1", "v0.3.0"},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
2018-12-18 00:40:58 +00:00
|
|
|
|
|
|
|
// 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) {
|
|
|
|
t.Parallel()
|
|
|
|
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")
|
2018-09-07 23:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_Internal_Config(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
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"}
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
2018-09-07 23:58:40 +00:00
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"foo": "1",
|
|
|
|
"bar": "2",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: plugins[1],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[0],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[1],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
}
|
|
|
|
|
2018-09-26 20:39:09 +00:00
|
|
|
// Tests that an external config can override the config of an internal plugin
|
|
|
|
func TestPluginLoader_Internal_ExternalConfig(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create the harness
|
|
|
|
h := newHarness(t, nil)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
|
|
|
plugin := "mock-device"
|
|
|
|
pluginVersion := "v0.0.1"
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
2018-09-26 20:39:09 +00:00
|
|
|
|
|
|
|
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{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-26 20:39:09 +00:00
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
id: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true),
|
2018-09-26 20:39:09 +00:00
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugin,
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersion,
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-26 20:39:09 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
|
|
|
|
// Check the config
|
|
|
|
loaded, ok := l.plugins[id]
|
|
|
|
require.True(ok)
|
|
|
|
require.EqualValues(expectedConfig, loaded.config)
|
|
|
|
}
|
|
|
|
|
2018-09-07 23:58:40 +00:00
|
|
|
// Pass a config but make sure it is fatal
|
|
|
|
func TestPluginLoader_Internal_Config_Bad(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create the harness
|
|
|
|
h := newHarness(t, nil)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
|
|
|
plugins := []string{"mock-device"}
|
|
|
|
pluginVersions := []string{"v0.0.1"}
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
2018-09-07 23:58:40 +00:00
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"foo": "1",
|
|
|
|
"bar": "2",
|
2018-09-15 23:42:38 +00:00
|
|
|
"non-existent": "3",
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := NewPluginLoader(lconfig)
|
|
|
|
require.Error(err)
|
2018-09-15 23:42:38 +00:00
|
|
|
require.Contains(err.Error(), "No argument or block type is named \"non-existent\"")
|
2018-09-07 23:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_InternalOverrideExternal(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create two plugins
|
|
|
|
plugins := []string{"mock-device"}
|
|
|
|
pluginVersions := []string{"v0.0.1", "v0.0.2"}
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
|
|
|
|
2018-09-07 23:58:40 +00:00
|
|
|
h := newHarness(t, plugins)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", pluginApiVersions[0]},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_ExternalOverrideInternal(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create two plugins
|
|
|
|
plugins := []string{"mock-device"}
|
|
|
|
pluginVersions := []string{"v0.0.1", "v0.0.2"}
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
|
|
|
|
2018-09-07 23:58:40 +00:00
|
|
|
h := newHarness(t, plugins)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", pluginApiVersions[0]},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[1],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_Dispense_External(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
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"
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugin,
|
|
|
|
Args: []string{"-plugin", "-name", plugin,
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"res_key": expKey,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
l, err := NewPluginLoader(lconfig)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
// Dispense a device plugin
|
2018-10-17 02:21:15 +00:00
|
|
|
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
|
2018-09-07 23:58:40 +00:00
|
|
|
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) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create two plugins
|
|
|
|
plugin := "mock-device"
|
|
|
|
pluginVersion := "v0.0.1"
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
2018-09-07 23:58:40 +00:00
|
|
|
h := newHarness(t, nil)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
|
|
|
expKey := "set_config_worked"
|
2018-12-18 00:40:58 +00:00
|
|
|
expNomadConfig := &base.AgentConfig{
|
2018-10-30 01:34:34 +00:00
|
|
|
Driver: &base.ClientDriverConfig{
|
2018-10-19 03:31:01 +00:00
|
|
|
ClientMinPort: 100,
|
|
|
|
},
|
2018-10-17 02:41:39 +00:00
|
|
|
}
|
2018-09-07 23:58:40 +00:00
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugin,
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true),
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"res_key": expKey,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
l, err := NewPluginLoader(lconfig)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
// Dispense a device plugin
|
2018-10-17 02:41:39 +00:00
|
|
|
p, err := l.Dispense(plugin, base.PluginTypeDevice, expNomadConfig, logger)
|
2018-09-07 23:58:40 +00:00
|
|
|
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)
|
2018-10-17 02:41:39 +00:00
|
|
|
|
|
|
|
mock, ok := p.Plugin().(*mockPlugin)
|
|
|
|
require.True(ok)
|
|
|
|
require.Exactly(expNomadConfig, mock.nomadConfig)
|
2018-12-18 00:40:58 +00:00
|
|
|
require.Equal(device.ApiVersion010, mock.negotiatedApiVersion)
|
2018-09-07 23:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_Dispense_NoConfigSchema_External(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
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"
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugin,
|
|
|
|
Args: []string{"-plugin", "-config-schema=false", "-name", plugin,
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
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
|
2018-10-17 02:21:15 +00:00
|
|
|
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
|
2018-09-07 23:58:40 +00:00
|
|
|
require.NoError(err)
|
|
|
|
defer p.Kill()
|
|
|
|
|
|
|
|
_, ok := p.Plugin().(device.DevicePlugin)
|
|
|
|
require.True(ok)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create two plugins
|
|
|
|
plugin := "mock-device"
|
|
|
|
pluginVersion := "v0.0.1"
|
2018-12-18 00:40:58 +00:00
|
|
|
pluginApiVersions := []string{device.ApiVersion010}
|
2018-09-07 23:58:40 +00:00
|
|
|
h := newHarness(t, nil)
|
|
|
|
defer h.cleanup()
|
|
|
|
|
|
|
|
expKey := "set_config_worked"
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
pid := PluginID{
|
|
|
|
Name: plugin,
|
|
|
|
PluginType: base.PluginTypeDevice,
|
|
|
|
}
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
InternalPlugins: map[PluginID]*InternalPluginConfig{
|
|
|
|
pid: {
|
2018-12-18 00:40:58 +00:00
|
|
|
Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, false),
|
2018-09-07 23:58:40 +00:00
|
|
|
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
|
2018-12-18 00:40:58 +00:00
|
|
|
lconfig.InternalPlugins[pid].Factory = mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true)
|
2018-09-07 23:58:40 +00:00
|
|
|
l, err := NewPluginLoader(lconfig)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
// Dispense a device plugin
|
2018-10-17 02:21:15 +00:00
|
|
|
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
|
2018-09-07 23:58:40 +00:00
|
|
|
require.NoError(err)
|
|
|
|
defer p.Kill()
|
|
|
|
|
|
|
|
_, ok := p.Plugin().(device.DevicePlugin)
|
|
|
|
require.True(ok)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPluginLoader_Reattach_External(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
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"
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugin,
|
|
|
|
Args: []string{"-plugin", "-name", plugin,
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010},
|
2018-09-07 23:58:40 +00:00
|
|
|
Config: map[string]interface{}{
|
|
|
|
"res_key": expKey,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
l, err := NewPluginLoader(lconfig)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
// Dispense a device plugin
|
2018-10-17 02:21:15 +00:00
|
|
|
p, err := l.Dispense(plugin, base.PluginTypeDevice, nil, logger)
|
2018-09-07 23:58:40 +00:00
|
|
|
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)
|
|
|
|
|
2018-09-18 17:08:46 +00:00
|
|
|
p2, err := l.Reattach(plugin, base.PluginTypeDevice, reattach)
|
2018-09-07 23:58:40 +00:00
|
|
|
require.NoError(err)
|
|
|
|
|
2018-09-11 00:29:28 +00:00
|
|
|
// Get the reattached plugin and ensure its the same
|
2018-09-07 23:58:40 +00:00
|
|
|
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) {
|
|
|
|
t.Parallel()
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create a plugin
|
|
|
|
plugin := "mock-device"
|
|
|
|
h := newHarness(t, []string{plugin})
|
|
|
|
defer h.cleanup()
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-07 23:58:40 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-07 23:58:40 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugin,
|
|
|
|
Args: []string{"-bad-flag"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err := NewPluginLoader(lconfig)
|
|
|
|
require.Error(err)
|
|
|
|
require.Contains(err.Error(), "failed to fingerprint plugin")
|
|
|
|
}
|
2018-09-11 00:29:28 +00:00
|
|
|
|
|
|
|
// Test that we skip directories, non-executables and follow symlinks
|
|
|
|
func TestPluginLoader_External_SkipBadFiles(t *testing.T) {
|
2019-01-10 09:59:04 +00:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
t.Skip("Windows currently does not skip non exe files")
|
|
|
|
}
|
2018-09-11 00:29:28 +00:00
|
|
|
t.Parallel()
|
|
|
|
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))
|
|
|
|
|
2018-09-18 17:48:37 +00:00
|
|
|
logger := testlog.HCLogger(t)
|
2018-09-11 00:29:28 +00:00
|
|
|
logger.SetLevel(log.Trace)
|
|
|
|
lconfig := &PluginLoaderConfig{
|
2018-12-18 00:40:58 +00:00
|
|
|
Logger: logger,
|
|
|
|
PluginDir: h.pluginDir(),
|
|
|
|
SupportedVersions: supportedApiVersions,
|
2018-09-11 00:29:28 +00:00
|
|
|
Configs: []*config.PluginConfig{
|
|
|
|
{
|
|
|
|
Name: plugins[0],
|
|
|
|
Args: []string{"-plugin", "-name", plugins[0],
|
2018-12-18 00:40:58 +00:00
|
|
|
"-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010},
|
2018-09-11 00:29:28 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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{
|
|
|
|
{
|
2018-12-18 00:40:58 +00:00
|
|
|
Name: plugins[0],
|
|
|
|
Type: base.PluginTypeDevice,
|
|
|
|
PluginVersion: pluginVersions[0],
|
|
|
|
PluginApiVersions: []string{device.ApiVersion010},
|
2018-09-11 00:29:28 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
require.EqualValues(expected, detected)
|
|
|
|
}
|
2018-12-18 00:40:58 +00:00
|
|
|
|
|
|
|
func TestPluginLoader_ConvertVersions(t *testing.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])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|