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 }