diff --git a/plugin.go b/plugin.go index 6063cc9..4ef91f3 100644 --- a/plugin.go +++ b/plugin.go @@ -12,8 +12,8 @@ import ( type PluginType string const ( - // SymbolName defines the symbol which will be loaded from the shared library. - SymbolName string = "Plugin" + // PluginSymbolName defines the symbol which will be loaded from the shared library. + PluginSymbolName string = "Plugin" OutputPlugin PluginType = "output" ) @@ -26,42 +26,45 @@ var ( ErrUnconfigurablePlugin = fmt.Errorf("plugin cannot be configured, does not implement the 'ConfigurablePlugin' interface") ) -type PluginMetadataProvider interface { - Name() string - Type() PluginType -} - -type ConfigurablePlugin interface { - // ConfigPointer must return a pointer (!) to the config struct of the plugin. - // The configuration will be unmarshalled using YAML into the given struct. - // Struct-tags can be used. - ConfigPointer() interface{} - // Configure is called after the configuration of the plugin is injected. - // The plugin can then configure whatever it needs. - Configure() error -} - -type Plugin interface { - PluginMetadataProvider - Run() error -} +type ( + // PluginConstructor is a function which needs to be exported with the symbol SymbolName by the plugins. + PluginConstructor func() Plugin + ConfigurablePlugin interface { + // ConfigPointer must return a pointer (!) to the config struct of the plugin. + // The configuration will be unmarshalled using YAML into the given struct. + // Struct-tags can be used. + ConfigPointer() interface{} + // Configure is called after the configuration of the plugin is injected. + // The plugin can then configure whatever it needs. + Configure() error + } + PluginMetadataProvider interface { + Name() string + Type() PluginType + } + Plugin interface { + PluginMetadataProvider + Run() error + } +) type PluginManager struct { - plugins map[PluginType]map[string]Plugin - configuredPlugins map[PluginType]map[string][]Plugin - pluginPaths map[PluginType]map[string]string + plugins map[PluginType]map[string]Plugin + configuredPlugins map[PluginType]map[string][]Plugin + pluginConstructors map[PluginType]map[string]PluginConstructor } func NewPluginManager() *PluginManager { pm := &PluginManager{ - plugins: make(map[PluginType]map[string]Plugin), - pluginPaths: make(map[PluginType]map[string]string), - configuredPlugins: make(map[PluginType]map[string][]Plugin), + plugins: make(map[PluginType]map[string]Plugin), + pluginConstructors: make(map[PluginType]map[string]PluginConstructor), + configuredPlugins: make(map[PluginType]map[string][]Plugin), } + // for every plugin type, create the sub-maps for _, t := range []PluginType{OutputPlugin} { pm.plugins[t] = make(map[string]Plugin) - pm.pluginPaths[t] = make(map[string]string) + pm.pluginConstructors[t] = make(map[string]PluginConstructor) pm.configuredPlugins[t] = make(map[string][]Plugin) } @@ -69,44 +72,48 @@ func NewPluginManager() *PluginManager { } func (pm *PluginManager) LoadPlugin(path string) error { - plugin, err := pm.loadPlugin(path) + pluginConstructor, err := pm.loadPluginConstructor(path) if err != nil { return err } + plugin := pluginConstructor() + if err := pm.validateMetadata(plugin); err != nil { + return err + } + if _, exists := pm.plugins[plugin.Type()][plugin.Name()]; exists { return fmt.Errorf("plugin %s already loaded", PluginID(plugin.Type(), plugin.Name())) } pm.plugins[plugin.Type()][plugin.Name()] = plugin + pm.pluginConstructors[plugin.Type()][plugin.Name()] = pluginConstructor return nil } -func (pm *PluginManager) loadPlugin(path string) (Plugin, error) { +func (pm *PluginManager) loadPluginConstructor(path string) (PluginConstructor, error) { p, err := plugin.Open(path) if err != nil { return nil, fmt.Errorf("unable to open plugin: %w", err) } - symbol, err := p.Lookup(SymbolName) + symbol, err := p.Lookup(PluginSymbolName) if err != nil { - return nil, fmt.Errorf("unable to find symbol '%s': %w", SymbolName, err) + return nil, fmt.Errorf("unable to find symbol '%s': %w", PluginSymbolName, err) } - loadedPlugin, validPlugin := symbol.(Plugin) - if !validPlugin { - return nil, fmt.Errorf("invalid plugin: does not implement Plugin interface") - } + a := symbol.(*PluginConstructor) + _ = a - if err := pm.validateMetadata(loadedPlugin); err != nil { - return nil, err + pluginConstructor, validPlugin := symbol.(*PluginConstructor) + if !validPlugin { + return nil, fmt.Errorf("invalid plugin: invalid symbol '%s', want=PluginConstructor, have=%T", PluginSymbolName, pluginConstructor) } - return loadedPlugin, nil + return *pluginConstructor, nil } -// TODO: maybe add a 'MustInitializePlugin' to check for the 'ConfigurePlugin' interface? func (pm *PluginManager) ConfigurePlugin(typ PluginType, name string, config data.Value) error { plugin, err := pm.GetPlugin(typ, name) if err != nil { @@ -123,18 +130,27 @@ func (pm *PluginManager) ConfigurePlugin(typ PluginType, name string, config dat // If the config is a slice, it means that we need to have multiple instances // of the plugin with different configs. - // WARN: how do we copy plugins? re-load? deep-copy? if config.IsSlice() { configs, _ := config.Slice() for _, c := range configs { - err = pm.unmarshalConfig(iplugin, data.NewValue(c)) + // create a new instance of the plugin and configure it using the data + plugin := pm.pluginConstructors[plugin.Type()][plugin.Name()]() + err = pm.unmarshalConfig(plugin.(ConfigurablePlugin), data.NewValue(c)) if err != nil { return err } - err = iplugin.Configure() + err = plugin.(ConfigurablePlugin).Configure() + if err != nil { + return fmt.Errorf("failed to configure plugin %s: %w", PluginID(plugin.Type(), plugin.Name()), err) + } + + pm.configuredPlugins[typ][name] = append(pm.configuredPlugins[typ][name], plugin) + + // TODO: remove + err = plugin.Run() if err != nil { - return fmt.Errorf("failed to initialize %s: %w", PluginID(plugin.Type(), plugin.Name()), err) + return err } } } @@ -150,7 +166,8 @@ func (pm *PluginManager) ConfigurePlugin(typ PluginType, name string, config dat if err != nil { return fmt.Errorf("failed to initialize %s: %w", PluginID(plugin.Type(), plugin.Name()), err) } - return nil + + pm.configuredPlugins[typ][name] = append(pm.configuredPlugins[typ][name], plugin) } return nil diff --git a/plugins/output/copy/main.go b/plugins/output/copy/main.go index 8098156..ef8dc69 100644 --- a/plugins/output/copy/main.go +++ b/plugins/output/copy/main.go @@ -1,20 +1,20 @@ package main import ( - "github.com/davecgh/go-spew/spew" + "fmt" + "io" + "os" + "path/filepath" + "github.com/lukasjarosch/skipper" "github.com/lukasjarosch/skipper/codec" "github.com/lukasjarosch/skipper/data" ) -// Plugin symbol which is loaded by skipper. -// If this does not exist, the plugin cannot be used. -var Plugin = Copy{} - type Config struct { SourcePaths []string `yaml:"sourcePaths"` TargetPath string `yaml:"targetPath"` - // OverwriteChmod os.FileMode `yaml:"overwriteChmod,omitempty"` + Overwrite bool `yaml:"overwrite"` } func NewConfig(raw map[string]interface{}) (Config, error) { @@ -34,80 +34,49 @@ func NewConfig(raw map[string]interface{}) (Config, error) { var ConfigPath = data.NewPath("config.plugins.output.copy") +// Plugin symbol which is loaded by skipper. +// If this does not exist, the plugin cannot be used. +var Plugin = NewCopy() + type Copy struct { config Config } -// ConfigPointer returns a pointer to [Config] which is going to be used by skipper -// to automatically load the config into said pointer, nice. -func (copy *Copy) ConfigPointer() interface{} { - return ©.config +func NewCopy() skipper.PluginConstructor { + return func() skipper.Plugin { + return &Copy{} + } } +// Configure checks that all configured source-paths exist and are readable. func (copy *Copy) Configure() error { - spew.Dump(copy.config) - // configValue, err := inventory.GetPath(ConfigPath) - // if err != nil { - // return fmt.Errorf("config path does not exist: %w", err) - // } - // - // // TODO: this should be done by the manager, not the plugin - // // The plugin supports multiple instances of its configuration - // switch typ := configValue.Raw.(type) { - // case map[string]interface{}: - // config, err := NewConfig(typ) - // if err != nil { - // return fmt.Errorf("cannot create config: %w", err) - // } - // copy.configs = append(copy.configs, config) - // break - // case []interface{}: - // for _, c := range typ { - // if _, ok := c.(map[string]interface{}); !ok { - // return fmt.Errorf("malformed config: expected slice of maps, got %T", c) - // } - // conf, err := NewConfig(c.(map[string]interface{})) - // if err != nil { - // return fmt.Errorf("cannot create config: %w", err) - // } - // copy.configs = append(copy.configs, conf) - // } - // break - // default: - // return fmt.Errorf("malformed config at '%s': either a slice of configs or a simple config is expected", ConfigPath) - // } - // - // // At ConfigPath there could be a slice of maps, indicating - // // multiple configurations of this plugin. - // if configs, ok := configValue.Raw.([]interface{}); ok { - // for _, c := range configs { - // if _, ok := c.(map[string]interface{}); !ok { - // return fmt.Errorf("malformed config: expected slice of maps, got %T", c) - // } - // conf, err := NewConfig(c.(map[string]interface{})) - // if err != nil { - // return fmt.Errorf("cannot create config: %w", err) - // } - // copy.configs = append(copy.configs, conf) - // } - // } - // - // // There could also be just one map with the plugin config - // else if c, ok := configValue.Raw.(map[string]interface{}); ok { - // config, err := NewConfig(c) - // if err != nil { - // return fmt.Errorf("cannot create config: %w", err) - // } - // copy.configs = append(copy.configs, config) - // } + // source-paths must be readable + for _, sourcePath := range copy.config.SourcePaths { + _, err := os.Open(sourcePath) + if err != nil { + return fmt.Errorf("cannot open source-path: %w", err) + } + } return nil } func (copy *Copy) Run() error { + for _, source := range copy.config.SourcePaths { + err := copyST(source, copy.config.TargetPath) + if err != nil { + return err + } + } return nil } +// ConfigPointer returns a pointer to [Config] which is going to be used by skipper +// to automatically load the config into said pointer, nice. +func (copy *Copy) ConfigPointer() interface{} { + return ©.config +} + func (copy *Copy) Name() string { return "copy" } @@ -115,3 +84,73 @@ func (copy *Copy) Name() string { func (copy *Copy) Type() skipper.PluginType { return skipper.OutputPlugin } + +// Copy recursively copies the contents of sourcePath to targetPath. +func copyST(sourcePath, targetPath string) error { + // Get the file information of the source + sourceInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + + // If the source is a directory, call the directory copy function + if sourceInfo.IsDir() { + return copyDir(sourcePath, targetPath) + } + + // If the source is a file, call the file copy function + return copyFile(sourcePath, targetPath) +} + +// copyDir copies the contents of a directory from sourcePath to targetPath. +func copyDir(sourcePath, targetPath string) error { + // Create the target directory with the same permissions as source directory + sourceInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + err = os.MkdirAll(targetPath, sourceInfo.Mode()) + if err != nil { + return err + } + + // Read the contents of the source directory + entries, err := os.ReadDir(sourcePath) + if err != nil { + return err + } + + // Recursively copy each entry in the source directory + for _, entry := range entries { + sourceEntryPath := filepath.Join(sourcePath, entry.Name()) + targetEntryPath := filepath.Join(targetPath, entry.Name()) + err = copyST(sourceEntryPath, targetEntryPath) + if err != nil { + return err + } + } + + return nil +} + +// copyFile copies a single file from sourcePath to targetPath. +func copyFile(sourcePath, targetPath string) error { + sourceFile, err := os.Open(sourcePath) + if err != nil { + return err + } + defer sourceFile.Close() + + targetFile, err := os.Create(targetPath) + if err != nil { + return err + } + defer targetFile.Close() + + _, err = io.Copy(targetFile, sourceFile) + if err != nil { + return err + } + + return nil +} diff --git a/testdata/plugins/output/inventory/config/plugins/output/copy.yaml b/testdata/plugins/output/inventory/config/plugins/output/copy.yaml index bc0f8e9..3c15f20 100644 --- a/testdata/plugins/output/inventory/config/plugins/output/copy.yaml +++ b/testdata/plugins/output/inventory/config/plugins/output/copy.yaml @@ -1,7 +1,8 @@ copy: - sourcePaths: - - foo - targetPath: /tmp/test - - sourcePaths: - - bar - targetPath: /tmp/ASDF + - /tmp/tmp.RwlmlBEstk + - /tmp/tmp.Axxtsgf6pd + targetPath: /tmp/test/ + # - sourcePaths: + # - bar + # targetPath: /tmp/ASDF