From 001e4d7db9392783259cbc7e85552c06a87961ab Mon Sep 17 00:00:00 2001 From: Katy Moe Date: Fri, 11 Sep 2020 13:00:39 +0100 Subject: [PATCH 1/4] add plugintest package --- go.mod | 3 +- go.sum | 2 - helper/resource/plugin.go | 4 +- helper/resource/testing.go | 6 +- helper/resource/testing_new.go | 10 +- helper/resource/testing_new_config.go | 4 +- helper/resource/testing_new_import_state.go | 4 +- internal/plugintest/config.go | 54 +++ internal/plugintest/doc.go | 7 + internal/plugintest/guard.go | 94 +++++ internal/plugintest/helper.go | 222 +++++++++++ internal/plugintest/util.go | 95 +++++ internal/plugintest/working_dir.go | 420 ++++++++++++++++++++ 13 files changed, 907 insertions(+), 18 deletions(-) create mode 100644 internal/plugintest/config.go create mode 100644 internal/plugintest/doc.go create mode 100644 internal/plugintest/guard.go create mode 100644 internal/plugintest/helper.go create mode 100644 internal/plugintest/util.go create mode 100644 internal/plugintest/working_dir.go diff --git a/go.mod b/go.mod index c170307b92c..7dce82eb8b4 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 // indirect + github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 github.com/hashicorp/go-hclog v0.9.2 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-plugin v1.3.0 @@ -28,7 +28,6 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.10.0 github.com/hashicorp/terraform-json v0.5.0 - github.com/hashicorp/terraform-plugin-test/v2 v2.1.2 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba github.com/kylelemons/godebug v1.1.0 // indirect diff --git a/go.sum b/go.sum index 84445f3a244..e99d53f1fbf 100644 --- a/go.sum +++ b/go.sum @@ -195,8 +195,6 @@ github.com/hashicorp/terraform-exec v0.10.0 h1:3nh/1e3u9gYRUQGOKWp/8wPR7ABlL2F14 github.com/hashicorp/terraform-exec v0.10.0/go.mod h1:tOT8j1J8rP05bZBGWXfMyU3HkLi1LWyqL3Bzsc3CJjo= github.com/hashicorp/terraform-json v0.5.0 h1:7TV3/F3y7QVSuN4r9BEXqnWqrAyeOtON8f0wvREtyzs= github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU= -github.com/hashicorp/terraform-plugin-test/v2 v2.1.2 h1:p96IIn+XpvVjw7AtN8y9MKxn0x69S7wtbGf7JgDJoIk= -github.com/hashicorp/terraform-plugin-test/v2 v2.1.2/go.mod h1:jerO5mrd+jVNALy8aiq+VZOg/CR8T2T1QR3jd6JKGOI= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= diff --git a/helper/resource/plugin.go b/helper/resource/plugin.go index 2f6065dea3e..51dbeae5667 100644 --- a/helper/resource/plugin.go +++ b/helper/resource/plugin.go @@ -14,13 +14,13 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" grpcplugin "github.com/hashicorp/terraform-plugin-sdk/v2/internal/helper/plugin" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" proto "github.com/hashicorp/terraform-plugin-sdk/v2/internal/tfplugin5" "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" - tftest "github.com/hashicorp/terraform-plugin-test/v2" testing "github.com/mitchellh/go-testing-interface" ) -func runProviderCommand(t testing.T, f func() error, wd *tftest.WorkingDir, factories map[string]func() (*schema.Provider, error)) error { +func runProviderCommand(t testing.T, f func() error, wd *plugintest.WorkingDir, factories map[string]func() (*schema.Provider, error)) error { // don't point to this as a test failure location // point to whatever called it t.Helper() diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 0787141bed3..54c758de679 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -11,12 +11,12 @@ import ( "strings" "github.com/hashicorp/go-multierror" - tftest "github.com/hashicorp/terraform-plugin-test/v2" testing "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/addrs" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -534,8 +534,8 @@ func Test(t testing.T, c TestCase) { if err != nil { t.Fatalf("Error getting working dir: %s", err) } - helper := tftest.AutoInitProviderHelper(sourceDir) - defer func(helper *tftest.Helper) { + helper := plugintest.AutoInitProviderHelper(sourceDir) + defer func(helper *plugintest.Helper) { err := helper.Close() if err != nil { log.Printf("Error cleaning up temporary test files: %s", err) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 02a9686ad7c..b45e8b2efcf 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -8,14 +8,14 @@ import ( "github.com/davecgh/go-spew/spew" tfjson "github.com/hashicorp/terraform-json" - tftest "github.com/hashicorp/terraform-plugin-test/v2" testing "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func runPostTestDestroy(t testing.T, c TestCase, wd *tftest.WorkingDir, factories map[string]func() (*schema.Provider, error), statePreDestroy *terraform.State) error { +func runPostTestDestroy(t testing.T, c TestCase, wd *plugintest.WorkingDir, factories map[string]func() (*schema.Provider, error), statePreDestroy *terraform.State) error { t.Helper() err := runProviderCommand(t, func() error { @@ -35,7 +35,7 @@ func runPostTestDestroy(t testing.T, c TestCase, wd *tftest.WorkingDir, factorie return nil } -func runNewTest(t testing.T, c TestCase, helper *tftest.Helper) { +func runNewTest(t testing.T, c TestCase, helper *plugintest.Helper) { t.Helper() spewConf := spew.NewDefaultConfig() @@ -137,7 +137,7 @@ func runNewTest(t testing.T, c TestCase, helper *tftest.Helper) { } } -func getState(t testing.T, wd *tftest.WorkingDir) *terraform.State { +func getState(t testing.T, wd *plugintest.WorkingDir) *terraform.State { t.Helper() jsonState := wd.RequireState(t) @@ -169,7 +169,7 @@ func planIsEmpty(plan *tfjson.Plan) bool { return true } -func testIDRefresh(c TestCase, t testing.T, wd *tftest.WorkingDir, step TestStep, r *terraform.ResourceState) error { +func testIDRefresh(c TestCase, t testing.T, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState) error { t.Helper() spewConf := spew.NewDefaultConfig() diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index 04f7ced6560..b16574abea5 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -4,13 +4,13 @@ import ( "fmt" tfjson "github.com/hashicorp/terraform-json" - tftest "github.com/hashicorp/terraform-plugin-test/v2" testing "github.com/mitchellh/go-testing-interface" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func testStepNewConfig(t testing.T, c TestCase, wd *tftest.WorkingDir, step TestStep) error { +func testStepNewConfig(t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep) error { t.Helper() var idRefreshCheck *terraform.ResourceState diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index f1cd5af6a4c..107cb92e412 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -5,13 +5,13 @@ import ( "strings" "github.com/davecgh/go-spew/spew" - tftest "github.com/hashicorp/terraform-plugin-test/v2" testing "github.com/mitchellh/go-testing-interface" + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func testStepNewImportState(t testing.T, c TestCase, helper *tftest.Helper, wd *tftest.WorkingDir, step TestStep, cfg string) error { +func testStepNewImportState(t testing.T, c TestCase, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg string) error { t.Helper() spewConf := spew.NewDefaultConfig() diff --git a/internal/plugintest/config.go b/internal/plugintest/config.go new file mode 100644 index 00000000000..88ae7eea9e9 --- /dev/null +++ b/internal/plugintest/config.go @@ -0,0 +1,54 @@ +package plugintest + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/hashicorp/terraform-exec/tfinstall" +) + +// Config is used to configure the test helper. In most normal test programs +// the configuration is discovered automatically by an Init* function using +// DiscoverConfig, but this is exposed so that more complex scenarios can be +// implemented by direct configuration. +type Config struct { + SourceDir string + TerraformExec string + execTempDir string + PreviousPluginExec string +} + +// DiscoverConfig uses environment variables and other means to automatically +// discover a reasonable test helper configuration. +func DiscoverConfig(sourceDir string) (*Config, error) { + tfVersion := os.Getenv("TF_ACC_TERRAFORM_VERSION") + tfPath := os.Getenv("TF_ACC_TERRAFORM_PATH") + + tempDir := os.Getenv("TF_ACC_TEMP_DIR") + tfDir, err := ioutil.TempDir(tempDir, "plugintest-terraform") + if err != nil { + return nil, fmt.Errorf("failed to create temp dir: %w", err) + } + + finders := []tfinstall.ExecPathFinder{} + switch { + case tfPath != "": + finders = append(finders, tfinstall.ExactPath(tfPath)) + case tfVersion != "": + finders = append(finders, tfinstall.ExactVersion(tfVersion, tfDir)) + default: + finders = append(finders, tfinstall.LookPath(), tfinstall.LatestVersion(tfDir, true)) + } + tfExec, err := tfinstall.Find(context.Background(), finders...) + if err != nil { + return nil, err + } + + return &Config{ + SourceDir: sourceDir, + TerraformExec: tfExec, + execTempDir: tfDir, + }, nil +} diff --git a/internal/plugintest/doc.go b/internal/plugintest/doc.go new file mode 100644 index 00000000000..3f84c6a37e4 --- /dev/null +++ b/internal/plugintest/doc.go @@ -0,0 +1,7 @@ +// Package plugintest contains utilities to help with writing tests for +// Terraform plugins. +// +// This is not a package for testing configurations or modules written in the +// Terraform language. It is for testing the plugins that allow Terraform to +// manage various cloud services and other APIs. +package plugintest diff --git a/internal/plugintest/guard.go b/internal/plugintest/guard.go new file mode 100644 index 00000000000..56eaa0c16dc --- /dev/null +++ b/internal/plugintest/guard.go @@ -0,0 +1,94 @@ +package plugintest + +import ( + "fmt" + "os" + "testing" +) + +// AcceptanceTest is a test guard that will produce a log and call SkipNow on +// the given TestControl if the environment variable TF_ACC isn't set to +// indicate that the caller wants to run acceptance tests. +// +// Call this immediately at the start of each acceptance test function to +// signal that it may cost money and thus requires this opt-in enviromment +// variable. +// +// For the purpose of this function, an "acceptance test" is any est that +// reaches out to services that are not directly controlled by the test program +// itself, particularly if those requests may lead to service charges. For any +// system where it is possible and realistic to run a local instance of the +// service for testing (e.g. in a daemon launched by the test program itself), +// prefer to do this and _don't_ call AcceptanceTest, thus allowing tests to be +// run more easily and without external cost by contributors. +func AcceptanceTest(t TestControl) { + t.Helper() + if os.Getenv("TF_ACC") != "" { + t.Log("TF_ACC is not set") + t.SkipNow() + } +} + +// LongTest is a test guard that will produce a log and call SkipNow on the +// given TestControl if the test harness is currently running in "short mode". +// +// What is considered a "long test" will always be pretty subjective, but test +// implementers should think of this in terms of what seems like it'd be +// inconvenient to run repeatedly for quick feedback while testing a new feature +// under development. +// +// When testing resource types that always take several minutes to complete +// operations, consider having a single general test that covers the basic +// functionality and then mark any other more specific tests as long tests so +// that developers can quickly smoke-test a particular feature when needed +// but can still run the full set of tests for a feature when needed. +func LongTest(t TestControl) { + t.Helper() + if testing.Short() { + t.Log("skipping long test because of short mode") + t.SkipNow() + } +} + +// TestControl is an interface requiring a subset of *testing.T which is used +// by the test guards and helpers in this package. Most callers can simply +// pass their *testing.T value here, but the interface allows other +// implementations to potentially be provided instead, for example to allow +// meta-testing (testing of the test utilities themselves). +// +// This interface also describes the subset of normal test functionality the +// guards and helpers can perform: they can only create log lines, fail tests, +// and skip tests. All other test control is the responsibility of the main +// test code. +type TestControl interface { + Helper() + Log(args ...interface{}) + FailNow() + SkipNow() +} + +// testingT wraps a TestControl to recover some of the convenience behaviors +// that would normally come from a real *testing.T, so we can keep TestControl +// small while still having these conveniences. This is an abstraction +// inversion, but accepted because it makes the public API more convenient +// without any considerable disadvantage. +type testingT struct { + TestControl +} + +func (t testingT) Logf(f string, args ...interface{}) { + t.Helper() + t.Log(fmt.Sprintf(f, args...)) +} + +func (t testingT) Fatalf(f string, args ...interface{}) { + t.Helper() + t.Log(fmt.Sprintf(f, args...)) + t.FailNow() +} + +func (t testingT) Skipf(f string, args ...interface{}) { + t.Helper() + t.Log(fmt.Sprintf(f, args...)) + t.SkipNow() +} diff --git a/internal/plugintest/helper.go b/internal/plugintest/helper.go new file mode 100644 index 00000000000..e13e1d88fb1 --- /dev/null +++ b/internal/plugintest/helper.go @@ -0,0 +1,222 @@ +package plugintest + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + + getter "github.com/hashicorp/go-getter" +) + +const subprocessCurrentSigil = "4acd63807899403ca4859f5bb948d2c6" +const subprocessPreviousSigil = "2279afb8cf71423996be1fd65d32f13b" + +// AutoInitProviderHelper is the main entrypoint for testing provider plugins +// using this package. It is intended to be called during TestMain to prepare +// for provider testing. +// +// AutoInitProviderHelper will discover the location of a current Terraform CLI +// executable to test against, detect whether a prior version of the plugin is +// available for upgrade tests, and then will return an object containing the +// results of that initialization which can then be stored in a global variable +// for use in other tests. +func AutoInitProviderHelper(sourceDir string) *Helper { + helper, err := AutoInitHelper(sourceDir) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot run Terraform provider tests: %s\n", err) + os.Exit(1) + } + return helper +} + +// Helper is intended as a per-package singleton created in TestMain which +// other tests in a package can use to create Terraform execution contexts +type Helper struct { + baseDir string + + // sourceDir is the dir containing the provider source code, needed + // for tests that use fixture files. + sourceDir string + terraformExec string + + // execTempDir is created during DiscoverConfig to store any downloaded + // binaries + execTempDir string +} + +// AutoInitHelper uses the auto-discovery behavior of DiscoverConfig to prepare +// a configuration and then calls InitHelper with it. This is a convenient +// way to get the standard init behavior based on environment variables, and +// callers should use this unless they have an unusual requirement that calls +// for constructing a config in a different way. +func AutoInitHelper(sourceDir string) (*Helper, error) { + config, err := DiscoverConfig(sourceDir) + if err != nil { + return nil, err + } + + return InitHelper(config) +} + +// InitHelper prepares a testing helper with the given configuration. +// +// For most callers it is sufficient to call AutoInitHelper instead, which +// will construct a configuration automatically based on certain environment +// variables. +// +// If this function returns an error then it may have left some temporary files +// behind in the system's temporary directory. There is currently no way to +// automatically clean those up. +func InitHelper(config *Config) (*Helper, error) { + tempDir := os.Getenv("TF_ACC_TEMP_DIR") + baseDir, err := ioutil.TempDir(tempDir, "plugintest") + if err != nil { + return nil, fmt.Errorf("failed to create temporary directory for test helper: %s", err) + } + + return &Helper{ + baseDir: baseDir, + sourceDir: config.SourceDir, + terraformExec: config.TerraformExec, + execTempDir: config.execTempDir, + }, nil +} + +// symlinkAuxiliaryProviders discovers auxiliary provider binaries, used in +// multi-provider tests, and symlinks them to the plugin directory. +// +// Auxiliary provider binaries should be included in the provider source code +// directory, under the path terraform.d/plugins/$GOOS_$GOARCH/provider-name. +// +// The environment variable TF_ACC_PROVIDER_ROOT_DIR must be set to the path of +// the provider source code directory root in order to use this feature. +func symlinkAuxiliaryProviders(pluginDir string) error { + providerRootDir := os.Getenv("TF_ACC_PROVIDER_ROOT_DIR") + if providerRootDir == "" { + // common case; assume intentional and do not log + return nil + } + + _, err := os.Stat(filepath.Join(providerRootDir, "terraform.d", "plugins")) + if os.IsNotExist(err) { + fmt.Printf("No terraform.d/plugins directory found: continuing. Unset TF_ACC_PROVIDER_ROOT_DIR or supply provider binaries in terraform.d/plugins/$GOOS_$GOARCH to disable this message.") + return nil + } else if err != nil { + return fmt.Errorf("Unexpected error: %s", err) + } + + auxiliaryProviderDir := filepath.Join(providerRootDir, "terraform.d", "plugins", runtime.GOOS+"_"+runtime.GOARCH) + + // If we can't os.Stat() terraform.d/plugins/$GOOS_$GOARCH, however, + // assume the omission was unintentional, and error. + _, err = os.Stat(auxiliaryProviderDir) + if os.IsNotExist(err) { + return fmt.Errorf("error finding auxiliary provider dir %s: %s", auxiliaryProviderDir, err) + } else if err != nil { + return fmt.Errorf("Unexpected error: %s", err) + } + + // now find all the providers in that dir and symlink them to the plugin dir + providers, err := ioutil.ReadDir(auxiliaryProviderDir) + if err != nil { + return fmt.Errorf("error reading auxiliary providers: %s", err) + } + + zipDecompressor := new(getter.ZipDecompressor) + + for _, provider := range providers { + filename := provider.Name() + filenameExt := filepath.Ext(filename) + name := strings.TrimSuffix(filename, filenameExt) + path := filepath.Join(auxiliaryProviderDir, name) + symlinkPath := filepath.Join(pluginDir, name) + + // exit early if we have already symlinked this provider + _, err := os.Stat(symlinkPath) + if err == nil { + continue + } + + // if filename ends in .zip, assume it is a zip and extract it + // otherwise assume it is a provider binary + if filenameExt == ".zip" { + _, err = os.Stat(path) + if os.IsNotExist(err) { + zipDecompressor.Decompress(path, filepath.Join(auxiliaryProviderDir, filename), false) + } else if err != nil { + return fmt.Errorf("Unexpected error: %s", err) + } + } + + err = symlinkFile(path, symlinkPath) + if err != nil { + return fmt.Errorf("error symlinking auxiliary provider %s: %s", name, err) + } + } + + return nil +} + +// Close cleans up temporary files and directories created to support this +// helper, returning an error if any of the cleanup fails. +// +// Call this before returning from TestMain to minimize the amount of detritus +// left behind in the filesystem after the tests complete. +func (h *Helper) Close() error { + if h.execTempDir != "" { + err := os.RemoveAll(h.execTempDir) + if err != nil { + return err + } + } + return os.RemoveAll(h.baseDir) +} + +// NewWorkingDir creates a new working directory for use in the implementation +// of a single test, returning a WorkingDir object representing that directory. +// +// If the working directory object is not itself closed by the time the test +// program exits, the Close method on the helper itself will attempt to +// delete it. +func (h *Helper) NewWorkingDir() (*WorkingDir, error) { + dir, err := ioutil.TempDir(h.baseDir, "work") + if err != nil { + return nil, err + } + + // symlink the provider source files into the base directory + err = symlinkDirectoriesOnly(h.sourceDir, dir) + if err != nil { + return nil, err + } + + return &WorkingDir{ + h: h, + baseDir: dir, + terraformExec: h.terraformExec, + }, nil +} + +// RequireNewWorkingDir is a variant of NewWorkingDir that takes a TestControl +// object and will immediately fail the running test if the creation of the +// working directory fails. +func (h *Helper) RequireNewWorkingDir(t TestControl) *WorkingDir { + t.Helper() + + wd, err := h.NewWorkingDir() + if err != nil { + t := testingT{t} + t.Fatalf("failed to create new working directory: %s", err) + return nil + } + return wd +} + +// TerraformExecPath returns the location of the Terraform CLI executable that +// should be used when running tests. +func (h *Helper) TerraformExecPath() string { + return h.terraformExec +} diff --git a/internal/plugintest/util.go b/internal/plugintest/util.go new file mode 100644 index 00000000000..df1cb92fda9 --- /dev/null +++ b/internal/plugintest/util.go @@ -0,0 +1,95 @@ +package plugintest + +import ( + "os" + "path/filepath" +) + +func symlinkFile(src string, dest string) (err error) { + err = os.Symlink(src, dest) + if err == nil { + srcInfo, err := os.Stat(src) + if err != nil { + err = os.Chmod(dest, srcInfo.Mode()) + } + } + + return +} + +// symlinkDir is a simplistic function for recursively symlinking all files in a directory to a new path. +// It is intended only for limited internal use and does not cover all edge cases. +func symlinkDir(srcDir string, destDir string) (err error) { + srcInfo, err := os.Stat(srcDir) + if err != nil { + return err + } + + err = os.MkdirAll(destDir, srcInfo.Mode()) + if err != nil { + return err + } + + directory, _ := os.Open(srcDir) + defer directory.Close() + objects, err := directory.Readdir(-1) + + for _, obj := range objects { + srcPath := filepath.Join(srcDir, obj.Name()) + destPath := filepath.Join(destDir, obj.Name()) + + if obj.IsDir() { + err = symlinkDir(srcPath, destPath) + if err != nil { + return err + } + } else { + err = symlinkFile(srcPath, destPath) + if err != nil { + return err + } + } + + } + return +} + +// symlinkDirectoriesOnly finds only the first-level child directories in srcDir +// and symlinks them into destDir. +// Unlike symlinkDir, this is done non-recursively in order to limit the number +// of file descriptors used. +func symlinkDirectoriesOnly(srcDir string, destDir string) (err error) { + srcInfo, err := os.Stat(srcDir) + if err != nil { + return err + } + + err = os.MkdirAll(destDir, srcInfo.Mode()) + if err != nil { + return err + } + + directory, err := os.Open(srcDir) + if err != nil { + return err + } + defer directory.Close() + objects, err := directory.Readdir(-1) + if err != nil { + return err + } + + for _, obj := range objects { + srcPath := filepath.Join(srcDir, obj.Name()) + destPath := filepath.Join(destDir, obj.Name()) + + if obj.IsDir() { + err = symlinkFile(srcPath, destPath) + if err != nil { + return err + } + } + + } + return +} diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go new file mode 100644 index 00000000000..3e10eba1d9c --- /dev/null +++ b/internal/plugintest/working_dir.go @@ -0,0 +1,420 @@ +package plugintest + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" +) + +// WorkingDir represents a distinct working directory that can be used for +// running tests. Each test should construct its own WorkingDir by calling +// NewWorkingDir or RequireNewWorkingDir on its package's singleton +// plugintest.Helper. +type WorkingDir struct { + h *Helper + + // baseDir is the root of the working directory tree + baseDir string + + // baseArgs is arguments that should be appended to all commands + baseArgs []string + + // configDir contains the singular config file generated for each test + configDir string + + // tf is the instance of tfexec.Terraform used for running Terraform commands + tf *tfexec.Terraform + + // terraformExec is a path to a terraform binary, inherited from Helper + terraformExec string + + // reattachInfo stores the gRPC socket info required for Terraform's + // plugin reattach functionality + reattachInfo tfexec.ReattachInfo + + env map[string]string +} + +// Close deletes the directories and files created to represent the receiving +// working directory. After this method is called, the working directory object +// is invalid and may no longer be used. +func (wd *WorkingDir) Close() error { + return os.RemoveAll(wd.baseDir) +} + +// Setenv sets an environment variable on the WorkingDir. +func (wd *WorkingDir) Setenv(envVar, val string) { + if wd.env == nil { + wd.env = map[string]string{} + } + wd.env[envVar] = val +} + +// Unsetenv removes an environment variable from the WorkingDir. +func (wd *WorkingDir) Unsetenv(envVar string) { + delete(wd.env, envVar) +} + +func (wd *WorkingDir) SetReattachInfo(reattachInfo tfexec.ReattachInfo) { + wd.reattachInfo = reattachInfo +} + +func (wd *WorkingDir) UnsetReattachInfo() { + wd.reattachInfo = nil +} + +// GetHelper returns the Helper set on the WorkingDir. +func (wd *WorkingDir) GetHelper() *Helper { + return wd.h +} + +func (wd *WorkingDir) relativeConfigDir() (string, error) { + relPath, err := filepath.Rel(wd.baseDir, wd.configDir) + if err != nil { + return "", fmt.Errorf("Error determining relative path of configuration directory: %w", err) + } + return relPath, nil +} + +// SetConfig sets a new configuration for the working directory. +// +// This must be called at least once before any call to Init, Plan, Apply, or +// Destroy to establish the configuration. Any previously-set configuration is +// discarded and any saved plan is cleared. +func (wd *WorkingDir) SetConfig(cfg string) error { + // Each call to SetConfig creates a new directory under our baseDir. + // We create them within so that our final cleanup step will delete them + // automatically without any additional tracking. + configDir, err := ioutil.TempDir(wd.baseDir, "config") + if err != nil { + return err + } + configFilename := filepath.Join(configDir, "terraform_plugin_test.tf") + err = ioutil.WriteFile(configFilename, []byte(cfg), 0700) + if err != nil { + return err + } + + tf, err := tfexec.NewTerraform(wd.baseDir, wd.terraformExec) + if err != nil { + return err + } + + var mismatch *tfexec.ErrVersionMismatch + err = tf.SetDisablePluginTLS(true) + if err != nil && !errors.As(err, &mismatch) { + return err + } + err = tf.SetSkipProviderVerify(true) + if err != nil && !errors.As(err, &mismatch) { + return err + } + + if p := os.Getenv("TF_ACC_LOG_PATH"); p != "" { + tf.SetLogPath(p) + } + + wd.configDir = configDir + wd.tf = tf + + // Changing configuration invalidates any saved plan. + err = wd.ClearPlan() + if err != nil { + return err + } + return nil +} + +// RequireSetConfig is a variant of SetConfig that will fail the test via the +// given TestControl if the configuration cannot be set. +func (wd *WorkingDir) RequireSetConfig(t TestControl, cfg string) { + t.Helper() + if err := wd.SetConfig(cfg); err != nil { + t := testingT{t} + t.Fatalf("failed to set config: %s", err) + } +} + +// ClearState deletes any Terraform state present in the working directory. +// +// Any remote objects tracked by the state are not destroyed first, so this +// will leave them dangling in the remote system. +func (wd *WorkingDir) ClearState() error { + err := os.Remove(filepath.Join(wd.baseDir, "terraform.tfstate")) + if os.IsNotExist(err) { + return nil + } + return err +} + +// RequireClearState is a variant of ClearState that will fail the test via the +// given TestControl if the state cannot be cleared. +func (wd *WorkingDir) RequireClearState(t TestControl) { + t.Helper() + if err := wd.ClearState(); err != nil { + t := testingT{t} + t.Fatalf("failed to clear state: %s", err) + } +} + +// ClearPlan deletes any saved plan present in the working directory. +func (wd *WorkingDir) ClearPlan() error { + err := os.Remove(wd.planFilename()) + if os.IsNotExist(err) { + return nil + } + return err +} + +// RequireClearPlan is a variant of ClearPlan that will fail the test via the +// given TestControl if the plan cannot be cleared. +func (wd *WorkingDir) RequireClearPlan(t TestControl) { + t.Helper() + if err := wd.ClearPlan(); err != nil { + t := testingT{t} + t.Fatalf("failed to clear plan: %s", err) + } +} + +// Init runs "terraform init" for the given working directory, forcing Terraform +// to use the current version of the plugin under test. +func (wd *WorkingDir) Init() error { + if wd.configDir == "" { + return fmt.Errorf("must call SetConfig before Init") + } + + return wd.tf.Init(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Dir(wd.configDir)) +} + +// RequireInit is a variant of Init that will fail the test via the given +// TestControl if init fails. +func (wd *WorkingDir) RequireInit(t TestControl) { + t.Helper() + if err := wd.Init(); err != nil { + t := testingT{t} + t.Fatalf("init failed: %s", err) + } +} + +func (wd *WorkingDir) planFilename() string { + return filepath.Join(wd.baseDir, "tfplan") +} + +// CreatePlan runs "terraform plan" to create a saved plan file, which if successful +// will then be used for the next call to Apply. +func (wd *WorkingDir) CreatePlan() error { + _, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out("tfplan"), tfexec.Dir(wd.configDir)) + return err +} + +// RequireCreatePlan is a variant of CreatePlan that will fail the test via +// the given TestControl if plan creation fails. +func (wd *WorkingDir) RequireCreatePlan(t TestControl) { + t.Helper() + if err := wd.CreatePlan(); err != nil { + t := testingT{t} + t.Fatalf("failed to create plan: %s", err) + } +} + +// CreateDestroyPlan runs "terraform plan -destroy" to create a saved plan +// file, which if successful will then be used for the next call to Apply. +func (wd *WorkingDir) CreateDestroyPlan() error { + _, err := wd.tf.Plan(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Out("tfplan"), tfexec.Destroy(true), tfexec.Dir(wd.configDir)) + return err +} + +// Apply runs "terraform apply". If CreatePlan has previously completed +// successfully and the saved plan has not been cleared in the meantime then +// this will apply the saved plan. Otherwise, it will implicitly create a new +// plan and apply it. +func (wd *WorkingDir) Apply() error { + args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)} + if wd.HasSavedPlan() { + args = append(args, tfexec.DirOrPlan("tfplan")) + } else { + // we need to use a relative config dir here or we get an + // error about Terraform not having any configuration. See + // https://github.com/hashicorp/terraform-plugin-sdk/issues/495 + // for more info. + configDir, err := wd.relativeConfigDir() + if err != nil { + return err + } + args = append(args, tfexec.DirOrPlan(configDir)) + } + return wd.tf.Apply(context.Background(), args...) +} + +// RequireApply is a variant of Apply that will fail the test via +// the given TestControl if the apply operation fails. +func (wd *WorkingDir) RequireApply(t TestControl) { + t.Helper() + if err := wd.Apply(); err != nil { + t := testingT{t} + t.Fatalf("failed to apply: %s", err) + } +} + +// Destroy runs "terraform destroy". It does not consider or modify any saved +// plan, and is primarily for cleaning up at the end of a test run. +// +// If destroy fails then remote objects might still exist, and continue to +// exist after a particular test is concluded. +func (wd *WorkingDir) Destroy() error { + return wd.tf.Destroy(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Dir(wd.configDir)) +} + +// RequireDestroy is a variant of Destroy that will fail the test via +// the given TestControl if the destroy operation fails. +// +// If destroy fails then remote objects might still exist, and continue to +// exist after a particular test is concluded. +func (wd *WorkingDir) RequireDestroy(t TestControl) { + t.Helper() + if err := wd.Destroy(); err != nil { + t := testingT{t} + t.Logf("WARNING: destroy failed, so remote objects may still exist and be subject to billing") + t.Fatalf("failed to destroy: %s", err) + } +} + +// HasSavedPlan returns true if there is a saved plan in the working directory. If +// so, a subsequent call to Apply will apply that saved plan. +func (wd *WorkingDir) HasSavedPlan() bool { + _, err := os.Stat(wd.planFilename()) + return err == nil +} + +// SavedPlan returns an object describing the current saved plan file, if any. +// +// If no plan is saved or if the plan file cannot be read, SavedPlan returns +// an error. +func (wd *WorkingDir) SavedPlan() (*tfjson.Plan, error) { + if !wd.HasSavedPlan() { + return nil, fmt.Errorf("there is no current saved plan") + } + + return wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo)) +} + +// RequireSavedPlan is a variant of SavedPlan that will fail the test via +// the given TestControl if the plan cannot be read. +func (wd *WorkingDir) RequireSavedPlan(t TestControl) *tfjson.Plan { + t.Helper() + ret, err := wd.SavedPlan() + if err != nil { + t := testingT{t} + t.Fatalf("failed to read saved plan: %s", err) + } + return ret +} + +// SavedPlanStdout returns a stdout capture of the current saved plan file, if any. +// +// If no plan is saved or if the plan file cannot be read, SavedPlanStdout returns +// an error. +func (wd *WorkingDir) SavedPlanStdout() (string, error) { + if !wd.HasSavedPlan() { + return "", fmt.Errorf("there is no current saved plan") + } + + var ret bytes.Buffer + + wd.tf.SetStdout(&ret) + defer wd.tf.SetStdout(ioutil.Discard) + _, err := wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo)) + if err != nil { + return "", err + } + + return ret.String(), nil +} + +// RequireSavedPlanStdout is a variant of SavedPlanStdout that will fail the test via +// the given TestControl if the plan cannot be read. +func (wd *WorkingDir) RequireSavedPlanStdout(t TestControl) string { + t.Helper() + ret, err := wd.SavedPlanStdout() + if err != nil { + t := testingT{t} + t.Fatalf("failed to read saved plan: %s", err) + } + return ret +} + +// State returns an object describing the current state. +// +// If the state cannot be read, State returns an error. +func (wd *WorkingDir) State() (*tfjson.State, error) { + return wd.tf.Show(context.Background(), tfexec.Reattach(wd.reattachInfo)) +} + +// RequireState is a variant of State that will fail the test via +// the given TestControl if the state cannot be read. +func (wd *WorkingDir) RequireState(t TestControl) *tfjson.State { + t.Helper() + ret, err := wd.State() + if err != nil { + t := testingT{t} + t.Fatalf("failed to read state plan: %s", err) + } + return ret +} + +// Import runs terraform import +func (wd *WorkingDir) Import(resource, id string) error { + return wd.tf.Import(context.Background(), resource, id, tfexec.Config(wd.configDir), tfexec.Reattach(wd.reattachInfo)) +} + +// RequireImport is a variant of Import that will fail the test via +// the given TestControl if the import is non successful. +func (wd *WorkingDir) RequireImport(t TestControl, resource, id string) { + t.Helper() + if err := wd.Import(resource, id); err != nil { + t := testingT{t} + t.Fatalf("failed to import: %s", err) + } +} + +// Refresh runs terraform refresh +func (wd *WorkingDir) Refresh() error { + return wd.tf.Refresh(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.State(filepath.Join(wd.baseDir, "terraform.tfstate")), tfexec.Dir(wd.configDir)) +} + +// RequireRefresh is a variant of Refresh that will fail the test via +// the given TestControl if the refresh is non successful. +func (wd *WorkingDir) RequireRefresh(t TestControl) { + t.Helper() + if err := wd.Refresh(); err != nil { + t := testingT{t} + t.Fatalf("failed to refresh: %s", err) + } +} + +// Schemas returns an object describing the provider schemas. +// +// If the schemas cannot be read, Schemas returns an error. +func (wd *WorkingDir) Schemas() (*tfjson.ProviderSchemas, error) { + return wd.tf.ProvidersSchema(context.Background()) +} + +// RequireSchemas is a variant of Schemas that will fail the test via +// the given TestControl if the schemas cannot be read. +func (wd *WorkingDir) RequireSchemas(t TestControl) *tfjson.ProviderSchemas { + t.Helper() + + ret, err := wd.Schemas() + if err != nil { + t := testingT{t} + t.Fatalf("failed to read schemas: %s", err) + } + return ret +} From 17a7f0a8bc31b4774d9dfe90ca3838afec8434d1 Mon Sep 17 00:00:00 2001 From: Katy Moe Date: Fri, 11 Sep 2020 14:42:22 +0100 Subject: [PATCH 2/4] remove Require versions of test funcs --- helper/resource/testing_new.go | 50 +++++-- helper/resource/testing_new_config.go | 23 +++- helper/resource/testing_new_import_state.go | 21 ++- internal/plugintest/working_dir.go | 144 +------------------- 4 files changed, 70 insertions(+), 168 deletions(-) diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index b45e8b2efcf..662c6c5c692 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -19,8 +19,7 @@ func runPostTestDestroy(t testing.T, c TestCase, wd *plugintest.WorkingDir, fact t.Helper() err := runProviderCommand(t, func() error { - wd.RequireDestroy(t) - return nil + return wd.Destroy() }, wd, factories) if err != nil { return err @@ -44,8 +43,12 @@ func runNewTest(t testing.T, c TestCase, helper *plugintest.Helper) { defer func() { var statePreDestroy *terraform.State - err := runProviderCommand(t, func() error { - statePreDestroy = getState(t, wd) + var err error + err = runProviderCommand(t, func() error { + statePreDestroy, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { @@ -68,10 +71,12 @@ func runNewTest(t testing.T, c TestCase, helper *plugintest.Helper) { t.Fatal(err) } - wd.RequireSetConfig(t, providerCfg) + err = wd.SetConfig(providerCfg) + if err != nil { + t.Fatalf("Error setting test config: %s", err) + } err = runProviderCommand(t, func() error { - wd.RequireInit(t) - return nil + return wd.Init() }, wd, c.ProviderFactories) if err != nil { t.Fatalf("Error running init: %s", err.Error()) @@ -137,15 +142,18 @@ func runNewTest(t testing.T, c TestCase, helper *plugintest.Helper) { } } -func getState(t testing.T, wd *plugintest.WorkingDir) *terraform.State { +func getState(t testing.T, wd *plugintest.WorkingDir) (*terraform.State, error) { t.Helper() - jsonState := wd.RequireState(t) + jsonState, err := wd.State() + if err != nil { + return nil, err + } state, err := shimStateFromJson(jsonState) if err != nil { t.Fatal(err) } - return state + return state, nil } func stateIsEmpty(state *terraform.State) bool { @@ -187,13 +195,27 @@ func testIDRefresh(c TestCase, t testing.T, wd *plugintest.WorkingDir, step Test if err != nil { return err } - wd.RequireSetConfig(t, cfg) - defer wd.RequireSetConfig(t, step.Config) + err = wd.SetConfig(cfg) + if err != nil { + t.Fatalf("Error setting import test config: %s", err) + } + defer func() { + err = wd.SetConfig(step.Config) + if err != nil { + t.Fatalf("Error resetting test config: %s", err) + } + }() // Refresh! err = runProviderCommand(t, func() error { - wd.RequireRefresh(t) - state = getState(t, wd) + err = wd.Refresh() + if err != nil { + t.Fatalf("Error running terraform refresh: %s", err) + } + state, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index b16574abea5..c75af6d8205 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -18,8 +18,12 @@ func testStepNewConfig(t testing.T, c TestCase, wd *plugintest.WorkingDir, step if !step.Destroy { var state *terraform.State - err := runProviderCommand(t, func() error { - state = getState(t, wd) + var err error + err = runProviderCommand(t, func() error { + state, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { @@ -64,7 +68,10 @@ func testStepNewConfig(t testing.T, c TestCase, wd *plugintest.WorkingDir, step // check function var stateBeforeApplication *terraform.State err = runProviderCommand(t, func() error { - stateBeforeApplication = getState(t, wd) + stateBeforeApplication, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { @@ -85,7 +92,10 @@ func testStepNewConfig(t testing.T, c TestCase, wd *plugintest.WorkingDir, step // Get the new state var state *terraform.State err = runProviderCommand(t, func() error { - state = getState(t, wd) + state, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { @@ -194,7 +204,10 @@ func testStepNewConfig(t testing.T, c TestCase, wd *plugintest.WorkingDir, step // empty, find the first resource and test it. var state *terraform.State err = runProviderCommand(t, func() error { - state = getState(t, wd) + state, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 107cb92e412..31a631916e0 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -23,8 +23,12 @@ func testStepNewImportState(t testing.T, c TestCase, helper *plugintest.Helper, // get state from check sequence var state *terraform.State - err := runProviderCommand(t, func() error { - state = getState(t, wd) + var err error + err = runProviderCommand(t, func() error { + state, err = getState(t, wd) + if err != nil { + return err + } return nil }, wd, c.ProviderFactories) if err != nil { @@ -60,11 +64,13 @@ func testStepNewImportState(t testing.T, c TestCase, helper *plugintest.Helper, } importWd := helper.RequireNewWorkingDir(t) defer importWd.Close() - importWd.RequireSetConfig(t, step.Config) + err = importWd.SetConfig(step.Config) + if err != nil { + t.Fatalf("Error setting test config: %s", err) + } err = runProviderCommand(t, func() error { - importWd.RequireInit(t) - return nil + return importWd.Init() }, importWd, c.ProviderFactories) if err != nil { t.Fatalf("Error running init: %s", err) @@ -79,7 +85,10 @@ func testStepNewImportState(t testing.T, c TestCase, helper *plugintest.Helper, var importState *terraform.State err = runProviderCommand(t, func() error { - importState = getState(t, importWd) + importState, err = getState(t, importWd) + if err != nil { + return err + } return nil }, importWd, c.ProviderFactories) if err != nil { diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 3e10eba1d9c..a4a24357c88 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -132,16 +132,6 @@ func (wd *WorkingDir) SetConfig(cfg string) error { return nil } -// RequireSetConfig is a variant of SetConfig that will fail the test via the -// given TestControl if the configuration cannot be set. -func (wd *WorkingDir) RequireSetConfig(t TestControl, cfg string) { - t.Helper() - if err := wd.SetConfig(cfg); err != nil { - t := testingT{t} - t.Fatalf("failed to set config: %s", err) - } -} - // ClearState deletes any Terraform state present in the working directory. // // Any remote objects tracked by the state are not destroyed first, so this @@ -154,16 +144,6 @@ func (wd *WorkingDir) ClearState() error { return err } -// RequireClearState is a variant of ClearState that will fail the test via the -// given TestControl if the state cannot be cleared. -func (wd *WorkingDir) RequireClearState(t TestControl) { - t.Helper() - if err := wd.ClearState(); err != nil { - t := testingT{t} - t.Fatalf("failed to clear state: %s", err) - } -} - // ClearPlan deletes any saved plan present in the working directory. func (wd *WorkingDir) ClearPlan() error { err := os.Remove(wd.planFilename()) @@ -173,16 +153,6 @@ func (wd *WorkingDir) ClearPlan() error { return err } -// RequireClearPlan is a variant of ClearPlan that will fail the test via the -// given TestControl if the plan cannot be cleared. -func (wd *WorkingDir) RequireClearPlan(t TestControl) { - t.Helper() - if err := wd.ClearPlan(); err != nil { - t := testingT{t} - t.Fatalf("failed to clear plan: %s", err) - } -} - // Init runs "terraform init" for the given working directory, forcing Terraform // to use the current version of the plugin under test. func (wd *WorkingDir) Init() error { @@ -193,16 +163,6 @@ func (wd *WorkingDir) Init() error { return wd.tf.Init(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Dir(wd.configDir)) } -// RequireInit is a variant of Init that will fail the test via the given -// TestControl if init fails. -func (wd *WorkingDir) RequireInit(t TestControl) { - t.Helper() - if err := wd.Init(); err != nil { - t := testingT{t} - t.Fatalf("init failed: %s", err) - } -} - func (wd *WorkingDir) planFilename() string { return filepath.Join(wd.baseDir, "tfplan") } @@ -214,16 +174,6 @@ func (wd *WorkingDir) CreatePlan() error { return err } -// RequireCreatePlan is a variant of CreatePlan that will fail the test via -// the given TestControl if plan creation fails. -func (wd *WorkingDir) RequireCreatePlan(t TestControl) { - t.Helper() - if err := wd.CreatePlan(); err != nil { - t := testingT{t} - t.Fatalf("failed to create plan: %s", err) - } -} - // CreateDestroyPlan runs "terraform plan -destroy" to create a saved plan // file, which if successful will then be used for the next call to Apply. func (wd *WorkingDir) CreateDestroyPlan() error { @@ -253,16 +203,6 @@ func (wd *WorkingDir) Apply() error { return wd.tf.Apply(context.Background(), args...) } -// RequireApply is a variant of Apply that will fail the test via -// the given TestControl if the apply operation fails. -func (wd *WorkingDir) RequireApply(t TestControl) { - t.Helper() - if err := wd.Apply(); err != nil { - t := testingT{t} - t.Fatalf("failed to apply: %s", err) - } -} - // Destroy runs "terraform destroy". It does not consider or modify any saved // plan, and is primarily for cleaning up at the end of a test run. // @@ -272,20 +212,6 @@ func (wd *WorkingDir) Destroy() error { return wd.tf.Destroy(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false), tfexec.Dir(wd.configDir)) } -// RequireDestroy is a variant of Destroy that will fail the test via -// the given TestControl if the destroy operation fails. -// -// If destroy fails then remote objects might still exist, and continue to -// exist after a particular test is concluded. -func (wd *WorkingDir) RequireDestroy(t TestControl) { - t.Helper() - if err := wd.Destroy(); err != nil { - t := testingT{t} - t.Logf("WARNING: destroy failed, so remote objects may still exist and be subject to billing") - t.Fatalf("failed to destroy: %s", err) - } -} - // HasSavedPlan returns true if there is a saved plan in the working directory. If // so, a subsequent call to Apply will apply that saved plan. func (wd *WorkingDir) HasSavedPlan() bool { @@ -305,18 +231,6 @@ func (wd *WorkingDir) SavedPlan() (*tfjson.Plan, error) { return wd.tf.ShowPlanFile(context.Background(), wd.planFilename(), tfexec.Reattach(wd.reattachInfo)) } -// RequireSavedPlan is a variant of SavedPlan that will fail the test via -// the given TestControl if the plan cannot be read. -func (wd *WorkingDir) RequireSavedPlan(t TestControl) *tfjson.Plan { - t.Helper() - ret, err := wd.SavedPlan() - if err != nil { - t := testingT{t} - t.Fatalf("failed to read saved plan: %s", err) - } - return ret -} - // SavedPlanStdout returns a stdout capture of the current saved plan file, if any. // // If no plan is saved or if the plan file cannot be read, SavedPlanStdout returns @@ -338,83 +252,27 @@ func (wd *WorkingDir) SavedPlanStdout() (string, error) { return ret.String(), nil } -// RequireSavedPlanStdout is a variant of SavedPlanStdout that will fail the test via -// the given TestControl if the plan cannot be read. -func (wd *WorkingDir) RequireSavedPlanStdout(t TestControl) string { - t.Helper() - ret, err := wd.SavedPlanStdout() - if err != nil { - t := testingT{t} - t.Fatalf("failed to read saved plan: %s", err) - } - return ret -} - // State returns an object describing the current state. // + // If the state cannot be read, State returns an error. func (wd *WorkingDir) State() (*tfjson.State, error) { return wd.tf.Show(context.Background(), tfexec.Reattach(wd.reattachInfo)) } -// RequireState is a variant of State that will fail the test via -// the given TestControl if the state cannot be read. -func (wd *WorkingDir) RequireState(t TestControl) *tfjson.State { - t.Helper() - ret, err := wd.State() - if err != nil { - t := testingT{t} - t.Fatalf("failed to read state plan: %s", err) - } - return ret -} - // Import runs terraform import func (wd *WorkingDir) Import(resource, id string) error { return wd.tf.Import(context.Background(), resource, id, tfexec.Config(wd.configDir), tfexec.Reattach(wd.reattachInfo)) } -// RequireImport is a variant of Import that will fail the test via -// the given TestControl if the import is non successful. -func (wd *WorkingDir) RequireImport(t TestControl, resource, id string) { - t.Helper() - if err := wd.Import(resource, id); err != nil { - t := testingT{t} - t.Fatalf("failed to import: %s", err) - } -} - // Refresh runs terraform refresh func (wd *WorkingDir) Refresh() error { return wd.tf.Refresh(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.State(filepath.Join(wd.baseDir, "terraform.tfstate")), tfexec.Dir(wd.configDir)) } -// RequireRefresh is a variant of Refresh that will fail the test via -// the given TestControl if the refresh is non successful. -func (wd *WorkingDir) RequireRefresh(t TestControl) { - t.Helper() - if err := wd.Refresh(); err != nil { - t := testingT{t} - t.Fatalf("failed to refresh: %s", err) - } -} - // Schemas returns an object describing the provider schemas. // // If the schemas cannot be read, Schemas returns an error. func (wd *WorkingDir) Schemas() (*tfjson.ProviderSchemas, error) { return wd.tf.ProvidersSchema(context.Background()) } - -// RequireSchemas is a variant of Schemas that will fail the test via -// the given TestControl if the schemas cannot be read. -func (wd *WorkingDir) RequireSchemas(t TestControl) *tfjson.ProviderSchemas { - t.Helper() - - ret, err := wd.Schemas() - if err != nil { - t := testingT{t} - t.Fatalf("failed to read schemas: %s", err) - } - return ret -} From 1944f95b82ae7e53e987364dd1367426cb7e2f78 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Wed, 30 Sep 2020 06:31:15 -0700 Subject: [PATCH 3/4] Update to v1.5.0 of go-getter. --- go.mod | 3 +-- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7dce82eb8b4..f79390663a7 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 + github.com/hashicorp/go-getter v1.5.0 github.com/hashicorp/go-hclog v0.9.2 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-plugin v1.3.0 @@ -37,7 +37,6 @@ require ( github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/reflectwalk v1.0.1 github.com/pierrec/lz4 v2.0.5+incompatible - github.com/ulikunitz/xz v0.5.7 // indirect github.com/vmihailenco/msgpack v4.0.1+incompatible // indirect github.com/zclconf/go-cty v1.2.1 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 diff --git a/go.sum b/go.sum index e99d53f1fbf..aaafc83436c 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,8 @@ github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUK github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= github.com/hashicorp/go-getter v1.4.0 h1:ENHNi8494porjD0ZhIrjlAHnveSFhY7hvOJrV/fsKkw= github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= -github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 h1:l1KB3bHVdvegcIf5upQ5mjcHjs2qsWnKh4Yr9xgIuu8= -github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= +github.com/hashicorp/go-getter v1.5.0 h1:ciWJaeZWSMbc5OiLMpKp40MKFPqO44i0h3uyfXPBkkk= +github.com/hashicorp/go-getter v1.5.0/go.mod h1:a7z7NPPfNQpJWcn4rSWFtdrSldqLdLPEF3d8nFMsSLM= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= @@ -276,8 +276,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4= -github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= From 4954d20234dc5a6634fdc8635a0507c20067cbf5 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Wed, 30 Sep 2020 06:38:49 -0700 Subject: [PATCH 4/4] Drop symlinkAuxiliarProviders helper. We don't actually use this helper anywhere, and it was incompatible with v1.5.0 of go-getter, for reasons. --- go.mod | 2 +- internal/plugintest/helper.go | 80 ----------------------------------- 2 files changed, 1 insertion(+), 81 deletions(-) diff --git a/go.mod b/go.mod index f79390663a7..9d33f35448a 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-getter v1.5.0 + github.com/hashicorp/go-getter v1.5.0 // indirect github.com/hashicorp/go-hclog v0.9.2 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-plugin v1.3.0 diff --git a/internal/plugintest/helper.go b/internal/plugintest/helper.go index e13e1d88fb1..d7fc0c6ff6a 100644 --- a/internal/plugintest/helper.go +++ b/internal/plugintest/helper.go @@ -4,11 +4,6 @@ import ( "fmt" "io/ioutil" "os" - "path/filepath" - "runtime" - "strings" - - getter "github.com/hashicorp/go-getter" ) const subprocessCurrentSigil = "4acd63807899403ca4859f5bb948d2c6" @@ -85,81 +80,6 @@ func InitHelper(config *Config) (*Helper, error) { }, nil } -// symlinkAuxiliaryProviders discovers auxiliary provider binaries, used in -// multi-provider tests, and symlinks them to the plugin directory. -// -// Auxiliary provider binaries should be included in the provider source code -// directory, under the path terraform.d/plugins/$GOOS_$GOARCH/provider-name. -// -// The environment variable TF_ACC_PROVIDER_ROOT_DIR must be set to the path of -// the provider source code directory root in order to use this feature. -func symlinkAuxiliaryProviders(pluginDir string) error { - providerRootDir := os.Getenv("TF_ACC_PROVIDER_ROOT_DIR") - if providerRootDir == "" { - // common case; assume intentional and do not log - return nil - } - - _, err := os.Stat(filepath.Join(providerRootDir, "terraform.d", "plugins")) - if os.IsNotExist(err) { - fmt.Printf("No terraform.d/plugins directory found: continuing. Unset TF_ACC_PROVIDER_ROOT_DIR or supply provider binaries in terraform.d/plugins/$GOOS_$GOARCH to disable this message.") - return nil - } else if err != nil { - return fmt.Errorf("Unexpected error: %s", err) - } - - auxiliaryProviderDir := filepath.Join(providerRootDir, "terraform.d", "plugins", runtime.GOOS+"_"+runtime.GOARCH) - - // If we can't os.Stat() terraform.d/plugins/$GOOS_$GOARCH, however, - // assume the omission was unintentional, and error. - _, err = os.Stat(auxiliaryProviderDir) - if os.IsNotExist(err) { - return fmt.Errorf("error finding auxiliary provider dir %s: %s", auxiliaryProviderDir, err) - } else if err != nil { - return fmt.Errorf("Unexpected error: %s", err) - } - - // now find all the providers in that dir and symlink them to the plugin dir - providers, err := ioutil.ReadDir(auxiliaryProviderDir) - if err != nil { - return fmt.Errorf("error reading auxiliary providers: %s", err) - } - - zipDecompressor := new(getter.ZipDecompressor) - - for _, provider := range providers { - filename := provider.Name() - filenameExt := filepath.Ext(filename) - name := strings.TrimSuffix(filename, filenameExt) - path := filepath.Join(auxiliaryProviderDir, name) - symlinkPath := filepath.Join(pluginDir, name) - - // exit early if we have already symlinked this provider - _, err := os.Stat(symlinkPath) - if err == nil { - continue - } - - // if filename ends in .zip, assume it is a zip and extract it - // otherwise assume it is a provider binary - if filenameExt == ".zip" { - _, err = os.Stat(path) - if os.IsNotExist(err) { - zipDecompressor.Decompress(path, filepath.Join(auxiliaryProviderDir, filename), false) - } else if err != nil { - return fmt.Errorf("Unexpected error: %s", err) - } - } - - err = symlinkFile(path, symlinkPath) - if err != nil { - return fmt.Errorf("error symlinking auxiliary provider %s: %s", name, err) - } - } - - return nil -} - // Close cleans up temporary files and directories created to support this // helper, returning an error if any of the cleanup fails. //