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}, }