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_test/lib/plugin.go

277 lines
8.0 KiB

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
mutex sync.Mutex
}
func (ts *PackerTestSuite) StorePluginVersion(pluginVersion, path string) {
ts.compiledPlugins.mutex.Lock()
defer ts.compiledPlugins.mutex.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.mutex.Lock()
defer ts.compiledPlugins.mutex.Unlock()
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))
}