packer-cn/packer/plugin/discover_test.go

370 lines
9.5 KiB
Go

package plugin
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
"github.com/hashicorp/packer-plugin-sdk/tmp"
)
func newConfig() Config {
var conf Config
conf.PluginMinPort = 10000
conf.PluginMaxPort = 25000
return conf
}
func TestDiscoverReturnsIfMagicCookieSet(t *testing.T) {
config := newConfig()
os.Setenv(pluginsdk.MagicCookieKey, pluginsdk.MagicCookieValue)
defer os.Unsetenv(pluginsdk.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 generateFakePlugins(dirname string, pluginNames []string) (string, []string, func(), error) {
dir, err := ioutil.TempDir("", dirname)
if err != nil {
return "", nil, 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 := 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, nil, fmt.Errorf("failed to create temporary plugin file (%s): %v", plug, err)
}
}
return dir, plugins, cleanUpFunc, nil
}
// TestHelperProcess isn't a real test. It's used as a helper process
// for multiplugin-binary tests.
func TestHelperPlugins(t *testing.T) {
if os.Getenv("PKR_WANT_TEST_PLUGINS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
pluginName, args := args[0], args[1:]
allMocks := []map[string]pluginsdk.Set{mockPlugins, defaultNameMock, doubleDefaultMock, badDefaultNameMock}
for _, mock := range allMocks {
plugin, found := mock[pluginName]
if found {
err := plugin.RunCommand(args...)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
os.Exit(0)
}
}
fmt.Fprintf(os.Stderr, "No %q plugin found\n", pluginName)
os.Exit(2)
}
// HasExec reports whether the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
func HasExec() bool {
switch runtime.GOOS {
case "js":
return false
case "darwin":
if runtime.GOARCH == "arm64" {
return false
}
case "windows":
// TODO(azr): Fix this once versioning is added and we know more
return false
}
return true
}
// MustHaveExec checks that the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
// If not, MustHaveExec calls t.Skip with an explanation.
func MustHaveExec(t testing.TB) {
if !HasExec() {
t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
}
}
func MustHaveCommand(t testing.TB, cmd string) string {
path, err := exec.LookPath(cmd)
if err != nil {
t.Skipf("skipping test: cannot find the %q command: %v", cmd, err)
}
return path
}
func helperCommand(t *testing.T, s ...string) []string {
MustHaveExec(t)
cmd := []string{os.Args[0], "-test.run=TestHelperPlugins", "--"}
return append(cmd, s...)
}
func createMockPlugins(t *testing.T, plugins map[string]pluginsdk.Set) {
pluginDir, err := tmp.Dir("pkr-multiplugin-test-*")
{
// create an exectutable file with a `sh` sheebang
// this file will look like:
// #!/bin/sh
// PKR_WANT_TEST_PLUGINS=1 ...plugin/debug.test -test.run=TestHelperPlugins -- bird $@
// 'bird' is the mock plugin we want to start
// $@ just passes all passed arguments
// This will allow to run the fake plugin from go tests which in turn
// will run go tests callback to `TestHelperPlugins`, this one will be
// transparently calling our mock multiplugins `mockPlugins`.
if err != nil {
t.Fatal(err)
}
t.Logf("putting temporary mock plugins in %s", pluginDir)
shPath := MustHaveCommand(t, "bash")
for name := range plugins {
plugin := path.Join(pluginDir, "packer-plugin-"+name)
fileContent := ""
fileContent = fmt.Sprintf("#!%s\n", shPath)
fileContent += strings.Join(
append([]string{"PKR_WANT_TEST_PLUGINS=1"}, helperCommand(t, name, "$@")...),
" ")
if err := ioutil.WriteFile(plugin, []byte(fileContent), os.ModePerm); err != nil {
t.Fatalf("failed to create fake plugin binary: %v", err)
}
}
}
os.Setenv("PACKER_PLUGIN_PATH", pluginDir)
}
var (
mockPlugins = map[string]pluginsdk.Set{
"bird": pluginsdk.Set{
Builders: map[string]packersdk.Builder{
"feather": nil,
"guacamole": nil,
},
},
"chimney": pluginsdk.Set{
PostProcessors: map[string]packersdk.PostProcessor{
"smoke": nil,
},
},
}
defaultNameMock = map[string]pluginsdk.Set{
"foo": pluginsdk.Set{
Builders: map[string]packersdk.Builder{
"bar": nil,
"baz": nil,
pluginsdk.DEFAULT_NAME: nil,
},
},
}
doubleDefaultMock = map[string]pluginsdk.Set{
"yolo": pluginsdk.Set{
Builders: map[string]packersdk.Builder{
"bar": nil,
"baz": nil,
pluginsdk.DEFAULT_NAME: nil,
},
PostProcessors: map[string]packersdk.PostProcessor{
pluginsdk.DEFAULT_NAME: nil,
},
},
}
badDefaultNameMock = map[string]pluginsdk.Set{
"foo": pluginsdk.Set{
Builders: map[string]packersdk.Builder{
"bar": nil,
"baz": nil,
pluginsdk.DEFAULT_NAME: nil,
},
},
}
)
func Test_multiplugin_describe(t *testing.T) {
createMockPlugins(t, mockPlugins)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := Config{}
err := c.Discover()
if err != nil {
t.Fatalf("error discovering plugins; %s", err.Error())
}
for mockPluginName, plugin := range mockPlugins {
for mockBuilderName := range plugin.Builders {
expectedBuilderName := mockPluginName + "-" + mockBuilderName
if _, found := c.builders[expectedBuilderName]; !found {
t.Fatalf("expected to find builder %q; got %#v", expectedBuilderName, c.builders)
}
}
for mockProvisionerName := range plugin.Provisioners {
expectedProvisionerName := mockPluginName + "-" + mockProvisionerName
if _, found := c.provisioners[expectedProvisionerName]; !found {
t.Fatalf("expected to find builder %q", expectedProvisionerName)
}
}
for mockPostProcessorName := range plugin.PostProcessors {
expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName
if _, found := c.postProcessors[expectedPostProcessorName]; !found {
t.Fatalf("expected to find post-processor %q", expectedPostProcessorName)
}
}
}
}
func Test_multiplugin_defaultName(t *testing.T) {
createMockPlugins(t, defaultNameMock)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := Config{}
err := c.Discover()
if err != nil {
t.Fatalf("error discovering plugins; %s ; mocks are %#v", err.Error(), defaultNameMock)
}
expectedBuilderNames := []string{"foo-bar", "foo-baz", "foo"}
for _, mockBuilderName := range expectedBuilderNames {
if _, found := c.builders[mockBuilderName]; !found {
t.Fatalf("expected to find builder %q; builders is %#v", mockBuilderName, c.builders)
}
}
}
func Test_only_one_multiplugin_defaultName_each_plugin_type(t *testing.T) {
createMockPlugins(t, doubleDefaultMock)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := Config{}
err := c.Discover()
if err != nil {
t.Fatal("Should not have error because pluginsdk.DEFAULT_NAME is used twice but only once per plugin type.")
}
}