From fb76323c4d296df5a2424bd5daa63d480e9c4f40 Mon Sep 17 00:00:00 2001 From: nywilken Date: Tue, 7 Jan 2020 16:15:45 -0500 Subject: [PATCH] config: Fix loading external plugins from a packerconfig This change introduces a loadExternalComponent which can be used for loading a single plugin path. The function is a combination of the discoverSingle and discoverExternalComponents functions. --- config.go | 81 +++++++++++++++++++++++-- config_test.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 11 ++-- 3 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 config_test.go diff --git a/config.go b/config.go index 6270f466b..33e88505f 100644 --- a/config.go +++ b/config.go @@ -27,19 +27,88 @@ type config struct { DisableCheckpointSignature bool `json:"disable_checkpoint_signature"` PluginMinPort int PluginMaxPort int - - Builders packer.MapOfBuilder - Provisioners packer.MapOfProvisioner - PostProcessors packer.MapOfPostProcessor `json:"post-processors"` + RawBuilders map[string]string `json:"builders"` + RawProvisioners map[string]string `json:"provisioners"` + RawPostProcessors map[string]string `json:"post-processors"` + Builders packer.MapOfBuilder `json:"-"` + Provisioners packer.MapOfProvisioner `json:"-"` + PostProcessors packer.MapOfPostProcessor `json:"-"` } -// Decodes configuration in JSON format from the given io.Reader into +// decodeConfig decodes configuration in JSON format from the given io.Reader into // the config object pointed to. func decodeConfig(r io.Reader, c *config) error { decoder := json.NewDecoder(r) return decoder.Decode(c) } +// LoadExternalComponentsFromConfig loads plugins defined in RawBuilders, RawProvisioners, and RawPostProcessors. +func (c *config) LoadExternalComponentsFromConfig() { + // helper to build up list of plugin paths + extractPaths := func(m map[string]string) []string { + paths := make([]string, 0, len(m)) + for _, v := range m { + paths = append(paths, v) + } + + return paths + } + + var pluginPaths []string + pluginPaths = append(pluginPaths, extractPaths(c.RawProvisioners)...) + pluginPaths = append(pluginPaths, extractPaths(c.RawBuilders)...) + pluginPaths = append(pluginPaths, extractPaths(c.RawPostProcessors)...) + + var externallyUsed = make([]string, 0, len(pluginPaths)) + for _, pluginPath := range pluginPaths { + if name, ok := c.loadExternalComponent(pluginPath); ok { + log.Printf("[DEBUG] Loaded plugin: %s = %s", name, pluginPath) + externallyUsed = append(externallyUsed, name) + } + } + + if len(externallyUsed) > 0 { + sort.Strings(externallyUsed) + log.Printf("using external plugins %v", externallyUsed) + } +} + +func (c *config) loadExternalComponent(path string) (string, bool) { + pluginName := filepath.Base(path) + + // On Windows, ignore any plugins that don't end in .exe. + // We could do a full PATHEXT parse, but this is probably good enough. + if runtime.GOOS == "windows" && strings.ToLower(filepath.Ext(pluginName)) != ".exe" { + log.Printf("[DEBUG] Ignoring plugin %s, no exe extension", path) + return "", false + } + + // If the filename has a ".", trim up to there + if idx := strings.Index(pluginName, "."); idx >= 0 { + pluginName = pluginName[:idx] + } + + switch { + case strings.HasPrefix(pluginName, "packer-builder-"): + pluginName = pluginName[len("packer-builder-"):] + c.Builders[pluginName] = func() (packer.Builder, error) { + return c.pluginClient(path).Builder() + } + case strings.HasPrefix(pluginName, "packer-post-processor-"): + pluginName = pluginName[len("packer-post-processor-"):] + c.PostProcessors[pluginName] = func() (packer.PostProcessor, error) { + return c.pluginClient(path).PostProcessor() + } + case strings.HasPrefix(pluginName, "packer-provisioner-"): + pluginName = pluginName[len("packer-provisioner-"):] + c.Provisioners[pluginName] = func() (packer.Provisioner, error) { + return c.pluginClient(path).Provisioner() + } + } + + return pluginName, true +} + // Discover discovers plugins. // // Search the directory of the executable, then the plugins directory, and @@ -194,7 +263,7 @@ func (c *config) discoverSingle(glob string) (map[string]string, error) { for _, match := range matches { file := filepath.Base(match) - // One Windows, ignore any plugins that don't end in .exe. + // On Windows, ignore any plugins that don't end in .exe. // We could do a full PATHEXT parse, but this is probably good enough. if runtime.GOOS == "windows" && strings.ToLower(filepath.Ext(file)) != ".exe" { log.Printf( diff --git a/config_test.go b/config_test.go new file mode 100644 index 000000000..7a4e10172 --- /dev/null +++ b/config_test.go @@ -0,0 +1,161 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func TestDecodeConfig(t *testing.T) { + + packerConfig := ` + { + "PluginMinPort": 10, + "PluginMaxPort": 25, + "disable_checkpoint": true, + "disable_checkpoint_signature": true, + "provisioners": { + "super-shell": "packer-provisioner-super-shell" + } + }` + + var cfg config + err := decodeConfig(strings.NewReader(packerConfig), &cfg) + if err != nil { + t.Fatalf("error encountered decoding configuration: %v", err) + } + + var expectedCfg config + json.NewDecoder(strings.NewReader(packerConfig)).Decode(&expectedCfg) + if !reflect.DeepEqual(cfg, expectedCfg) { + t.Errorf("failed to load custom configuration data; expected %v got %v", expectedCfg, cfg) + } + +} + +func TestLoadExternalComponentsFromConfig(t *testing.T) { + packerConfigData, cleanUpFunc, err := generateFakePackerConfigData() + if err != nil { + t.Fatalf("error encountered while creating fake Packer configuration data %v", err) + } + defer cleanUpFunc() + + var cfg config + cfg.Builders = packer.MapOfBuilder{} + cfg.PostProcessors = packer.MapOfPostProcessor{} + cfg.Provisioners = packer.MapOfProvisioner{} + + if err := decodeConfig(strings.NewReader(packerConfigData), &cfg); err != nil { + t.Fatalf("error encountered decoding configuration: %v", err) + } + + cfg.LoadExternalComponentsFromConfig() + + if len(cfg.Builders) != 1 || !cfg.Builders.Has("cloud-xyz") { + t.Errorf("failed to load external builders; got %v as the resulting config", cfg.Builders) + } + + if len(cfg.Provisioners) != 1 || !cfg.Provisioners.Has("super-shell") { + t.Errorf("failed to load external provisioners; got %v as the resulting config", cfg.Provisioners) + } + + if len(cfg.PostProcessors) != 1 || !cfg.PostProcessors.Has("noop") { + t.Errorf("failed to load external post-processors; got %v as the resulting config", cfg.PostProcessors) + } +} + +func TestLoadExternalComponentsFromConfig_onlyProvisioner(t *testing.T) { + packerConfigData, cleanUpFunc, err := generateFakePackerConfigData() + if err != nil { + t.Fatalf("error encountered while creating fake Packer configuration data %v", err) + } + defer cleanUpFunc() + + var cfg config + cfg.Provisioners = packer.MapOfProvisioner{} + + if err := decodeConfig(strings.NewReader(packerConfigData), &cfg); err != nil { + t.Fatalf("error encountered decoding configuration: %v", err) + } + + /* Let's clear out any custom Builders or PostProcessors that were part of the config. + This step does not remove them from disk, it just removes them from of plugins Packer knows about. + */ + cfg.RawBuilders = nil + cfg.RawPostProcessors = nil + + cfg.LoadExternalComponentsFromConfig() + + if len(cfg.Builders) != 0 || cfg.Builders.Has("cloud-xyz") { + t.Errorf("loaded external builders when it wasn't supposed to; got %v as the resulting config", cfg.Builders) + } + + if len(cfg.Provisioners) != 1 || !cfg.Provisioners.Has("super-shell") { + t.Errorf("failed to load external provisioners; got %v as the resulting config", cfg.Provisioners) + } + + if len(cfg.PostProcessors) != 0 || cfg.PostProcessors.Has("noop") { + t.Errorf("loaded external post-processors when it wasn't supposed to; got %v as the resulting config", cfg.PostProcessors) + } +} + +/* generateFakePackerConfigData creates a collection of mock plugins along with a basic packerconfig. +The return packerConfigData is a valid packerconfig file that can be used for configuring external plugins, cleanUpFunc is a function that should be called for cleaning up any generated mock data. +This function will only clean up if there is an error, on successful runs the caller +is responsible for cleaning up the data via cleanUpFunc(). +*/ +func generateFakePackerConfigData() (packerConfigData string, cleanUpFunc func(), err error) { + dir, err := ioutil.TempDir("", "random-testdata") + if err != nil { + return "", nil, fmt.Errorf("failed to create temporary test directory: %v", err) + } + + cleanUpFunc = func() { + os.RemoveAll(dir) + } + + var suffix string + if runtime.GOOS == "windows" { + suffix = ".exe" + } + + plugins := [...]string{ + filepath.Join(dir, "packer-builder-cloud-xyz"+suffix), + filepath.Join(dir, "packer-provisioner-super-shell"+suffix), + filepath.Join(dir, "packer-post-processor-noop"+suffix), + } + for _, plugin := range plugins { + _, err := os.Create(plugin) + if err != nil { + cleanUpFunc() + return "", nil, fmt.Errorf("failed to create temporary plugin file (%s): %v", plugin, err) + } + } + + packerConfigData = fmt.Sprintf(` + { + "PluginMinPort": 10, + "PluginMaxPort": 25, + "disable_checkpoint": true, + "disable_checkpoint_signature": true, + "builders": { + "cloud-xyz": %q + }, + "provisioners": { + "super-shell": %q + }, + "post-processors": { + "noop": %q + } + }`, plugins[0], plugins[1], plugins[2]) + + return +} diff --git a/main.go b/main.go index bed90598c..9d13a7f9a 100644 --- a/main.go +++ b/main.go @@ -299,13 +299,14 @@ func loadConfig() (*config, error) { return nil, err } + // start by loading from PACKER_CONFIG if available + log.Print("Checking 'PACKER_CONFIG' for a config file path") configFilePath := os.Getenv("PACKER_CONFIG") - if configFilePath != "" { - log.Printf("'PACKER_CONFIG' set, loading config from environment.") - } else { + + if configFilePath == "" { var err error + log.Print("'PACKER_CONFIG' not set; checking the default config file path") configFilePath, err = packer.ConfigFile() - if err != nil { log.Printf("Error detecting default config file path: %s", err) } @@ -331,6 +332,8 @@ func loadConfig() (*config, error) { return nil, err } + config.LoadExternalComponentsFromConfig() + return &config, nil }