github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/resource/testing_config.go (about)

     1  package resource
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    13  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/hcl2shim"
    14  	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
    15  
    16  	"github.com/hashicorp/errwrap"
    17  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
    18  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    19  	"github.com/hashicorp/terraform-plugin-sdk/terraform"
    20  )
    21  
    22  // testStepConfig runs a config-mode test step
    23  func testStepConfig(
    24  	opts terraform.ContextOpts,
    25  	state *terraform.State,
    26  	step TestStep) (*terraform.State, error) {
    27  	return testStep(opts, state, step)
    28  }
    29  
    30  func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) {
    31  	if !step.Destroy {
    32  		if err := testStepTaint(state, step); err != nil {
    33  			return state, err
    34  		}
    35  	}
    36  
    37  	cfg, err := testConfig(opts, step)
    38  	if err != nil {
    39  		return state, err
    40  	}
    41  
    42  	var stepDiags tfdiags.Diagnostics
    43  
    44  	// Build the context
    45  	opts.Config = cfg
    46  	opts.State, err = terraform.ShimLegacyState(state)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	opts.Destroy = step.Destroy
    52  	ctx, stepDiags := terraform.NewContext(&opts)
    53  	if stepDiags.HasErrors() {
    54  		return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err())
    55  	}
    56  	if stepDiags := ctx.Validate(); len(stepDiags) > 0 {
    57  		if stepDiags.HasErrors() {
    58  			return state, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err())
    59  		}
    60  
    61  		log.Printf("[WARN] Config warnings:\n%s", stepDiags)
    62  	}
    63  
    64  	// Refresh!
    65  	newState, stepDiags := ctx.Refresh()
    66  	// shim the state first so the test can check the state on errors
    67  
    68  	state, err = shimNewState(newState, step.providers)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	if stepDiags.HasErrors() {
    73  		return state, newOperationError("refresh", stepDiags)
    74  	}
    75  
    76  	// If this step is a PlanOnly step, skip over this first Plan and subsequent
    77  	// Apply, and use the follow up Plan that checks for perpetual diffs
    78  	if !step.PlanOnly {
    79  		// Plan!
    80  		if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
    81  			return state, newOperationError("plan", stepDiags)
    82  		} else {
    83  			log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
    84  		}
    85  
    86  		// We need to keep a copy of the state prior to destroying
    87  		// such that destroy steps can verify their behavior in the check
    88  		// function
    89  		stateBeforeApplication := state.DeepCopy()
    90  
    91  		// Apply the diff, creating real resources.
    92  		newState, stepDiags = ctx.Apply()
    93  		// shim the state first so the test can check the state on errors
    94  		state, err = shimNewState(newState, step.providers)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		if stepDiags.HasErrors() {
    99  			return state, newOperationError("apply", stepDiags)
   100  		}
   101  
   102  		// Run any configured checks
   103  		if step.Check != nil {
   104  			if step.Destroy {
   105  				if err := step.Check(stateBeforeApplication); err != nil {
   106  					return state, fmt.Errorf("Check failed: %s", err)
   107  				}
   108  			} else {
   109  				if err := step.Check(state); err != nil {
   110  					return state, fmt.Errorf("Check failed: %s", err)
   111  				}
   112  			}
   113  		}
   114  	}
   115  
   116  	// Now, verify that Plan is now empty and we don't have a perpetual diff issue
   117  	// We do this with TWO plans. One without a refresh.
   118  	var p *plans.Plan
   119  	if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
   120  		return state, newOperationError("follow-up plan", stepDiags)
   121  	}
   122  	if !p.Changes.Empty() {
   123  		if step.ExpectNonEmptyPlan {
   124  			log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
   125  		} else {
   126  			return state, fmt.Errorf(
   127  				"After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
   128  		}
   129  	}
   130  
   131  	// And another after a Refresh.
   132  	if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
   133  		newState, stepDiags = ctx.Refresh()
   134  		if stepDiags.HasErrors() {
   135  			return state, newOperationError("follow-up refresh", stepDiags)
   136  		}
   137  
   138  		state, err = shimNewState(newState, step.providers)
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  	}
   143  	if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
   144  		return state, newOperationError("second follow-up refresh", stepDiags)
   145  	}
   146  	empty := p.Changes.Empty()
   147  
   148  	// Data resources are tricky because they legitimately get instantiated
   149  	// during refresh so that they will be already populated during the
   150  	// plan walk. Because of this, if we have any data resources in the
   151  	// config we'll end up wanting to destroy them again here. This is
   152  	// acceptable and expected, and we'll treat it as "empty" for the
   153  	// sake of this testing.
   154  	if step.Destroy && !empty {
   155  		empty = true
   156  		for _, change := range p.Changes.Resources {
   157  			if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode {
   158  				empty = false
   159  				break
   160  			}
   161  		}
   162  	}
   163  
   164  	if !empty {
   165  		if step.ExpectNonEmptyPlan {
   166  			log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
   167  		} else {
   168  			return state, fmt.Errorf(
   169  				"After applying this step and refreshing, "+
   170  					"the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
   171  		}
   172  	}
   173  
   174  	// Made it here, but expected a non-empty plan, fail!
   175  	if step.ExpectNonEmptyPlan && empty {
   176  		return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
   177  	}
   178  
   179  	// Made it here? Good job test step!
   180  	return state, nil
   181  }
   182  
   183  // legacyPlanComparisonString produces a string representation of the changes
   184  // from a plan and a given state togther, as was formerly produced by the
   185  // String method of terraform.Plan.
   186  //
   187  // This is here only for compatibility with existing tests that predate our
   188  // new plan and state types, and should not be used in new tests. Instead, use
   189  // a library like "cmp" to do a deep equality  and diff on the two
   190  // data structures.
   191  func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
   192  	return fmt.Sprintf(
   193  		"DIFF:\n\n%s\n\nSTATE:\n\n%s",
   194  		legacyDiffComparisonString(changes),
   195  		state.String(),
   196  	)
   197  }
   198  
   199  // legacyDiffComparisonString produces a string representation of the changes
   200  // from a planned changes object, as was formerly produced by the String method
   201  // of terraform.Diff.
   202  //
   203  // This is here only for compatibility with existing tests that predate our
   204  // new plan types, and should not be used in new tests. Instead, use a library
   205  // like "cmp" to do a deep equality check and diff on the two data structures.
   206  func legacyDiffComparisonString(changes *plans.Changes) string {
   207  	// The old string representation of a plan was grouped by module, but
   208  	// our new plan structure is not grouped in that way and so we'll need
   209  	// to preprocess it in order to produce that grouping.
   210  	type ResourceChanges struct {
   211  		Current *plans.ResourceInstanceChangeSrc
   212  		Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc
   213  	}
   214  	byModule := map[string]map[string]*ResourceChanges{}
   215  	resourceKeys := map[string][]string{}
   216  	requiresReplace := map[string][]string{}
   217  	var moduleKeys []string
   218  	for _, rc := range changes.Resources {
   219  		if rc.Action == plans.NoOp {
   220  			// We won't mention no-op changes here at all, since the old plan
   221  			// model we are emulating here didn't have such a concept.
   222  			continue
   223  		}
   224  		moduleKey := rc.Addr.Module.String()
   225  		if _, exists := byModule[moduleKey]; !exists {
   226  			moduleKeys = append(moduleKeys, moduleKey)
   227  			byModule[moduleKey] = make(map[string]*ResourceChanges)
   228  		}
   229  		resourceKey := rc.Addr.Resource.String()
   230  		if _, exists := byModule[moduleKey][resourceKey]; !exists {
   231  			resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey)
   232  			byModule[moduleKey][resourceKey] = &ResourceChanges{
   233  				Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc),
   234  			}
   235  		}
   236  
   237  		if rc.DeposedKey == states.NotDeposed {
   238  			byModule[moduleKey][resourceKey].Current = rc
   239  		} else {
   240  			byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc
   241  		}
   242  
   243  		rr := []string{}
   244  		for _, p := range rc.RequiredReplace.List() {
   245  			rr = append(rr, hcl2shim.FlatmapKeyFromPath(p))
   246  		}
   247  		requiresReplace[resourceKey] = rr
   248  	}
   249  	sort.Strings(moduleKeys)
   250  	for _, ks := range resourceKeys {
   251  		sort.Strings(ks)
   252  	}
   253  
   254  	var buf bytes.Buffer
   255  
   256  	for _, moduleKey := range moduleKeys {
   257  		rcs := byModule[moduleKey]
   258  		var mBuf bytes.Buffer
   259  
   260  		for _, resourceKey := range resourceKeys[moduleKey] {
   261  			rc := rcs[resourceKey]
   262  
   263  			forceNewAttrs := requiresReplace[resourceKey]
   264  
   265  			crud := "UPDATE"
   266  			if rc.Current != nil {
   267  				switch rc.Current.Action {
   268  				case plans.DeleteThenCreate:
   269  					crud = "DESTROY/CREATE"
   270  				case plans.CreateThenDelete:
   271  					crud = "CREATE/DESTROY"
   272  				case plans.Delete:
   273  					crud = "DESTROY"
   274  				case plans.Create:
   275  					crud = "CREATE"
   276  				}
   277  			} else {
   278  				// We must be working on a deposed object then, in which
   279  				// case destroying is the only possible action.
   280  				crud = "DESTROY"
   281  			}
   282  
   283  			extra := ""
   284  			if rc.Current == nil && len(rc.Deposed) > 0 {
   285  				extra = " (deposed only)"
   286  			}
   287  
   288  			fmt.Fprintf(
   289  				&mBuf, "%s: %s%s\n",
   290  				crud, resourceKey, extra,
   291  			)
   292  
   293  			attrNames := map[string]bool{}
   294  			var oldAttrs map[string]string
   295  			var newAttrs map[string]string
   296  			if rc.Current != nil {
   297  				if before := rc.Current.Before; before != nil {
   298  					ty, err := before.ImpliedType()
   299  					if err == nil {
   300  						val, err := before.Decode(ty)
   301  						if err == nil {
   302  							oldAttrs = hcl2shim.FlatmapValueFromHCL2(val)
   303  							for k := range oldAttrs {
   304  								attrNames[k] = true
   305  							}
   306  						}
   307  					}
   308  				}
   309  				if after := rc.Current.After; after != nil {
   310  					ty, err := after.ImpliedType()
   311  					if err == nil {
   312  						val, err := after.Decode(ty)
   313  						if err == nil {
   314  							newAttrs = hcl2shim.FlatmapValueFromHCL2(val)
   315  							for k := range newAttrs {
   316  								attrNames[k] = true
   317  							}
   318  						}
   319  					}
   320  				}
   321  			}
   322  			if oldAttrs == nil {
   323  				oldAttrs = make(map[string]string)
   324  			}
   325  			if newAttrs == nil {
   326  				newAttrs = make(map[string]string)
   327  			}
   328  
   329  			attrNamesOrder := make([]string, 0, len(attrNames))
   330  			keyLen := 0
   331  			for n := range attrNames {
   332  				attrNamesOrder = append(attrNamesOrder, n)
   333  				if len(n) > keyLen {
   334  					keyLen = len(n)
   335  				}
   336  			}
   337  			sort.Strings(attrNamesOrder)
   338  
   339  			for _, attrK := range attrNamesOrder {
   340  				v := newAttrs[attrK]
   341  				u := oldAttrs[attrK]
   342  
   343  				if v == hcl2shim.UnknownVariableValue {
   344  					v = "<computed>"
   345  				}
   346  				// NOTE: we don't support <sensitive> here because we would
   347  				// need schema to do that. Excluding sensitive values
   348  				// is now done at the UI layer, and so should not be tested
   349  				// at the core layer.
   350  
   351  				updateMsg := ""
   352  
   353  				// This may not be as precise as in the old diff, as it matches
   354  				// everything under the attribute that was originally marked as
   355  				// ForceNew, but should help make it easier to determine what
   356  				// caused replacement here.
   357  				for _, k := range forceNewAttrs {
   358  					if strings.HasPrefix(attrK, k) {
   359  						updateMsg = " (forces new resource)"
   360  						break
   361  					}
   362  				}
   363  
   364  				fmt.Fprintf(
   365  					&mBuf, "  %s:%s %#v => %#v%s\n",
   366  					attrK,
   367  					strings.Repeat(" ", keyLen-len(attrK)),
   368  					u, v,
   369  					updateMsg,
   370  				)
   371  			}
   372  		}
   373  
   374  		if moduleKey == "" { // root module
   375  			buf.Write(mBuf.Bytes())
   376  			buf.WriteByte('\n')
   377  			continue
   378  		}
   379  
   380  		fmt.Fprintf(&buf, "%s:\n", moduleKey)
   381  		s := bufio.NewScanner(&mBuf)
   382  		for s.Scan() {
   383  			buf.WriteString(fmt.Sprintf("  %s\n", s.Text()))
   384  		}
   385  	}
   386  
   387  	return buf.String()
   388  }
   389  
   390  func testStepTaint(state *terraform.State, step TestStep) error {
   391  	for _, p := range step.Taint {
   392  		m := state.RootModule()
   393  		if m == nil {
   394  			return errors.New("no state")
   395  		}
   396  		rs, ok := m.Resources[p]
   397  		if !ok {
   398  			return fmt.Errorf("resource %q not found in state", p)
   399  		}
   400  		log.Printf("[WARN] Test: Explicitly tainting resource %q", p)
   401  		rs.Taint()
   402  	}
   403  	return nil
   404  }