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.
This commit is contained in:
nywilken 2020-01-07 16:15:45 -05:00
parent 0e177915f8
commit fb76323c4d
3 changed files with 244 additions and 11 deletions

View File

@ -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(

161
config_test.go Normal file
View File

@ -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
}

13
main.go
View File

@ -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 {
var err error
configFilePath, err = packer.ConfigFile()
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
}