diff --git a/config.go b/config.go index 1f47de5a5..43293c04f 100644 --- a/config.go +++ b/config.go @@ -130,7 +130,7 @@ func (c *config) Discover() error { return nil } - // First, look in the same directory as the executable. + // 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) @@ -140,7 +140,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) @@ -155,6 +155,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 { diff --git a/config_test.go b/config_test.go index 580854fa7..30e8ff02c 100644 --- a/config_test.go +++ b/config_test.go @@ -12,8 +12,107 @@ 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.") + } + defer os.Remove(decoyDir) + + 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 := ` @@ -145,18 +244,13 @@ func TestLoadSingleComponent(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) } @@ -165,19 +259,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, 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