12 "github.com/hashicorp/terraform/addrs"
13 "github.com/hashicorp/terraform/config"
14 "github.com/hashicorp/terraform/config/hcl2shim"
15 "github.com/hashicorp/terraform/states"
17 "github.com/hashicorp/errwrap"
18 "github.com/hashicorp/terraform/plans"
19 "github.com/hashicorp/terraform/terraform"
20 "github.com/hashicorp/terraform/tfdiags"
23 // testStepConfig runs a config-mode test step
25 opts terraform.ContextOpts,
26 state *terraform.State,
27 step TestStep) (*terraform.State, error) {
28 return testStep(opts, state, step)
31 func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) {
33 if err := testStepTaint(state, step); err != nil {
38 cfg, err := testConfig(opts, step)
43 var stepDiags tfdiags.Diagnostics
47 opts.State, err = terraform.ShimLegacyState(state)
52 opts.Destroy = step.Destroy
53 ctx, stepDiags := terraform.NewContext(&opts)
54 if stepDiags.HasErrors() {
55 return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err())
57 if stepDiags := ctx.Validate(); len(stepDiags) > 0 {
58 if stepDiags.HasErrors() {
59 return state, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err())
62 log.Printf("[WARN] Config warnings:\n%s", stepDiags)
66 newState, stepDiags := ctx.Refresh()
67 // shim the state first so the test can check the state on errors
69 state, err = shimNewState(newState, step.providers)
73 if stepDiags.HasErrors() {
74 return state, newOperationError("refresh", stepDiags)
77 // If this step is a PlanOnly step, skip over this first Plan and subsequent
78 // Apply, and use the follow up Plan that checks for perpetual diffs
81 if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
82 return state, newOperationError("plan", stepDiags)
84 log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
87 // We need to keep a copy of the state prior to destroying
88 // such that destroy steps can verify their behavior in the check
90 stateBeforeApplication := state.DeepCopy()
92 // Apply the diff, creating real resources.
93 newState, stepDiags = ctx.Apply()
94 // shim the state first so the test can check the state on errors
95 state, err = shimNewState(newState, step.providers)
99 if stepDiags.HasErrors() {
100 return state, newOperationError("apply", stepDiags)
103 // Run any configured checks
104 if step.Check != nil {
106 if err := step.Check(stateBeforeApplication); err != nil {
107 return state, fmt.Errorf("Check failed: %s", err)
110 if err := step.Check(state); err != nil {
111 return state, fmt.Errorf("Check failed: %s", err)
117 // Now, verify that Plan is now empty and we don't have a perpetual diff issue
118 // We do this with TWO plans. One without a refresh.
120 if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
121 return state, newOperationError("follow-up plan", stepDiags)
123 if !p.Changes.Empty() {
124 if step.ExpectNonEmptyPlan {
125 log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
127 return state, fmt.Errorf(
128 "After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
132 // And another after a Refresh.
133 if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
134 newState, stepDiags = ctx.Refresh()
135 if stepDiags.HasErrors() {
136 return state, newOperationError("follow-up refresh", stepDiags)
139 state, err = shimNewState(newState, step.providers)
144 if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
145 return state, newOperationError("second follow-up refresh", stepDiags)
147 empty := p.Changes.Empty()
149 // Data resources are tricky because they legitimately get instantiated
150 // during refresh so that they will be already populated during the
151 // plan walk. Because of this, if we have any data resources in the
152 // config we'll end up wanting to destroy them again here. This is
153 // acceptable and expected, and we'll treat it as "empty" for the
154 // sake of this testing.
155 if step.Destroy && !empty {
157 for _, change := range p.Changes.Resources {
158 if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode {
166 if step.ExpectNonEmptyPlan {
167 log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
169 return state, fmt.Errorf(
170 "After applying this step and refreshing, "+
171 "the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
175 // Made it here, but expected a non-empty plan, fail!
176 if step.ExpectNonEmptyPlan && empty {
177 return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
180 // Made it here? Good job test step!
184 // legacyPlanComparisonString produces a string representation of the changes
185 // from a plan and a given state togther, as was formerly produced by the
186 // String method of terraform.Plan.
188 // This is here only for compatibility with existing tests that predate our
189 // new plan and state types, and should not be used in new tests. Instead, use
190 // a library like "cmp" to do a deep equality and diff on the two
192 func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
194 "DIFF:\n\n%s\n\nSTATE:\n\n%s",
195 legacyDiffComparisonString(changes),
200 // legacyDiffComparisonString produces a string representation of the changes
201 // from a planned changes object, as was formerly produced by the String method
202 // of terraform.Diff.
204 // This is here only for compatibility with existing tests that predate our
205 // new plan types, and should not be used in new tests. Instead, use a library
206 // like "cmp" to do a deep equality check and diff on the two data structures.
207 func legacyDiffComparisonString(changes *plans.Changes) string {
208 // The old string representation of a plan was grouped by module, but
209 // our new plan structure is not grouped in that way and so we'll need
210 // to preprocess it in order to produce that grouping.
211 type ResourceChanges struct {
212 Current *plans.ResourceInstanceChangeSrc
213 Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc
215 byModule := map[string]map[string]*ResourceChanges{}
216 resourceKeys := map[string][]string{}
217 requiresReplace := map[string][]string{}
218 var moduleKeys []string
219 for _, rc := range changes.Resources {
220 if rc.Action == plans.NoOp {
221 // We won't mention no-op changes here at all, since the old plan
222 // model we are emulating here didn't have such a concept.
225 moduleKey := rc.Addr.Module.String()
226 if _, exists := byModule[moduleKey]; !exists {
227 moduleKeys = append(moduleKeys, moduleKey)
228 byModule[moduleKey] = make(map[string]*ResourceChanges)
230 resourceKey := rc.Addr.Resource.String()
231 if _, exists := byModule[moduleKey][resourceKey]; !exists {
232 resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey)
233 byModule[moduleKey][resourceKey] = &ResourceChanges{
234 Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc),
238 if rc.DeposedKey == states.NotDeposed {
239 byModule[moduleKey][resourceKey].Current = rc
241 byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc
245 for _, p := range rc.RequiredReplace.List() {
246 rr = append(rr, hcl2shim.FlatmapKeyFromPath(p))
248 requiresReplace[resourceKey] = rr
250 sort.Strings(moduleKeys)
251 for _, ks := range resourceKeys {
257 for _, moduleKey := range moduleKeys {
258 rcs := byModule[moduleKey]
259 var mBuf bytes.Buffer
261 for _, resourceKey := range resourceKeys[moduleKey] {
262 rc := rcs[resourceKey]
264 forceNewAttrs := requiresReplace[resourceKey]
267 if rc.Current != nil {
268 switch rc.Current.Action {
269 case plans.DeleteThenCreate:
270 crud = "DESTROY/CREATE"
271 case plans.CreateThenDelete:
272 crud = "CREATE/DESTROY"
279 // We must be working on a deposed object then, in which
280 // case destroying is the only possible action.
285 if rc.Current == nil && len(rc.Deposed) > 0 {
286 extra = " (deposed only)"
291 crud, resourceKey, extra,
294 attrNames := map[string]bool{}
295 var oldAttrs map[string]string
296 var newAttrs map[string]string
297 if rc.Current != nil {
298 if before := rc.Current.Before; before != nil {
299 ty, err := before.ImpliedType()
301 val, err := before.Decode(ty)
303 oldAttrs = hcl2shim.FlatmapValueFromHCL2(val)
304 for k := range oldAttrs {
310 if after := rc.Current.After; after != nil {
311 ty, err := after.ImpliedType()
313 val, err := after.Decode(ty)
315 newAttrs = hcl2shim.FlatmapValueFromHCL2(val)
316 for k := range newAttrs {
324 oldAttrs = make(map[string]string)
327 newAttrs = make(map[string]string)
330 attrNamesOrder := make([]string, 0, len(attrNames))
332 for n := range attrNames {
333 attrNamesOrder = append(attrNamesOrder, n)
338 sort.Strings(attrNamesOrder)
340 for _, attrK := range attrNamesOrder {
344 if v == config.UnknownVariableValue {
347 // NOTE: we don't support <sensitive> here because we would
348 // need schema to do that. Excluding sensitive values
349 // is now done at the UI layer, and so should not be tested
350 // at the core layer.
354 // This may not be as precise as in the old diff, as it matches
355 // everything under the attribute that was originally marked as
356 // ForceNew, but should help make it easier to determine what
357 // caused replacement here.
358 for _, k := range forceNewAttrs {
359 if strings.HasPrefix(attrK, k) {
360 updateMsg = " (forces new resource)"
366 &mBuf, " %s:%s %#v => %#v%s\n",
368 strings.Repeat(" ", keyLen-len(attrK)),
375 if moduleKey == "" { // root module
376 buf.Write(mBuf.Bytes())
381 fmt.Fprintf(&buf, "%s:\n", moduleKey)
382 s := bufio.NewScanner(&mBuf)
384 buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
391 func testStepTaint(state *terraform.State, step TestStep) error {
392 for _, p := range step.Taint {
393 m := state.RootModule()
395 return errors.New("no state")
397 rs, ok := m.Resources[p]
399 return fmt.Errorf("resource %q not found in state", p)
401 log.Printf("[WARN] Test: Explicitly tainting resource %q", p)