From 83ecebbf378adb55cda8e55ac8c3961347e49696 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 16 Jan 2020 13:02:31 -0800 Subject: [PATCH 1/4] add PACKER_PLUGIN_PATH for plugin discovery --- config.go | 20 +++++++++++++++++-- website/source/docs/extending/plugins.html.md | 9 +++++++++ .../docs/other/environment-variables.html.md | 8 ++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index 33e88505f..715ce03f5 100644 --- a/config.go +++ b/config.go @@ -123,7 +123,23 @@ func (c *config) Discover() error { return nil } - // First, look in the same directory as the executable. + // First, check whether there is a custom Plugin directory defined. This gets + // absolute preference. + if packerPluginPath := os.Getenv("PACKER_PLUGIN_PATH"); packerPluginPath != "" { + sep := ":" + if runtime.GOOS == "windows" { + // on windows, PATH is semicolon-separated + sep = ";" + } + plugPaths := strings.Split(packerPluginPath, sep) + for _, plugPath := range plugPaths { + if err := c.discoverExternalComponents(plugPath); err != nil { + return err + } + } + } + + // Next, look in the same directory as the executable. exePath, err := osext.Executable() if err != nil { log.Printf("[ERR] Error loading exe directory: %s", err) @@ -133,7 +149,7 @@ func (c *config) Discover() error { } } - // Next, look in the plugins directory. + // Next, look in the default plugins directory inside the configdir/.packer.d/plugins. dir, err := packer.ConfigDir() if err != nil { log.Printf("[ERR] Error loading config directory: %s", err) diff --git a/website/source/docs/extending/plugins.html.md b/website/source/docs/extending/plugins.html.md index 24d337c43..995830b62 100644 --- a/website/source/docs/extending/plugins.html.md +++ b/website/source/docs/extending/plugins.html.md @@ -51,6 +51,7 @@ Once the plugin is named properly, Packer automatically discovers plugins in the following directories in the given order. If a conflicting plugin is found later, it will take precedence over one found earlier. + 1. The directory where `packer` is, or the executable directory. 2. The `$HOME/.packer.d/plugins` directory, if `$HOME` is defined (unix) @@ -62,6 +63,14 @@ later, it will take precedence over one found earlier. 5. The current working directory. +6. The directory defined in the env var `PACKER_PLUGIN_PATH`. There can be more +than one directory defined; for example, `~/custom-dir-1:~/custom-dir-2`. +Separate directories in the PATH string using a colon (`:`) on posix systems and +a semicolon (`;`) on windows systems. The above example path would be able to +find a provisioner named `packer-provisioner-foo` in either +`~/custom-dir-1/packer-provisioner-foo` or +`~/custom-dir-2/packer-provisioner-foo`. + The valid types for plugins are: - `builder` - Plugins responsible for building images for a specific diff --git a/website/source/docs/other/environment-variables.html.md b/website/source/docs/other/environment-variables.html.md index 257336482..eaebae6ec 100644 --- a/website/source/docs/other/environment-variables.html.md +++ b/website/source/docs/other/environment-variables.html.md @@ -39,6 +39,14 @@ each can be found below: connections on your local host. The default is 10,000. See the [core configuration page](/docs/other/core-configuration.html). +- `PACKER_PLUGIN_PATH` - a PATH variable for finding third-party packer +plugins. For example: `~/custom-dir-1:~/custom-dir-2`. +Separate directories in the PATH string using a colon (`:`) on posix systems and +a semicolon (`;`) on windows systems. The above example path would be able to +find a provisioner named `packer-provisioner-foo` in either +`~/custom-dir-1/packer-provisioner-foo` or +`~/custom-dir-2/packer-provisioner-foo`. + - `CHECKPOINT_DISABLE` - When Packer is invoked it sometimes calls out to [checkpoint.hashicorp.com](https://checkpoint.hashicorp.com/) to look for new versions of Packer. If you want to disable this for security or privacy From a163ff738886933c652bc4f60ebe7f7d1887d2ea Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 17 Jan 2020 12:40:30 -0800 Subject: [PATCH 2/4] swap ordering so PACKER_PLUGIN_PATH gets precedence --- config.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/config.go b/config.go index 715ce03f5..4dbb4b330 100644 --- a/config.go +++ b/config.go @@ -123,22 +123,6 @@ func (c *config) Discover() error { return nil } - // First, check whether there is a custom Plugin directory defined. This gets - // absolute preference. - if packerPluginPath := os.Getenv("PACKER_PLUGIN_PATH"); packerPluginPath != "" { - sep := ":" - if runtime.GOOS == "windows" { - // on windows, PATH is semicolon-separated - sep = ";" - } - plugPaths := strings.Split(packerPluginPath, sep) - for _, plugPath := range plugPaths { - if err := c.discoverExternalComponents(plugPath); err != nil { - return err - } - } - } - // Next, look in the same directory as the executable. exePath, err := osext.Executable() if err != nil { @@ -164,6 +148,22 @@ func (c *config) Discover() error { return err } + // Check whether there is a custom Plugin directory defined. This gets + // absolute preference. + if packerPluginPath := os.Getenv("PACKER_PLUGIN_PATH"); packerPluginPath != "" { + sep := ":" + if runtime.GOOS == "windows" { + // on windows, PATH is semicolon-separated + sep = ";" + } + plugPaths := strings.Split(packerPluginPath, sep) + for _, plugPath := range plugPaths { + if err := c.discoverExternalComponents(plugPath); err != nil { + return err + } + } + } + // Finally, try to use an internal plugin. Note that this will not override // any previously-loaded plugins. if err := c.discoverInternalComponents(); err != nil { From 69e7d39fd3261f37e00c396e1c5d627a33e5154a Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 27 Jan 2020 16:19:03 -0800 Subject: [PATCH 3/4] add some tests for packer_plugin_path --- config_test.go | 144 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 127 insertions(+), 17 deletions(-) diff --git a/config_test.go b/config_test.go index 7a4e10172..79f16de13 100644 --- a/config_test.go +++ b/config_test.go @@ -12,8 +12,106 @@ import ( "testing" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/packer/plugin" ) +func newConfig() config { + var conf config + conf.PluginMinPort = 10000 + conf.PluginMaxPort = 25000 + conf.Builders = packer.MapOfBuilder{} + conf.PostProcessors = packer.MapOfPostProcessor{} + conf.Provisioners = packer.MapOfProvisioner{} + + return conf +} +func TestDiscoverReturnsIfMagicCookieSet(t *testing.T) { + config := newConfig() + + os.Setenv(plugin.MagicCookieKey, plugin.MagicCookieValue) + defer os.Unsetenv(plugin.MagicCookieKey) + + err := config.Discover() + if err != nil { + t.Fatalf("Should not have errored: %s", err) + } + + if len(config.Builders) != 0 { + t.Fatalf("Should not have tried to find builders") + } +} + +func TestEnvVarPackerPluginPath(t *testing.T) { + // Create a temporary directory to store plugins in + dir, _, cleanUpFunc, err := generateFakePlugins("custom_plugin_dir", + []string{"packer-provisioner-partyparrot"}) + if err != nil { + t.Fatalf("Error creating fake custom plugins: %s", err) + } + + defer cleanUpFunc() + + // Add temp dir to path. + os.Setenv("PACKER_PLUGIN_PATH", dir) + defer os.Unsetenv("PACKER_PLUGIN_PATH") + + config := newConfig() + + err = config.Discover() + if err != nil { + t.Fatalf("Should not have errored: %s", err) + } + + if len(config.Provisioners) == 0 { + t.Fatalf("Should have found partyparrot provisioner") + } + if _, ok := config.Provisioners["partyparrot"]; !ok { + t.Fatalf("Should have found partyparrot provisioner.") + } +} + +func TestEnvVarPackerPluginPath_MultiplePaths(t *testing.T) { + // Create a temporary directory to store plugins in + dir, _, cleanUpFunc, err := generateFakePlugins("custom_plugin_dir", + []string{"packer-provisioner-partyparrot"}) + if err != nil { + t.Fatalf("Error creating fake custom plugins: %s", err) + } + + defer cleanUpFunc() + + pathsep := ":" + if runtime.GOOS == "windows" { + pathsep = ";" + } + + // Create a second dir to look in that will be empty + decoyDir, err := ioutil.TempDir("", "decoy") + if err != nil { + t.Fatalf("Failed to create a temporary test dir.") + } + + pluginPath := dir + pathsep + decoyDir + + // Add temp dir to path. + os.Setenv("PACKER_PLUGIN_PATH", pluginPath) + defer os.Unsetenv("PACKER_PLUGIN_PATH") + + config := newConfig() + + err = config.Discover() + if err != nil { + t.Fatalf("Should not have errored: %s", err) + } + + if len(config.Provisioners) == 0 { + t.Fatalf("Should have found partyparrot provisioner") + } + if _, ok := config.Provisioners["partyparrot"]; !ok { + t.Fatalf("Should have found partyparrot provisioner.") + } +} + func TestDecodeConfig(t *testing.T) { packerConfig := ` @@ -107,18 +205,13 @@ func TestLoadExternalComponentsFromConfig_onlyProvisioner(t *testing.T) { } } -/* 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") +func generateFakePlugins(dirname string, pluginNames []string) (string, []string, func(), error) { + dir, err := ioutil.TempDir("", dirname) if err != nil { - return "", nil, fmt.Errorf("failed to create temporary test directory: %v", err) + return "", nil, nil, fmt.Errorf("failed to create temporary test directory: %v", err) } - cleanUpFunc = func() { + cleanUpFunc := func() { os.RemoveAll(dir) } @@ -127,19 +220,36 @@ func generateFakePackerConfigData() (packerConfigData string, cleanUpFunc func() 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) + plugins := make([]string, len(pluginNames)) + for i, plugin := range pluginNames { + plug := filepath.Join(dir, plugin+suffix) + plugins[i] = plug + _, err := os.Create(plug) if err != nil { cleanUpFunc() - return "", nil, fmt.Errorf("failed to create temporary plugin file (%s): %v", plugin, err) + return "", nil, nil, fmt.Errorf("failed to create temporary plugin file (%s): %v", plug, err) } } + return dir, plugins, cleanUpFunc, nil +} + +/* 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) { + _, plugins, cleanUpFunc, err := generateFakePlugins("random-testdata", + []string{"packer-builder-cloud-xyz", + "packer-provisioner-super-shell", + "packer-post-processor-noop"}) + + if err != nil { + cleanUpFunc() + return "", nil, err + } + packerConfigData = fmt.Sprintf(` { "PluginMinPort": 10, From 6eda9504c96532608eb419bbdc7f084741c9b4b7 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 27 Jan 2020 16:20:59 -0800 Subject: [PATCH 4/4] remove second dir --- config_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config_test.go b/config_test.go index 79f16de13..1a342b46c 100644 --- a/config_test.go +++ b/config_test.go @@ -90,6 +90,7 @@ func TestEnvVarPackerPluginPath_MultiplePaths(t *testing.T) { if err != nil { t.Fatalf("Failed to create a temporary test dir.") } + defer os.Remove(decoyDir) pluginPath := dir + pathsep + decoyDir