Skip to content

Commit c4b29eb

Browse files
committed
testing for import blocks
Add a new sub-mode of TestStep.ImportState for ImportBlock testing.
1 parent dfd28ce commit c4b29eb

File tree

4 files changed

+608
-1
lines changed

4 files changed

+608
-1
lines changed

helper/resource/testing.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,12 +560,25 @@ type TestStep struct {
560560
//---------------------------------------------------------------
561561
// ImportState testing
562562
//---------------------------------------------------------------
563+
// Terraform has two workflows for importing resources: the import CLI
564+
// command, which writes directly to state, and the import block in HCL,
565+
// which imports to state via the normal plan and apply workflow.
563566

564567
// ImportState, if true, will test the functionality of ImportState
565568
// by importing the resource with ResourceName (must be set) and the
566569
// ID of that resource.
570+
// By default, the "terraform import" command will be run. To test import
571+
// block functionality instead, set ImportBlock to true.
567572
ImportState bool
568573

574+
// ImportBlock, if true, enables a sub-mode of ImportState testing. In this
575+
// mode, an import block is added to the config, and plan and apply are run.
576+
ImportBlock bool
577+
578+
// ImportBlockConfig is an optional string with the import block
579+
// configuration to use when ImportBlock is true.
580+
ImportBlockConfig string
581+
569582
// ImportStateId is the ID to perform an ImportState operation with.
570583
// This is optional. If it isn't set, then the resource ID is automatically
571584
// determined by inspecting the state for ResourceName's ID.

helper/resource/testing_new.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,12 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
214214
if step.ImportState {
215215
logging.HelperResourceTrace(ctx, "TestStep is ImportState mode")
216216

217-
err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers)
217+
var err error
218+
if step.ImportBlock {
219+
err = testStepNewImportBlock(ctx, t, helper, wd, step, appliedCfg, providers)
220+
} else {
221+
err = testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers)
222+
}
218223
if step.ExpectError != nil {
219224
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError")
220225
if err == nil {
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package resource
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"reflect"
10+
"strings"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/mitchellh/go-testing-interface"
14+
15+
"github.com/hashicorp/terraform-plugin-testing/terraform"
16+
17+
"github.com/hashicorp/terraform-plugin-testing/internal/logging"
18+
"github.com/hashicorp/terraform-plugin-testing/internal/plugintest"
19+
)
20+
21+
// Generates a config with import block, then plans and applies the import.
22+
// Optionally attempts to generate configuration during the plan step.
23+
func testStepNewImportBlock(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg string, providers *providerFactories) error {
24+
t.Helper()
25+
26+
if step.ResourceName == "" {
27+
t.Fatal("ResourceName is required for an import state test")
28+
}
29+
30+
// get state from check sequence
31+
var state *terraform.State
32+
var err error
33+
err = runProviderCommand(ctx, t, func() error {
34+
state, err = getState(ctx, t, wd)
35+
if err != nil {
36+
return err
37+
}
38+
return nil
39+
}, wd, providers)
40+
if err != nil {
41+
t.Fatalf("Error getting state: %s", err)
42+
}
43+
44+
// Determine the ID to import
45+
var importId string
46+
switch {
47+
case step.ImportStateIdFunc != nil:
48+
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateIdFunc for import identifier")
49+
50+
var err error
51+
52+
logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateIdFunc")
53+
54+
importId, err = step.ImportStateIdFunc(state)
55+
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
60+
logging.HelperResourceDebug(ctx, "Called TestStep ImportStateIdFunc")
61+
case step.ImportStateId != "":
62+
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateId for import identifier")
63+
64+
importId = step.ImportStateId
65+
default:
66+
logging.HelperResourceTrace(ctx, "Using resource identifier for import identifier")
67+
68+
resource, err := testResource(step, state)
69+
if err != nil {
70+
t.Fatal(err)
71+
}
72+
importId = resource.Primary.ID
73+
}
74+
75+
if step.ImportStateIdPrefix != "" {
76+
logging.HelperResourceTrace(ctx, "Prepending TestStep ImportStateIdPrefix for import identifier")
77+
78+
importId = step.ImportStateIdPrefix + importId
79+
}
80+
81+
logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId))
82+
83+
// Create working directory for import tests
84+
if step.Config == "" {
85+
logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import")
86+
87+
step.Config = cfg
88+
if step.Config == "" {
89+
t.Fatal("Cannot import state with no specified config")
90+
}
91+
}
92+
93+
var importWd *plugintest.WorkingDir
94+
95+
// Use the same working directory to persist the state from import
96+
if step.ImportStatePersist {
97+
importWd = wd
98+
} else {
99+
importWd = helper.RequireNewWorkingDir(ctx, t, "")
100+
defer importWd.Close()
101+
}
102+
103+
var importBlockConfig string
104+
105+
if step.ImportBlockConfig != "" {
106+
importBlockConfig = step.ImportBlockConfig + "\n"
107+
} else {
108+
importBlockConfig = fmt.Sprintf(`import {
109+
to = %s
110+
id = "%s"
111+
}
112+
`, step.ResourceName, importId)
113+
}
114+
115+
err = importWd.SetConfig(ctx, importBlockConfig+step.Config)
116+
if err != nil {
117+
t.Fatalf("Error setting test config: %s", err)
118+
}
119+
120+
logging.HelperResourceDebug(ctx, "Running Terraform CLI init and plan")
121+
122+
if !step.ImportStatePersist {
123+
err = runProviderCommand(ctx, t, func() error {
124+
return importWd.Init(ctx)
125+
}, importWd, providers)
126+
if err != nil {
127+
t.Fatalf("Error running init: %s", err)
128+
}
129+
}
130+
131+
err = runProviderCommand(ctx, t, func() error {
132+
return importWd.CreatePlan(ctx)
133+
}, importWd, providers)
134+
if err != nil {
135+
return err
136+
}
137+
138+
logging.HelperResourceDebug(ctx, "Running Terraform CLI apply")
139+
140+
err = runProviderCommand(ctx, t, func() error {
141+
return importWd.Apply(ctx)
142+
}, importWd, providers)
143+
if err != nil {
144+
return err
145+
}
146+
147+
var importState *terraform.State
148+
err = runProviderCommand(ctx, t, func() error {
149+
importState, err = getState(ctx, t, importWd)
150+
if err != nil {
151+
return err
152+
}
153+
return nil
154+
}, importWd, providers)
155+
if err != nil {
156+
t.Fatalf("Error getting state: %s", err)
157+
}
158+
159+
// Go through the imported state and verify
160+
if step.ImportStateCheck != nil {
161+
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateCheck")
162+
163+
var states []*terraform.InstanceState
164+
for address, r := range importState.RootModule().Resources {
165+
if strings.HasPrefix(address, "data.") {
166+
continue
167+
}
168+
169+
if r.Primary == nil {
170+
continue
171+
}
172+
173+
is := r.Primary.DeepCopy()
174+
is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type
175+
states = append(states, is)
176+
}
177+
178+
logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateCheck")
179+
180+
if err := step.ImportStateCheck(states); err != nil {
181+
t.Fatal(err)
182+
}
183+
184+
logging.HelperResourceDebug(ctx, "Called TestStep ImportStateCheck")
185+
}
186+
187+
// Verify that all the states match
188+
if step.ImportStateVerify {
189+
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateVerify")
190+
191+
// Ensure that we do not match against data sources as they
192+
// cannot be imported and are not what we want to verify.
193+
// Mode is not present in ResourceState so we use the
194+
// stringified ResourceStateKey for comparison.
195+
newResources := make(map[string]*terraform.ResourceState)
196+
for k, v := range importState.RootModule().Resources {
197+
if !strings.HasPrefix(k, "data.") {
198+
newResources[k] = v
199+
}
200+
}
201+
oldResources := make(map[string]*terraform.ResourceState)
202+
for k, v := range state.RootModule().Resources {
203+
if !strings.HasPrefix(k, "data.") {
204+
oldResources[k] = v
205+
}
206+
}
207+
208+
for _, r := range newResources {
209+
// Find the existing resource
210+
var oldR *terraform.ResourceState
211+
for _, r2 := range oldResources {
212+
213+
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type && r2.Provider == r.Provider {
214+
oldR = r2
215+
break
216+
}
217+
}
218+
if oldR == nil || oldR.Primary == nil {
219+
t.Fatalf(
220+
"Failed state verification, resource with ID %s not found",
221+
r.Primary.ID)
222+
}
223+
224+
// don't add empty flatmapped containers, so we can more easily
225+
// compare the attributes
226+
skipEmpty := func(k, v string) bool {
227+
if strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%") {
228+
if v == "0" {
229+
return true
230+
}
231+
}
232+
return false
233+
}
234+
235+
// Compare their attributes
236+
actual := make(map[string]string)
237+
for k, v := range r.Primary.Attributes {
238+
if skipEmpty(k, v) {
239+
continue
240+
}
241+
actual[k] = v
242+
}
243+
244+
expected := make(map[string]string)
245+
for k, v := range oldR.Primary.Attributes {
246+
if skipEmpty(k, v) {
247+
continue
248+
}
249+
expected[k] = v
250+
}
251+
252+
// Remove fields we're ignoring
253+
for _, v := range step.ImportStateVerifyIgnore {
254+
for k := range actual {
255+
if strings.HasPrefix(k, v) {
256+
delete(actual, k)
257+
}
258+
}
259+
for k := range expected {
260+
if strings.HasPrefix(k, v) {
261+
delete(expected, k)
262+
}
263+
}
264+
}
265+
266+
// timeouts are only _sometimes_ added to state. To
267+
// account for this, just don't compare timeouts at
268+
// all.
269+
for k := range actual {
270+
if strings.HasPrefix(k, "timeouts.") {
271+
delete(actual, k)
272+
}
273+
if k == "timeouts" {
274+
delete(actual, k)
275+
}
276+
}
277+
for k := range expected {
278+
if strings.HasPrefix(k, "timeouts.") {
279+
delete(expected, k)
280+
}
281+
if k == "timeouts" {
282+
delete(expected, k)
283+
}
284+
}
285+
286+
if !reflect.DeepEqual(actual, expected) {
287+
// Determine only the different attributes
288+
// go-cmp tries to show surrounding identical map key/value for
289+
// context of differences, which may be confusing.
290+
for k, v := range expected {
291+
if av, ok := actual[k]; ok && v == av {
292+
delete(expected, k)
293+
delete(actual, k)
294+
}
295+
}
296+
297+
if diff := cmp.Diff(expected, actual); diff != "" {
298+
return fmt.Errorf("ImportStateVerify attributes not equivalent. Difference is shown below. The - symbol indicates attributes missing after import.\n\n%s", diff)
299+
}
300+
}
301+
}
302+
}
303+
304+
return nil
305+
}

0 commit comments

Comments
 (0)