package resource
import (
+ "bytes"
"flag"
"fmt"
"io"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/logutils"
- "github.com/hashicorp/terraform/config/module"
+ "github.com/mitchellh/colorstring"
+
+ "github.com/hashicorp/terraform/addrs"
+ "github.com/hashicorp/terraform/command/format"
+ "github.com/hashicorp/terraform/configs"
+ "github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/helper/logging"
+ "github.com/hashicorp/terraform/internal/initwd"
+ "github.com/hashicorp/terraform/providers"
+ "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
+ "github.com/hashicorp/terraform/tfdiags"
)
// flagSweep is a flag available when running tests on the command line. It
// be refreshed and don't matter.
ImportStateVerify bool
ImportStateVerifyIgnore []string
+
+ // provider s is used internally to maintain a reference to the
+ // underlying providers during the tests
+ providers map[string]terraform.ResourceProvider
}
// Set to a file mask in sprintf format where %s is test name
c.PreCheck()
}
+ // get instances of all providers, so we can use the individual
+ // resources to shim the state during the tests.
+ providers := make(map[string]terraform.ResourceProvider)
+ for name, pf := range testProviderFactories(c) {
+ p, err := pf()
+ if err != nil {
+ t.Fatal(err)
+ }
+ providers[name] = p
+ }
+
providerResolver, err := testProviderResolver(c)
if err != nil {
t.Fatal(err)
}
+
opts := terraform.ContextOpts{ProviderResolver: providerResolver}
// A single state variable to track the lifecycle, starting with no state
idRefresh := c.IDRefreshName != ""
errored := false
for i, step := range c.Steps {
+ // insert the providers into the step so we can get the resources for
+ // shimming the state
+ step.providers = providers
+
var err error
log.Printf("[DEBUG] Test: Executing step %d", i)
}
} else {
errored = true
- t.Error(fmt.Sprintf(
- "Step %d error: %s", i, err))
+ t.Error(fmt.Sprintf("Step %d error: %s", i, detailedErrorMessage(err)))
break
}
}
Destroy: true,
PreventDiskCleanup: lastStep.PreventDiskCleanup,
PreventPostDestroyRefresh: c.PreventPostDestroyRefresh,
+ providers: providers,
}
log.Printf("[WARN] Test: Executing destroy step")
return strings.Join(lines, "")
}
-// testProviderResolver is a helper to build a ResourceProviderResolver
-// with pre instantiated ResourceProviders, so that we can reset them for the
-// test, while only calling the factory function once.
-// Any errors are stored so that they can be returned by the factory in
-// terraform to match non-test behavior.
-func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error) {
- ctxProviders := c.ProviderFactories
- if ctxProviders == nil {
- ctxProviders = make(map[string]terraform.ResourceProviderFactory)
+// testProviderFactories combines the fixed Providers and
+// ResourceProviderFactory functions into a single map of
+// ResourceProviderFactory functions.
+func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFactory {
+ ctxProviders := make(map[string]terraform.ResourceProviderFactory)
+ for k, pf := range c.ProviderFactories {
+ ctxProviders[k] = pf
}
// add any fixed providers
for k, p := range c.Providers {
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
}
+ return ctxProviders
+}
+
+// testProviderResolver is a helper to build a ResourceProviderResolver
+// with pre instantiated ResourceProviders, so that we can reset them for the
+// test, while only calling the factory function once.
+// Any errors are stored so that they can be returned by the factory in
+// terraform to match non-test behavior.
+func testProviderResolver(c TestCase) (providers.Resolver, error) {
+ ctxProviders := testProviderFactories(c)
+
+ // wrap the old provider factories in the test grpc server so they can be
+ // called from terraform.
+ newProviders := make(map[string]providers.Factory)
- // reset the providers if needed
for k, pf := range ctxProviders {
- // we can ignore any errors here, if we don't have a provider to reset
- // the error will be handled later
- p, err := pf()
- if err != nil {
- return nil, err
- }
- if p, ok := p.(TestProvider); ok {
- err := p.TestReset()
+ factory := pf // must copy to ensure each closure sees its own value
+ newProviders[k] = func() (providers.Interface, error) {
+ p, err := factory()
if err != nil {
- return nil, fmt.Errorf("[ERROR] failed to reset provider %q: %s", k, err)
+ return nil, err
}
+
+ // The provider is wrapped in a GRPCTestProvider so that it can be
+ // passed back to terraform core as a providers.Interface, rather
+ // than the legacy ResourceProvider.
+ return GRPCTestProvider(p), nil
}
}
- return terraform.ResourceProviderResolverFixed(ctxProviders), nil
+ return providers.ResolverFixed(newProviders), nil
}
// UnitTest is a helper to force the acceptance testing harness to run in the
return nil
}
- name := fmt.Sprintf("%s.foo", r.Type)
+ addr := addrs.Resource{
+ Mode: addrs.ManagedResourceMode,
+ Type: r.Type,
+ Name: "foo",
+ }.Instance(addrs.NoKey)
+ absAddr := addr.Absolute(addrs.RootModuleInstance)
// Build the state. The state is just the resource with an ID. There
// are no attributes. We only set what is needed to perform a refresh.
- state := terraform.NewState()
- state.RootModule().Resources[name] = &terraform.ResourceState{
- Type: r.Type,
- Primary: &terraform.InstanceState{
- ID: r.Primary.ID,
+ state := states.NewState()
+ state.RootModule().SetResourceInstanceCurrent(
+ addr,
+ &states.ResourceInstanceObjectSrc{
+ AttrsFlat: r.Primary.Attributes,
+ Status: states.ObjectReady,
},
- }
+ addrs.ProviderConfig{Type: "placeholder"}.Absolute(addrs.RootModuleInstance),
+ )
// Create the config module. We use the full config because Refresh
// doesn't have access to it and we may need things like provider
// configurations. The initial implementation of id-only checks used
// an empty config module, but that caused the aforementioned problems.
- mod, err := testModule(opts, step)
+ cfg, err := testConfig(opts, step)
if err != nil {
return err
}
// Initialize the context
- opts.Module = mod
+ opts.Config = cfg
opts.State = state
- ctx, err := terraform.NewContext(&opts)
- if err != nil {
- return err
+ ctx, ctxDiags := terraform.NewContext(&opts)
+ if ctxDiags.HasErrors() {
+ return ctxDiags.Err()
}
if diags := ctx.Validate(); len(diags) > 0 {
if diags.HasErrors() {
}
// Refresh!
- state, err = ctx.Refresh()
- if err != nil {
- return fmt.Errorf("Error refreshing: %s", err)
+ state, refreshDiags := ctx.Refresh()
+ if refreshDiags.HasErrors() {
+ return refreshDiags.Err()
}
// Verify attribute equivalence.
- actualR := state.RootModule().Resources[name]
+ actualR := state.ResourceInstance(absAddr)
if actualR == nil {
return fmt.Errorf("Resource gone!")
}
- if actualR.Primary == nil {
+ if actualR.Current == nil {
return fmt.Errorf("Resource has no primary instance")
}
- actual := actualR.Primary.Attributes
+ actual := actualR.Current.AttrsFlat
expected := r.Primary.Attributes
// Remove fields we're ignoring
for _, v := range c.IDRefreshIgnore {
return nil
}
-func testModule(opts terraform.ContextOpts, step TestStep) (*module.Tree, error) {
+func testConfig(opts terraform.ContextOpts, step TestStep) (*configs.Config, error) {
if step.PreConfig != nil {
step.PreConfig()
}
cfgPath, err := ioutil.TempDir("", "tf-test")
if err != nil {
- return nil, fmt.Errorf(
- "Error creating temporary directory for config: %s", err)
+ return nil, fmt.Errorf("Error creating temporary directory for config: %s", err)
}
if step.PreventDiskCleanup {
defer os.RemoveAll(cfgPath)
}
- // Write the configuration
- cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf"))
+ // Write the main configuration file
+ err = ioutil.WriteFile(filepath.Join(cfgPath, "main.tf"), []byte(step.Config), os.ModePerm)
if err != nil {
- return nil, fmt.Errorf(
- "Error creating temporary file for config: %s", err)
+ return nil, fmt.Errorf("Error creating temporary file for config: %s", err)
}
- _, err = io.Copy(cfgF, strings.NewReader(step.Config))
- cfgF.Close()
+ // Create directory for our child modules, if any.
+ modulesDir := filepath.Join(cfgPath, ".modules")
+ err = os.Mkdir(modulesDir, os.ModePerm)
if err != nil {
- return nil, fmt.Errorf(
- "Error creating temporary file for config: %s", err)
+ return nil, fmt.Errorf("Error creating child modules directory: %s", err)
}
- // Parse the configuration
- mod, err := module.NewTreeModule("", cfgPath)
- if err != nil {
- return nil, fmt.Errorf(
- "Error loading configuration: %s", err)
+ inst := initwd.NewModuleInstaller(modulesDir, nil)
+ _, installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{})
+ if installDiags.HasErrors() {
+ return nil, installDiags.Err()
}
- // Load the modules
- modStorage := &module.Storage{
- StorageDir: filepath.Join(cfgPath, ".tfmodules"),
- Mode: module.GetModeGet,
- }
- err = mod.Load(modStorage)
+ loader, err := configload.NewLoader(&configload.Config{
+ ModulesDir: modulesDir,
+ })
if err != nil {
- return nil, fmt.Errorf("Error downloading modules: %s", err)
+ return nil, fmt.Errorf("failed to create config loader: %s", err)
+ }
+
+ config, configDiags := loader.LoadConfig(cfgPath)
+ if configDiags.HasErrors() {
+ return nil, configDiags
}
- return mod, nil
+ return config, nil
}
func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) {
// TestCheckModuleResourceAttrSet - as per TestCheckResourceAttrSet but with
// support for non-root modules
func TestCheckModuleResourceAttrSet(mp []string, name string, key string) TestCheckFunc {
+ mpt := addrs.Module(mp).UnkeyedInstanceShim()
return func(s *terraform.State) error {
- is, err := modulePathPrimaryInstanceState(s, mp, name)
+ is, err := modulePathPrimaryInstanceState(s, mpt, name)
if err != nil {
return err
}
// TestCheckModuleResourceAttr - as per TestCheckResourceAttr but with
// support for non-root modules
func TestCheckModuleResourceAttr(mp []string, name string, key string, value string) TestCheckFunc {
+ mpt := addrs.Module(mp).UnkeyedInstanceShim()
return func(s *terraform.State) error {
- is, err := modulePathPrimaryInstanceState(s, mp, name)
+ is, err := modulePathPrimaryInstanceState(s, mpt, name)
if err != nil {
return err
}
}
func testCheckResourceAttr(is *terraform.InstanceState, name string, key string, value string) error {
+ // Empty containers may be elided from the state.
+ // If the intent here is to check for an empty container, allow the key to
+ // also be non-existent.
+ emptyCheck := false
+ if value == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) {
+ emptyCheck = true
+ }
+
if v, ok := is.Attributes[key]; !ok || v != value {
+ if emptyCheck && !ok {
+ return nil
+ }
+
if !ok {
return fmt.Errorf("%s: Attribute '%s' not found", name, key)
}
// TestCheckModuleNoResourceAttr - as per TestCheckNoResourceAttr but with
// support for non-root modules
func TestCheckModuleNoResourceAttr(mp []string, name string, key string) TestCheckFunc {
+ mpt := addrs.Module(mp).UnkeyedInstanceShim()
return func(s *terraform.State) error {
- is, err := modulePathPrimaryInstanceState(s, mp, name)
+ is, err := modulePathPrimaryInstanceState(s, mpt, name)
if err != nil {
return err
}
}
func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key string) error {
- if _, ok := is.Attributes[key]; ok {
+ // Empty containers may sometimes be included in the state.
+ // If the intent here is to check for an empty container, allow the value to
+ // also be "0".
+ emptyCheck := false
+ if strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%") {
+ emptyCheck = true
+ }
+
+ val, exists := is.Attributes[key]
+ if emptyCheck && val == "0" {
+ return nil
+ }
+
+ if exists {
return fmt.Errorf("%s: Attribute '%s' found when not expected", name, key)
}
// TestModuleMatchResourceAttr - as per TestMatchResourceAttr but with
// support for non-root modules
func TestModuleMatchResourceAttr(mp []string, name string, key string, r *regexp.Regexp) TestCheckFunc {
+ mpt := addrs.Module(mp).UnkeyedInstanceShim()
return func(s *terraform.State) error {
- is, err := modulePathPrimaryInstanceState(s, mp, name)
+ is, err := modulePathPrimaryInstanceState(s, mpt, name)
if err != nil {
return err
}
// TestCheckModuleResourceAttrPair - as per TestCheckResourceAttrPair but with
// support for non-root modules
func TestCheckModuleResourceAttrPair(mpFirst []string, nameFirst string, keyFirst string, mpSecond []string, nameSecond string, keySecond string) TestCheckFunc {
+ mptFirst := addrs.Module(mpFirst).UnkeyedInstanceShim()
+ mptSecond := addrs.Module(mpSecond).UnkeyedInstanceShim()
return func(s *terraform.State) error {
- isFirst, err := modulePathPrimaryInstanceState(s, mpFirst, nameFirst)
+ isFirst, err := modulePathPrimaryInstanceState(s, mptFirst, nameFirst)
if err != nil {
return err
}
- isSecond, err := modulePathPrimaryInstanceState(s, mpSecond, nameSecond)
+ isSecond, err := modulePathPrimaryInstanceState(s, mptSecond, nameSecond)
if err != nil {
return err
}
}
func testCheckResourceAttrPair(isFirst *terraform.InstanceState, nameFirst string, keyFirst string, isSecond *terraform.InstanceState, nameSecond string, keySecond string) error {
- vFirst, ok := isFirst.Attributes[keyFirst]
- if !ok {
- return fmt.Errorf("%s: Attribute '%s' not found", nameFirst, keyFirst)
+ vFirst, okFirst := isFirst.Attributes[keyFirst]
+ vSecond, okSecond := isSecond.Attributes[keySecond]
+
+ // Container count values of 0 should not be relied upon, and not reliably
+ // maintained by helper/schema. For the purpose of tests, consider unset and
+ // 0 to be equal.
+ if len(keyFirst) > 2 && len(keySecond) > 2 && keyFirst[len(keyFirst)-2:] == keySecond[len(keySecond)-2:] &&
+ (strings.HasSuffix(keyFirst, ".#") || strings.HasSuffix(keyFirst, ".%")) {
+ // they have the same suffix, and it is a collection count key.
+ if vFirst == "0" || vFirst == "" {
+ okFirst = false
+ }
+ if vSecond == "0" || vSecond == "" {
+ okSecond = false
+ }
}
- vSecond, ok := isSecond.Attributes[keySecond]
- if !ok {
- return fmt.Errorf("%s: Attribute '%s' not found", nameSecond, keySecond)
+ if okFirst != okSecond {
+ if !okFirst {
+ return fmt.Errorf("%s: Attribute %q not set, but %q is set in %s as %q", nameFirst, keyFirst, keySecond, nameSecond, vSecond)
+ }
+ return fmt.Errorf("%s: Attribute %q is %q, but %q is not set in %s", nameFirst, keyFirst, vFirst, keySecond, nameSecond)
+ }
+ if !(okFirst || okSecond) {
+ // If they both don't exist then they are equally unset, so that's okay.
+ return nil
}
if vFirst != vSecond {
// modulePathPrimaryInstanceState returns the primary instance state for the
// given resource name in a given module path.
-func modulePathPrimaryInstanceState(s *terraform.State, mp []string, name string) (*terraform.InstanceState, error) {
+func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, name string) (*terraform.InstanceState, error) {
ms := s.ModuleByPath(mp)
if ms == nil {
return nil, fmt.Errorf("No module found at: %s", mp)
ms := s.RootModule()
return modulePrimaryInstanceState(s, ms, name)
}
+
+// operationError is a specialized implementation of error used to describe
+// failures during one of the several operations performed for a particular
+// test case.
+type operationError struct {
+ OpName string
+ Diags tfdiags.Diagnostics
+}
+
+func newOperationError(opName string, diags tfdiags.Diagnostics) error {
+ return operationError{opName, diags}
+}
+
+// Error returns a terse error string containing just the basic diagnostic
+// messages, for situations where normal Go error behavior is appropriate.
+func (err operationError) Error() string {
+ return fmt.Sprintf("errors during %s: %s", err.OpName, err.Diags.Err().Error())
+}
+
+// ErrorDetail is like Error except it includes verbosely-rendered diagnostics
+// similar to what would come from a normal Terraform run, which include
+// additional context not included in Error().
+func (err operationError) ErrorDetail() string {
+ var buf bytes.Buffer
+ fmt.Fprintf(&buf, "errors during %s:", err.OpName)
+ clr := &colorstring.Colorize{Disable: true, Colors: colorstring.DefaultColors}
+ for _, diag := range err.Diags {
+ diagStr := format.Diagnostic(diag, nil, clr, 78)
+ buf.WriteByte('\n')
+ buf.WriteString(diagStr)
+ }
+ return buf.String()
+}
+
+// detailedErrorMessage is a helper for calling ErrorDetail on an error if
+// it is an operationError or just taking Error otherwise.
+func detailedErrorMessage(err error) string {
+ switch tErr := err.(type) {
+ case operationError:
+ return tErr.ErrorDetail()
+ default:
+ return err.Error()
+ }
+}