From 823ff3443432476e6e56740864ccf92a0e14b99d Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 3 Dec 2020 16:32:42 -0800 Subject: [PATCH 1/8] remove noop ui, replace in adapter and ansible tests with sdk's TestUi --- packer-plugin-sdk/adapter/adapter_test.go | 3 +- packer-plugin-sdk/packer/ui.go | 50 ++++++++++++++ packer-plugin-sdk/packer/ui_mock.go | 2 + packer/ui.go | 65 ------------------- provisioner/ansible-local/provisioner_test.go | 4 +- provisioner/ansible/provisioner.go | 3 +- provisioner/inspec/provisioner.go | 3 +- 7 files changed, 57 insertions(+), 73 deletions(-) diff --git a/packer-plugin-sdk/adapter/adapter_test.go b/packer-plugin-sdk/adapter/adapter_test.go index bfc3e7d3d..dd48de72f 100644 --- a/packer-plugin-sdk/adapter/adapter_test.go +++ b/packer-plugin-sdk/adapter/adapter_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/hashicorp/packer/packer" packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" "golang.org/x/crypto/ssh" @@ -26,7 +25,7 @@ func TestAdapter_Serve(t *testing.T) { config := &ssh.ServerConfig{} - ui := new(packer.NoopUi) + ui := packersdk.TestUi(t) sut := NewAdapter(done, &l, config, "", ui, communicator{}) go func() { diff --git a/packer-plugin-sdk/packer/ui.go b/packer-plugin-sdk/packer/ui.go index 4c97b2a0c..7d75fc5a8 100644 --- a/packer-plugin-sdk/packer/ui.go +++ b/packer-plugin-sdk/packer/ui.go @@ -151,6 +151,56 @@ func (rw *BasicUi) TrackProgress(src string, currentSize, totalSize int64, strea return rw.PB.TrackProgress(src, currentSize, totalSize, stream) } +// Safe is a UI that wraps another UI implementation and +// provides concurrency-safe access +type SafeUi struct { + Sem chan int + Ui Ui + PB getter.ProgressTracker +} + +var _ Ui = new(SafeUi) + +func (u *SafeUi) Ask(s string) (string, error) { + u.Sem <- 1 + ret, err := u.Ui.Ask(s) + <-u.Sem + + return ret, err +} + +func (u *SafeUi) Say(s string) { + u.Sem <- 1 + u.Ui.Say(s) + <-u.Sem +} + +func (u *SafeUi) Message(s string) { + u.Sem <- 1 + u.Ui.Message(s) + <-u.Sem +} + +func (u *SafeUi) Error(s string) { + u.Sem <- 1 + u.Ui.Error(s) + <-u.Sem +} + +func (u *SafeUi) Machine(t string, args ...string) { + u.Sem <- 1 + u.Ui.Machine(t, args...) + <-u.Sem +} + +func (u *SafeUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) (body io.ReadCloser) { + u.Sem <- 1 + ret := u.Ui.TrackProgress(src, currentSize, totalSize, stream) + <-u.Sem + + return ret +} + // NoopProgressTracker is a progress tracker // that displays nothing. type NoopProgressTracker struct{} diff --git a/packer-plugin-sdk/packer/ui_mock.go b/packer-plugin-sdk/packer/ui_mock.go index e1e165f5e..74600e1d9 100644 --- a/packer-plugin-sdk/packer/ui_mock.go +++ b/packer-plugin-sdk/packer/ui_mock.go @@ -7,6 +7,8 @@ import ( "testing" ) +// TestUi creates a simple UI for use in testing. +// It's not meant for "real" use. func TestUi(t *testing.T) Ui { var buf bytes.Buffer return &BasicUi{ diff --git a/packer/ui.go b/packer/ui.go index 4dc2c3490..05a947df6 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -30,21 +30,6 @@ const ( UiColorCyan = 36 ) -type NoopUi struct { - PB packersdk.NoopProgressTracker -} - -var _ packersdk.Ui = new(NoopUi) - -func (*NoopUi) Ask(string) (string, error) { return "", errors.New("this is a noop ui") } -func (*NoopUi) Say(string) { return } -func (*NoopUi) Message(string) { return } -func (*NoopUi) Error(string) { return } -func (*NoopUi) Machine(string, ...string) { return } -func (u *NoopUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { - return u.PB.TrackProgress(src, currentSize, totalSize, stream) -} - // ColoredUi is a UI that is colored using terminal colors. type ColoredUi struct { Color UiColor @@ -267,53 +252,3 @@ func (u *TimestampedUi) TrackProgress(src string, currentSize, totalSize int64, func (u *TimestampedUi) timestampLine(string string) string { return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string) } - -// Safe is a UI that wraps another UI implementation and -// provides concurrency-safe access -type SafeUi struct { - Sem chan int - Ui packersdk.Ui - PB getter.ProgressTracker -} - -var _ packersdk.Ui = new(SafeUi) - -func (u *SafeUi) Ask(s string) (string, error) { - u.Sem <- 1 - ret, err := u.Ui.Ask(s) - <-u.Sem - - return ret, err -} - -func (u *SafeUi) Say(s string) { - u.Sem <- 1 - u.Ui.Say(s) - <-u.Sem -} - -func (u *SafeUi) Message(s string) { - u.Sem <- 1 - u.Ui.Message(s) - <-u.Sem -} - -func (u *SafeUi) Error(s string) { - u.Sem <- 1 - u.Ui.Error(s) - <-u.Sem -} - -func (u *SafeUi) Machine(t string, args ...string) { - u.Sem <- 1 - u.Ui.Machine(t, args...) - <-u.Sem -} - -func (u *SafeUi) TrackProgress(src string, currentSize, totalSize int64, stream io.ReadCloser) (body io.ReadCloser) { - u.Sem <- 1 - ret := u.Ui.TrackProgress(src, currentSize, totalSize, stream) - <-u.Sem - - return ret -} diff --git a/provisioner/ansible-local/provisioner_test.go b/provisioner/ansible-local/provisioner_test.go index 755443464..b0bef4449 100644 --- a/provisioner/ansible-local/provisioner_test.go +++ b/provisioner/ansible-local/provisioner_test.go @@ -135,7 +135,7 @@ func TestProvisionerProvision_PlaybookFiles(t *testing.T) { } comm := &communicatorMock{} - if err := p.Provision(context.Background(), new(packer.NoopUi), comm, make(map[string]interface{})); err != nil { + if err := p.Provision(context.Background(), packersdk.TestUi(t), comm, make(map[string]interface{})); err != nil { t.Fatalf("err: %s", err) } @@ -169,7 +169,7 @@ func TestProvisionerProvision_PlaybookFilesWithPlaybookDir(t *testing.T) { } comm := &communicatorMock{} - if err := p.Provision(context.Background(), new(packer.NoopUi), comm, make(map[string]interface{})); err != nil { + if err := p.Provision(context.Background(), packersdk.TestUi(t), comm, make(map[string]interface{})); err != nil { t.Fatalf("err: %s", err) } diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index b1568fe51..e1b919002 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -30,7 +30,6 @@ import ( "golang.org/x/crypto/ssh" "github.com/hashicorp/hcl/v2/hcldec" - "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer-plugin-sdk/adapter" "github.com/hashicorp/packer/packer-plugin-sdk/common" "github.com/hashicorp/packer/packer-plugin-sdk/multistep/commonsteps" @@ -433,7 +432,7 @@ func (p *Provisioner) setupAdapter(ui packersdk.Ui, comm packersdk.Communicator) return "", err } - ui = &packer.SafeUi{ + ui = &packersdk.SafeUi{ Sem: make(chan int, 1), Ui: ui, } diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index 0c7ab0492..f0e1b6add 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -28,7 +28,6 @@ import ( "golang.org/x/crypto/ssh" "github.com/hashicorp/hcl/v2/hcldec" - "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer-plugin-sdk/adapter" "github.com/hashicorp/packer/packer-plugin-sdk/common" packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" @@ -304,7 +303,7 @@ func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packe return err } - ui = &packer.SafeUi{ + ui = &packersdk.SafeUi{ Sem: make(chan int, 1), Ui: ui, } From bc7dae2dff7d7a12591f9d0d8e7bdcc4a0565d9e Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 3 Dec 2020 17:01:10 -0800 Subject: [PATCH 2/8] fix ansible acceptance tests not to import packer core; add override to Builder acctest TestCase so you can supply your own provisionerstore. --- packer-plugin-sdk/acctest/testing.go | 20 ++++- provisioner/ansible-local/provisioner_test.go | 88 ++++++------------- 2 files changed, 44 insertions(+), 64 deletions(-) diff --git a/packer-plugin-sdk/acctest/testing.go b/packer-plugin-sdk/acctest/testing.go index 799682842..d28b5f2dd 100644 --- a/packer-plugin-sdk/acctest/testing.go +++ b/packer-plugin-sdk/acctest/testing.go @@ -49,6 +49,15 @@ type TestCase struct { // If SkipArtifactTeardown is true, we will not attempt to destroy the // artifact created in this test run. SkipArtifactTeardown bool + // If set, overrides the default provisioner store with custom provisioners. + // This can be useful for running acceptance tests for a particular + // provisioner using a specific builder. + // Default provisioner store: + // ProvisionerStore: packersdk.MapOfProvisioner{ + // "shell": func() (packersdk.Provisioner, error) { return &shellprovisioner.Provisioner{}, nil }, + // "file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil }, + // }, + ProvisionerStore packersdk.MapOfProvisioner } // TestCheckFunc is the callback used for Check in TestStep. @@ -112,6 +121,12 @@ func Test(t TestT, c TestCase) { return } + if c.ProvisionerStore == nil { + c.ProvisionerStore = packersdk.MapOfProvisioner{ + "shell": func() (packersdk.Provisioner, error) { return &shellprovisioner.Provisioner{}, nil }, + "file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil }, + } + } // Build the core log.Printf("[DEBUG] Initializing core...") core := packer.NewCore(&packer.CoreConfig{ @@ -125,10 +140,7 @@ func Test(t TestT, c TestCase) { return nil, nil }, }, - ProvisionerStore: packersdk.MapOfProvisioner{ - "shell": func() (packersdk.Provisioner, error) { return &shellprovisioner.Provisioner{}, nil }, - "file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil }, - }, + ProvisionerStore: c.ProvisionerStore, }, Template: tpl, }) diff --git a/provisioner/ansible-local/provisioner_test.go b/provisioner/ansible-local/provisioner_test.go index b0bef4449..66238b11d 100644 --- a/provisioner/ansible-local/provisioner_test.go +++ b/provisioner/ansible-local/provisioner_test.go @@ -12,9 +12,8 @@ import ( "os/exec" "github.com/hashicorp/packer/builder/docker" - "github.com/hashicorp/packer/packer" + builderT "github.com/hashicorp/packer/packer-plugin-sdk/acctest" packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" - "github.com/hashicorp/packer/packer-plugin-sdk/template" "github.com/hashicorp/packer/provisioner/file" ) @@ -329,69 +328,38 @@ func testProvisionerProvisionDockerWithPlaybookFiles(t *testing.T, templateStrin t.Skip("This test is only run with PACKER_ACC=1") } - ui := packersdk.TestUi(t) - - tpl, err := template.Parse(strings.NewReader(templateString)) - if err != nil { - t.Fatalf("Unable to parse config: %s", err) - } - - // Check if docker executable can be found. - _, err = exec.LookPath("docker") + // this should be a precheck + cmd := exec.Command("docker", "-v") + err := cmd.Run() if err != nil { t.Error("docker command not found; please make sure docker is installed") } - // Setup the builder - builder := &docker.Builder{} - _, warnings, err := builder.Prepare(tpl.Builders["docker"].Config) - if err != nil { - t.Fatalf("Error preparing configuration %s", err) - } - if len(warnings) > 0 { - t.Fatal("Encountered configuration warnings; aborting") - } + builderT.Test(t, builderT.TestCase{ + Builder: &docker.Builder{}, + Template: templateString, + Check: func(a []packersdk.Artifact) error { - ansible := &Provisioner{} - err = ansible.Prepare(tpl.Provisioners[0].Config) - if err != nil { - t.Fatalf("Error preparing ansible-local provisioner: %s", err) - } + actualContent, err := ioutil.ReadFile("hello_world") + if err != nil { + return fmt.Errorf("Expected file not found: %s", err) + } - download := &file.Provisioner{} - err = download.Prepare(tpl.Provisioners[1].Config) - if err != nil { - t.Fatalf("Error preparing download: %s", err) - } - - // Add hooks so the provisioners run during the build - hooks := map[string][]packersdk.Hook{} - hooks[packersdk.HookProvision] = []packersdk.Hook{ - &packer.ProvisionHook{ - Provisioners: []*packer.HookedProvisioner{ - {Provisioner: ansible, Config: nil, TypeName: ""}, - {Provisioner: download, Config: nil, TypeName: ""}, - }, + expectedContent := "Hello world!" + if string(actualContent) != expectedContent { + return fmt.Errorf(`Unexpected file content: expected="%s", actual="%s"`, expectedContent, actualContent) + } + return nil }, - } - hook := &packersdk.DispatchHook{Mapping: hooks} - - artifact, err := builder.Run(context.Background(), ui, hook) - if err != nil { - t.Fatalf("Error running build %s", err) - } - defer os.Remove("hello_world") - defer artifact.Destroy() - - actualContent, err := ioutil.ReadFile("hello_world") - if err != nil { - t.Fatalf("Expected file not found: %s", err) - } - - expectedContent := "Hello world!" - if string(actualContent) != expectedContent { - t.Fatalf(`Unexpected file content: expected="%s", actual="%s"`, expectedContent, actualContent) - } + Teardown: func() error { + os.Remove("hello_world") + return nil + }, + ProvisionerStore: packersdk.MapOfProvisioner{ + "ansible-local": func() (packersdk.Provisioner, error) { return &Provisioner{}, nil }, + "file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil }, + }, + }) } func assertPlaybooksExecuted(comm *communicatorMock, playbooks []string) { @@ -477,7 +445,7 @@ const playbookFilesDockerTemplate = ` { "builders": [ { - "type": "docker", + "type": "test", "image": "williamyeh/ansible:centos7", "discard": true } @@ -504,7 +472,7 @@ const playbookFilesWithPlaybookDirDockerTemplate = ` { "builders": [ { - "type": "docker", + "type": "test", "image": "williamyeh/ansible:centos7", "discard": true } From 76177b50cef6127529b6623b848c9535d3e1a117 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 4 Dec 2020 15:14:45 -0800 Subject: [PATCH 3/8] Refactor provisioner acceptance tests to shell directly out to Packer rather than import the core. Modify test case formatting to more closely reflect the builder test cases. --- Makefile | 2 +- .../iso/acceptance/builder_acceptance.go | 2 +- .../acctest/provisioneracc/builders.go | 49 +++ .../acctest/provisioneracc/provisioners.go | 314 ++++++++++++++---- .../test-fixtures/amazon-ebs/amazon-ebs.txt | 12 + .../amazon-ebs/amazon-ebs_windows.txt | 23 ++ .../amazon-ebs/scripts/bootstrap_win.txt | 41 +++ .../test-fixtures/virtualbox/http/preseed.cfg | 42 +++ .../virtualbox/virtualbox-iso.txt | 43 +++ .../powershell/provisioner_acc_test.go | 170 +++++----- .../shell-local/provisioner_acc_test.go | 91 ++--- .../shell-local/test-fixtures/script.sh | 5 +- provisioner/shell/provisioner_acc_test.go | 101 +++--- .../shell/test-fixtures/shell-provisioner.txt | 4 +- 14 files changed, 667 insertions(+), 232 deletions(-) create mode 100644 packer-plugin-sdk/acctest/provisioneracc/builders.go create mode 100644 packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt create mode 100644 packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs_windows.txt create mode 100644 packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/scripts/bootstrap_win.txt create mode 100644 packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/http/preseed.cfg create mode 100644 packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/virtualbox-iso.txt mode change 100644 => 100755 provisioner/shell-local/test-fixtures/script.sh diff --git a/Makefile b/Makefile index 0d0657bba..8f80a2474 100644 --- a/Makefile +++ b/Makefile @@ -140,7 +140,7 @@ test: mode-check vet ## Run unit tests @go test -count $(COUNT) $(TEST) $(TESTARGS) -timeout=3m # acctest runs provisioners acceptance tests -provisioners-acctest: install-build-deps generate +provisioners-acctest: #install-build-deps generate ACC_TEST_BUILDERS=$(ACC_TEST_BUILDERS) ACC_TEST_PROVISIONERS=$(ACC_TEST_PROVISIONERS) go test ./provisioner/... -timeout=1h # testacc runs acceptance tests diff --git a/builder/virtualbox/iso/acceptance/builder_acceptance.go b/builder/virtualbox/iso/acceptance/builder_acceptance.go index 11db2c342..589efb91a 100644 --- a/builder/virtualbox/iso/acceptance/builder_acceptance.go +++ b/builder/virtualbox/iso/acceptance/builder_acceptance.go @@ -27,7 +27,7 @@ func (v *VirtualBoxISOAccTest) GetConfigs() (map[string]string, error) { file, err := ioutil.ReadAll(config) if err != nil { - return nil, fmt.Errorf("Uneble to read %s", filePath) + return nil, fmt.Errorf("Unable to read %s", filePath) } return map[string]string{"linux": string(file)}, nil } diff --git a/packer-plugin-sdk/acctest/provisioneracc/builders.go b/packer-plugin-sdk/acctest/provisioneracc/builders.go new file mode 100644 index 000000000..eb7cf5222 --- /dev/null +++ b/packer-plugin-sdk/acctest/provisioneracc/builders.go @@ -0,0 +1,49 @@ +package provisioneracc + +import ( + "github.com/hashicorp/packer/packer-plugin-sdk/acctest/testutils" +) + +var AmasonEBSBuilderFixtureLinux = &BuilderFixture{ + Name: "Amazon-ebs Linux builder", + TemplatePath: "amazon-ebs/amazon-ebs.txt", + GuestOS: "linux", + HostOS: "any", + Teardown: func() error { + // TODO + // helper := AWSHelper{ + // Region: "us-east-1", + // AMIName: "packer-acc-test", + // } + // return helper.CleanUpAmi() + return nil + }, +} + +var AmasonEBSBuilderFixtureWindows = &BuilderFixture{ + Name: "Amazon-ebs Windows builder", + TemplatePath: "amazon-ebs/amazon-ebs_windows.txt", + GuestOS: "windows", + HostOS: "any", + Teardown: func() error { + // TODO + // helper := AWSHelper{ + // Region: "us-east-1", + // AMIName: "packer-acc-test", + // } + // return helper.CleanUpAmi() + return nil + }, +} + +var VirtualboxBuilderFixtureWindows = &BuilderFixture{ + Name: "Virtualbox Windows builder", + TemplatePath: "virtualbox/virtualbox-iso.txt", + GuestOS: "linux", + HostOS: "any", + Teardown: func() error { + testutils.CleanupFiles("virtualbox-iso-packer-acc-test") + testutils.CleanupFiles("packer_cache") + return nil + }, +} diff --git a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go index 56ecaae8a..66733bcec 100644 --- a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go +++ b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go @@ -3,65 +3,243 @@ package provisioneracc import ( "bytes" "fmt" + "io/ioutil" "log" "os" + "os/exec" "path/filepath" + "runtime" "strings" "testing" - amazonEBS "github.com/hashicorp/packer/builder/amazon/ebs/acceptance" - virtualboxISO "github.com/hashicorp/packer/builder/virtualbox/iso/acceptance" - "github.com/hashicorp/packer/command" - "github.com/hashicorp/packer/packer-plugin-sdk/acctest/testutils" + builderT "github.com/hashicorp/packer/packer-plugin-sdk/acctest" packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" ) -func TestProvisionersAgainstBuilders(provisionerAcc ProvisionerAcceptance, t *testing.T) { - provisioner := provisionerAcc.GetName() - builders := checkBuilders(t) +// 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 +} - // build template file and run a build for each builder with the provisioner - for _, builder := range builders { - builderAcc := BuildersAccTest[builder] - builderConfigs, err := builderAcc.GetConfigs() - if err != nil { - t.Fatalf("bad: failed to read builder config: %s", err.Error()) - } +// 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 - for vmOS, builderConfig := range builderConfigs { - if !provisionerAcc.IsCompatible(builder, vmOS) { + // 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) { + TestProvisionersPreCheck(testCase.Type, t) + TestProvisionersAgainstBuilders(testCase, t) +} + +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("testing %s builder against %s provisioner", builder, provisioner) + 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) { - provisionerConfig, err := provisionerAcc.GetConfig() + builderFragment, err := LoadBuilderFragment(buildFixture.TemplatePath) if err != nil { - t.Fatalf("bad: failed to read provisioner config: %s", err.Error()) + t.Fatalf("failed to load builder fragment: %s", err) } - // Write json template + // Combine provisioner and builder template fragments; write to + // file. out := bytes.NewBuffer(nil) - fmt.Fprintf(out, `{"builders": [%s],"provisioners": [%s]}`, builderConfig, provisionerConfig) - fileName := fmt.Sprintf("%s_%s.json", builder, provisioner) - filePath := filepath.Join("./", fileName) - writeJsonTemplate(out, filePath, t) + 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) - // set pre-config with necessary builder and provisioner - c := buildCommand(t, builderAcc, provisionerAcc) - args := []string{ - filePath, + // Run build + // TODO: stream logs _and_ store in a logfile. + buildCommand := exec.Command("packer", "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) } - err = provisionerAcc.RunTest(c, args) - // Cleanup created resources - testutils.CleanupFiles(fileName) - cleanErr := builderAcc.CleanUp() + // preemptive cleanup + defer os.Remove(templatePath) + defer os.Remove(logfile) + + // Cleanup stuff created by builder. + cleanErr := buildFixture.Teardown() if cleanErr != nil { - log.Printf("bad: failed to clean up resources: %s", cleanErr.Error()) + log.Printf("bad: failed to clean up builder-created resources: %s", cleanErr.Error()) } - if err != nil { - t.Fatalf("bad: failed to to run build: %s", err.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 { + t.Fatalf(checkErr.Error()) } }) } @@ -88,6 +266,8 @@ func TestProvisionersPreCheck(name string, t *testing.T) { } +// 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 @@ -119,32 +299,52 @@ func writeJsonTemplate(out *bytes.Buffer, filePath string, t *testing.T) { outputFile.Sync() } -func buildCommand(t *testing.T, builder BuilderAcceptance, provisioner ProvisionerAcceptance) *command.BuildCommand { - c := &command.BuildCommand{ - Meta: TestMetaFile(t), - } - c.CoreConfig.Components.BuilderStore = builder.GetBuilderStore() - c.CoreConfig.Components.ProvisionerStore = provisioner.GetProvisionerStore() - - return c -} - -type ProvisionerAcceptance interface { - GetName() string - GetConfig() (string, error) - GetProvisionerStore() packersdk.MapOfProvisioner - IsCompatible(builder string, vmOS string) bool - RunTest(c *command.BuildCommand, args []string) error -} - +// BuilderAcceptance is specialized tooling implemented by individual builders +// To add your builder to the provisioner testing framework, create a struct +// that implements the 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 } -// List of all builders available for acceptance test -var BuildersAccTest = map[string]BuilderAcceptance{ - "virtualbox-iso": new(virtualboxISO.VirtualBoxISOAccTest), - "amazon-ebs": new(amazonEBS.AmazonEBSAccTest), +// Mapping of all builder fixtures defined for a given builder type. +var BuildersAccTest = map[string][]*BuilderFixture{ + "virtualbox-iso": []*BuilderFixture{VirtualboxBuilderFixtureWindows}, + "amazon-ebs": []*BuilderFixture{AmasonEBSBuilderFixtureLinux, AmasonEBSBuilderFixtureWindows}, } diff --git a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt new file mode 100644 index 000000000..1245cf9e5 --- /dev/null +++ b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt @@ -0,0 +1,12 @@ +{ + "type": "amazon-ebs", + "ami_name": "packer-acc-test", + "instance_type": "m1.small", + "region": "us-east-1", + "ssh_username": "ubuntu", + "source_ami": "ami-0568456c", + "force_deregister" : true, + "tags": { + "packer-test": "true" + } +} \ No newline at end of file diff --git a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs_windows.txt b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs_windows.txt new file mode 100644 index 000000000..e57ea4808 --- /dev/null +++ b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs_windows.txt @@ -0,0 +1,23 @@ +{ + "type": "amazon-ebs", + "region": "us-east-1", + "instance_type": "t2.micro", + "source_ami_filter": { + "filters": { + "virtualization-type": "hvm", + "name": "*Windows_Server-2012-R2*English-64Bit-Base*", + "root-device-type": "ebs" + }, + "most_recent": true, + "owners": "amazon" + }, + "ami_name": "packer-acc-test", + "user_data_file": "../../builder/amazon/ebs/acceptance/test-fixtures/scripts/bootstrap_win.txt", + "communicator": "winrm", + "winrm_username": "Administrator", + "winrm_password": "SuperS3cr3t!!!!", + "force_deregister" : true, + "tags": { + "packer-test": "true" + } +} \ No newline at end of file diff --git a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/scripts/bootstrap_win.txt b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/scripts/bootstrap_win.txt new file mode 100644 index 000000000..e7641d7a4 --- /dev/null +++ b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/scripts/bootstrap_win.txt @@ -0,0 +1,41 @@ + +# Set administrator password +net user Administrator SuperS3cr3t!!!! +wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE + +# First, make sure WinRM can't be connected to +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block + +# Delete any existing WinRM listeners +winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null +winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null + +# Disable group policies which block basic authentication and unencrypted login + +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowBasic -Value 1 +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowUnencryptedTraffic -Value 1 +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowBasic -Value 1 +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowUnencryptedTraffic -Value 1 + + +# Create a new WinRM listener and configure +winrm create winrm/config/listener?Address=*+Transport=HTTP +winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}' +winrm set winrm/config '@{MaxTimeoutms="7200000"}' +winrm set winrm/config/service '@{AllowUnencrypted="true"}' +winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}' +winrm set winrm/config/service/auth '@{Basic="true"}' +winrm set winrm/config/client/auth '@{Basic="true"}' + +# Configure UAC to allow privilege elevation in remote shells +$Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' +$Setting = 'LocalAccountTokenFilterPolicy' +Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force + +# Configure and restart the WinRM Service; Enable the required firewall exception +Stop-Service -Name WinRM +Set-Service -Name WinRM -StartupType Automatic +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any +Start-Service -Name WinRM + + diff --git a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/http/preseed.cfg b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/http/preseed.cfg new file mode 100644 index 000000000..57885df31 --- /dev/null +++ b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/http/preseed.cfg @@ -0,0 +1,42 @@ +# Preseeding only locale sets language, country and locale. +d-i debian-installer/locale string en_US + +# Keyboard selection. +d-i console-setup/ask_detect boolean false +d-i keyboard-configuration/xkb-keymap select us + +choose-mirror-bin mirror/http/proxy string +d-i base-installer/kernel/override-image string linux-server +d-i clock-setup/utc boolean true +d-i clock-setup/utc-auto boolean true +d-i finish-install/reboot_in_progress note +d-i grub-installer/only_debian boolean true +d-i grub-installer/with_other_os boolean true +d-i mirror/country string manual +d-i mirror/http/directory string /ubuntu/ +d-i mirror/http/hostname string archive.ubuntu.com +d-i mirror/http/proxy string +d-i partman-auto-lvm/guided_size string max +d-i partman-auto/choose_recipe select atomic +d-i partman-auto/method string lvm +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman-lvm/device_remove_lvm boolean true +d-i partman/choose_partition select finish +d-i partman/confirm boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/confirm_write_new_label boolean true +d-i passwd/user-fullname string vagrant +d-i passwd/user-uid string 1000 +d-i passwd/user-password password vagrant +d-i passwd/user-password-again password vagrant +d-i passwd/username string vagrant +d-i pkgsel/include string openssh-server cryptsetup build-essential libssl-dev libreadline-dev zlib1g-dev linux-source dkms nfs-kernel-server nfs-common linux-headers-$(uname -r) perl +d-i pkgsel/install-language-support boolean false +d-i pkgsel/update-policy select none +d-i pkgsel/upgrade select full-upgrade +d-i time/zone string UTC +d-i user-setup/allow-password-weak boolean true +d-i user-setup/encrypt-home boolean false +tasksel tasksel/first multiselect standard, server diff --git a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/virtualbox-iso.txt b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/virtualbox-iso.txt new file mode 100644 index 000000000..ba41daa0e --- /dev/null +++ b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/virtualbox/virtualbox-iso.txt @@ -0,0 +1,43 @@ +{ + "type": "virtualbox-iso", + "iso_checksum": "sha256:946a6077af6f5f95a51f82fdc44051c7aa19f9cfc5f737954845a6050543d7c2", + "iso_url": "http://old-releases.ubuntu.com/releases/14.04.1/ubuntu-14.04.1-server-amd64.iso", + "disk_size": "40960", + "guest_os_type": "Ubuntu_64", + "headless" : "true", + + "ssh_password": "vagrant", + "ssh_username": "vagrant", + "ssh_wait_timeout": "10000s", + + "http_directory": "../../builder/virtualbox/iso/acceptance/test-fixtures/http", + "boot_wait": "10s", + "boot_command": [ + "", + "", + "", + "/install/vmlinuz", + " auto", + " console-setup/ask_detect=false", + " console-setup/layoutcode=us", + " console-setup/modelcode=pc105", + " debconf/frontend=noninteractive", + " debian-installer=en_US.UTF-8", + " fb=false", + " initrd=/install/initrd.gz", + " kbd-chooser/method=us", + " keyboard-configuration/layout=USA", + " keyboard-configuration/variant=USA", + " locale=en_US.UTF-8", + " netcfg/get_domain=vm", + " netcfg/get_hostname=vagrant", + " noapic", + " preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg", + " -- ", + "" + ], + + "output_directory": "virtualbox-iso-packer-acc-test", + "shutdown_command": "echo 'vagrant' | sudo -S shutdown -P now", + "post_shutdown_delay": "60s" +} \ No newline at end of file diff --git a/provisioner/powershell/provisioner_acc_test.go b/provisioner/powershell/provisioner_acc_test.go index e6a8c4af7..5488752c6 100644 --- a/provisioner/powershell/provisioner_acc_test.go +++ b/provisioner/powershell/provisioner_acc_test.go @@ -1,94 +1,110 @@ package powershell_test import ( - "bytes" "fmt" "io/ioutil" "os" + "os/exec" "path/filepath" + "runtime" "testing" - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/packer/command" "github.com/hashicorp/packer/packer-plugin-sdk/acctest/provisioneracc" - packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" - "github.com/hashicorp/packer/provisioner/powershell" - windowsshellprovisioner "github.com/hashicorp/packer/provisioner/windows-shell" ) -const TestProvisionerName = "powershell" +const TestProvisionerType = "powershell" -func TestAccPowershellProvisioner_basic(t *testing.T) { - provisioneracc.TestProvisionersPreCheck(TestProvisionerName, t) - - testProvisioner := PowershellProvisionerAccTest{"powershell-provisioner-cleanup.txt"} - provisioneracc.TestProvisionersAgainstBuilders(&testProvisioner, t) -} - -func TestAccPowershellProvisioner_Inline(t *testing.T) { - provisioneracc.TestProvisionersPreCheck(TestProvisionerName, t) - - testProvisioner := PowershellProvisionerAccTest{"powershell-inline-provisioner.txt"} - provisioneracc.TestProvisionersAgainstBuilders(&testProvisioner, t) -} - -func TestAccPowershellProvisioner_Script(t *testing.T) { - provisioneracc.TestProvisionersPreCheck(TestProvisionerName, t) - - testProvisioner := PowershellProvisionerAccTest{"powershell-script-provisioner.txt"} - provisioneracc.TestProvisionersAgainstBuilders(&testProvisioner, t) -} - -type PowershellProvisionerAccTest struct { - ConfigName string -} - -func (s *PowershellProvisionerAccTest) GetName() string { - return TestProvisionerName -} - -func (s *PowershellProvisionerAccTest) GetConfig() (string, error) { - filePath := filepath.Join("./test-fixtures", s.ConfigName) - config, err := os.Open(filePath) - if err != nil { - return "", fmt.Errorf("os.Open:%v", err) - } - defer config.Close() - - file, err := ioutil.ReadAll(config) - if err != nil { - return "", fmt.Errorf("ioutil.ReadAll:%v", err) - } - return string(file), nil -} - -func (s *PowershellProvisionerAccTest) GetProvisionerStore() packersdk.MapOfProvisioner { - return packersdk.MapOfProvisioner{ - TestProvisionerName: func() (packersdk.Provisioner, error) { return &powershell.Provisioner{}, nil }, - "windows-shell": func() (packersdk.Provisioner, error) { return &windowsshellprovisioner.Provisioner{}, nil }, - } -} - -func (s *PowershellProvisionerAccTest) IsCompatible(builder string, vmOS string) bool { +func powershellIsCompatible(builder string, vmOS string) bool { return vmOS == "windows" } -func (s *PowershellProvisionerAccTest) RunTest(c *command.BuildCommand, args []string) error { - UUID := os.Getenv("PACKER_RUN_UUID") - if UUID == "" { - UUID, _ = uuid.GenerateUUID() - os.Setenv("PACKER_RUN_UUID", UUID) - } - - if code := c.Run(args); code != 0 { - ui := c.Meta.Ui.(*packersdk.BasicUi) - out := ui.Writer.(*bytes.Buffer) - err := ui.ErrorWriter.(*bytes.Buffer) - return fmt.Errorf( - "Bad exit code.\n\nStdout:\n\n%s\n\nStderr:\n\n%s", - out.String(), - err.String()) - } - - return nil +func fixtureDir() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "test-fixtures") +} + +func LoadProvisionerFragment(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 TestAccPowershellProvisioner_basic(t *testing.T) { + templateString, err := LoadProvisionerFragment("powershell-provisioner-cleanup.txt") + if err != nil { + t.Fatalf("Couldn't load test fixture; %s", err.Error()) + } + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: powershellIsCompatible, + Name: "powershell-provisioner-cleanup", + Template: templateString, + Type: TestProvisionerType, + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) +} + +func TestAccPowershellProvisioner_Inline(t *testing.T) { + templateString, err := LoadProvisionerFragment("powershell-inline-provisioner.txt") + if err != nil { + t.Fatalf("Couldn't load test fixture; %s", err.Error()) + } + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: powershellIsCompatible, + Name: "powershell-provisioner-inline", + Template: templateString, + Type: TestProvisionerType, + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) +} + +func TestAccPowershellProvisioner_Script(t *testing.T) { + templateString, err := LoadProvisionerFragment("powershell-script-provisioner.txt") + if err != nil { + t.Fatalf("Couldn't load test fixture; %s", err.Error()) + } + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: powershellIsCompatible, + Name: "powershell-provisioner-script", + Template: templateString, + Type: TestProvisionerType, + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) } diff --git a/provisioner/shell-local/provisioner_acc_test.go b/provisioner/shell-local/provisioner_acc_test.go index 9aef8d04f..1ecf47c09 100644 --- a/provisioner/shell-local/provisioner_acc_test.go +++ b/provisioner/shell-local/provisioner_acc_test.go @@ -1,65 +1,76 @@ package shell_test import ( - "bytes" "fmt" "io/ioutil" "os" + "os/exec" "path/filepath" + "runtime" + "strings" "testing" "github.com/hashicorp/packer/packer-plugin-sdk/acctest/provisioneracc" - - "github.com/hashicorp/packer/provisioner/shell" - - packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" - - "github.com/hashicorp/packer/command" + "github.com/hashicorp/packer/packer-plugin-sdk/acctest/testutils" ) -func TestShellLocalProvisionerWithRetryOption(t *testing.T) { - provisioneracc.TestProvisionersPreCheck("shell-local", t) - provisioneracc.TestProvisionersAgainstBuilders(new(ShellLocalProvisionerAccTest), t) +func fixtureDir() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "test-fixtures") } -type ShellLocalProvisionerAccTest struct{} - -func (s *ShellLocalProvisionerAccTest) GetName() string { - return "file" -} - -func (s *ShellLocalProvisionerAccTest) GetConfig() (string, error) { - filePath := filepath.Join("./test-fixtures", "shell-local-provisioner.txt") - config, err := os.Open(filePath) +func loadFile(templateFragmentPath string) (string, error) { + dir := fixtureDir() + fragmentAbsPath := filepath.Join(dir, templateFragmentPath) + fragmentFile, err := os.Open(fragmentAbsPath) if err != nil { - return "", fmt.Errorf("Expected to find %s", filePath) + return "", fmt.Errorf("Unable find %s", fragmentAbsPath) } - defer config.Close() + defer fragmentFile.Close() - file, err := ioutil.ReadAll(config) - return string(file), err + fragmentString, err := ioutil.ReadAll(fragmentFile) + if err != nil { + return "", fmt.Errorf("Unable to read %s", fragmentAbsPath) + } + + return string(fragmentString), nil } -func (s *ShellLocalProvisionerAccTest) GetProvisionerStore() packersdk.MapOfProvisioner { - return packersdk.MapOfProvisioner{ - "shell-local": func() (packersdk.Provisioner, error) { return &shell.Provisioner{}, nil }, - } -} - -func (s *ShellLocalProvisionerAccTest) IsCompatible(builder string, vmOS string) bool { +func IsCompatible(builder string, vmOS string) bool { return vmOS == "linux" } -func (s *ShellLocalProvisionerAccTest) RunTest(c *command.BuildCommand, args []string) error { - if code := c.Run(args); code != 0 { - ui := c.Meta.Ui.(*packersdk.BasicUi) - out := ui.Writer.(*bytes.Buffer) - err := ui.ErrorWriter.(*bytes.Buffer) - return fmt.Errorf( - "Bad exit code.\n\nStdout:\n\n%s\n\nStderr:\n\n%s", - out.String(), - err.String()) +func TestAccShellProvisioner_basic(t *testing.T) { + templateString, err := loadFile("shell-local-provisioner.txt") + if err != nil { + t.Fatalf("Couldn't load test fixture; %s", err.Error()) } - return nil + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: IsCompatible, + Name: "shell-local-provisioner-basic", + Teardown: func() error { + testutils.CleanupFiles("test-fixtures/file.txt") + return nil + }, + Template: templateString, + Type: "shell-local", + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + filecontents, err := loadFile("file.txt") + if err != nil { + return err + } + if !strings.Contains(filecontents, "hello") { + return fmt.Errorf("file contents were wrong: %s", filecontents) + } + return nil + }, + } + + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) } diff --git a/provisioner/shell-local/test-fixtures/script.sh b/provisioner/shell-local/test-fixtures/script.sh old mode 100644 new mode 100755 index 77637c719..9b1797265 --- a/provisioner/shell-local/test-fixtures/script.sh +++ b/provisioner/shell-local/test-fixtures/script.sh @@ -1,6 +1,7 @@ #!/bin/bash -if [[ ! -f file.txt ]] ; then - echo 'hello' > file.txt +# On first try, exits 1; on second try, passes. +if [[ ! -f test-fixtures/file.txt ]] ; then + echo 'hello' > test-fixtures/file.txt exit 1 fi \ No newline at end of file diff --git a/provisioner/shell/provisioner_acc_test.go b/provisioner/shell/provisioner_acc_test.go index 3704feeba..98b986697 100644 --- a/provisioner/shell/provisioner_acc_test.go +++ b/provisioner/shell/provisioner_acc_test.go @@ -1,80 +1,77 @@ package shell_test import ( - "bytes" "fmt" "io/ioutil" "os" + "os/exec" "path/filepath" + "regexp" + "runtime" "testing" "github.com/hashicorp/packer/packer-plugin-sdk/acctest/provisioneracc" "github.com/hashicorp/packer/packer-plugin-sdk/acctest/testutils" - "github.com/hashicorp/packer/provisioner/file" - "github.com/hashicorp/packer/provisioner/shell" - - packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/packer/command" ) -func TestShellProvisioner(t *testing.T) { - provisioneracc.TestProvisionersPreCheck("shell", t) - provisioneracc.TestProvisionersAgainstBuilders(new(ShellProvisionerAccTest), t) +func fixtureDir() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(file), "test-fixtures") } -type ShellProvisionerAccTest struct{} - -func (s *ShellProvisionerAccTest) GetName() string { - return "shell" -} - -func (s *ShellProvisionerAccTest) GetConfig() (string, error) { - filePath := filepath.Join("./test-fixtures", "shell-provisioner.txt") - config, err := os.Open(filePath) +func loadFile(templateFragmentPath string) (string, error) { + dir := fixtureDir() + fragmentAbsPath := filepath.Join(dir, templateFragmentPath) + fragmentFile, err := os.Open(fragmentAbsPath) if err != nil { - return "", fmt.Errorf("Expected to find %s", filePath) + return "", fmt.Errorf("Unable find %s", fragmentAbsPath) } - defer config.Close() + defer fragmentFile.Close() - file, err := ioutil.ReadAll(config) - return string(file), err + fragmentString, err := ioutil.ReadAll(fragmentFile) + if err != nil { + return "", fmt.Errorf("Unable to read %s", fragmentAbsPath) + } + + return string(fragmentString), nil } -func (s *ShellProvisionerAccTest) GetProvisionerStore() packersdk.MapOfProvisioner { - return packersdk.MapOfProvisioner{ - "shell": func() (packersdk.Provisioner, error) { return &shell.Provisioner{}, nil }, - "file": func() (packersdk.Provisioner, error) { return &file.Provisioner{}, nil }, - } -} - -func (s *ShellProvisionerAccTest) IsCompatible(builder string, vmOS string) bool { +func IsCompatible(builder string, vmOS string) bool { return vmOS == "linux" } -func (s *ShellProvisionerAccTest) RunTest(c *command.BuildCommand, args []string) error { - UUID := os.Getenv("PACKER_RUN_UUID") - if UUID == "" { - UUID, _ = uuid.GenerateUUID() - os.Setenv("PACKER_RUN_UUID", UUID) +func TestAccShellProvisioner_basic(t *testing.T) { + templateString, err := loadFile("shell-provisioner.txt") + if err != nil { + t.Fatalf("Couldn't load test fixture; %s", err.Error()) } - file := "provisioner.shell." + UUID + ".txt" - defer testutils.CleanupFiles(file) - - if code := c.Run(args); code != 0 { - ui := c.Meta.Ui.(*packersdk.BasicUi) - out := ui.Writer.(*bytes.Buffer) - err := ui.ErrorWriter.(*bytes.Buffer) - return fmt.Errorf( - "Bad exit code.\n\nStdout:\n\n%s\n\nStderr:\n\n%s", - out.String(), - err.String()) + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: IsCompatible, + Name: "shell-provisioner-basic", + Teardown: func() error { + testutils.CleanupFiles("test-fixtures/provisioner.shell.txt") + return nil + }, + Template: templateString, + Type: "shell", + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + filecontents, err := loadFile("provisioner.shell.txt") + if err != nil { + return err + } + re := regexp.MustCompile(`build ID is .* and build UUID is [[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}`) + if !re.MatchString(filecontents) { + return fmt.Errorf("Bad file contents \"%s\"", filecontents) + } + return nil + }, } - if !testutils.FileExists(file) { - return fmt.Errorf("Expected to find %s", file) - } - return nil + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) } diff --git a/provisioner/shell/test-fixtures/shell-provisioner.txt b/provisioner/shell/test-fixtures/shell-provisioner.txt index c368eda47..7bf3dcd03 100644 --- a/provisioner/shell/test-fixtures/shell-provisioner.txt +++ b/provisioner/shell/test-fixtures/shell-provisioner.txt @@ -1,12 +1,12 @@ { "type": "shell", "inline": [ - "echo {{ build `ID`}} > provisioner.{{ build `PackerRunUUID`}}.txt" + "echo build ID is {{ build `ID`}} and build UUID is {{ build `PackerRunUUID` }}> provisioner.{{ build `PackerRunUUID`}}.txt" ] }, { "type": "file", "source": "provisioner.{{ build `PackerRunUUID`}}.txt", - "destination": "provisioner.shell.{{ build `PackerRunUUID`}}.txt", + "destination": "./test-fixtures/provisioner.shell.txt", "direction": "download" } \ No newline at end of file From 6ecdd3ca164eb0174ffd9fc24e60063a83f677b0 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 7 Dec 2020 15:06:46 -0800 Subject: [PATCH 4/8] small tweaks --- Makefile | 2 +- .../acctest/provisioneracc/provisioners.go | 35 +++++-------------- .../test-fixtures/amazon-ebs/amazon-ebs.txt | 12 +++++-- packer-plugin-sdk/acctest/testing.go | 1 - 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index 8f80a2474..87f0829d9 100644 --- a/Makefile +++ b/Makefile @@ -141,7 +141,7 @@ test: mode-check vet ## Run unit tests # acctest runs provisioners acceptance tests provisioners-acctest: #install-build-deps generate - ACC_TEST_BUILDERS=$(ACC_TEST_BUILDERS) ACC_TEST_PROVISIONERS=$(ACC_TEST_PROVISIONERS) go test ./provisioner/... -timeout=1h + ACC_TEST_BUILDERS=$(ACC_TEST_BUILDERS) go test $(TEST) $(TESTARGS) -timeout=1h # testacc runs acceptance tests testacc: # install-build-deps generate ## Run acceptance tests diff --git a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go index 66733bcec..3fea78db2 100644 --- a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go +++ b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go @@ -163,10 +163,10 @@ func LoadBuilderFragment(templateFragmentPath string) (string, error) { } func RunProvisionerAccTest(testCase *ProvisionerTestCase, t *testing.T) { - TestProvisionersPreCheck(testCase.Type, t) TestProvisionersAgainstBuilders(testCase, t) } +//nolint:errcheck func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T) { // retrieve user-desired builders. builderTypes := checkBuilders(t) @@ -207,7 +207,6 @@ func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T logfile := fmt.Sprintf("packer_log_%s_%s.txt", builderType, testCase.Type) // Run build - // TODO: stream logs _and_ store in a logfile. buildCommand := exec.Command("packer", "build", "--machine-readable", templatePath) buildCommand.Env = append(buildCommand.Env, os.Environ()...) buildCommand.Env = append(buildCommand.Env, "PACKER_LOG=1", @@ -220,10 +219,6 @@ func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T checkErr = testCase.Check(buildCommand, logfile) } - // preemptive cleanup - defer os.Remove(templatePath) - defer os.Remove(logfile) - // Cleanup stuff created by builder. cleanErr := buildFixture.Teardown() if cleanErr != nil { @@ -239,33 +234,19 @@ func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T // Fail test if check failed. if checkErr != nil { - t.Fatalf(checkErr.Error()) + t.Fatalf(fmt.Sprint("Error running provisioner acceptance"+ + " tests: %s\nLogs can be found at %s and the "+ + "acceptance test template can be found at %s", + checkErr.Error(), logfile, templatePath)) + } else { + os.Remove(templatePath) + os.Remove(logfile) } }) } } } -// TestProvisionersPreCheck checks if the Provisioner with name is set in ACC_TEST_PROVISIONERS environment variable -func TestProvisionersPreCheck(name string, t *testing.T) { - p := os.Getenv("ACC_TEST_PROVISIONERS") - - if p == "all" { - return - } - - provisioners := strings.Split(p, ",") - for _, provisioner := range provisioners { - if provisioner == name { - return - } - } - - msg := fmt.Sprintf("Provisioner %q not defined in ACC_TEST_PROVISIONERS", name) - t.Skip(msg) - -} - // checkBuilders retrieves all of the builders that the user has requested to // run acceptance tests against. func checkBuilders(t *testing.T) []string { diff --git a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt index 1245cf9e5..fd0af177c 100644 --- a/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt +++ b/packer-plugin-sdk/acctest/provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt @@ -1,10 +1,18 @@ { "type": "amazon-ebs", "ami_name": "packer-acc-test", - "instance_type": "m1.small", + "instance_type": "t2.micro", "region": "us-east-1", "ssh_username": "ubuntu", - "source_ami": "ami-0568456c", + "source_ami_filter": { + "filters": { + "virtualization-type": "hvm", + "name": "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*", + "root-device-type": "ebs" + }, + "owners": ["099720109477"], + "most_recent": true + }, "force_deregister" : true, "tags": { "packer-test": "true" diff --git a/packer-plugin-sdk/acctest/testing.go b/packer-plugin-sdk/acctest/testing.go index d28b5f2dd..acdd0bf51 100644 --- a/packer-plugin-sdk/acctest/testing.go +++ b/packer-plugin-sdk/acctest/testing.go @@ -175,7 +175,6 @@ func Test(t TestT, c TestCase) { // Run it! We use a temporary directory for caching and discard // any UI output. We discard since it shows up in logs anyways. log.Printf("[DEBUG] Running 'test' build") - // ui := packersdk.TestUi(t) ui := &packersdk.BasicUi{ Reader: os.Stdin, Writer: ioutil.Discard, From 00c61f9987d8fcbee5e035a9d457cd3d74cdcca6 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 7 Dec 2020 15:30:59 -0800 Subject: [PATCH 5/8] fix documentation for acceptance tests --- .github/CONTRIBUTING.md | 369 ++++++++++++++++++++++++---------------- 1 file changed, 223 insertions(+), 146 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e028b0446..064259792 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -301,15 +301,15 @@ You can run tests for individual packages using commands like this: make test TEST=./builder/amazon/... ``` -#### Running Acceptance Tests +#### Running Builder Acceptance Tests Packer has [acceptance tests](https://en.wikipedia.org/wiki/Acceptance_testing) for various builders. These typically require an API key (AWS, GCE), or additional software to be installed on your computer (VirtualBox, VMware). If you're working on a new builder or builder feature and want to verify it is -functioning (and also hasn't broken anything else), we recommend running the -acceptance tests. +functioning (and also hasn't broken anything else), we recommend creating or +running the acceptance tests. **Warning:** The acceptance tests create/destroy/modify _real resources_, which may incur costs for real money. In the presence of a bug, it is possible that @@ -340,7 +340,7 @@ Acceptance tests typically require other environment variables to be set for things such as API tokens and keys. Each test should error and tell you which credentials are missing, so those are not documented here. -#### Running Provisioners Acceptance Tests +#### Running Provisioner Acceptance Tests **Warning:** The acceptance tests create/destroy/modify _real resources_, which may incur costs for real money. In the presence of a bug, it is possible that @@ -351,124 +351,164 @@ resources are not accidentally destroyed or overwritten during testing. Also, these typically require an API key (AWS, GCE), or additional software to be installed on your computer (VirtualBox, VMware). -To run the Provisioners Acceptance Tests you should use both **ACC_TEST_BUILDERS** and **ACC_TEST_PROVISIONERS** variables to -tell which provisioner and builder the test should be run against. +To run the Provisioners Acceptance Tests you should use the +**ACC_TEST_BUILDERS** environment variable to tell the tests which builder the +test should be run against. Examples of usage: - Run the Shell provisioner acceptance tests against the Amazon EBS builder. ``` - ACC_TEST_BUILDERS=amazon-ebs ACC_TEST_PROVISIONERS=shell go test ./provisioner/shell/... -v -timeout=1h + ACC_TEST_BUILDERS=amazon-ebs go test ./provisioner/shell/... -v -timeout=1h ``` - Do the same but using the Makefile ``` - ACC_TEST_BUILDERS=amazon-ebs ACC_TEST_PROVISIONERS=shell make provisioners-acctest + ACC_TEST_BUILDERS=amazon-ebs make provisioners-acctest TEST=./provisioner/shell ``` -- Run the all Shell and Powershell provisioners acceptance tests against the Amazon EBS builder. +- Run all provisioner acceptance tests against the Amazon EBS builder. ``` - ACC_TEST_BUILDERS=amazon-ebs ACC_TEST_PROVISIONERS=shell,powershell make provisioners-acctest + ACC_TEST_BUILDERS=amazon-ebs make provisioners-acctest TEST=./... ``` -- Run the all provisioners acceptance tests against the Amazon EBS builder. +- Run all provisioner acceptance tests against all builders whenever they are compatible. ``` - ACC_TEST_BUILDERS=amazon-ebs ACC_TEST_PROVISIONERS=all make provisioners-acctest - ``` -- Run the all provisioners acceptance tests against all builders whenever they are compatible. - ``` - ACC_TEST_BUILDERS=all ACC_TEST_PROVISIONERS=all make provisioners-acctest + ACC_TEST_BUILDERS=all make provisioners-acctest TEST=./... ``` -Both **ACC_TEST_BUILDERS** and **ACC_TEST_PROVISIONERS** allows defining a list of builders and provisioners separated by comma -(e.g. `ACC_TEST_BUILDERS=amazon-ebs,virtualbox-iso`) +The **ACC_TEST_BUILDERS** env variable accepts a list of builders separated by +commas. (e.g. `ACC_TEST_BUILDERS=amazon-ebs,virtualbox-iso`) #### Writing Provisioner Acceptance Tests -Packer has an already implemented structure that will run the provisioner against builders and you can find it in `helper/tests/acc/provisioners.go`. -All provisioners should use this structure in their acceptance tests. - -To start writing a new provisioner acceptance test, you should add a test file named as `provisioner_acc_test.go` in the provisioner folder -and the package should be `_test`. This file should have a struct that will implement the ProvisionerAcceptance interface. +Packer has implemented a `ProvisionerTestCase` structure to help write +provisioner acceptance tests. ```go -type ProvisionerAcceptance interface { - GetName() string - GetConfig() (string, error) - GetProvisionerStore() packer.MapOfProvisioner - IsCompatible(builder string, vmOS string) bool - RunTest(c *command.BuildCommand, args []string) error +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 } + ``` -- **GetName()** should return the provisioner type. For example for the Shell provisioner the method returns "shell". +To start writing a new provisioner acceptance test, you should add a test file +named `provisioner_acc_test.go` in the same folder as your provisioner is +defined. Create a test case by implementing the above struct, and run it +by calling `provisioneracc.TestProvisionersAgainstBuilders(testCase, t)` -- **GetConfig()** should read a text file with the json configuration block for the provisioner and any other necessary provisioner. -For the Shell one the file contains: +The following example has been adapted from a shell-local provisioner test: - ``` - { - "type": "shell", - "inline": [ - "echo {{ build `ID`}} > provisioner.{{ build `PackerRunUUID`}}.txt" - ] +``` +import ( + "github.com/hashicorp/packer/packer-plugin-sdk/acctest/provisioneracc" + "github.com/hashicorp/packer/packer-plugin-sdk/acctest/testutils" +) + +// ... + +func TestAccShellProvisioner_basic(t *testing.T) { + // Create a json template fragment containing just the provisioners you want + // to run. + templateString := `{ + "type": "shell-local", + "script": "test-fixtures/script.sh", + "max_retries" : 5 +}` + + // instantiate a test case. + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: func() bool {return true}, + Name: "shell-local-provisioner-basic", + Teardown: func() error { + testutils.CleanupFiles("test-fixtures/file.txt") + return nil }, - { - "type": "file", - "source": "provisioner.{{ build `PackerRunUUID`}}.txt", - "destination": "provisioner.shell.{{ build `PackerRunUUID`}}.txt", - "direction": "download" - } - ``` - The file should be placed under the `test-fixtures` folder. - In this case, it's necessary to use the File provisioner to validate if the Shell provisioner test is successful or not. - This config should be returned as string that will be later merged with the builder config into a full template. - -- **GetProvisionerStore()** this returns the provisioner store where we declare the available provisioners for running the build. -For the Shell provisioners this is: - ```go - func (s *ShellProvisionerAccTest) GetProvisionerStore() packer.MapOfProvisioner { - return packer.MapOfProvisioner{ - "shell": func() (packer.Provisioner, error) { return &shell.Provisioner{}, nil }, - "file": func() (packer.Provisioner, error) { return &file.Provisioner{}, nil }, - } + Template: templateString, + Type: "shell-local", + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } } - ``` - -- **IsCompatible(builder string, vmOS string)** returns true or false whether the provisioner should run against a -specific builder or/and specific OS. - -- **RunTest(c \*command.BuildCommand, args []string)** it will actually run the build and return any error if it fails the validations. -For the Shell provisioner this is: - ```go - func (s *ShellProvisionerAccTest) RunTest(c *command.BuildCommand, args []string) error { - // Provisioner specific setup - UUID := os.Getenv("PACKER_RUN_UUID") - if UUID == "" { - UUID, _ = uuid.GenerateUUID() - os.Setenv("PACKER_RUN_UUID", UUID) - } - file := "provisioner.shell." + UUID + ".txt" - defer testutils.CleanupFiles(file) - - // Run build - // All provisioner acc tests should contain this code and validation - if code := c.Run(args); code != 0 { - ui := c.Meta.Ui.(*packer.BasicUi) - out := ui.Writer.(*bytes.Buffer) - err := ui.ErrorWriter.(*bytes.Buffer) - return fmt.Errorf( - "Bad exit code.\n\nStdout:\n\n%s\n\nStderr:\n\n%s", - out.String(), - err.String()) - } - - // Any other extra specific validation - if !testutils.FileExists(file) { - return fmt.Errorf("Expected to find %s", file) - } - return nil + filecontents, err := loadFile("file.txt") + if err != nil { + return err } + if !strings.Contains(filecontents, "hello") { + return fmt.Errorf("file contents were wrong: %s", filecontents) + } + return nil + }, + } + + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) +} + +``` - ``` After writing the struct and implementing the interface, now is time to write the test that will run all of this code you wrote. Your test should be like: @@ -480,64 +520,101 @@ func TestShellProvisioner(t *testing.T) { } ``` -If the environment variable **ACC_TEST_PROVISIONERS** is set as `all` or contains the provisioner type, then the test should run, otherwise the test should skip. -In case of running it, you'll need to call the helper function `acc.TestProvisionersAgainstBuilders` passing a pointer to the test struct created above and the test testing pointer. +The method `TestProvisionersAgainstBuilders` will run the provisioner against +all available and compatible builders. If there are not builders compatible with +the test you want to run, you can add a builder using the following steps: -The method `TestProvisionersAgainstBuilders` will run the provisioner against all available and compatible builders. An available builder -is the one that has the necessary code for running this type of test. In case the builder you want to run against is not available for testing, you can write it following the next steps. +Create a subdirectory in provisioneracc/test-fixtures for the type of builder +you are adding. In this subdirectory, add one json file containing a single +builder fragment. For example, one of our amazon-ebs builders is defined in +provisioneracc/test-fixtures/amazon-ebs/amazon-ebs.txt and contains: -To add a new builder to the available builders for provisioners acc testing, you'll need to create a new folder under the builder folder -called `acceptance` and inside you create the `builder_acceptance.go` file and the package should be `_acc`. Like the provisioners, you'll need to create a struct that will -implement the BuilderAcceptance interface. -```go -type BuilderAcceptance interface { - GetConfigs() (map[string]string, error) - GetBuilderStore() packer.MapOfBuilder - CleanUp() error -} -``` -- **GetConfigs()** should read a text file with the json configuration block for the builder and return a map of configs by OS type. -For the Amazon EBS builder the file contains: - ``` - { - "type": "amazon-ebs", - "ami_name": "packer-acc-test", - "instance_type": "m1.small", - "region": "us-east-1", - "ssh_username": "ubuntu", - "source_ami": "ami-0568456c", - "force_deregister" : true, - "tags": { - "packer-test": "true" - } - } - ``` - The file should be placed under the `test-fixtures` folder. - In case you need to make references to another file, you'll need to add the relative path to provisioners folder like: - `../../builder/amazon/ebs/acceptance/test-fixtures/file.txt`. - -- **GetBuilderStore()** this returns the builder store where we declare the available builders for running the build. -For the Amazon EBS builder this is: - ```go - func (s *AmazonEBSAccTest) GetBuilderStore() packer.MapOfBuilder { - return packer.MapOfBuilder{ - "amazon-ebs": func() (packer.Builder, error) { return &amazonebsbuilder.Builder{}, nil }, - } - } - ``` - -- **CleanUp()** cleans any resource created by the builder whether local or remote. - -Once you created the builder necessary code, the last step is adding it to the `BuildersAccTest` map in `helper/tests/acc/provisioners.go`. -```go -var BuildersAccTest = map[string]BuilderAcceptance{ - ... - "amazon-ebs": new(amazonEBS.AmazonEBSAccTest), - ... +```json +{ + "type": "amazon-ebs", + "ami_name": "packer-acc-test", + "instance_type": "t2.micro", + "region": "us-east-1", + "ssh_username": "ubuntu", + "source_ami_filter": { + "filters": { + "virtualization-type": "hvm", + "name": "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*", + "root-device-type": "ebs" + }, + "owners": ["099720109477"], + "most_recent": true + }, + "force_deregister" : true, + "tags": { + "packer-test": "true" + } } ``` -Once you finish the steps, you should be ready to run your new provisioner acceptance test. +note that this fragment does not contain anything other than a single builder +definition. The testing framework will combine this with the provisioner +fragment to create a working json template. + +In order to tell the testing framework how to use this builder fragment, you +need to implement a `BuilderFixture` struct: + +```go +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 +} +``` +Implement this struct to the file "provisioneracc/builders.go", then add +the new implementation to the `BuildersAccTest` map in +`provisioneracc/provisioners.go` + +Once you finish these steps, you should be ready to run your new provisioner +acceptance test by setting the name used in the BuildersAccTest map as your +`ACC_TEST_BUILDERS` environment variable. #### Debugging Plugins From 3c346867f4dd42d560e0736917dc6d276535396a Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 7 Dec 2020 15:41:40 -0800 Subject: [PATCH 6/8] fix nil pointer dereference --- builder/amazon/common/step_run_source_instance.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/builder/amazon/common/step_run_source_instance.go b/builder/amazon/common/step_run_source_instance.go index 7b07e23e8..4543b520b 100644 --- a/builder/amazon/common/step_run_source_instance.go +++ b/builder/amazon/common/step_run_source_instance.go @@ -251,8 +251,10 @@ func (s *StepRunSourceInstance) Run(ctx context.Context, state multistep.StateBa if resp, e := ec2conn.DescribeInstances(describeInstance); e == nil { if len(resp.Reservations) > 0 && len(resp.Reservations[0].Instances) > 0 { instance := resp.Reservations[0].Instances[0] - ui.Error(fmt.Sprintf("Instance state change details: %s: %s", - *instance.StateTransitionReason, *instance.StateReason.Message)) + if instance.StateTransitionReason != nil && instance.StateReason.Message != nil { + ui.Error(fmt.Sprintf("Instance state change details: %s: %s", + *instance.StateTransitionReason, *instance.StateReason.Message)) + } } } return multistep.ActionHalt From 2797c686e01092d72196e3e22687d173b52aa75d Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 8 Dec 2020 08:24:26 -0800 Subject: [PATCH 7/8] Update packer-plugin-sdk/acctest/provisioneracc/provisioners.go Co-authored-by: Sylvia Moss --- packer-plugin-sdk/acctest/provisioneracc/provisioners.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go index 3fea78db2..39c7aa073 100644 --- a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go +++ b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go @@ -282,7 +282,7 @@ func writeJsonTemplate(out *bytes.Buffer, filePath string, t *testing.T) { // BuilderAcceptance is specialized tooling implemented by individual builders // To add your builder to the provisioner testing framework, create a struct -// that implements the this interface, add it to the BuildersAccTest map below. +// 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 { From 48a0172670990bc883134181a9e44f19ef5d8ec5 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 8 Dec 2020 09:59:04 -0800 Subject: [PATCH 8/8] better error handling and messaging in provisioner acc test framework --- .../acctest/provisioneracc/provisioners.go | 15 +++++++++++---- provisioner/shell-local/provisioner_acc_test.go | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go index 39c7aa073..5a6292ae4 100644 --- a/packer-plugin-sdk/acctest/provisioneracc/provisioners.go +++ b/packer-plugin-sdk/acctest/provisioneracc/provisioners.go @@ -206,8 +206,13 @@ func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T 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("packer", "build", "--machine-readable", templatePath) + 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)) @@ -234,10 +239,12 @@ func TestProvisionersAgainstBuilders(testCase *ProvisionerTestCase, t *testing.T // Fail test if check failed. if checkErr != nil { - t.Fatalf(fmt.Sprint("Error running provisioner acceptance"+ - " tests: %s\nLogs can be found at %s and the "+ + 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(), logfile, templatePath)) + checkErr.Error(), filepath.Join(cwd, logfile), + filepath.Join(cwd, templatePath))) } else { os.Remove(templatePath) os.Remove(logfile) diff --git a/provisioner/shell-local/provisioner_acc_test.go b/provisioner/shell-local/provisioner_acc_test.go index 1ecf47c09..c3998694e 100644 --- a/provisioner/shell-local/provisioner_acc_test.go +++ b/provisioner/shell-local/provisioner_acc_test.go @@ -58,7 +58,7 @@ func TestAccShellProvisioner_basic(t *testing.T) { Check: func(buildcommand *exec.Cmd, logfile string) error { if buildcommand.ProcessState != nil { if buildcommand.ProcessState.ExitCode() != 0 { - return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + return fmt.Errorf("Bad exit code. Logfile: %s\n", logfile) } } filecontents, err := loadFile("file.txt")