You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
packer/packer/plugin_discover_test.go

551 lines
18 KiB

// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package packer
import (
"crypto/sha256"
"fmt"
"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"
"github.com/hashicorp/packer-plugin-sdk/version"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
)
func newPluginConfig() PluginConfig {
var conf PluginConfig
conf.PluginMinPort = 10000
conf.PluginMaxPort = 25000
return conf
}
func TestDiscoverReturnsIfMagicCookieSet(t *testing.T) {
config := newPluginConfig()
t.Setenv(pluginsdk.MagicCookieKey, pluginsdk.MagicCookieValue)
err := config.Discover()
if err != nil {
t.Fatalf("Should not have errored: %s", err)
}
if len(config.Builders.List()) != 0 {
t.Fatalf("Should not have tried to find builders")
}
}
func TestMultiPlugin_describe(t *testing.T) {
createMockPlugins(t, mockPlugins)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := PluginConfig{}
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 !c.Builders.Has(expectedBuilderName) {
t.Errorf("expected to find builder %q", expectedBuilderName)
}
}
for mockProvisionerName := range plugin.Provisioners {
expectedProvisionerName := mockPluginName + "-" + mockProvisionerName
if !c.Provisioners.Has(expectedProvisionerName) {
t.Errorf("expected to find builder %q", expectedProvisionerName)
}
}
for mockPostProcessorName := range plugin.PostProcessors {
expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName
if !c.PostProcessors.Has(expectedPostProcessorName) {
t.Errorf("expected to find post-processor %q", expectedPostProcessorName)
}
}
for mockDatasourceName := range plugin.Datasources {
expectedDatasourceName := mockPluginName + "-" + mockDatasourceName
if !c.DataSources.Has(expectedDatasourceName) {
t.Errorf("expected to find datasource %q", expectedDatasourceName)
}
}
}
}
func TestMultiPlugin_describe_installed(t *testing.T) {
createMockInstalledPlugins(t, mockInstalledPlugins, createMockChecksumFile)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := PluginConfig{}
err := c.Discover()
if err != nil {
t.Fatalf("error discovering plugins; %s", err.Error())
}
for mockPluginName, plugin := range mockInstalledPlugins {
mockPluginName = strings.Split(mockPluginName, "_")[0]
for mockBuilderName := range plugin.Builders {
expectedBuilderName := mockPluginName + "-" + mockBuilderName
if !c.Builders.Has(expectedBuilderName) {
t.Fatalf("expected to find builder %q", expectedBuilderName)
}
}
for mockProvisionerName := range plugin.Provisioners {
expectedProvisionerName := mockPluginName + "-" + mockProvisionerName
if !c.Provisioners.Has(expectedProvisionerName) {
t.Fatalf("expected to find builder %q", expectedProvisionerName)
}
}
for mockPostProcessorName := range plugin.PostProcessors {
expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName
if !c.PostProcessors.Has(expectedPostProcessorName) {
t.Fatalf("expected to find post-processor %q", expectedPostProcessorName)
}
}
for mockDatasourceName := range plugin.Datasources {
expectedDatasourceName := mockPluginName + "-" + mockDatasourceName
if !c.DataSources.Has(expectedDatasourceName) {
t.Fatalf("expected to find datasource %q", expectedDatasourceName)
}
}
}
}
func TestMultiPlugin_describe_installed_for_invalid(t *testing.T) {
tc := []struct {
desc string
installedPluginsMock map[string]pluginsdk.Set
createMockFn func(*testing.T, map[string]pluginsdk.Set)
}{
{
desc: "Incorrectly named plugins",
installedPluginsMock: invalidInstalledPluginsMock,
createMockFn: func(t *testing.T, mocks map[string]pluginsdk.Set) {
createMockInstalledPlugins(t, mocks, createMockChecksumFile)
},
},
{
desc: "Plugins missing checksums",
installedPluginsMock: mockInstalledPlugins,
createMockFn: func(t *testing.T, mocks map[string]pluginsdk.Set) {
createMockInstalledPlugins(t, mocks)
},
},
}
for _, tt := range tc {
t.Run(tt.desc, func(t *testing.T) {
tt.createMockFn(t, tt.installedPluginsMock)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := PluginConfig{}
err := c.Discover()
if err != nil {
t.Fatalf("error discovering plugins; %s", err.Error())
}
if c.Builders.Has("feather") {
t.Fatalf("expected to not find builder %q", "feather")
}
for mockPluginName, plugin := range tt.installedPluginsMock {
mockPluginName = strings.Split(mockPluginName, "_")[0]
for mockBuilderName := range plugin.Builders {
expectedBuilderName := mockPluginName + "-" + mockBuilderName
if c.Builders.Has(expectedBuilderName) {
t.Fatalf("expected to not find builder %q", expectedBuilderName)
}
}
for mockProvisionerName := range plugin.Provisioners {
expectedProvisionerName := mockPluginName + "-" + mockProvisionerName
if c.Provisioners.Has(expectedProvisionerName) {
t.Fatalf("expected to not find builder %q", expectedProvisionerName)
}
}
for mockPostProcessorName := range plugin.PostProcessors {
expectedPostProcessorName := mockPluginName + "-" + mockPostProcessorName
if c.PostProcessors.Has(expectedPostProcessorName) {
t.Fatalf("expected to not find post-processor %q", expectedPostProcessorName)
}
}
for mockDatasourceName := range plugin.Datasources {
expectedDatasourceName := mockPluginName + "-" + mockDatasourceName
if c.DataSources.Has(expectedDatasourceName) {
t.Fatalf("expected to not find datasource %q", expectedDatasourceName)
}
}
}
})
}
}
func TestMultiPlugin_defaultName(t *testing.T) {
createMockPlugins(t, defaultNameMock)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := PluginConfig{}
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 !c.Builders.Has(mockBuilderName) {
t.Fatalf("expected to find builder %q; builders is %#v", mockBuilderName, c.Builders)
}
}
}
func TestMultiPlugin_IgnoreChecksumFile(t *testing.T) {
createMockPlugins(t, defaultNameMock)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
fooPluginName := fmt.Sprintf("packer-plugin-foo_v1.0.0_x5.0_%s_%s", runtime.GOOS, runtime.GOARCH)
fooPluginPath := filepath.Join(pluginDir, "github.com", "hashicorp", "foo", fooPluginName)
csFile, err := generateMockChecksumFile(fooPluginPath)
if err != nil {
t.Fatal(err.Error())
}
// Copy plugin contents into checksum file to validate that it is not only skipped but that it never gets loaded
if err := os.Rename(fooPluginPath, csFile); err != nil {
t.Fatalf("failed to rename plugin bin file to checkfum file needed for test: %s", err)
}
c := PluginConfig{}
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 c.Builders.Has(mockBuilderName) {
t.Fatalf("expected to not find builder %q; builders is %#v", mockBuilderName, c.Builders)
}
}
}
func TestMultiPlugin_defaultName_each_plugin_type(t *testing.T) {
createMockPlugins(t, doubleDefaultMock)
pluginDir := os.Getenv("PACKER_PLUGIN_PATH")
defer os.RemoveAll(pluginDir)
c := PluginConfig{}
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.")
}
}
// TestHelperProcess isn't a real test. It's used as a helper process
// for multi-component plugin 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 {
plugin.SetVersion(version.NewPluginVersion("1.0.0", "", ""))
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 "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-multi-component-plugin-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 multi-component plugins `mockPlugins`.
if err != nil {
t.Fatal(err)
}
t.Logf("putting temporary mock plugins in %s", pluginDir)
shPath := MustHaveCommand(t, "bash")
for name := range plugins {
pluginName := fmt.Sprintf("packer-plugin-%s_v1.0.0_x5.0_%s_%s", name, runtime.GOOS, runtime.GOARCH)
pluginSubDir := fmt.Sprintf("github.com/hashicorp/%s", name)
err := os.MkdirAll(path.Join(pluginDir, pluginSubDir), 0755)
if err != nil {
t.Fatalf("failed to create plugin hierarchy: %s", err)
}
plugin := path.Join(pluginDir, pluginSubDir, pluginName)
t.Logf("creating fake plugin %s", plugin)
fileContent := ""
fileContent = fmt.Sprintf("#!%s\n", shPath)
fileContent += strings.Join(
append([]string{"PKR_WANT_TEST_PLUGINS=1"}, helperCommand(t, name, "$@")...),
" ")
if err := os.WriteFile(plugin, []byte(fileContent), os.ModePerm); err != nil {
t.Fatalf("failed to create fake plugin binary: %v", err)
}
if _, err := generateMockChecksumFile(plugin); err != nil {
t.Fatalf("failed to create fake plugin binary checksum file: %v", err)
}
}
}
t.Setenv("PACKER_PLUGIN_PATH", pluginDir)
}
func createMockChecksumFile(t testing.TB, filePath string) {
t.Helper()
cs, err := generateMockChecksumFile(filePath)
if err != nil {
t.Fatalf("%s", err.Error())
}
t.Logf("created fake plugin checksum file %s", cs)
}
func generateMockChecksumFile(filePath string) (string, error) {
cs := plugingetter.Checksummer{
Type: "sha256",
Hash: sha256.New(),
}
f, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("failed to open fake plugin binary: %v", err)
}
defer f.Close()
sum, err := cs.Sum(f)
if err != nil {
return "", fmt.Errorf("failed to checksum fake plugin binary: %v", err)
}
sumfile := filePath + cs.FileExt()
if err := os.WriteFile(sumfile, []byte(fmt.Sprintf("%x", sum)), os.ModePerm); err != nil {
return "", fmt.Errorf("failed to write checksum fake plugin binary: %v", err)
}
return sumfile, nil
}
func createMockInstalledPlugins(t *testing.T, plugins map[string]pluginsdk.Set, opts ...func(tb testing.TB, filePath string)) {
pluginDir, err := tmp.Dir("pkr-multi-component-plugin-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 multi-component plugins `mockPlugins`.
if err != nil {
t.Fatal(err)
}
dir, err := os.MkdirTemp(pluginDir, "github.com")
if err != nil {
t.Fatalf("failed to create temporary test directory: %v", err)
}
dir, err = os.MkdirTemp(dir, "hashicorp")
if err != nil {
t.Fatalf("failed to create temporary test directory: %v", err)
}
dir, err = os.MkdirTemp(dir, "plugin")
if err != nil {
t.Fatalf("failed to create temporary test directory: %v", err)
}
t.Logf("putting temporary mock installed plugins in %s", dir)
shPath := MustHaveCommand(t, "bash")
for name := range plugins {
plugin := path.Join(dir, "packer-plugin-"+name)
t.Logf("creating fake plugin %s", plugin)
fileContent := ""
fileContent = fmt.Sprintf("#!%s\n", shPath)
fileContent += strings.Join(
append([]string{"PKR_WANT_TEST_PLUGINS=1"}, helperCommand(t, strings.Split(name, "_")[0], "$@")...),
" ")
if err := os.WriteFile(plugin, []byte(fileContent), os.ModePerm); err != nil {
t.Fatalf("failed to create fake plugin binary: %v", err)
}
for _, opt := range opts {
opt(t, plugin)
}
}
}
t.Setenv("PACKER_PLUGIN_PATH", pluginDir)
}
func getFormattedInstalledPluginSuffix() string {
return fmt.Sprintf("v1.0.0_x5.0_%s_%s", runtime.GOOS, runtime.GOARCH)
}
var (
mockPlugins = map[string]pluginsdk.Set{}
mockInstalledPlugins = map[string]pluginsdk.Set{}
invalidInstalledPluginsMock = map[string]pluginsdk.Set{}
defaultNameMock = map[string]pluginsdk.Set{}
doubleDefaultMock = map[string]pluginsdk.Set{}
badDefaultNameMock = map[string]pluginsdk.Set{}
)
func init() {
mockPluginsBird := pluginsdk.NewSet()
mockPluginsBird.Builders = map[string]packersdk.Builder{
"feather": nil,
"guacamole": nil,
}
mockPluginsChim := pluginsdk.NewSet()
mockPluginsChim.PostProcessors = map[string]packersdk.PostProcessor{
"smoke": nil,
}
mockPluginsData := pluginsdk.NewSet()
mockPluginsData.Datasources = map[string]packersdk.Datasource{
"source": nil,
}
mockPlugins["bird"] = *mockPluginsBird
mockPlugins["chimney"] = *mockPluginsChim
mockPlugins["data"] = *mockPluginsData
mockInstalledPluginsBird := pluginsdk.NewSet()
mockInstalledPluginsBird.Builders = map[string]packersdk.Builder{
"feather": nil,
"guacamole": nil,
}
mockInstalledPluginsChim := pluginsdk.NewSet()
mockInstalledPluginsChim.PostProcessors = map[string]packersdk.PostProcessor{
"smoke": nil,
}
mockInstalledPluginsData := pluginsdk.NewSet()
mockInstalledPluginsData.Datasources = map[string]packersdk.Datasource{
"source": nil,
}
mockInstalledPlugins[fmt.Sprintf("bird_%s", getFormattedInstalledPluginSuffix())] = *mockInstalledPluginsBird
mockInstalledPlugins[fmt.Sprintf("chimney_%s", getFormattedInstalledPluginSuffix())] = *mockInstalledPluginsChim
mockInstalledPlugins[fmt.Sprintf("data_%s", getFormattedInstalledPluginSuffix())] = *mockInstalledPluginsData
invalidInstalledPluginsMockBird := pluginsdk.NewSet()
invalidInstalledPluginsMockBird.Builders = map[string]packersdk.Builder{
"feather": nil,
"guacamole": nil,
}
invalidInstalledPluginsMockChimney := pluginsdk.NewSet()
invalidInstalledPluginsMockChimney.PostProcessors = map[string]packersdk.PostProcessor{
"smoke": nil,
}
invalidInstalledPluginsMockData := pluginsdk.NewSet()
invalidInstalledPluginsMockData.Datasources = map[string]packersdk.Datasource{
"source": nil,
}
invalidInstalledPluginsMock["bird_v0.1.1_x5.0_wrong_architecture"] = *invalidInstalledPluginsMockBird
invalidInstalledPluginsMock["chimney_cool_ranch"] = *invalidInstalledPluginsMockChimney
invalidInstalledPluginsMock["data"] = *invalidInstalledPluginsMockData
defaultNameFooSet := pluginsdk.NewSet()
defaultNameFooSet.Builders = map[string]packersdk.Builder{
"bar": nil,
"baz": nil,
pluginsdk.DEFAULT_NAME: nil,
}
defaultNameMock["foo"] = *defaultNameFooSet
doubleDefaultYoloSet := pluginsdk.NewSet()
doubleDefaultYoloSet.Builders = map[string]packersdk.Builder{
"bar": nil,
"baz": nil,
pluginsdk.DEFAULT_NAME: nil,
}
doubleDefaultYoloSet.PostProcessors = map[string]packersdk.PostProcessor{
pluginsdk.DEFAULT_NAME: nil,
}
doubleDefaultMock["yolo"] = *doubleDefaultYoloSet
badDefaultSet := pluginsdk.NewSet()
badDefaultSet.Builders = map[string]packersdk.Builder{
"bar": nil,
"baz": nil,
pluginsdk.DEFAULT_NAME: nil,
}
badDefaultNameMock["foo"] = *badDefaultSet
}