Skip to content

Commit c2a5b46

Browse files
committed
helper/resource: add ImportBlockWithResourceIdentity kind
1 parent 526853c commit c2a5b46

File tree

5 files changed

+242
-22
lines changed

5 files changed

+242
-22
lines changed

helper/resource/importstate/examplecloud_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
1212
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource"
1313
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource"
14+
"github.com/hashicorp/terraform-plugin-testing/internal/teststep"
1415
)
1516

1617
func examplecloudDataSource() testprovider.DataSource {
@@ -56,6 +57,16 @@ func examplecloudResource() testprovider.Resource {
5657
"name": tftypes.NewValue(tftypes.String, "somevalue"),
5758
},
5859
),
60+
NewIdentity: teststep.Pointer(tftypes.NewValue(
61+
tftypes.Object{
62+
AttributeTypes: map[string]tftypes.Type{
63+
"id": tftypes.String,
64+
},
65+
},
66+
map[string]tftypes.Value{
67+
"id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"),
68+
},
69+
)),
5970
},
6071
ReadResponse: &resource.ReadResponse{
6172
NewState: tftypes.NewValue(
@@ -72,6 +83,16 @@ func examplecloudResource() testprovider.Resource {
7283
"name": tftypes.NewValue(tftypes.String, "somevalue"),
7384
},
7485
),
86+
NewIdentity: teststep.Pointer(tftypes.NewValue(
87+
tftypes.Object{
88+
AttributeTypes: map[string]tftypes.Type{
89+
"id": tftypes.String,
90+
},
91+
},
92+
map[string]tftypes.Value{
93+
"id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"),
94+
},
95+
)),
7596
},
7697
ImportStateResponse: &resource.ImportStateResponse{
7798
State: tftypes.NewValue(
@@ -88,6 +109,16 @@ func examplecloudResource() testprovider.Resource {
88109
"name": tftypes.NewValue(tftypes.String, "somevalue"),
89110
},
90111
),
112+
Identity: teststep.Pointer(tftypes.NewValue(
113+
tftypes.Object{
114+
AttributeTypes: map[string]tftypes.Type{
115+
"id": tftypes.String,
116+
},
117+
},
118+
map[string]tftypes.Value{
119+
"id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"),
120+
},
121+
)),
91122
},
92123
SchemaResponse: &resource.SchemaResponse{
93124
Schema: &tfprotov6.Schema{
@@ -100,6 +131,18 @@ func examplecloudResource() testprovider.Resource {
100131
},
101132
},
102133
},
134+
IdentitySchemaResponse: &resource.IdentitySchemaResponse{
135+
Schema: &tfprotov6.ResourceIdentitySchema{
136+
Version: 1,
137+
IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{
138+
{
139+
Name: "id",
140+
Type: tftypes.String,
141+
RequiredForImport: true,
142+
},
143+
},
144+
},
145+
},
103146
}
104147
}
105148

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package importstate_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
11+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
12+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver"
13+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
14+
15+
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
16+
)
17+
18+
func TestImportBlock_WithResourceIdentity(t *testing.T) {
19+
t.Parallel()
20+
21+
r.UnitTest(t, r.TestCase{
22+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
23+
tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later
24+
},
25+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
26+
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
27+
Resources: map[string]testprovider.Resource{
28+
"examplecloud_container": examplecloudResource(),
29+
},
30+
}),
31+
},
32+
Steps: []r.TestStep{
33+
{
34+
Config: `
35+
resource "examplecloud_container" "test" {
36+
location = "westeurope"
37+
name = "somevalue"
38+
}`,
39+
},
40+
{
41+
RefreshState: true,
42+
},
43+
{
44+
ResourceName: "examplecloud_container.test",
45+
ImportState: true,
46+
ImportStateKind: r.ImportBlockWithResourceIdentity,
47+
},
48+
},
49+
})
50+
}

helper/resource/plugin.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/hashicorp/go-hclog"
1515
"github.com/hashicorp/terraform-exec/tfexec"
16+
tfjson "github.com/hashicorp/terraform-json"
1617
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
1718
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
1819
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -113,6 +114,58 @@ type providerFactories struct {
113114
protov6 protov6ProviderFactories
114115
}
115116

117+
func runProviderCommandApply(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error {
118+
t.Helper()
119+
120+
fn := func() error {
121+
return wd.Apply(ctx)
122+
}
123+
return runProviderCommand(ctx, t, fn, wd, factories)
124+
}
125+
126+
func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error {
127+
t.Helper()
128+
129+
fn := func() error {
130+
return wd.CreatePlan(ctx)
131+
}
132+
return runProviderCommand(ctx, t, fn, wd, factories)
133+
}
134+
135+
func runProviderCommandGetStateJSON(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.State, error) {
136+
t.Helper()
137+
138+
var stateJSON *tfjson.State
139+
fn := func() error {
140+
var err error
141+
stateJSON, err = wd.State(ctx)
142+
return err
143+
}
144+
err := runProviderCommand(ctx, t, fn, wd, factories)
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
return stateJSON, nil
150+
}
151+
152+
func runProviderCommandSavedPlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.Plan, error) {
153+
t.Helper()
154+
155+
var plan *tfjson.Plan
156+
fn := func() error {
157+
var err error
158+
plan, err = wd.SavedPlan(ctx)
159+
return err
160+
}
161+
err := runProviderCommand(ctx, t, fn, wd, factories)
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
return plan, nil
167+
}
168+
116169
func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error {
117170
// don't point to this as a test failure location
118171
// point to whatever called it

helper/resource/testing.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,10 +467,16 @@ const (
467467
ImportBlockWithResourceIdentity
468468
)
469469

470+
// plannable reports whether this kind indicates the use of plannable import blocks
470471
func (kind ImportStateKind) plannable() bool {
471472
return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity
472473
}
473474

475+
// resourceIdentity reports whether this this kind indicates the use of resource identity in import blocks
476+
func (kind ImportStateKind) resourceIdentity() bool {
477+
return kind == ImportBlockWithResourceIdentity
478+
}
479+
474480
func (kind ImportStateKind) String() string {
475481
return map[ImportStateKind]string{
476482
ImportCommandWithID: "ImportCommandWithID",

helper/resource/testing_new_import_state.go

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/google/go-cmp/cmp"
1515
"github.com/mitchellh/go-testing-interface"
1616

17-
"github.com/hashicorp/terraform-exec/tfexec"
1817
"github.com/hashicorp/terraform-plugin-testing/config"
1918
"github.com/hashicorp/terraform-plugin-testing/internal/logging"
2019
"github.com/hashicorp/terraform-plugin-testing/internal/plugintest"
@@ -67,9 +66,6 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
6766
t.Fatalf("Error getting state: %s", err)
6867
}
6968

70-
// TODO: this statement is a placeholder -- it simply prevents stateJSON from being unused
71-
logging.HelperResourceTrace(ctx, fmt.Sprintf("State before import: values %v", stateJSON.Values != nil))
72-
7369
// Determine the ID to import
7470
var importId string
7571
switch {
@@ -109,6 +105,8 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
109105

110106
logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId))
111107

108+
var priorIdentityValues map[string]any
109+
112110
// Append to previous step config unless using ConfigFile or ConfigDirectory
113111
if testStepConfig == nil || step.Config != "" {
114112
importConfig := step.Config
@@ -117,7 +115,13 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
117115
importConfig = cfgRaw
118116
}
119117

120-
if kind.plannable() {
118+
if kind.plannable() && kind.resourceIdentity() {
119+
priorIdentityValues = identityValuesFromState(stateJSON, resourceName)
120+
if len(priorIdentityValues) == 0 {
121+
return fmt.Errorf("importing resource %s: expected prior state to have resource identity values, got none", resourceName)
122+
}
123+
importConfig = appendImportBlockWithIdentity(importConfig, resourceName, priorIdentityValues)
124+
} else if kind.plannable() {
121125
importConfig = appendImportBlock(importConfig, resourceName, importId)
122126
}
123127

@@ -179,22 +183,16 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
179183

180184
var plan *tfjson.Plan
181185
if kind.plannable() {
182-
var opts []tfexec.PlanOption
186+
// TODO: extract to a function -- this is a long `if` :)
183187

184-
err = runProviderCommand(ctx, t, func() error {
185-
return importWd.CreatePlan(ctx, opts...)
186-
}, importWd, providers)
188+
err := runProviderCommandCreatePlan(ctx, t, importWd, providers)
187189
if err != nil {
188-
return err
190+
return fmt.Errorf("generating plan with import config: %s", err)
189191
}
190192

191-
err = runProviderCommand(ctx, t, func() error {
192-
var err error
193-
plan, err = importWd.SavedPlan(ctx)
194-
return err
195-
}, importWd, providers)
193+
plan, err = runProviderCommandSavedPlan(ctx, t, importWd, providers)
196194
if err != nil {
197-
return err
195+
return fmt.Errorf("reading generated plan with import config: %s", err)
198196
}
199197

200198
logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges)))
@@ -231,16 +229,40 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
231229
if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil {
232230
return err
233231
}
234-
} else {
235-
err = runProviderCommand(ctx, t, func() error {
236-
return importWd.Import(ctx, resourceName, importId)
237-
}, importWd, providers)
238-
if err != nil {
239-
return err
232+
233+
{
234+
if kind.resourceIdentity() {
235+
err := runProviderCommandApply(ctx, t, wd, providers)
236+
if err != nil {
237+
return fmt.Errorf("applying plan with import config: %s", err)
238+
}
239+
240+
newStateJSON, err := runProviderCommandGetStateJSON(ctx, t, wd, providers)
241+
if err != nil {
242+
return fmt.Errorf("getting state after applying plan with import config: %s", err)
243+
}
244+
245+
newIdentityValues := identityValuesFromState(newStateJSON, resourceName)
246+
if !cmp.Equal(priorIdentityValues, newIdentityValues) {
247+
return fmt.Errorf("importing resource %s: expected identity values %v, got %v", resourceName, priorIdentityValues, newIdentityValues)
248+
}
249+
}
240250
}
251+
252+
return nil
241253
}
242254

255+
// TODO: extract to a function -- this is an implicit `else` for the long `if` above :)
256+
243257
var importState *terraform.State
258+
259+
err = runProviderCommand(ctx, t, func() error {
260+
return importWd.Import(ctx, resourceName, importId)
261+
}, importWd, providers)
262+
if err != nil {
263+
return err
264+
}
265+
244266
err = runProviderCommand(ctx, t, func() error {
245267
_, importState, err = getState(ctx, t, importWd)
246268
if err != nil {
@@ -410,6 +432,25 @@ func appendImportBlock(config string, resourceName string, importID string) stri
410432
resourceName, importID)
411433
}
412434

435+
func appendImportBlockWithIdentity(config string, resourceName string, identityValues map[string]any) string {
436+
configBuilder := config
437+
configBuilder += fmt.Sprintf(``+"\n"+
438+
`import {`+"\n"+
439+
` to = %s`+"\n"+
440+
` identity = {`+"\n",
441+
resourceName)
442+
443+
for k, v := range identityValues {
444+
configBuilder += fmt.Sprintf(` %q = %q`+"\n", k, v)
445+
}
446+
447+
configBuilder += `` +
448+
` }` + "\n" +
449+
`}` + "\n"
450+
451+
return configBuilder
452+
}
453+
413454
func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error {
414455
t.Helper()
415456

@@ -437,6 +478,33 @@ func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestS
437478
return nil
438479
}
439480

481+
func resourcesFromState(state *tfjson.State) []*tfjson.StateResource {
482+
stateValues := state.Values
483+
if stateValues == nil || stateValues.RootModule == nil {
484+
return []*tfjson.StateResource{}
485+
}
486+
487+
return stateValues.RootModule.Resources
488+
}
489+
490+
func identityValuesFromState(state *tfjson.State, resourceName string) map[string]any {
491+
var resource *tfjson.StateResource
492+
resources := resourcesFromState(state)
493+
494+
for _, r := range resources {
495+
if r.Address == resourceName {
496+
resource = r
497+
break
498+
}
499+
}
500+
501+
if resource == nil || len(resource.IdentityValues) == 0 {
502+
return map[string]any{}
503+
}
504+
505+
return resource.IdentityValues
506+
}
507+
440508
func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) {
441509
t.Helper()
442510

0 commit comments

Comments
 (0)