mirror of https://github.com/hashicorp/packer
backport of commit ef50474a9f
parent
fbb0ce8e21
commit
b4e6a84012
@ -0,0 +1,32 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// BuildTestPacker builds a new Packer binary based on the current state of the repository.
|
||||
//
|
||||
// If for some reason the binary cannot be built, we will immediately exit with an error.
|
||||
func BuildTestPacker(t *testing.T) (string, error) {
|
||||
testDir, err := currentDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to compile packer binary: %s", err)
|
||||
}
|
||||
|
||||
packerCoreDir := filepath.Dir(testDir)
|
||||
|
||||
outBin := filepath.Join(os.TempDir(), fmt.Sprintf("packer_core-%d", rand.Int()))
|
||||
|
||||
compileCommand := exec.Command("go", "build", "-C", packerCoreDir, "-o", outBin)
|
||||
logs, err := compileCommand.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile Packer core: %s\ncompilation logs: %s", err, logs)
|
||||
}
|
||||
|
||||
return outBin, nil
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type packerCommand struct {
|
||||
once sync.Once
|
||||
packerPath string
|
||||
args []string
|
||||
env map[string]string
|
||||
stderr *strings.Builder
|
||||
stdout *strings.Builder
|
||||
err error
|
||||
}
|
||||
|
||||
// PackerCommand creates a skeleton of packer command with the ability to execute gadgets on the outputs of the command.
|
||||
func (ts *PackerTestSuite) PackerCommand() *packerCommand {
|
||||
stderr := &strings.Builder{}
|
||||
stdout := &strings.Builder{}
|
||||
|
||||
return &packerCommand{
|
||||
packerPath: ts.packerPath,
|
||||
env: map[string]string{
|
||||
"PACKER_LOG": "1",
|
||||
},
|
||||
stderr: stderr,
|
||||
stdout: stdout,
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *packerCommand) SetArgs(args ...string) *packerCommand {
|
||||
pc.args = args
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *packerCommand) AddEnv(key, val string) *packerCommand {
|
||||
pc.env[key] = val
|
||||
return pc
|
||||
}
|
||||
|
||||
// Run executes the packer command with the args/env requested and returns the
|
||||
// output streams (stdout, stderr)
|
||||
//
|
||||
// Note: "Run" will only execute the command once, and return the streams and
|
||||
// error from the only execution for every subsequent call
|
||||
func (pc *packerCommand) Run(t *testing.T) (string, string, error) {
|
||||
pc.once.Do(pc.doRun)
|
||||
|
||||
if strings.Contains(pc.stdout.String(), "PACKER CRASH") || strings.Contains(pc.stderr.String(), "PACKER CRASH") {
|
||||
t.Fatalf("Packer has crashed while running the following command: packer %#v", pc.args)
|
||||
}
|
||||
|
||||
return pc.stdout.String(), pc.stderr.String(), pc.err
|
||||
}
|
||||
|
||||
func (pc *packerCommand) doRun() {
|
||||
cmd := exec.Command("packer", pc.args...)
|
||||
for key, val := range pc.env {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, val))
|
||||
}
|
||||
cmd.Stdout = pc.stdout
|
||||
cmd.Stderr = pc.stderr
|
||||
|
||||
pc.err = cmd.Run()
|
||||
}
|
||||
|
||||
func (pc *packerCommand) Assert(t *testing.T, checks ...Checker) {
|
||||
stdout, stderr, err := pc.Run(t)
|
||||
|
||||
for _, check := range checks {
|
||||
checkErr := check.Check(stdout, stderr, err)
|
||||
if checkErr != nil {
|
||||
t.Errorf("check %q failed: %s", check.Name(), checkErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Stream int
|
||||
|
||||
const (
|
||||
// BothStreams will use both stdout and stderr for performing a check
|
||||
BothStreams Stream = iota
|
||||
// OnlyStdout will only use stdout for performing a check
|
||||
OnlyStdout
|
||||
// OnlySterr will only use stderr for performing a check
|
||||
OnlyStderr
|
||||
)
|
||||
|
||||
func (s Stream) String() string {
|
||||
switch s {
|
||||
case BothStreams:
|
||||
return "Both streams"
|
||||
case OnlyStdout:
|
||||
return "Stdout"
|
||||
case OnlyStderr:
|
||||
return "Stderr"
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Unknown stream value: %d", s))
|
||||
}
|
||||
|
||||
type Checker interface {
|
||||
Check(stdout, stderr string, err error) error
|
||||
Name() string
|
||||
}
|
||||
|
||||
type MustSucceed struct{}
|
||||
|
||||
func (_ MustSucceed) Check(stdout, stderr string, err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (_ MustSucceed) Name() string {
|
||||
return "Must succeed"
|
||||
}
|
||||
|
||||
// Grep is essentially the equivalent to a normal grep -E on the command line.
|
||||
//
|
||||
// The `expect` string is meant to be a regexp, which will be compiled on-demand,
|
||||
// and will panic if it isn't a valid POSIX extended regexp.
|
||||
type Grep struct {
|
||||
streams Stream
|
||||
expect string
|
||||
}
|
||||
|
||||
func (g Grep) Check(stdout, stderr string, err error) error {
|
||||
re := regexp.MustCompilePOSIX(g.expect)
|
||||
|
||||
streams := []string{}
|
||||
|
||||
switch g.streams {
|
||||
case BothStreams:
|
||||
streams = append(streams, stdout, stderr)
|
||||
case OnlyStdout:
|
||||
streams = append(streams, stdout)
|
||||
case OnlyStderr:
|
||||
streams = append(streams, stderr)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, stream := range streams {
|
||||
found = found || re.MatchString(stream)
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("streams %q did not match the expected regexp %q", g.streams, g.expect)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g Grep) Name() string {
|
||||
return fmt.Sprintf("command (%s) | grep -E %q", g.streams, g.expect)
|
||||
}
|
||||
|
||||
type Dump struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (d Dump) Check(stdout, stderr string, err error) error {
|
||||
d.t.Logf("Dumping command result.")
|
||||
d.t.Logf("Stdout: %s", stdout)
|
||||
d.t.Logf("stderr: %s", stderr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ Dump) Name() string {
|
||||
return "dump"
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (ts *PackerTestSuite) TestLoadingOrder() {
|
||||
t := ts.T()
|
||||
|
||||
pluginDir := ts.MakePluginDir(t, "1.0.9", "1.0.10")
|
||||
defer func() {
|
||||
err := os.RemoveAll(pluginDir)
|
||||
if err != nil {
|
||||
t.Logf("failed to remove temporary plugin directory %q: %s. This may need manual intervention.", pluginDir, err)
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
templatePath string
|
||||
}{
|
||||
{
|
||||
"HCL2 - No required_plugins, 1.0.10 is the most recent and should load",
|
||||
"./templates/simple.pkr.hcl",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ts.PackerCommand().
|
||||
SetArgs("build", tt.templatePath).
|
||||
AddEnv("PACKER_PLUGIN_PATH", pluginDir).
|
||||
Assert(t, MustSucceed{}, Grep{
|
||||
streams: BothStreams,
|
||||
expect: "packer-plugin-tester_v1\\.0\\.10[^\n]+ plugin:",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,192 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var compiledPlugins = struct {
|
||||
pluginVersions map[string]string
|
||||
RWMutex sync.RWMutex
|
||||
}{
|
||||
pluginVersions: map[string]string{},
|
||||
}
|
||||
|
||||
func StorePluginVersion(pluginVersion, path string) {
|
||||
compiledPlugins.RWMutex.Lock()
|
||||
defer compiledPlugins.RWMutex.Unlock()
|
||||
compiledPlugins.pluginVersions[pluginVersion] = path
|
||||
}
|
||||
|
||||
func LoadPluginVersion(pluginVersion string) (string, bool) {
|
||||
compiledPlugins.RWMutex.RLock()
|
||||
defer compiledPlugins.RWMutex.RUnlock()
|
||||
|
||||
path, ok := compiledPlugins.pluginVersions[pluginVersion]
|
||||
return path, ok
|
||||
}
|
||||
|
||||
var tempPluginBinaryPath = struct {
|
||||
path string
|
||||
once sync.Once
|
||||
}{}
|
||||
|
||||
// PluginBinaryDir returns the path to the directory where temporary binaries will be compiled
|
||||
func PluginBinaryDir() string {
|
||||
tempPluginBinaryPath.once.Do(func() {
|
||||
tempDir, err := os.MkdirTemp("", "packer-core-acc-test-")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create temporary directory for compiled plugins: %s", err))
|
||||
}
|
||||
|
||||
tempPluginBinaryPath.path = tempDir
|
||||
})
|
||||
|
||||
return tempPluginBinaryPath.path
|
||||
}
|
||||
|
||||
type PluginBuildConfig struct {
|
||||
version *version.Version
|
||||
}
|
||||
|
||||
func NewPluginBuildConfig(versionStr string) *PluginBuildConfig {
|
||||
return &PluginBuildConfig{
|
||||
version.Must(version.NewVersion(versionStr)),
|
||||
}
|
||||
}
|
||||
|
||||
// Version is the core version string of the test plugin.
|
||||
//
|
||||
// If the version isn't set, it'll default to 1.0.0
|
||||
func (pc PluginBuildConfig) Version() string {
|
||||
return pc.version.Core().String()
|
||||
}
|
||||
|
||||
func (pc PluginBuildConfig) PreRelease() string {
|
||||
return pc.version.Prerelease()
|
||||
}
|
||||
|
||||
func (pc PluginBuildConfig) Metadata() string {
|
||||
return pc.version.Metadata()
|
||||
}
|
||||
|
||||
// LDFlags compiles the ldflags for the plugin to compile based on the information provided.
|
||||
func (pc PluginBuildConfig) LDFlags() string {
|
||||
pluginPackage := "github.com/hashicorp/packer-plugin-tester"
|
||||
|
||||
ldflagsArg := fmt.Sprintf("-X %s/version.Version=%s", pluginPackage, pc.Version())
|
||||
if pc.PreRelease() != "" {
|
||||
ldflagsArg = fmt.Sprintf("%s -X %s/version.VersionPrerelease=%s", ldflagsArg, pluginPackage, pc.PreRelease())
|
||||
}
|
||||
if pc.Metadata() != "" {
|
||||
ldflagsArg = fmt.Sprintf("%s -X %s/version.VersionMetadata=%s", ldflagsArg, pluginPackage, pc.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 (pc PluginBuildConfig) BinaryName() string {
|
||||
retStr := fmt.Sprintf("mini-plugin_%s", pc.Version())
|
||||
if pc.PreRelease() != "" {
|
||||
retStr = fmt.Sprintf("%s-%s", retStr, pc.PreRelease())
|
||||
}
|
||||
if pc.Metadata() != "" {
|
||||
retStr = fmt.Sprintf("%s+%s", retStr, pc.Metadata())
|
||||
}
|
||||
|
||||
return retStr
|
||||
}
|
||||
|
||||
// BuildSimplePlugin creates a plugin that essentially does nothing.
|
||||
//
|
||||
// The plugin's code is contained in a subdirectory of this, and lets us
|
||||
// change the attributes of the plugin binary itself, like the SDK version,
|
||||
// the plugin's version, etc.
|
||||
//
|
||||
// The plugin is functional, and can be used to run builds with.
|
||||
// There won't be anything substantial created though, its goal is only
|
||||
// to validate the core functionality of Packer.
|
||||
//
|
||||
// The path to the plugin is returned, it won't be removed automatically
|
||||
// though, deletion is the caller's responsibility.
|
||||
func BuildSimplePlugin(config *PluginBuildConfig, t *testing.T) {
|
||||
t.Logf("Building plugin in version %v", config.version)
|
||||
|
||||
testDir, err := currentDir()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile plugin binary: %s", err)
|
||||
}
|
||||
|
||||
miniPluginDir := filepath.Join(testDir, "mini_plugin")
|
||||
outBin := filepath.Join(PluginBinaryDir(), config.BinaryName())
|
||||
|
||||
compileCommand := exec.Command("go", "build", "-C", miniPluginDir, "-o", outBin, "-ldflags", config.LDFlags(), ".")
|
||||
logs, err := compileCommand.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile plugin binary: %s\ncompiler logs: %s", err, logs)
|
||||
}
|
||||
|
||||
StorePluginVersion(config.version.String(), outBin)
|
||||
}
|
||||
|
||||
// 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(t *testing.T, pluginVersions ...string) (pluginTempDir string) {
|
||||
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, _ := LoadPluginVersion(pluginVersion)
|
||||
cmd := ts.PackerCommand().SetArgs("plugins", "install", "--path", path, "github.com/hashicorp/tester").AddEnv("PACKER_PLUGIN_PATH", pluginTempDir)
|
||||
cmd.Assert(t, MustSucceed{})
|
||||
out, stderr, cmdErr := cmd.Run(t)
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type PackerTestSuite struct {
|
||||
suite.Suite
|
||||
// pluginsDirectory is the directory in which plugins are compiled.
|
||||
//
|
||||
// Those binaries are not necessarily meant to be used as-is, but
|
||||
// instead should be used for composing plugin installation directories.
|
||||
pluginsDirectory string
|
||||
// packerPath is the location in which the Packer executable is compiled
|
||||
//
|
||||
// Since we don't necessarily want to manually compile Packer beforehand,
|
||||
// we compile it on demand, and use this executable for the tests.
|
||||
packerPath string
|
||||
}
|
||||
|
||||
func (ts *PackerTestSuite) buildPluginBinaries(t *testing.T) {
|
||||
BuildSimplePlugin(NewPluginBuildConfig("1.0.0"), t)
|
||||
BuildSimplePlugin(NewPluginBuildConfig("1.0.1-dev"), t)
|
||||
BuildSimplePlugin(NewPluginBuildConfig("1.0.1"), t)
|
||||
BuildSimplePlugin(NewPluginBuildConfig("1.0.9"), t)
|
||||
BuildSimplePlugin(NewPluginBuildConfig("1.0.10"), t)
|
||||
}
|
||||
|
||||
func Test_PackerCoreSuite(t *testing.T) {
|
||||
ts := &PackerTestSuite{}
|
||||
|
||||
pluginsDirectory := PluginBinaryDir()
|
||||
defer func() {
|
||||
err := os.RemoveAll(pluginsDirectory)
|
||||
if err != nil {
|
||||
t.Logf("failed to cleanup directory %q: %s. This will need manual action", pluginsDirectory, err)
|
||||
}
|
||||
}()
|
||||
|
||||
ts.pluginsDirectory = pluginsDirectory
|
||||
ts.buildPluginBinaries(t)
|
||||
|
||||
t.Logf("Building test packer binary...")
|
||||
packerPath, err := BuildTestPacker(t)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build Packer binary: %s", err)
|
||||
}
|
||||
ts.packerPath = packerPath
|
||||
t.Logf("Done")
|
||||
|
||||
defer func() {
|
||||
err := os.Remove(ts.packerPath)
|
||||
if err != nil {
|
||||
t.Logf("failed to cleanup compiled packer binary %q: %s. This will need manual aciton", packerPath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
suite.Run(t, ts)
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
packer {
|
||||
required_plugins {
|
||||
tester = {
|
||||
source = "github.com/hashicorp/tester"
|
||||
version = ">= 1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source "tester-dynamic" "test" {}
|
||||
|
||||
build {
|
||||
sources = ["tester-dynamic.test"]
|
||||
}
|
||||
Loading…
Reference in new issue