diff --git a/client/config/config.go b/client/config/config.go index 062a453a6..13581e63b 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -9,7 +9,6 @@ import ( "time" log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs/config" @@ -368,8 +367,8 @@ func (c *Config) ReadStringListToMapDefault(key, defaultValue string) map[string } // NomadPluginConfig produces the NomadConfig struct which is sent to Nomad plugins -func (c *Config) NomadPluginConfig() *base.ClientAgentConfig { - return &base.ClientAgentConfig{ +func (c *Config) NomadPluginConfig() *base.AgentConfig { + return &base.AgentConfig{ Driver: &base.ClientDriverConfig{ ClientMinPort: c.ClientMinPort, ClientMaxPort: c.ClientMaxPort, diff --git a/client/devicemanager/instance.go b/client/devicemanager/instance.go index ab67f496f..f834062e1 100644 --- a/client/devicemanager/instance.go +++ b/client/devicemanager/instance.go @@ -40,7 +40,7 @@ type instanceManagerConfig struct { StoreReattach StorePluginReattachFn // PluginConfig is the config passed to the launched plugins - PluginConfig *base.ClientAgentConfig + PluginConfig *base.AgentConfig // Id is the ID of the plugin being managed Id *loader.PluginID @@ -70,7 +70,7 @@ type instanceManager struct { storeReattach StorePluginReattachFn // pluginConfig is the config passed to the launched plugins - pluginConfig *base.ClientAgentConfig + pluginConfig *base.AgentConfig // id is the ID of the plugin being managed id *loader.PluginID diff --git a/client/devicemanager/manager.go b/client/devicemanager/manager.go index bf101677b..2709b2230 100644 --- a/client/devicemanager/manager.go +++ b/client/devicemanager/manager.go @@ -63,7 +63,7 @@ type Config struct { Loader loader.PluginCatalog // PluginConfig is the config passed to the launched plugins - PluginConfig *base.ClientAgentConfig + PluginConfig *base.AgentConfig // Updater is used to update the node when device information changes Updater UpdateNodeDevicesFn @@ -91,7 +91,7 @@ type manager struct { loader loader.PluginCatalog // pluginConfig is the config passed to the launched plugins - pluginConfig *base.ClientAgentConfig + pluginConfig *base.AgentConfig // updater is used to update the node when device information changes updater UpdateNodeDevicesFn diff --git a/client/devicemanager/manager_test.go b/client/devicemanager/manager_test.go index 253c115db..c59e1fb04 100644 --- a/client/devicemanager/manager_test.go +++ b/client/devicemanager/manager_test.go @@ -121,7 +121,7 @@ func baseTestConfig(t *testing.T) ( // Create the config config = &Config{ Logger: testlog.HCLogger(t), - PluginConfig: &base.ClientAgentConfig{}, + PluginConfig: &base.AgentConfig{}, StatsInterval: 100 * time.Millisecond, State: state.NewMemDB(), Updater: updateFn, @@ -133,7 +133,7 @@ func baseTestConfig(t *testing.T) ( func configureCatalogWith(catalog *loader.MockCatalog, plugins map[*base.PluginInfoResponse]loader.PluginInstance) { - catalog.DispenseF = func(name, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + catalog.DispenseF = func(name, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { for info, v := range plugins { if info.Name == name { return v, nil @@ -167,10 +167,10 @@ func configureCatalogWith(catalog *loader.MockCatalog, plugins map[*base.PluginI func pluginInfoResponse(name string) *base.PluginInfoResponse { return &base.PluginInfoResponse{ - Type: base.PluginTypeDevice, - PluginApiVersion: "v0.0.1", - PluginVersion: "v0.0.1", - Name: name, + Type: base.PluginTypeDevice, + PluginApiVersions: []string{"v0.0.1"}, + PluginVersion: "v0.0.1", + Name: name, } } @@ -209,7 +209,7 @@ func nvidiaAndIntelDefaultPlugins(catalog *loader.MockCatalog) { ReserveF: deviceReserveFn, StatsF: device.StaticStats([]*device.DeviceGroupStats{nvidiaDeviceGroupStats}), } - pluginNvidia := loader.MockBasicExternalPlugin(deviceNvidia) + pluginNvidia := loader.MockBasicExternalPlugin(deviceNvidia, device.ApiVersion010) pluginInfoIntel := pluginInfoResponse("intel") deviceIntel := &device.MockDevicePlugin{ @@ -222,7 +222,7 @@ func nvidiaAndIntelDefaultPlugins(catalog *loader.MockCatalog) { ReserveF: deviceReserveFn, StatsF: device.StaticStats([]*device.DeviceGroupStats{intelDeviceGroupStats}), } - pluginIntel := loader.MockBasicExternalPlugin(deviceIntel) + pluginIntel := loader.MockBasicExternalPlugin(deviceIntel, device.ApiVersion010) // Configure the catalog with two plugins configureCatalogWith(catalog, map[*base.PluginInfoResponse]loader.PluginInstance{ @@ -454,7 +454,7 @@ func TestManager_Shutdown(t *testing.T) { m.Shutdown() for _, resp := range catalog.Catalog()[base.PluginTypeDevice] { - pinst, _ := catalog.Dispense(resp.Name, resp.Type, &base.ClientAgentConfig{}, config.Logger) + pinst, _ := catalog.Dispense(resp.Name, resp.Type, &base.AgentConfig{}, config.Logger) require.True(pinst.Exited()) } } @@ -486,7 +486,7 @@ func TestManager_Run_ShutdownOld(t *testing.T) { testutil.WaitForResult(func() (bool, error) { for _, resp := range catalog.Catalog()[base.PluginTypeDevice] { - pinst, _ := catalog.Dispense(resp.Name, resp.Type, &base.ClientAgentConfig{}, config.Logger) + pinst, _ := catalog.Dispense(resp.Name, resp.Type, &base.AgentConfig{}, config.Logger) if !pinst.Exited() { return false, fmt.Errorf("plugin %q not shutdown", resp.Name) } diff --git a/command/agent/plugins.go b/command/agent/plugins.go index 6637d69f9..0cc73a816 100644 --- a/command/agent/plugins.go +++ b/command/agent/plugins.go @@ -18,10 +18,11 @@ func (a *Agent) setupPlugins() error { // Build the plugin loader config := &loader.PluginLoaderConfig{ - Logger: a.logger, - PluginDir: a.config.PluginDir, - Configs: a.config.Plugins, - InternalPlugins: internal, + Logger: a.logger, + PluginDir: a.config.PluginDir, + Configs: a.config.Plugins, + InternalPlugins: internal, + SupportedVersions: loader.AgentSupportedApiVersions, } l, err := loader.NewPluginLoader(config) if err != nil { diff --git a/devices/gpu/nvidia/device.go b/devices/gpu/nvidia/device.go index e5995eb7c..97be7d153 100644 --- a/devices/gpu/nvidia/device.go +++ b/devices/gpu/nvidia/device.go @@ -37,10 +37,10 @@ const ( var ( // pluginInfo describes the plugin pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDevice, - PluginApiVersion: "0.0.1", // XXX This should be an array and should be consts - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDevice, + PluginApiVersions: []string{"v0.2.0"}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the specification of the plugin's configuration @@ -111,10 +111,12 @@ func (d *NvidiaDevice) ConfigSchema() (*hclspec.Spec, error) { } // SetConfig is used to set the configuration of the plugin. -func (d *NvidiaDevice) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *NvidiaDevice) SetConfig(cfg *base.Config) error { var config Config - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(cfg.PluginConfig) != 0 { + if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil { + return err + } } for _, ignoredGPUId := range config.IgnoredGPUIDs { diff --git a/drivers/docker/config.go b/drivers/docker/config.go index 5efd383e5..fa3edb3ce 100644 --- a/drivers/docker/config.go +++ b/drivers/docker/config.go @@ -122,10 +122,10 @@ var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -502,10 +502,12 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *Driver) SetConfig(c *base.Config) error { var config DriverConfig - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(c.PluginConfig) != 0 { + if err := base.MsgPackDecode(c.PluginConfig, &config); err != nil { + return err + } } d.config = &config @@ -517,8 +519,8 @@ func (d *Driver) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { d.config.GC.imageDelayDuration = dur } - if cfg != nil { - d.clientConfig = cfg.Driver + if c.AgentConfig != nil { + d.clientConfig = c.AgentConfig.Driver } dockerClient, _, err := d.dockerClients() diff --git a/drivers/exec/driver.go b/drivers/exec/driver.go index c6616f3c4..d86422d8c 100644 --- a/drivers/exec/driver.go +++ b/drivers/exec/driver.go @@ -49,10 +49,10 @@ var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -136,9 +136,9 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(_ []byte, cfg *base.ClientAgentConfig) error { - if cfg != nil { - d.nomadConfig = cfg.Driver +func (d *Driver) SetConfig(cfg *base.Config) error { + if cfg != nil && cfg.AgentConfig != nil { + d.nomadConfig = cfg.AgentConfig.Driver } return nil } diff --git a/drivers/java/driver.go b/drivers/java/driver.go index 71ba92ccb..3575324e5 100644 --- a/drivers/java/driver.go +++ b/drivers/java/driver.go @@ -54,10 +54,10 @@ var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -156,9 +156,9 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(_ []byte, cfg *base.ClientAgentConfig) error { - if cfg != nil { - d.nomadConfig = cfg.Driver +func (d *Driver) SetConfig(cfg *base.Config) error { + if cfg != nil && cfg.AgentConfig != nil { + d.nomadConfig = cfg.AgentConfig.Driver } return nil } diff --git a/drivers/lxc/driver.go b/drivers/lxc/driver.go index 6d51e0e35..028c356da 100644 --- a/drivers/lxc/driver.go +++ b/drivers/lxc/driver.go @@ -62,10 +62,10 @@ func PluginLoader(opts map[string]string) (map[string]interface{}, error) { var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -200,15 +200,17 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *Driver) SetConfig(cfg *base.Config) error { var config Config - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(cfg.PluginConfig) != 0 { + if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil { + return err + } } d.config = &config - if cfg != nil { - d.nomadConfig = cfg.Driver + if cfg.AgentConfig != nil { + d.nomadConfig = cfg.AgentConfig.Driver } return nil diff --git a/drivers/mock/driver.go b/drivers/mock/driver.go index 1b9abe436..1593e04fc 100644 --- a/drivers/mock/driver.go +++ b/drivers/mock/driver.go @@ -45,10 +45,10 @@ var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -225,10 +225,12 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *Driver) SetConfig(cfg *base.Config) error { var config Config - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(cfg.PluginConfig) != 0 { + if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil { + return err + } } d.config = &config diff --git a/drivers/qemu/driver.go b/drivers/qemu/driver.go index 17ccf7f11..bcf6110a7 100644 --- a/drivers/qemu/driver.go +++ b/drivers/qemu/driver.go @@ -74,10 +74,10 @@ var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -167,9 +167,9 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(_ []byte, cfg *base.ClientAgentConfig) error { - if cfg != nil { - d.nomadConfig = cfg.Driver +func (d *Driver) SetConfig(cfg *base.Config) error { + if cfg.AgentConfig != nil { + d.nomadConfig = cfg.AgentConfig.Driver } return nil } diff --git a/drivers/rawexec/driver.go b/drivers/rawexec/driver.go index d839df7fe..42c0db31b 100644 --- a/drivers/rawexec/driver.go +++ b/drivers/rawexec/driver.go @@ -63,10 +63,10 @@ func PluginLoader(opts map[string]string) (map[string]interface{}, error) { var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -174,15 +174,17 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *Driver) SetConfig(cfg *base.Config) error { var config Config - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(cfg.PluginConfig) != 0 { + if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil { + return err + } } d.config = &config - if cfg != nil { - d.nomadConfig = cfg.Driver + if cfg.AgentConfig != nil { + d.nomadConfig = cfg.AgentConfig.Driver } return nil } diff --git a/drivers/rawexec/driver_test.go b/drivers/rawexec/driver_test.go index 5a7083e5e..1eb42f6dd 100644 --- a/drivers/rawexec/driver_test.go +++ b/drivers/rawexec/driver_test.go @@ -42,25 +42,30 @@ func TestRawExecDriver_SetConfig(t *testing.T) { harness := dtestutil.NewDriverHarness(t, d) defer harness.Kill() + bconfig := &basePlug.Config{} + // Disable raw exec. config := &Config{} var data []byte require.NoError(basePlug.MsgPackEncode(&data, config)) - require.NoError(harness.SetConfig(data, nil)) + bconfig.PluginConfig = data + require.NoError(harness.SetConfig(bconfig)) require.Exactly(config, d.(*Driver).config) config.Enabled = true config.NoCgroups = true data = []byte{} require.NoError(basePlug.MsgPackEncode(&data, config)) - require.NoError(harness.SetConfig(data, nil)) + bconfig.PluginConfig = data + require.NoError(harness.SetConfig(bconfig)) require.Exactly(config, d.(*Driver).config) config.NoCgroups = false data = []byte{} require.NoError(basePlug.MsgPackEncode(&data, config)) - require.NoError(harness.SetConfig(data, nil)) + bconfig.PluginConfig = data + require.NoError(harness.SetConfig(bconfig)) require.Exactly(config, d.(*Driver).config) } @@ -76,7 +81,10 @@ func TestRawExecDriver_Fingerprint(t *testing.T) { var data []byte require.NoError(basePlug.MsgPackEncode(&data, config)) - require.NoError(harness.SetConfig(data, nil)) + bconfig := &basePlug.Config{ + PluginConfig: data, + } + require.NoError(harness.SetConfig(bconfig)) fingerCh, err := harness.Fingerprint(context.Background()) require.NoError(err) @@ -168,7 +176,8 @@ func TestRawExecDriver_StartWaitStop(t *testing.T) { config := &Config{NoCgroups: true} var data []byte require.NoError(basePlug.MsgPackEncode(&data, config)) - require.NoError(harness.SetConfig(data, nil)) + bconfig := &basePlug.Config{PluginConfig: data} + require.NoError(harness.SetConfig(bconfig)) task := &drivers.TaskConfig{ ID: uuid.Generate(), @@ -234,7 +243,8 @@ func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) { config := &Config{NoCgroups: true} var data []byte require.NoError(basePlug.MsgPackEncode(&data, config)) - require.NoError(harness.SetConfig(data, nil)) + bconfig := &basePlug.Config{PluginConfig: data} + require.NoError(harness.SetConfig(bconfig)) task := &drivers.TaskConfig{ ID: uuid.Generate(), diff --git a/drivers/rkt/driver.go b/drivers/rkt/driver.go index 118ee026e..643fbba87 100644 --- a/drivers/rkt/driver.go +++ b/drivers/rkt/driver.go @@ -86,10 +86,10 @@ func PluginLoader(opts map[string]string) (map[string]interface{}, error) { var ( // pluginInfo is the response returned for the PluginInfo RPC pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDriver, - PluginApiVersion: "0.0.1", - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDriver, + PluginApiVersions: []string{drivers.ApiVersion010}, + PluginVersion: "0.1.0", + Name: pluginName, } // configSpec is the hcl specification returned by the ConfigSchema RPC @@ -216,15 +216,17 @@ func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { return configSpec, nil } -func (d *Driver) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *Driver) SetConfig(cfg *base.Config) error { var config Config - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(cfg.PluginConfig) != 0 { + if err := base.MsgPackDecode(cfg.PluginConfig, &config); err != nil { + return err + } } d.config = &config - if cfg != nil { - d.nomadConfig = cfg.Driver + if cfg.AgentConfig != nil { + d.nomadConfig = cfg.AgentConfig.Driver } return nil } diff --git a/plugins/device/cmd/example/device.go b/plugins/device/cmd/example/device.go index 1c96ea348..475f115b0 100644 --- a/plugins/device/cmd/example/device.go +++ b/plugins/device/cmd/example/device.go @@ -9,9 +9,8 @@ import ( "sync" "time" - "github.com/hashicorp/nomad/helper" - log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/plugins/base" "github.com/hashicorp/nomad/plugins/device" "github.com/hashicorp/nomad/plugins/shared/hclspec" @@ -38,10 +37,10 @@ const ( var ( // pluginInfo describes the plugin pluginInfo = &base.PluginInfoResponse{ - Type: base.PluginTypeDevice, - PluginApiVersion: "0.0.1", // XXX This should be an array and should be consts - PluginVersion: "0.1.0", - Name: pluginName, + Type: base.PluginTypeDevice, + PluginApiVersions: []string{device.ApiVersion010}, + PluginVersion: "v0.1.0", + Name: pluginName, } // configSpec is the specification of the plugin's configuration @@ -109,9 +108,9 @@ func (d *FsDevice) ConfigSchema() (*hclspec.Spec, error) { } // SetConfig is used to set the configuration of the plugin. -func (d *FsDevice) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (d *FsDevice) SetConfig(c *base.Config) error { var config Config - if err := base.MsgPackDecode(data, &config); err != nil { + if err := base.MsgPackDecode(c.PluginConfig, &config); err != nil { return err } diff --git a/plugins/device/plugin_test.go b/plugins/device/plugin_test.go index cd8fcd8b2..a07fa329b 100644 --- a/plugins/device/plugin_test.go +++ b/plugins/device/plugin_test.go @@ -24,27 +24,30 @@ func TestDevicePlugin_PluginInfo(t *testing.T) { t.Parallel() require := require.New(t) + var ( + apiVersions = []string{"v0.1.0", "v0.2.0"} + ) + const ( - apiVersion = "v0.1.0" pluginVersion = "v0.2.1" pluginName = "mock_device" ) knownType := func() (*base.PluginInfoResponse, error) { info := &base.PluginInfoResponse{ - Type: base.PluginTypeDevice, - PluginApiVersion: apiVersion, - PluginVersion: pluginVersion, - Name: pluginName, + Type: base.PluginTypeDevice, + PluginApiVersions: apiVersions, + PluginVersion: pluginVersion, + Name: pluginName, } return info, nil } unknownType := func() (*base.PluginInfoResponse, error) { info := &base.PluginInfoResponse{ - Type: "bad", - PluginApiVersion: apiVersion, - PluginVersion: pluginVersion, - Name: pluginName, + Type: "bad", + PluginApiVersions: apiVersions, + PluginVersion: pluginVersion, + Name: pluginName, } return info, nil } @@ -74,7 +77,7 @@ func TestDevicePlugin_PluginInfo(t *testing.T) { resp, err := impl.PluginInfo() require.NoError(err) - require.Equal(apiVersion, resp.PluginApiVersion) + require.Equal(apiVersions, resp.PluginApiVersions) require.Equal(pluginVersion, resp.PluginVersion) require.Equal(pluginName, resp.Name) require.Equal(base.PluginTypeDevice, resp.Type) @@ -129,17 +132,17 @@ func TestDevicePlugin_SetConfig(t *testing.T) { MockPlugin: &base.MockPlugin{ PluginInfoF: func() (*base.PluginInfoResponse, error) { return &base.PluginInfoResponse{ - Type: base.PluginTypeDevice, - PluginApiVersion: "v0.0.1", - PluginVersion: "v0.0.1", - Name: "mock_device", + Type: base.PluginTypeDevice, + PluginApiVersions: []string{"v0.0.1"}, + PluginVersion: "v0.0.1", + Name: "mock_device", }, nil }, ConfigSchemaF: func() (*hclspec.Spec, error) { return base.TestSpec, nil }, - SetConfigF: func(data []byte, cfg *base.ClientAgentConfig) error { - receivedData = data + SetConfigF: func(cfg *base.Config) error { + receivedData = cfg.PluginConfig return nil }, }, @@ -169,7 +172,7 @@ func TestDevicePlugin_SetConfig(t *testing.T) { }) cdata, err := msgpack.Marshal(config, config.Type()) require.NoError(err) - require.NoError(impl.SetConfig(cdata, nil)) + require.NoError(impl.SetConfig(&base.Config{PluginConfig: cdata})) require.Equal(cdata, receivedData) // Decode the value back diff --git a/plugins/device/versions.go b/plugins/device/versions.go new file mode 100644 index 000000000..f10057f2c --- /dev/null +++ b/plugins/device/versions.go @@ -0,0 +1,6 @@ +package device + +const ( + // ApiVersion010 is the initial API version for the device plugins + ApiVersion010 = "v0.1.0" +) diff --git a/plugins/drivers/versions.go b/plugins/drivers/versions.go new file mode 100644 index 000000000..4b94d901c --- /dev/null +++ b/plugins/drivers/versions.go @@ -0,0 +1,6 @@ +package drivers + +const ( + // ApiVersion010 is the initial API version for the device plugins + ApiVersion010 = "v0.1.0" +) diff --git a/plugins/shared/catalog/testing.go b/plugins/shared/catalog/testing.go index 59bcc6cf1..19ed78bb2 100644 --- a/plugins/shared/catalog/testing.go +++ b/plugins/shared/catalog/testing.go @@ -51,10 +51,11 @@ func TestPluginLoaderWithOptions(t testing.T, // Build the plugin loader config := &loader.PluginLoaderConfig{ - Logger: logger, - PluginDir: "", - Configs: configs, - InternalPlugins: internal, + Logger: logger, + PluginDir: "", + Configs: configs, + InternalPlugins: internal, + SupportedVersions: loader.AgentSupportedApiVersions, } l, err := loader.NewPluginLoader(config) if err != nil { diff --git a/plugins/shared/cmd/launcher/command/device.go b/plugins/shared/cmd/launcher/command/device.go index aadc215eb..01855da7b 100644 --- a/plugins/shared/cmd/launcher/command/device.go +++ b/plugins/shared/cmd/launcher/command/device.go @@ -120,7 +120,7 @@ func (c *Device) Run(args []string) int { } c.spec = spec - if err := c.setConfig(spec, config, nil); err != nil { + if err := c.setConfig(spec, device.ApiVersion010, config, nil); err != nil { c.logger.Error("failed to set config", "error", err) return 1 } @@ -188,7 +188,7 @@ func (c *Device) getSpec() (hcldec.Spec, error) { return schema, nil } -func (c *Device) setConfig(spec hcldec.Spec, config []byte, nmdCfg *base.ClientAgentConfig) error { +func (c *Device) setConfig(spec hcldec.Spec, apiVersion string, config []byte, nmdCfg *base.AgentConfig) error { // Parse the config into hcl configVal, err := hclConfigToInterface(config) if err != nil { @@ -216,8 +216,14 @@ func (c *Device) setConfig(spec hcldec.Spec, config []byte, nmdCfg *base.ClientA return err } + req := &base.Config{ + PluginConfig: config, + AgentConfig: nmdCfg, + ApiVersion: apiVersion, + } + c.logger.Trace("msgpack config", "config", string(cdata)) - if err := c.dev.SetConfig(cdata, nmdCfg); err != nil { + if err := c.dev.SetConfig(req); err != nil { return err } diff --git a/plugins/shared/loader/api_versions.go b/plugins/shared/loader/api_versions.go new file mode 100644 index 000000000..8ceb02da0 --- /dev/null +++ b/plugins/shared/loader/api_versions.go @@ -0,0 +1,15 @@ +package loader + +import ( + "github.com/hashicorp/nomad/plugins/base" + "github.com/hashicorp/nomad/plugins/device" +) + +var ( + // AgentSupportedApiVersions is the set of API versions supported by the + // Nomad agent by plugin type. + AgentSupportedApiVersions = map[string][]string{ + base.PluginTypeDevice: []string{device.ApiVersion010}, + base.PluginTypeDriver: []string{device.ApiVersion010}, + } +) diff --git a/plugins/shared/loader/init.go b/plugins/shared/loader/init.go index 9013fb13d..89f09198c 100644 --- a/plugins/shared/loader/init.go +++ b/plugins/shared/loader/init.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "sort" multierror "github.com/hashicorp/go-multierror" plugin "github.com/hashicorp/go-plugin" @@ -130,6 +131,18 @@ func (l *PluginLoader) initInternal(plugins map[PluginID]*InternalPluginConfig, } info.version = v + // Detect the plugin API version to use + av, err := l.selectApiVersion(i) + if err != nil { + multierror.Append(&mErr, fmt.Errorf("failed to validate API versions %v for internal plugin %s: %v", i.PluginApiVersions, k, err)) + continue + } + if av == "" { + l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", k) + continue + } + info.apiVersion = av + // Get the config schema schema, err := base.ConfigSchema() if err != nil { @@ -142,9 +155,66 @@ func (l *PluginLoader) initInternal(plugins map[PluginID]*InternalPluginConfig, fingerprinted[k] = info } + if err := mErr.ErrorOrNil(); err != nil { + return nil, err + } + return fingerprinted, nil } +// selectApiVersion takes in PluginInfo and returns the highest compatable +// version or an error if the plugins response is malformed. If there is no +// overlap, an empty string is returned. +func (l *PluginLoader) selectApiVersion(i *base.PluginInfoResponse) (string, error) { + if i == nil { + return "", fmt.Errorf("nil plugin info given") + } + if len(i.PluginApiVersions) == 0 { + return "", fmt.Errorf("plugin provided no compatible API versions") + } + + pluginVersions, err := convertVersions(i.PluginApiVersions) + if err != nil { + return "", fmt.Errorf("plugin provided invalid versions: %v", err) + } + + // Lookup the supported versions. These will be sorted highest to lowest + supportedVersions, ok := l.supportedVersions[i.Type] + if !ok { + return "", fmt.Errorf("unsupported plugin type %q", i.Type) + } + + for _, sv := range supportedVersions { + for _, pv := range pluginVersions { + if sv.Equal(pv) { + return pv.Original(), nil + } + } + } + + return "", nil +} + +// convertVersions takes a list of string versions and returns a sorted list of +// versions from highest to lowest. +func convertVersions(in []string) ([]*version.Version, error) { + converted := make([]*version.Version, len(in)) + for i, v := range in { + vv, err := version.NewVersion(v) + if err != nil { + return nil, fmt.Errorf("failed to convert version %q : %v", v, err) + } + + converted[i] = vv + } + + sort.Slice(converted, func(i, j int) bool { + return converted[i].GreaterThan(converted[j]) + }) + + return converted, nil +} + // scan scans the plugin directory and retrieves potentially eligible binaries func (l *PluginLoader) scan() ([]os.FileInfo, error) { if l.pluginDir == "" { @@ -200,10 +270,14 @@ func (l *PluginLoader) fingerprintPlugins(plugins []os.FileInfo, configs map[str c := configs[name] info, err := l.fingerprintPlugin(p, c) if err != nil { - l.logger.Error("failed to fingerprint plugin", "plugin", name) + l.logger.Error("failed to fingerprint plugin", "plugin", name, "error", err) multierror.Append(&mErr, err) continue } + if info == nil { + // Plugin was skipped for validation reasons + continue + } id := PluginID{ Name: info.baseInfo.Name, @@ -297,6 +371,17 @@ func (l *PluginLoader) fingerprintPlugin(pluginExe os.FileInfo, config *config.P } info.version = v + // Detect the plugin API version to use + av, err := l.selectApiVersion(i) + if err != nil { + return nil, fmt.Errorf("failed to validate API versions %v for plugin %s (%v): %v", i.PluginApiVersions, i.Name, info.exePath, err) + } + if av == "" { + l.logger.Warn("skipping plugin because supported API versions for plugin and Nomad do not overlap", "plugin", i.Name, "path", info.exePath) + return nil, nil + } + info.apiVersion = av + // Retrieve the schema schema, err := bplugin.ConfigSchema() if err != nil { @@ -404,12 +489,18 @@ func (l *PluginLoader) validePluginConfig(id PluginID, info *pluginInfo) error { } defer instance.Kill() - base, ok := instance.Plugin().(base.BasePlugin) + b, ok := instance.Plugin().(base.BasePlugin) if !ok { return fmt.Errorf("dispensed plugin %s doesn't meet base plugin interface", id) } - if err := base.SetConfig(cdata, nil); err != nil { + c := &base.Config{ + PluginConfig: cdata, + AgentConfig: nil, + ApiVersion: info.apiVersion, + } + + if err := b.SetConfig(c); err != nil { return fmt.Errorf("setting config on plugin failed: %v", err) } return nil diff --git a/plugins/shared/loader/instance.go b/plugins/shared/loader/instance.go index bb074e79f..d20bff4a2 100644 --- a/plugins/shared/loader/instance.go +++ b/plugins/shared/loader/instance.go @@ -22,11 +22,15 @@ type PluginInstance interface { // Exited returns whether the plugin has exited Exited() bool + + // ApiVersion returns the API version to be used with the plugin + ApiVersion() string } // internalPluginInstance wraps an internal plugin type internalPluginInstance struct { - instance interface{} + instance interface{} + apiVersion string } func (p *internalPluginInstance) Internal() bool { return true } @@ -34,16 +38,19 @@ func (p *internalPluginInstance) Kill() func (p *internalPluginInstance) ReattachConfig() (*plugin.ReattachConfig, bool) { return nil, false } func (p *internalPluginInstance) Plugin() interface{} { return p.instance } func (p *internalPluginInstance) Exited() bool { return false } +func (p *internalPluginInstance) ApiVersion() string { return p.apiVersion } // externalPluginInstance wraps an external plugin type externalPluginInstance struct { - client *plugin.Client - instance interface{} + client *plugin.Client + instance interface{} + apiVersion string } func (p *externalPluginInstance) Internal() bool { return false } func (p *externalPluginInstance) Plugin() interface{} { return p.instance } func (p *externalPluginInstance) Exited() bool { return p.client.Exited() } +func (p *externalPluginInstance) ApiVersion() string { return p.apiVersion } func (p *externalPluginInstance) ReattachConfig() (*plugin.ReattachConfig, bool) { return p.client.ReattachConfig(), true diff --git a/plugins/shared/loader/loader.go b/plugins/shared/loader/loader.go index d2175af07..b07904832 100644 --- a/plugins/shared/loader/loader.go +++ b/plugins/shared/loader/loader.go @@ -19,7 +19,7 @@ import ( type PluginCatalog interface { // Dispense returns the plugin given its name and type. This will also // configure the plugin - Dispense(name, pluginType string, config *base.ClientAgentConfig, logger log.Logger) (PluginInstance, error) + Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error) // Reattach is used to reattach to a previously launched external plugin. Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) @@ -28,17 +28,10 @@ type PluginCatalog interface { Catalog() map[string][]*base.PluginInfoResponse } -// PluginLoader is used to retrieve plugins either externally or from internal -// factories. -type PluginLoader struct { - // logger is the plugin loaders logger - logger log.Logger - - // pluginDir is the directory containing plugin binaries - pluginDir string - - // plugins maps a plugin to information required to launch it - plugins map[PluginID]*pluginInfo +// InternalPluginConfig is used to configure launching an internal plugin. +type InternalPluginConfig struct { + Config map[string]interface{} + Factory plugins.PluginFactory } // PluginID is a tuple identifying a plugin @@ -75,12 +68,25 @@ type PluginLoaderConfig struct { // InternalPlugins allows registering internal plugins. InternalPlugins map[PluginID]*InternalPluginConfig + + // SupportedVersions is a mapping of plugin type to the supported versions + SupportedVersions map[string][]string } -// InternalPluginConfig is used to configure launching an internal plugin. -type InternalPluginConfig struct { - Config map[string]interface{} - Factory plugins.PluginFactory +// PluginLoader is used to retrieve plugins either externally or from internal +// factories. +type PluginLoader struct { + // logger is the plugin loaders logger + logger log.Logger + + // supportedVersions is a mapping of plugin type to the supported versions + supportedVersions map[string][]*version.Version + + // pluginDir is the directory containing plugin binaries + pluginDir string + + // plugins maps a plugin to information required to launch it + plugins map[PluginID]*pluginInfo } // pluginInfo captures the necessary information to launch and configure a @@ -91,8 +97,9 @@ type pluginInfo struct { exePath string args []string - baseInfo *base.PluginInfoResponse - version *version.Version + baseInfo *base.PluginInfoResponse + version *version.Version + apiVersion string configSchema *hclspec.Spec config map[string]interface{} @@ -106,11 +113,22 @@ func NewPluginLoader(config *PluginLoaderConfig) (*PluginLoader, error) { return nil, fmt.Errorf("invalid plugin loader configuration passed: %v", err) } + // Convert the versions + supportedVersions := make(map[string][]*version.Version, len(config.SupportedVersions)) + for pType, versions := range config.SupportedVersions { + converted, err := convertVersions(versions) + if err != nil { + return nil, err + } + supportedVersions[pType] = converted + } + logger := config.Logger.Named("plugin_loader").With("plugin_dir", config.PluginDir) l := &PluginLoader{ - logger: logger, - pluginDir: config.PluginDir, - plugins: make(map[PluginID]*pluginInfo), + logger: logger, + supportedVersions: supportedVersions, + pluginDir: config.PluginDir, + plugins: make(map[PluginID]*pluginInfo), } if err := l.init(config); err != nil { @@ -122,7 +140,7 @@ func NewPluginLoader(config *PluginLoaderConfig) (*PluginLoader, error) { // Dispense returns a plugin instance, loading it either internally or by // launching an external plugin. -func (l *PluginLoader) Dispense(name, pluginType string, config *base.ClientAgentConfig, logger log.Logger) (PluginInstance, error) { +func (l *PluginLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (PluginInstance, error) { id := PluginID{ Name: name, PluginType: pluginType, @@ -136,26 +154,31 @@ func (l *PluginLoader) Dispense(name, pluginType string, config *base.ClientAgen var instance PluginInstance if pinfo.factory != nil { instance = &internalPluginInstance{ - instance: pinfo.factory(logger), + instance: pinfo.factory(logger), + apiVersion: pinfo.apiVersion, } } else { var err error - instance, err = l.dispensePlugin(pinfo.baseInfo.Type, pinfo.exePath, pinfo.args, nil, logger) + instance, err = l.dispensePlugin(pinfo.baseInfo.Type, pinfo.apiVersion, pinfo.exePath, pinfo.args, nil, logger) if err != nil { return nil, fmt.Errorf("failed to launch plugin: %v", err) } } // Cast to the base type and set the config - base, ok := instance.Plugin().(base.BasePlugin) + b, ok := instance.Plugin().(base.BasePlugin) if !ok { return nil, fmt.Errorf("plugin %s doesn't implement base plugin interface", id) } - if len(pinfo.msgpackConfig) != 0 { - if err := base.SetConfig(pinfo.msgpackConfig, config); err != nil { - return nil, fmt.Errorf("setting config for plugin %s failed: %v", id, err) - } + c := &base.Config{ + PluginConfig: pinfo.msgpackConfig, + AgentConfig: config, + ApiVersion: pinfo.apiVersion, + } + + if err := b.SetConfig(c); err != nil { + return nil, fmt.Errorf("setting config for plugin %s failed: %v", id, err) } return instance, nil @@ -163,12 +186,12 @@ func (l *PluginLoader) Dispense(name, pluginType string, config *base.ClientAgen // Reattach reattaches to a previously launched external plugin. func (l *PluginLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) { - return l.dispensePlugin(pluginType, "", nil, config, l.logger) + return l.dispensePlugin(pluginType, "", "", nil, config, l.logger) } // dispensePlugin is used to launch or reattach to an external plugin. func (l *PluginLoader) dispensePlugin( - pluginType, cmd string, args []string, reattach *plugin.ReattachConfig, + pluginType, apiVersion, cmd string, args []string, reattach *plugin.ReattachConfig, logger log.Logger) (PluginInstance, error) { var pluginCmd *exec.Cmd @@ -207,6 +230,31 @@ func (l *PluginLoader) dispensePlugin( client: client, instance: raw, } + + if apiVersion != "" { + instance.apiVersion = apiVersion + } else { + // We do not know the API version since we are reattaching, so discover + // it + bplugin := raw.(base.BasePlugin) + + // Retrieve base plugin information + i, err := bplugin.PluginInfo() + if err != nil { + return nil, fmt.Errorf("failed to get plugin info for plugin: %v", err) + } + + apiVersion, err := l.selectApiVersion(i) + if err != nil { + return nil, fmt.Errorf("failed to validate API versions %v for plugin %s: %v", i.PluginApiVersions, i.Name, err) + } + if apiVersion == "" { + return nil, fmt.Errorf("failed to reattach to plugin because supported API versions for the plugin and Nomad do not overlap") + } + + instance.apiVersion = apiVersion + } + return instance, nil } diff --git a/plugins/shared/loader/loader_test.go b/plugins/shared/loader/loader_test.go index 71363ad45..82c8bb0b2 100644 --- a/plugins/shared/loader/loader_test.go +++ b/plugins/shared/loader/loader_test.go @@ -6,9 +6,11 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" log "github.com/hashicorp/go-hclog" + version "github.com/hashicorp/go-version" "github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/nomad/structs/config" "github.com/hashicorp/nomad/plugins/base" @@ -16,6 +18,14 @@ import ( "github.com/stretchr/testify/require" ) +var ( + // supportedApiVersions is the set of api versions that the "client" can + // support + supportedApiVersions = map[string][]string{ + base.PluginTypeDevice: []string{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 { @@ -104,18 +114,21 @@ func TestPluginLoader_External(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + 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]}, + "-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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[1], + "-api-version", device.ApiVersion010, "-api-version", "v0.2.0"}, }, }, } @@ -133,21 +146,155 @@ func TestPluginLoader_External(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[0], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[0], + PluginApiVersions: []string{"v0.1.0"}, }, { - Name: plugins[1], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "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() + + logger := testlog.HCLogger(t) + logger.SetLevel(log.Trace) + lconfig := &PluginLoaderConfig{ + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: map[string][]string{ + base.PluginTypeDevice: []string{"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) { + 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") +} + func TestPluginLoader_External_Config(t *testing.T) { t.Parallel() require := require.New(t) @@ -161,13 +308,14 @@ func TestPluginLoader_External_Config(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + 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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, Config: map[string]interface{}{ "foo": "1", "bar": "2", @@ -176,7 +324,7 @@ func TestPluginLoader_External_Config(t *testing.T) { { Name: plugins[1], Args: []string{"-plugin", "-name", plugins[1], - "-type", base.PluginTypeDevice, "-version", pluginVersions[1]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010}, Config: map[string]interface{}{ "foo": "3", "bar": "4", @@ -198,16 +346,16 @@ func TestPluginLoader_External_Config(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[0], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[0], + PluginApiVersions: []string{device.ApiVersion010}, }, { - Name: plugins[1], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "v0.1.0", + Name: plugins[1], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[1], + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) @@ -227,13 +375,14 @@ func TestPluginLoader_External_Config_Bad(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + 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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, Config: map[string]interface{}{ "foo": "1", "bar": "2", @@ -261,18 +410,19 @@ func TestPluginLoader_External_VersionOverlap(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + 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]}, + "-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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", device.ApiVersion010}, }, }, } @@ -290,10 +440,10 @@ func TestPluginLoader_External_VersionOverlap(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[1], + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) @@ -309,24 +459,26 @@ func TestPluginLoader_Internal(t *testing.T) { 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(), + 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], true), + Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), }, { Name: plugins[1], PluginType: base.PluginTypeDevice, }: { - Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], true), + Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true), }, }, } @@ -344,21 +496,134 @@ func TestPluginLoader_Internal(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[0], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[0], + PluginApiVersions: []string{device.ApiVersion010}, }, { - Name: plugins[1], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "v0.1.0", + 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() + + logger := testlog.HCLogger(t) + logger.SetLevel(log.Trace) + lconfig := &PluginLoaderConfig{ + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: map[string][]string{ + base.PluginTypeDevice: []string{"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) { + 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") +} + func TestPluginLoader_Internal_Config(t *testing.T) { t.Parallel() require := require.New(t) @@ -369,18 +634,20 @@ func TestPluginLoader_Internal_Config(t *testing.T) { 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(), + 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], true), + Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), Config: map[string]interface{}{ "foo": "1", "bar": "2", @@ -390,7 +657,7 @@ func TestPluginLoader_Internal_Config(t *testing.T) { Name: plugins[1], PluginType: base.PluginTypeDevice, }: { - Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], true), + Factory: mockFactory(plugins[1], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true), Config: map[string]interface{}{ "foo": "3", "bar": "4", @@ -412,16 +679,16 @@ func TestPluginLoader_Internal_Config(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[0], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[0], + PluginApiVersions: []string{device.ApiVersion010}, }, { - Name: plugins[1], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "v0.1.0", + Name: plugins[1], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[1], + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) @@ -438,6 +705,7 @@ func TestPluginLoader_Internal_ExternalConfig(t *testing.T) { plugin := "mock-device" pluginVersion := "v0.0.1" + pluginApiVersions := []string{device.ApiVersion010} id := PluginID{ Name: plugin, @@ -451,11 +719,12 @@ func TestPluginLoader_Internal_ExternalConfig(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: supportedApiVersions, InternalPlugins: map[PluginID]*InternalPluginConfig{ id: { - Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, true), + Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true), Config: map[string]interface{}{ "foo": "1", "bar": "2", @@ -482,10 +751,10 @@ func TestPluginLoader_Internal_ExternalConfig(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugin, - Type: base.PluginTypeDevice, - PluginVersion: pluginVersion, - PluginApiVersion: "v0.1.0", + Name: plugin, + Type: base.PluginTypeDevice, + PluginVersion: pluginVersion, + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) @@ -507,18 +776,20 @@ func TestPluginLoader_Internal_Config_Bad(t *testing.T) { 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(), + 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], true), + Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), Config: map[string]interface{}{ "foo": "1", "bar": "2", @@ -540,19 +811,22 @@ func TestPluginLoader_InternalOverrideExternal(t *testing.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(), + 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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", pluginApiVersions[0]}, }, }, InternalPlugins: map[PluginID]*InternalPluginConfig{ @@ -560,7 +834,7 @@ func TestPluginLoader_InternalOverrideExternal(t *testing.T) { Name: plugins[0], PluginType: base.PluginTypeDevice, }: { - Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[1], true), + Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[1], pluginApiVersions, true), }, }, } @@ -578,10 +852,10 @@ func TestPluginLoader_InternalOverrideExternal(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[1], + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) @@ -594,19 +868,22 @@ func TestPluginLoader_ExternalOverrideInternal(t *testing.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(), + 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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[1], "-api-version", pluginApiVersions[0]}, }, }, InternalPlugins: map[PluginID]*InternalPluginConfig{ @@ -614,7 +891,7 @@ func TestPluginLoader_ExternalOverrideInternal(t *testing.T) { Name: plugins[0], PluginType: base.PluginTypeDevice, }: { - Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], true), + Factory: mockFactory(plugins[0], base.PluginTypeDevice, pluginVersions[0], pluginApiVersions, true), }, }, } @@ -632,10 +909,10 @@ func TestPluginLoader_ExternalOverrideInternal(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[1], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[1], + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) @@ -656,13 +933,14 @@ func TestPluginLoader_Dispense_External(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: supportedApiVersions, Configs: []*config.PluginConfig{ { Name: plugin, Args: []string{"-plugin", "-name", plugin, - "-type", base.PluginTypeDevice, "-version", pluginVersion}, + "-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010}, Config: map[string]interface{}{ "res_key": expKey, }, @@ -694,11 +972,12 @@ func TestPluginLoader_Dispense_Internal(t *testing.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.ClientAgentConfig{ + expNomadConfig := &base.AgentConfig{ Driver: &base.ClientDriverConfig{ ClientMinPort: 100, }, @@ -707,14 +986,15 @@ func TestPluginLoader_Dispense_Internal(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: supportedApiVersions, InternalPlugins: map[PluginID]*InternalPluginConfig{ { Name: plugin, PluginType: base.PluginTypeDevice, }: { - Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, true), + Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true), Config: map[string]interface{}{ "res_key": expKey, }, @@ -741,6 +1021,7 @@ func TestPluginLoader_Dispense_Internal(t *testing.T) { 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) { @@ -758,13 +1039,14 @@ func TestPluginLoader_Dispense_NoConfigSchema_External(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + 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}, + "-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010}, Config: map[string]interface{}{ "res_key": expKey, }, @@ -797,6 +1079,7 @@ func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) { // Create two plugins plugin := "mock-device" pluginVersion := "v0.0.1" + pluginApiVersions := []string{device.ApiVersion010} h := newHarness(t, nil) defer h.cleanup() @@ -809,11 +1092,12 @@ func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) { PluginType: base.PluginTypeDevice, } lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: supportedApiVersions, InternalPlugins: map[PluginID]*InternalPluginConfig{ pid: { - Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, false), + Factory: mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, false), Config: map[string]interface{}{ "res_key": expKey, }, @@ -826,7 +1110,7 @@ func TestPluginLoader_Dispense_NoConfigSchema_Internal(t *testing.T) { require.Contains(err.Error(), "configuration not allowed") // Remove the config and try again - lconfig.InternalPlugins[pid].Factory = mockFactory(plugin, base.PluginTypeDevice, pluginVersion, true) + lconfig.InternalPlugins[pid].Factory = mockFactory(plugin, base.PluginTypeDevice, pluginVersion, pluginApiVersions, true) l, err := NewPluginLoader(lconfig) require.NoError(err) @@ -854,13 +1138,14 @@ func TestPluginLoader_Reattach_External(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: supportedApiVersions, Configs: []*config.PluginConfig{ { Name: plugin, Args: []string{"-plugin", "-name", plugin, - "-type", base.PluginTypeDevice, "-version", pluginVersion}, + "-type", base.PluginTypeDevice, "-version", pluginVersion, "-api-version", device.ApiVersion010}, Config: map[string]interface{}{ "res_key": expKey, }, @@ -914,8 +1199,9 @@ func TestPluginLoader_Bad_Executable(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + Logger: logger, + PluginDir: h.pluginDir(), + SupportedVersions: supportedApiVersions, Configs: []*config.PluginConfig{ { Name: plugin, @@ -956,13 +1242,14 @@ func TestPluginLoader_External_SkipBadFiles(t *testing.T) { logger := testlog.HCLogger(t) logger.SetLevel(log.Trace) lconfig := &PluginLoaderConfig{ - Logger: logger, - PluginDir: h.pluginDir(), + 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]}, + "-type", base.PluginTypeDevice, "-version", pluginVersions[0], "-api-version", device.ApiVersion010}, }, }, } @@ -980,11 +1267,55 @@ func TestPluginLoader_External_SkipBadFiles(t *testing.T) { expected := []*base.PluginInfoResponse{ { - Name: plugins[0], - Type: base.PluginTypeDevice, - PluginVersion: pluginVersions[0], - PluginApiVersion: "v0.1.0", + Name: plugins[0], + Type: base.PluginTypeDevice, + PluginVersion: pluginVersions[0], + PluginApiVersions: []string{device.ApiVersion010}, }, } require.EqualValues(expected, detected) } + +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]) + } + } + }) + } +} diff --git a/plugins/shared/loader/plugin_test.go b/plugins/shared/loader/plugin_test.go index ea9b129fd..d619dd249 100644 --- a/plugins/shared/loader/plugin_test.go +++ b/plugins/shared/loader/plugin_test.go @@ -15,20 +15,33 @@ import ( "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, configSchema); err != nil { + if err := pluginMain(name, pluginType, pluginVersion, pluginApiVersions, configSchema); err != nil { fmt.Println(err.Error()) os.Exit(1) } @@ -38,7 +51,7 @@ func TestMain(m *testing.M) { } // pluginMain starts a mock plugin using the passed parameters -func pluginMain(name, pluginType, version string, config bool) error { +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") @@ -55,6 +68,7 @@ func pluginMain(name, pluginType, version string, config bool) error { name: name, ptype: pluginType, version: version, + apiVersions: apiVersions, configSchema: config, } @@ -79,12 +93,13 @@ func pluginMain(name, pluginType, version string, config bool) error { // mockFactory returns a PluginFactory method which creates the mock plugin with // the passed parameters -func mockFactory(name, ptype, version string, configSchema bool) func(log log.Logger) interface{} { +func mockFactory(name, ptype, version string, apiVersions []string, configSchema bool) func(log log.Logger) interface{} { return func(log log.Logger) interface{} { return &mockPlugin{ name: name, ptype: ptype, version: version, + apiVersions: apiVersions, configSchema: configSchema, } } @@ -96,12 +111,18 @@ 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.ClientAgentConfig + 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 @@ -118,10 +139,10 @@ type mockPluginConfig struct { // building the mock plugin func (m *mockPlugin) PluginInfo() (*base.PluginInfoResponse, error) { return &base.PluginInfoResponse{ - Type: m.ptype, - PluginApiVersion: "v0.1.0", - PluginVersion: m.version, - Name: m.name, + Type: m.ptype, + PluginApiVersions: m.apiVersions, + PluginVersion: m.version, + Name: m.name, }, nil } @@ -141,14 +162,17 @@ func (m *mockPlugin) ConfigSchema() (*hclspec.Spec, error) { } // SetConfig decodes the configuration and stores it -func (m *mockPlugin) SetConfig(data []byte, cfg *base.ClientAgentConfig) error { +func (m *mockPlugin) SetConfig(c *base.Config) error { var config mockPluginConfig - if err := base.MsgPackDecode(data, &config); err != nil { - return err + if len(c.PluginConfig) != 0 { + if err := base.MsgPackDecode(c.PluginConfig, &config); err != nil { + return err + } } m.config = &config - m.nomadConfig = cfg + m.nomadConfig = c.AgentConfig + m.negotiatedApiVersion = c.ApiVersion return nil } diff --git a/plugins/shared/loader/testing.go b/plugins/shared/loader/testing.go index 501e33166..f811804ec 100644 --- a/plugins/shared/loader/testing.go +++ b/plugins/shared/loader/testing.go @@ -12,12 +12,12 @@ import ( // MockCatalog provides a mock PluginCatalog to be used for testing type MockCatalog struct { - DispenseF func(name, pluginType string, cfg *base.ClientAgentConfig, logger log.Logger) (PluginInstance, error) + DispenseF func(name, pluginType string, cfg *base.AgentConfig, logger log.Logger) (PluginInstance, error) ReattachF func(name, pluginType string, config *plugin.ReattachConfig) (PluginInstance, error) CatalogF func() map[string][]*base.PluginInfoResponse } -func (m *MockCatalog) Dispense(name, pluginType string, cfg *base.ClientAgentConfig, logger log.Logger) (PluginInstance, error) { +func (m *MockCatalog) Dispense(name, pluginType string, cfg *base.AgentConfig, logger log.Logger) (PluginInstance, error) { return m.DispenseF(name, pluginType, cfg, logger) } @@ -36,6 +36,7 @@ type MockInstance struct { ReattachConfigF func() (*plugin.ReattachConfig, bool) PluginF func() interface{} ExitedF func() bool + ApiVersionF func() string } func (m *MockInstance) Internal() bool { return m.InternalPlugin } @@ -43,11 +44,12 @@ func (m *MockInstance) Kill() { m.KillF func (m *MockInstance) ReattachConfig() (*plugin.ReattachConfig, bool) { return m.ReattachConfigF() } func (m *MockInstance) Plugin() interface{} { return m.PluginF() } func (m *MockInstance) Exited() bool { return m.ExitedF() } +func (m *MockInstance) ApiVersion() string { return m.ApiVersionF() } // MockBasicExternalPlugin returns a MockInstance that simulates an external // plugin returning it has been exited after kill is called. It returns the // passed inst as the plugin -func MockBasicExternalPlugin(inst interface{}) *MockInstance { +func MockBasicExternalPlugin(inst interface{}, apiVersion string) *MockInstance { var killedLock sync.Mutex killed := helper.BoolToPtr(false) return &MockInstance{ @@ -79,5 +81,7 @@ func MockBasicExternalPlugin(inst interface{}) *MockInstance { defer killedLock.Unlock() return *killed }, + + ApiVersionF: func() string { return apiVersion }, } } diff --git a/plugins/shared/singleton/singleton.go b/plugins/shared/singleton/singleton.go index 100e69070..260b092c5 100644 --- a/plugins/shared/singleton/singleton.go +++ b/plugins/shared/singleton/singleton.go @@ -50,7 +50,7 @@ func (s *SingletonLoader) Catalog() map[string][]*base.PluginInfoResponse { // Dispense returns the plugin given its name and type. This will also // configure the plugin. If there is an instance of an already running plugin, // this is used. -func (s *SingletonLoader) Dispense(name, pluginType string, config *base.ClientAgentConfig, logger log.Logger) (loader.PluginInstance, error) { +func (s *SingletonLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (loader.PluginInstance, error) { return s.getPlugin(false, name, pluginType, logger, config, nil) } @@ -62,7 +62,7 @@ func (s *SingletonLoader) Reattach(name, pluginType string, config *plugin.Reatt // getPlugin is a helper that either dispenses or reattaches to a plugin using // futures to ensure only a single instance is retrieved func (s *SingletonLoader) getPlugin(reattach bool, name, pluginType string, logger log.Logger, - nomadConfig *base.ClientAgentConfig, config *plugin.ReattachConfig) (loader.PluginInstance, error) { + nomadConfig *base.AgentConfig, config *plugin.ReattachConfig) (loader.PluginInstance, error) { // Lock the instance map to prevent races s.instanceLock.Lock() @@ -102,7 +102,7 @@ func (s *SingletonLoader) getPlugin(reattach bool, name, pluginType string, logg // dispense should be called in a go routine to not block and creates the // desired plugin, setting the results in the future. -func (s *SingletonLoader) dispense(f *future, name, pluginType string, config *base.ClientAgentConfig, logger log.Logger) { +func (s *SingletonLoader) dispense(f *future, name, pluginType string, config *base.AgentConfig, logger log.Logger) { i, err := s.loader.Dispense(name, pluginType, config, logger) f.set(i, err) } diff --git a/plugins/shared/singleton/singleton_test.go b/plugins/shared/singleton/singleton_test.go index 3151f8a84..e1f122513 100644 --- a/plugins/shared/singleton/singleton_test.go +++ b/plugins/shared/singleton/singleton_test.go @@ -27,7 +27,7 @@ func TestSingleton_Dispense(t *testing.T) { dispenseCalled := 0 s, c := harness(t) - c.DispenseF = func(_, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { p := &base.MockPlugin{} i := &loader.MockInstance{ ExitedF: func() bool { return false }, @@ -77,7 +77,7 @@ func TestSingleton_Dispense_Exit_Dispense(t *testing.T) { exited := false dispenseCalled := 0 s, c := harness(t) - c.DispenseF = func(_, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { p := &base.MockPlugin{} i := &loader.MockInstance{ ExitedF: func() bool { return exited }, @@ -125,7 +125,7 @@ func TestSingleton_DispenseError_Dispense(t *testing.T) { require := require.New(t) dispenseCalled := 0 - good := func(_, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + good := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { p := &base.MockPlugin{} i := &loader.MockInstance{ ExitedF: func() bool { return false }, @@ -135,7 +135,7 @@ func TestSingleton_DispenseError_Dispense(t *testing.T) { return i, nil } - bad := func(_, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + bad := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { dispenseCalled++ return nil, fmt.Errorf("bad") } @@ -169,7 +169,7 @@ func TestSingleton_ReattachError_Dispense(t *testing.T) { dispenseCalled, reattachCalled := 0, 0 s, c := harness(t) - c.DispenseF = func(_, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { p := &base.MockPlugin{} i := &loader.MockInstance{ ExitedF: func() bool { return false }, @@ -209,7 +209,7 @@ func TestSingleton_Reattach_Dispense(t *testing.T) { dispenseCalled, reattachCalled := 0, 0 s, c := harness(t) - c.DispenseF = func(_, _ string, _ *base.ClientAgentConfig, _ log.Logger) (loader.PluginInstance, error) { + c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) { dispenseCalled++ return nil, fmt.Errorf("bad") } diff --git a/vendor/github.com/hashicorp/go-version/constraint.go b/vendor/github.com/hashicorp/go-version/constraint.go index 8c73df060..d05575961 100644 --- a/vendor/github.com/hashicorp/go-version/constraint.go +++ b/vendor/github.com/hashicorp/go-version/constraint.go @@ -2,6 +2,7 @@ package version import ( "fmt" + "reflect" "regexp" "strings" ) @@ -113,6 +114,26 @@ func parseSingle(v string) (*Constraint, error) { }, nil } +func prereleaseCheck(v, c *Version) bool { + switch vPre, cPre := v.Prerelease() != "", c.Prerelease() != ""; { + case cPre && vPre: + // A constraint with a pre-release can only match a pre-release version + // with the same base segments. + return reflect.DeepEqual(c.Segments64(), v.Segments64()) + + case !cPre && vPre: + // A constraint without a pre-release can only match a version without a + // pre-release. + return false + + case cPre && !vPre: + // OK, except with the pessimistic operator + case !cPre && !vPre: + // OK + } + return true +} + //------------------------------------------------------------------- // Constraint functions //------------------------------------------------------------------- @@ -126,22 +147,27 @@ func constraintNotEqual(v, c *Version) bool { } func constraintGreaterThan(v, c *Version) bool { - return v.Compare(c) == 1 + return prereleaseCheck(v, c) && v.Compare(c) == 1 } func constraintLessThan(v, c *Version) bool { - return v.Compare(c) == -1 + return prereleaseCheck(v, c) && v.Compare(c) == -1 } func constraintGreaterThanEqual(v, c *Version) bool { - return v.Compare(c) >= 0 + return prereleaseCheck(v, c) && v.Compare(c) >= 0 } func constraintLessThanEqual(v, c *Version) bool { - return v.Compare(c) <= 0 + return prereleaseCheck(v, c) && v.Compare(c) <= 0 } func constraintPessimistic(v, c *Version) bool { + // Using a pessimistic constraint with a pre-release, restricts versions to pre-releases + if !prereleaseCheck(v, c) || (c.Prerelease() != "" && v.Prerelease() == "") { + return false + } + // If the version being checked is naturally less than the constraint, then there // is no way for the version to be valid against the constraint if v.LessThan(c) { diff --git a/vendor/github.com/hashicorp/go-version/version.go b/vendor/github.com/hashicorp/go-version/version.go index dfe509caa..4d1e6e221 100644 --- a/vendor/github.com/hashicorp/go-version/version.go +++ b/vendor/github.com/hashicorp/go-version/version.go @@ -15,8 +15,8 @@ var versionRegexp *regexp.Regexp // The raw regular expression string used for testing the validity // of a version. const VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` + - `(-?([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + - `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + + `(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` + + `(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` + `?` // Version represents a single version. @@ -25,6 +25,7 @@ type Version struct { pre string segments []int64 si int + original string } func init() { @@ -59,11 +60,17 @@ func NewVersion(v string) (*Version, error) { segments = append(segments, 0) } + pre := matches[7] + if pre == "" { + pre = matches[4] + } + return &Version{ - metadata: matches[7], - pre: matches[4], + metadata: matches[10], + pre: pre, segments: segments, si: si, + original: v, }, nil } @@ -166,14 +173,16 @@ func comparePart(preSelf string, preOther string) int { return 0 } + var selfInt int64 selfNumeric := true - _, err := strconv.ParseInt(preSelf, 10, 64) + selfInt, err := strconv.ParseInt(preSelf, 10, 64) if err != nil { selfNumeric = false } + var otherInt int64 otherNumeric := true - _, err = strconv.ParseInt(preOther, 10, 64) + otherInt, err = strconv.ParseInt(preOther, 10, 64) if err != nil { otherNumeric = false } @@ -197,7 +206,9 @@ func comparePart(preSelf string, preOther string) int { return -1 } else if !selfNumeric && otherNumeric { return 1 - } else if preSelf > preOther { + } else if !selfNumeric && !otherNumeric && preSelf > preOther { + return 1 + } else if selfInt > otherInt { return 1 } @@ -297,11 +308,19 @@ func (v *Version) Segments() []int { // for a version "1.2.3-beta", segments will return a slice of // 1, 2, 3. func (v *Version) Segments64() []int64 { - return v.segments + result := make([]int64, len(v.segments)) + copy(result, v.segments) + return result } // String returns the full version string included pre-release // and metadata information. +// +// This value is rebuilt according to the parsed segments and other +// information. Therefore, ambiguities in the version string such as +// prefixed zeroes (1.04.0 => 1.4.0), `v` prefix (v1.0.0 => 1.0.0), and +// missing parts (1.0 => 1.0.0) will be made into a canonicalized form +// as shown in the parenthesized examples. func (v *Version) String() string { var buf bytes.Buffer fmtParts := make([]string, len(v.segments)) @@ -320,3 +339,9 @@ func (v *Version) String() string { return buf.String() } + +// Original returns the original parsed version as-is, including any +// potential whitespace, `v` prefix, etc. +func (v *Version) Original() string { + return v.original +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 0b07d500f..2a27be25d 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -196,7 +196,7 @@ {"path":"github.com/hashicorp/go-sockaddr/template","checksumSHA1":"PDp9DVLvf3KWxhs4G4DpIwauMSU=","revision":"6d291a969b86c4b633730bfc6b8b9d64c3aafed9","revisionTime":"2018-03-20T11:50:54Z"}, {"path":"github.com/hashicorp/go-syslog","checksumSHA1":"xZ7Ban1x//6uUIU1xtrTbCYNHBc=","revision":"42a2b573b664dbf281bd48c3cc12c086b17a39ba"}, {"path":"github.com/hashicorp/go-uuid","checksumSHA1":"mAkPa/RLuIwN53GbwIEMATexams=","revision":"64130c7a86d732268a38cb04cfbaf0cc987fda98","revisionTime":"2016-07-17T02:21:40Z"}, - {"path":"github.com/hashicorp/go-version","checksumSHA1":"tUGxc7rfX0cmhOOUDhMuAZ9rWsA=","revision":"03c5bf6be031b6dd45afec16b1cf94fc8938bc77","revisionTime":"2017-02-02T08:07:59Z"}, + {"path":"github.com/hashicorp/go-version","checksumSHA1":"r0pj5dMHCghpaQZ3f1BRGoKiSWw=","revision":"b5a281d3160aa11950a6182bd9a9dc2cb1e02d50","revisionTime":"2018-08-24T00:43:55Z"}, {"path":"github.com/hashicorp/golang-lru","checksumSHA1":"d9PxF1XQGLMJZRct2R8qVM/eYlE=","revision":"a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4","revisionTime":"2016-02-07T21:47:19Z"}, {"path":"github.com/hashicorp/golang-lru/simplelru","checksumSHA1":"2nOpYjx8Sn57bqlZq17yM4YJuM4=","revision":"a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"}, {"path":"github.com/hashicorp/hcl","checksumSHA1":"8OPDk+bKyRGJoKcS4QNw9F7dpE8=","revision":"6e968a3fcdcbab092f5307fd0d85479d5af1e4dc","revisionTime":"2016-11-01T18:00:25Z"},