339 lines
11 KiB
Go
339 lines
11 KiB
Go
package provisioneracc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
builderT "github.com/hashicorp/packer/packer-plugin-sdk/acctest"
|
|
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
|
|
)
|
|
|
|
// ProvisionerTestCase is a single set of tests to run for a provisioner.
|
|
// A ProvisionerTestCase should generally map 1:1 to each test method for your
|
|
// acceptance tests.
|
|
type ProvisionerTestCase struct {
|
|
// Check is called after this step is executed in order to test that
|
|
// the step executed successfully. If this is not set, then the next
|
|
// step will be called
|
|
Check func(*exec.Cmd, string) error
|
|
// IsCompatible checks whether a provisioner is able to run against a
|
|
// given builder type and guest operating system, and returns a boolean.
|
|
// if it returns true, the test combination is okay to run. If false, the
|
|
// test combination is not okay to run.
|
|
IsCompatible func(builderType string, BuilderGuestOS string) bool
|
|
// Name is the name of the test case. Be simple but unique and descriptive.
|
|
Name string
|
|
// Setup, if non-nil, will be called once before the test case
|
|
// runs. This can be used for some setup like setting environment
|
|
// variables, or for validation prior to the
|
|
// test running. For example, you can use this to make sure certain
|
|
// binaries are installed, or text fixtures are in place.
|
|
Setup func() error
|
|
// Teardown will be called before the test case is over regardless
|
|
// of if the test succeeded or failed. This should return an error
|
|
// in the case that the test can't guarantee all resources were
|
|
// properly cleaned up.
|
|
Teardown builderT.TestTeardownFunc
|
|
// Template is the provisioner template to use.
|
|
// The provisioner template fragment must be a json-formatted string
|
|
// containing the provisioner definition but no other portions of a packer
|
|
// template. For
|
|
// example:
|
|
//
|
|
// ```json
|
|
// {
|
|
// "type": "shell-local",
|
|
// "inline", ["echo hello world"]
|
|
// }
|
|
//```
|
|
//
|
|
// is a valid entry for "template" here, but the complete Packer template:
|
|
//
|
|
// ```json
|
|
// {
|
|
// "provisioners": [
|
|
// {
|
|
// "type": "shell-local",
|
|
// "inline", ["echo hello world"]
|
|
// }
|
|
// ]
|
|
// }
|
|
// ```
|
|
//
|
|
// is invalid as input.
|
|
//
|
|
// You may provide multiple provisioners in the same template. For example:
|
|
// ```json
|
|
// {
|
|
// "type": "shell-local",
|
|
// "inline", ["echo hello world"]
|
|
// },
|
|
// {
|
|
// "type": "shell-local",
|
|
// "inline", ["echo hello world 2"]
|
|
// }
|
|
// ```
|
|
Template string
|
|
// Type is the type of provisioner.
|
|
Type string
|
|
}
|
|
|
|
// BuilderFixtures are basic builder test configurations and metadata used
|
|
// in provisioner acceptance testing. These are frameworks to be used by
|
|
// provisioner tests, not tests in and of themselves. BuilderFixtures should
|
|
// generally be simple and not contain excessive or complex configurations.
|
|
// Instantiations of this struct are stored in the builders.go file in this
|
|
// module.
|
|
type BuilderFixture struct {
|
|
// Name is the name of the builder fixture.
|
|
// Be simple and descriptive.
|
|
Name string
|
|
// Setup creates necessary extra test fixtures, and renders their values
|
|
// into the BuilderFixture.Template.
|
|
Setup func()
|
|
// Template is the path to a builder template fragment.
|
|
// The builder template fragment must be a json-formatted file containing
|
|
// the builder definition but no other portions of a packer template. For
|
|
// example:
|
|
//
|
|
// ```json
|
|
// {
|
|
// "type": "null",
|
|
// "communicator", "none"
|
|
// }
|
|
//```
|
|
//
|
|
// is a valid entry for "template" here, but the complete Packer template:
|
|
//
|
|
// ```json
|
|
// {
|
|
// "builders": [
|
|
// "type": "null",
|
|
// "communicator": "none"
|
|
// ]
|
|
// }
|
|
// ```
|
|
//
|
|
// is invalid as input.
|
|
//
|
|
// Only provide one builder template fragment per file.
|
|
TemplatePath string
|
|
|
|
// GuestOS says what guest os type the builder template fragment creates.
|
|
// Valid values are "windows", "linux" or "darwin" guests.
|
|
GuestOS string
|
|
|
|
// HostOS says what host os type the builder is capable of running on.
|
|
// Valid values are "any", windows", or "posix". If you set "posix", then
|
|
// this builder can run on a "linux" or "darwin" platform. If you set
|
|
// "any", then this builder can be used on any platform.
|
|
HostOS string
|
|
|
|
Teardown builderT.TestTeardownFunc
|
|
}
|
|
|
|
func fixtureDir() string {
|
|
_, file, _, _ := runtime.Caller(0)
|
|
return filepath.Join(filepath.Dir(file), "test-fixtures")
|
|
}
|
|
|
|
func LoadBuilderFragment(templateFragmentPath string) (string, error) {
|
|
dir := fixtureDir()
|
|
fragmentAbsPath := filepath.Join(dir, templateFragmentPath)
|
|
fragmentFile, err := os.Open(fragmentAbsPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Unable find %s", fragmentAbsPath)
|
|
}
|
|
defer fragmentFile.Close()
|
|
|
|
fragmentString, err := ioutil.ReadAll(fragmentFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Unable to read %s", fragmentAbsPath)
|
|
}
|
|
|
|
return string(fragmentString), nil
|
|
}
|
|
|
|
func RunProvisionerAccTest(testCase *ProvisionerTestCase, t *testing.T) {
|
|
TestProvisionersAgainstBuilders(testCase, t)
|
|
}
|
|
|
|
//nolint:errcheck
|
|
func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T) {
|
|
// retrieve user-desired builders.
|
|
builderTypes := checkBuilders(t)
|
|
|
|
// Run this provisioner test case against each builder type requested.
|
|
for _, builderType := range builderTypes {
|
|
buildFixtures := BuildersAccTest[builderType]
|
|
// loop over individual build templates, merge with provisioner
|
|
// templates, and shell out to run test.
|
|
for _, buildFixture := range buildFixtures {
|
|
if !testCase.IsCompatible(builderType, buildFixture.GuestOS) {
|
|
continue
|
|
}
|
|
|
|
testName := fmt.Sprintf("%s on %s", testCase.Name, buildFixture.Name)
|
|
|
|
if testCase.Setup != nil {
|
|
err := testCase.Setup()
|
|
if err != nil {
|
|
t.Fatalf("test %s setup failed: %s", testName, err)
|
|
}
|
|
}
|
|
|
|
t.Run(testName, func(t *testing.T) {
|
|
builderFragment, err := LoadBuilderFragment(buildFixture.TemplatePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to load builder fragment: %s", err)
|
|
}
|
|
|
|
// Combine provisioner and builder template fragments; write to
|
|
// file.
|
|
out := bytes.NewBuffer(nil)
|
|
fmt.Fprintf(out, `{"builders": [%s],"provisioners": [%s]}`,
|
|
builderFragment, testCase.Template)
|
|
templateName := fmt.Sprintf("%s_%s.json", builderType, testCase.Type)
|
|
templatePath := filepath.Join("./", templateName)
|
|
writeJsonTemplate(out, templatePath, t)
|
|
logfile := fmt.Sprintf("packer_log_%s_%s.txt", builderType, testCase.Type)
|
|
|
|
// Make sure packer is installed:
|
|
packerbin, err := exec.LookPath("packer")
|
|
if err != nil {
|
|
t.Fatalf("Couldn't find packer binary installed on system: %s", err.Error())
|
|
}
|
|
// Run build
|
|
buildCommand := exec.Command(packerbin, "build", "--machine-readable", templatePath)
|
|
buildCommand.Env = append(buildCommand.Env, os.Environ()...)
|
|
buildCommand.Env = append(buildCommand.Env, "PACKER_LOG=1",
|
|
fmt.Sprintf("PACKER_LOG_PATH=%s", logfile))
|
|
buildCommand.Run()
|
|
|
|
// Check for test custom pass/fail before we clean up
|
|
var checkErr error
|
|
if testCase.Check != nil {
|
|
checkErr = testCase.Check(buildCommand, logfile)
|
|
}
|
|
|
|
// Cleanup stuff created by builder.
|
|
cleanErr := buildFixture.Teardown()
|
|
if cleanErr != nil {
|
|
log.Printf("bad: failed to clean up builder-created resources: %s", cleanErr.Error())
|
|
}
|
|
// Clean up anything created in provisioner run
|
|
if testCase.Teardown != nil {
|
|
cleanErr = testCase.Teardown()
|
|
if cleanErr != nil {
|
|
log.Printf("bad: failed to clean up test-created resources: %s", cleanErr.Error())
|
|
}
|
|
}
|
|
|
|
// Fail test if check failed.
|
|
if checkErr != nil {
|
|
cwd, _ := os.Getwd()
|
|
t.Fatalf(fmt.Sprintf("Error running provisioner acceptance"+
|
|
" tests: %s\nLogs can be found at %s\nand the "+
|
|
"acceptance test template can be found at %s",
|
|
checkErr.Error(), filepath.Join(cwd, logfile),
|
|
filepath.Join(cwd, templatePath)))
|
|
} else {
|
|
os.Remove(templatePath)
|
|
os.Remove(logfile)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkBuilders retrieves all of the builders that the user has requested to
|
|
// run acceptance tests against.
|
|
func checkBuilders(t *testing.T) []string {
|
|
b := os.Getenv("ACC_TEST_BUILDERS")
|
|
// validate if we want to run provisioners acc tests
|
|
if b == "" {
|
|
t.Skip("Provisioners Acceptance tests skipped unless env 'ACC_TEST_BUILDERS' is set")
|
|
}
|
|
|
|
// Get builders type to test provisioners against
|
|
var builders []string
|
|
for k := range BuildersAccTest {
|
|
// This will validate that only defined builders are executed against
|
|
if b != "all" && !strings.Contains(b, k) {
|
|
continue
|
|
}
|
|
builders = append(builders, k)
|
|
}
|
|
return builders
|
|
}
|
|
|
|
func writeJsonTemplate(out *bytes.Buffer, filePath string, t *testing.T) {
|
|
outputFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
t.Fatalf("bad: failed to create template file: %s", err.Error())
|
|
}
|
|
_, err = outputFile.Write(out.Bytes())
|
|
if err != nil {
|
|
t.Fatalf("bad: failed to write template file: %s", err.Error())
|
|
}
|
|
outputFile.Sync()
|
|
}
|
|
|
|
// BuilderAcceptance is specialized tooling implemented by individual builders.
|
|
// To add your builder to the provisioner testing framework, create a struct
|
|
// that implements this interface, add it to the BuildersAccTest map below.
|
|
// TODO add this interface to the plugin server so that Packer can request it
|
|
// From the plugin rather than importing it here.
|
|
type BuilderAcceptance interface {
|
|
// GetConfigs provides a mapping of guest OS architecture to builder
|
|
// template fragment.
|
|
// The builder template fragment must be a json-formatted string containing
|
|
// the builder definition but no other portions of a packer template. For
|
|
// example:
|
|
//
|
|
// ```json
|
|
// {
|
|
// "type": "null",
|
|
// "communicator", "none"
|
|
// }
|
|
//```
|
|
//
|
|
// is a valid entry for "template" here, but the complete Packer template:
|
|
//
|
|
// ```json
|
|
// {
|
|
// "builders": [
|
|
// "type": "null",
|
|
// "communicator": "none"
|
|
// ]
|
|
// }
|
|
// ```
|
|
//
|
|
// is invalid as input.
|
|
//
|
|
// Valid keys for the map are "linux" and "windows". These keys will be used
|
|
// to determine whether a given builder template is compatible with a given
|
|
// provisioner template.
|
|
GetConfigs() (map[string]string, error)
|
|
// GetBuilderStore() returns a MapOfBuilder that contains the actual builder
|
|
// struct definition being used for this test.
|
|
GetBuilderStore() packersdk.MapOfBuilder
|
|
// CleanUp cleans up any side-effects of the builder not already cleaned up
|
|
// by the builderT framework.
|
|
CleanUp() error
|
|
}
|
|
|
|
// Mapping of all builder fixtures defined for a given builder type.
|
|
var BuildersAccTest = map[string][]*BuilderFixture{
|
|
"virtualbox-iso": []*BuilderFixture{VirtualboxBuilderFixtureLinux},
|
|
"amazon-ebs": []*BuilderFixture{AmasonEBSBuilderFixtureLinux, AmasonEBSBuilderFixtureWindows},
|
|
}
|