291 lines
9.8 KiB
Go
291 lines
9.8 KiB
Go
package tftest
|
|
|
|
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(name string, sourceDir string) *Helper {
|
|
helper, err := AutoInitHelper("terraform-provider-"+name, 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
|
|
pluginName string
|
|
terraformExec string
|
|
thisPluginDir, prevPluginDir 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(pluginName string, sourceDir string) (*Helper, error) {
|
|
config, err := DiscoverConfig(pluginName, 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, "tftest-"+config.PluginName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temporary directory for test helper: %s", err)
|
|
}
|
|
|
|
var thisPluginDir, prevPluginDir string
|
|
if config.CurrentPluginExec != "" {
|
|
thisPluginDir, err = ioutil.TempDir(baseDir, "plugins-current")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temporary directory for -plugin-dir: %s", err)
|
|
}
|
|
currentExecPath := filepath.Join(thisPluginDir, config.PluginName)
|
|
err = symlinkFile(config.CurrentPluginExec, currentExecPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create symlink at %s to %s: %s", currentExecPath, config.CurrentPluginExec, err)
|
|
}
|
|
|
|
err = symlinkAuxiliaryProviders(thisPluginDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to symlink auxiliary providers: %s", err)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("CurrentPluginExec is not set")
|
|
}
|
|
if config.PreviousPluginExec != "" {
|
|
prevPluginDir, err = ioutil.TempDir(baseDir, "plugins-previous")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temporary directory for previous -plugin-dir: %s", err)
|
|
}
|
|
prevExecPath := filepath.Join(prevPluginDir, config.PluginName)
|
|
err = symlinkFile(config.PreviousPluginExec, prevExecPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create symlink at %s to %s: %s", prevExecPath, config.PreviousPluginExec, err)
|
|
}
|
|
|
|
err = symlinkAuxiliaryProviders(prevPluginDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to symlink auxiliary providers: %s", err)
|
|
}
|
|
}
|
|
|
|
return &Helper{
|
|
baseDir: baseDir,
|
|
sourceDir: config.SourceDir,
|
|
pluginName: config.PluginName,
|
|
terraformExec: config.TerraformExec,
|
|
thisPluginDir: thisPluginDir,
|
|
prevPluginDir: prevPluginDir,
|
|
}, 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
|
|
}
|
|
|
|
// GetPluginName returns the configured plugin name.
|
|
func (h *Helper) GetPluginName() string {
|
|
return h.pluginName
|
|
}
|
|
|
|
// 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 {
|
|
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
|
|
}
|
|
|
|
// symlink the provider binaries into the base directory
|
|
err = symlinkDir(h.thisPluginDir, dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &WorkingDir{
|
|
h: h,
|
|
baseArgs: []string{"-no-color"},
|
|
baseDir: dir,
|
|
}, 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
|
|
}
|
|
|
|
// HasPreviousVersion returns true if and only if the receiving helper has a
|
|
// previous plugin version available for use in tests.
|
|
func (h *Helper) HasPreviousVersion() bool {
|
|
return h.prevPluginDir != ""
|
|
}
|
|
|
|
// TerraformExecPath returns the location of the Terraform CLI executable that
|
|
// should be used when running tests.
|
|
func (h *Helper) TerraformExecPath() string {
|
|
return h.terraformExec
|
|
}
|
|
|
|
// PluginDir returns the directory that should be used as the -plugin-dir when
|
|
// running "terraform init" in order to make Terraform detect the current
|
|
// version of the plugin.
|
|
func (h *Helper) PluginDir() string {
|
|
return h.thisPluginDir
|
|
}
|
|
|
|
// PreviousPluginDir returns the directory that should be used as the -plugin-dir
|
|
// when running "terraform init" in order to make Terraform detect the previous
|
|
// version of the plugin, if available.
|
|
//
|
|
// If no previous version is available, this method will panic. Use
|
|
// RequirePreviousVersion or HasPreviousVersion to ensure a previous version is
|
|
// available before calling this.
|
|
func (h *Helper) PreviousPluginDir() string {
|
|
if h.prevPluginDir != "" {
|
|
panic("PreviousPluginDir not available")
|
|
}
|
|
return h.prevPluginDir
|
|
}
|