2588b3bc98
This fixes few cases where driver eventor goroutines are leaked during normal operations, but especially so in tests. This change makes few modifications: First, it switches drivers to use `Context`s to manage shutdown events. Previously, it relied on callers invoking `.Shutdown()` function that is specific to internal drivers only and require casting. Using `Contexts` provide a consistent idiomatic way to manage lifecycle for both internal and external drivers. Also, I discovered few places where we don't clean up a temporary driver instance in the plugin catalog code, where we dispense a driver to inspect and validate the schema config without properly cleaning it up.
196 lines
5.2 KiB
Go
196 lines
5.2 KiB
Go
package loader
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
|
plugin "github.com/hashicorp/go-plugin"
|
|
"github.com/hashicorp/nomad/plugins/base"
|
|
"github.com/hashicorp/nomad/plugins/device"
|
|
"github.com/hashicorp/nomad/plugins/shared/hclspec"
|
|
)
|
|
|
|
type stringSliceFlags []string
|
|
|
|
func (i *stringSliceFlags) String() string {
|
|
return "my string representation"
|
|
}
|
|
|
|
func (i *stringSliceFlags) Set(value string) error {
|
|
*i = append(*i, value)
|
|
return nil
|
|
}
|
|
|
|
// TestMain runs either the tests or runs a mock plugin based on the passed
|
|
// flags
|
|
func TestMain(m *testing.M) {
|
|
var plugin, configSchema bool
|
|
var name, pluginType, pluginVersion string
|
|
var pluginApiVersions stringSliceFlags
|
|
flag.BoolVar(&plugin, "plugin", false, "run binary as a plugin")
|
|
flag.BoolVar(&configSchema, "config-schema", true, "return a config schema")
|
|
flag.StringVar(&name, "name", "", "plugin name")
|
|
flag.StringVar(&pluginType, "type", "", "plugin type")
|
|
flag.StringVar(&pluginVersion, "version", "", "plugin version")
|
|
flag.Var(&pluginApiVersions, "api-version", "supported plugin API version")
|
|
flag.Parse()
|
|
|
|
if plugin {
|
|
if err := pluginMain(name, pluginType, pluginVersion, pluginApiVersions, configSchema); err != nil {
|
|
fmt.Println(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
os.Exit(m.Run())
|
|
}
|
|
}
|
|
|
|
// pluginMain starts a mock plugin using the passed parameters
|
|
func pluginMain(name, pluginType, version string, apiVersions []string, config bool) error {
|
|
// Validate passed parameters
|
|
if name == "" || pluginType == "" {
|
|
return fmt.Errorf("name and plugin type must be specified")
|
|
}
|
|
|
|
switch pluginType {
|
|
case base.PluginTypeDevice:
|
|
default:
|
|
return fmt.Errorf("unsupported plugin type %q", pluginType)
|
|
}
|
|
|
|
// Create the mock plugin
|
|
m := &mockPlugin{
|
|
name: name,
|
|
ptype: pluginType,
|
|
version: version,
|
|
apiVersions: apiVersions,
|
|
configSchema: config,
|
|
}
|
|
|
|
// Build the plugin map
|
|
pmap := map[string]plugin.Plugin{
|
|
base.PluginTypeBase: &base.PluginBase{Impl: m},
|
|
}
|
|
switch pluginType {
|
|
case base.PluginTypeDevice:
|
|
pmap[base.PluginTypeDevice] = &device.PluginDevice{Impl: m}
|
|
}
|
|
|
|
// Serve the plugin
|
|
plugin.Serve(&plugin.ServeConfig{
|
|
HandshakeConfig: base.Handshake,
|
|
Plugins: pmap,
|
|
GRPCServer: plugin.DefaultGRPCServer,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// mockFactory returns a PluginFactory method which creates the mock plugin with
|
|
// the passed parameters
|
|
func mockFactory(name, ptype, version string, apiVersions []string, configSchema bool) func(context.Context, log.Logger) interface{} {
|
|
return func(ctx context.Context, log log.Logger) interface{} {
|
|
return &mockPlugin{
|
|
name: name,
|
|
ptype: ptype,
|
|
version: version,
|
|
apiVersions: apiVersions,
|
|
configSchema: configSchema,
|
|
}
|
|
}
|
|
}
|
|
|
|
// mockPlugin is a plugin that meets various plugin interfaces but is only
|
|
// useful for testing.
|
|
type mockPlugin struct {
|
|
name string
|
|
ptype string
|
|
version string
|
|
apiVersions []string
|
|
configSchema bool
|
|
|
|
// config is built on SetConfig
|
|
config *mockPluginConfig
|
|
|
|
// nomadconfig is set on SetConfig
|
|
nomadConfig *base.AgentConfig
|
|
|
|
// negotiatedApiVersion is the version of the api to use and is set on
|
|
// SetConfig
|
|
negotiatedApiVersion string
|
|
}
|
|
|
|
// mockPluginConfig is the configuration for the mock plugin
|
|
type mockPluginConfig struct {
|
|
Foo string `codec:"foo"`
|
|
Bar int `codec:"bar"`
|
|
|
|
// ResKey is a key that is populated in the Env map when a device is
|
|
// reserved.
|
|
ResKey string `codec:"res_key"`
|
|
}
|
|
|
|
// PluginInfo returns the plugin information based on the passed fields when
|
|
// building the mock plugin
|
|
func (m *mockPlugin) PluginInfo() (*base.PluginInfoResponse, error) {
|
|
return &base.PluginInfoResponse{
|
|
Type: m.ptype,
|
|
PluginApiVersions: m.apiVersions,
|
|
PluginVersion: m.version,
|
|
Name: m.name,
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockPlugin) ConfigSchema() (*hclspec.Spec, error) {
|
|
if !m.configSchema {
|
|
return nil, nil
|
|
}
|
|
|
|
// configSpec is the hclspec for parsing the mock's configuration
|
|
configSpec := hclspec.NewObject(map[string]*hclspec.Spec{
|
|
"foo": hclspec.NewAttr("foo", "string", false),
|
|
"bar": hclspec.NewAttr("bar", "number", false),
|
|
"res_key": hclspec.NewAttr("res_key", "string", false),
|
|
})
|
|
|
|
return configSpec, nil
|
|
}
|
|
|
|
// SetConfig decodes the configuration and stores it
|
|
func (m *mockPlugin) SetConfig(c *base.Config) error {
|
|
var config mockPluginConfig
|
|
if len(c.PluginConfig) != 0 {
|
|
if err := base.MsgPackDecode(c.PluginConfig, &config); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
m.config = &config
|
|
m.nomadConfig = c.AgentConfig
|
|
m.negotiatedApiVersion = c.ApiVersion
|
|
return nil
|
|
}
|
|
|
|
func (m *mockPlugin) Fingerprint(ctx context.Context) (<-chan *device.FingerprintResponse, error) {
|
|
return make(chan *device.FingerprintResponse), nil
|
|
}
|
|
|
|
func (m *mockPlugin) Reserve(deviceIDs []string) (*device.ContainerReservation, error) {
|
|
if m.config == nil || m.config.ResKey == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
return &device.ContainerReservation{
|
|
Envs: map[string]string{m.config.ResKey: "config-set"},
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockPlugin) Stats(ctx context.Context, interval time.Duration) (<-chan *device.StatsResponse, error) {
|
|
return make(chan *device.StatsResponse), nil
|
|
}
|