Files
Packer-Cn/packer_test/lib/plugin.go
T
Lucas Bajolet d4ea91b478 packer_test: move compiledPlugins to attribute
The compiledPlugins map used to be a global variable, which can be
problematic as we move to independent test suites, since those test
suites run in the same process space, the global variable could point to
now deleted plugin versions/paths in separate suites, which would make
tests fail with random errors.

To avoid this, the map is now scoped to the test suite, and a new copy
is created lazily if used by the test suite.
2024-08-06 11:09:31 -04:00

277 lines
8.1 KiB
Go

package lib
import (
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer-plugin-sdk/plugin"
)
type compiledPlugins struct {
pluginVersions map[string]string
rwMutex sync.RWMutex
}
func (ts *PackerTestSuite) StorePluginVersion(pluginVersion, path string) {
ts.compiledPlugins.rwMutex.Lock()
defer ts.compiledPlugins.rwMutex.Unlock()
if ts.compiledPlugins.pluginVersions == nil {
ts.compiledPlugins.pluginVersions = map[string]string{}
}
ts.compiledPlugins.pluginVersions[pluginVersion] = path
}
func (ts *PackerTestSuite) LoadPluginVersion(pluginVersion string) (string, bool) {
ts.compiledPlugins.rwMutex.RLock()
defer ts.compiledPlugins.rwMutex.RUnlock()
if ts.compiledPlugins.pluginVersions == nil {
ts.compiledPlugins.pluginVersions = map[string]string{}
}
path, ok := ts.compiledPlugins.pluginVersions[pluginVersion]
return path, ok
}
// LDFlags compiles the ldflags for the plugin to compile based on the information provided.
func LDFlags(version *version.Version) string {
pluginPackage := "github.com/hashicorp/packer-plugin-tester"
ldflagsArg := fmt.Sprintf("-X %s/version.Version=%s", pluginPackage, version.Core())
if version.Prerelease() != "" {
ldflagsArg = fmt.Sprintf("%s -X %s/version.VersionPrerelease=%s", ldflagsArg, pluginPackage, version.Prerelease())
}
if version.Metadata() != "" {
ldflagsArg = fmt.Sprintf("%s -X %s/version.VersionMetadata=%s", ldflagsArg, pluginPackage, version.Metadata())
}
return ldflagsArg
}
// BinaryName is the raw name of the plugin binary to produce
//
// It's expected to be in the "mini-plugin_<version>[-<prerelease>][+<metadata>]" format
func BinaryName(version *version.Version) string {
retStr := fmt.Sprintf("mini-plugin_%s", version.Core())
if version.Prerelease() != "" {
retStr = fmt.Sprintf("%s-%s", retStr, version.Prerelease())
}
if version.Metadata() != "" {
retStr = fmt.Sprintf("%s+%s", retStr, version.Metadata())
}
return retStr
}
// ExpectedInstalledName is the expected full name of the plugin once installed.
func ExpectedInstalledName(versionStr string) string {
version.Must(version.NewVersion(versionStr))
versionStr = strings.ReplaceAll(versionStr, "v", "")
ext := ""
if runtime.GOOS == "windows" {
ext = ".exe"
}
return fmt.Sprintf("packer-plugin-tester_v%s_x%s.%s_%s_%s%s",
versionStr,
plugin.APIVersionMajor,
plugin.APIVersionMinor,
runtime.GOOS, runtime.GOARCH, ext)
}
// currentDir returns the directory in which the current file is located.
//
// Since we're in tests it's reliable as they're supposed to run on the same
// machine the binary's compiled from, but goes to say it's not meant for use
// in distributed binaries.
func currentDir() (string, error) {
// pc uintptr, file string, line int, ok bool
_, testDir, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("couldn't get the location of the test suite file")
}
return filepath.Dir(testDir), nil
}
// MakePluginDir installs a list of plugins into a temporary directory and returns its path
//
// This can be set in the environment for a test through a function like t.SetEnv(), so
// packer will be able to use that directory for running its functions.
//
// Deletion of the directory is the caller's responsibility.
func (ts *PackerTestSuite) MakePluginDir(pluginVersions ...string) (pluginTempDir string, cleanup func()) {
t := ts.T()
for _, ver := range pluginVersions {
ts.BuildSimplePlugin(ver, t)
}
var err error
defer func() {
if err != nil {
if pluginTempDir != "" {
os.RemoveAll(pluginTempDir)
}
t.Fatalf("failed to prepare temporary plugin directory %q: %s", pluginTempDir, err)
}
}()
pluginTempDir, err = os.MkdirTemp("", "packer-plugin-dir-temp-")
if err != nil {
return
}
for _, pluginVersion := range pluginVersions {
path, ok := ts.LoadPluginVersion(pluginVersion)
if !ok {
err = fmt.Errorf("failed to get path to version %q, was it compiled?", pluginVersion)
}
cmd := ts.PackerCommand().SetArgs("plugins", "install", "--path", path, "github.com/hashicorp/tester").AddEnv("PACKER_PLUGIN_PATH", pluginTempDir)
cmd.Assert(MustSucceed())
out, stderr, cmdErr := cmd.Run()
if cmdErr != nil {
err = fmt.Errorf("failed to install tester plugin version %q: %s\nCommand stdout: %s\nCommand stderr: %s", pluginVersion, err, out, stderr)
return
}
}
return pluginTempDir, func() {
err := os.RemoveAll(pluginTempDir)
if err != nil {
t.Logf("failed to remove temporary plugin directory %q: %s. This may need manual intervention.", pluginTempDir, err)
}
}
}
// CopyFile essentially replicates the `cp` command, for a file only.
//
// # Permissions are copied over from the source to destination
//
// The function detects if destination is a directory or a file (existent or not).
//
// If this is the former, we append the source file's basename to the
// directory and create the file from that inferred path.
func CopyFile(t *testing.T, dest, src string) {
st, err := os.Stat(src)
if err != nil {
t.Fatalf("failed to stat origin file %q: %s", src, err)
}
// If the stat call fails, we assume dest is the destination file.
dstStat, err := os.Stat(dest)
if err == nil && dstStat.IsDir() {
dest = filepath.Join(dest, filepath.Base(src))
}
destFD, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, st.Mode().Perm())
if err != nil {
t.Fatalf("failed to create cp destination file %q: %s", dest, err)
}
defer destFD.Close()
srcFD, err := os.Open(src)
if err != nil {
t.Fatalf("failed to open source file to copy: %s", err)
}
defer srcFD.Close()
_, err = io.Copy(destFD, srcFD)
if err != nil {
t.Fatalf("failed to copy from %q -> %q: %s", src, dest, err)
}
}
// WriteFile writes `content` to a file `dest`
//
// The default permissions of that file is 0644
func WriteFile(t *testing.T, dest string, content string) {
outFile, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
t.Fatalf("failed to open/create %q: %s", dest, err)
}
defer outFile.Close()
_, err = fmt.Fprintf(outFile, content)
if err != nil {
t.Fatalf("failed to write to file %q: %s", dest, err)
}
}
// TempWorkdir creates a working directory for a Packer test with the list of files
// given as input.
//
// The files should either have a path relative to the test that invokes it, or should
// be absolute.
// Each file will be copied to the root of the workdir being created.
//
// If any file cannot be found, this function will fail
func TempWorkdir(t *testing.T, files ...string) (string, func()) {
var err error
tempDir, err := os.MkdirTemp("", "packer-test-workdir-")
if err != nil {
t.Fatalf("failed to create temporary working directory: %s", err)
}
defer func() {
if err != nil {
os.RemoveAll(tempDir)
t.Errorf("failed to create temporary workdir: %s", err)
}
}()
for _, file := range files {
CopyFile(t, tempDir, file)
}
return tempDir, func() {
err := os.RemoveAll(tempDir)
if err != nil {
t.Logf("failed to remove temporary workdir %q: %s. This will need manual action.", tempDir, err)
}
}
}
// SHA256Sum computes the SHA256 digest for an input file
//
// The digest is returned as a hexstring
func SHA256Sum(t *testing.T, file string) string {
fl, err := os.ReadFile(file)
if err != nil {
t.Fatalf("failed to compute sha256sum for %q: %s", file, err)
}
sha := sha256.New()
sha.Write(fl)
return fmt.Sprintf("%x", sha.Sum([]byte{}))
}
// ManualPluginInstall emulates how Packer installs plugins with `packer plugins install`
//
// This is used for some tests if we want to install a plugin that cannot be installed
// through the normal commands (typically because Packer rejects it).
func ManualPluginInstall(t *testing.T, dest, srcPlugin, versionStr string) {
err := os.MkdirAll(dest, 0755)
if err != nil {
t.Fatalf("failed to create destination directories %q: %s", dest, err)
}
pluginName := ExpectedInstalledName(versionStr)
destPath := filepath.Join(dest, pluginName)
CopyFile(t, destPath, srcPlugin)
shaPath := fmt.Sprintf("%s_SHA256SUM", destPath)
WriteFile(t, shaPath, SHA256Sum(t, destPath))
}