Skip to content

Commit b986ce0

Browse files
committed
helper/resource: add ImportBlockWithResourceIdentity kind
1 parent 576fa95 commit b986ce0

File tree

7 files changed

+238
-30
lines changed

7 files changed

+238
-30
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: 59 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,64 @@ type providerFactories struct {
113114
protov6 protov6ProviderFactories
114115
}
115116

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

helper/resource/testing.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ func (kind ImportStateKind) plannable() bool {
471471
return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity
472472
}
473473

474+
func (kind ImportStateKind) resourceIdentity() bool {
475+
return kind == ImportBlockWithResourceIdentity
476+
}
477+
474478
func (kind ImportStateKind) String() string {
475479
return map[ImportStateKind]string{
476480
ImportCommandWithID: "ImportCommandWithID",

helper/resource/testing_new_import_state.go

Lines changed: 79 additions & 29 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,10 @@ 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+
importConfig = appendImportBlockWithIdentity(importConfig, resourceName, priorIdentityValues)
121+
} else if kind.plannable() {
121122
importConfig = appendImportBlock(importConfig, resourceName, importId)
122123
}
123124

@@ -179,23 +180,8 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
179180

180181
var plan *tfjson.Plan
181182
if kind.plannable() {
182-
var opts []tfexec.PlanOption
183-
184-
err = runProviderCommand(ctx, t, func() error {
185-
return importWd.CreatePlan(ctx, opts...)
186-
}, importWd, providers)
187-
if err != nil {
188-
return err
189-
}
190-
191-
err = runProviderCommand(ctx, t, func() error {
192-
var err error
193-
plan, err = importWd.SavedPlan(ctx)
194-
return err
195-
}, importWd, providers)
196-
if err != nil {
197-
return err
198-
}
183+
runProviderCommandCreatePlan(ctx, t, importWd, providers)
184+
plan = runProviderCommandSavedPlan(ctx, t, importWd, providers)
199185

200186
logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges)))
201187

@@ -224,23 +210,40 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
224210
case importing == nil:
225211
return fmt.Errorf("importing resource %s: expected an import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, importWd, providers))
226212

227-
case !actions.NoOp(): // TODO: is this reachable? possible to have anything other than no-op when importing != nil ?
213+
case !actions.NoOp():
228214
return fmt.Errorf("importing resource %s: expected a no-op import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, importWd, providers))
229215
}
230216

231217
if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil {
232218
return err
233219
}
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
220+
221+
{
222+
if kind.resourceIdentity() {
223+
runProviderCommandApply(ctx, t, wd, providers)
224+
newStateJSON := runProviderCommandGetStateJSON(ctx, t, wd, providers)
225+
226+
newIdentityValues := identityValuesFromState(newStateJSON, resourceName)
227+
if !cmp.Equal(priorIdentityValues, newIdentityValues) {
228+
return fmt.Errorf("importing resource %s: expected identity values %v, got %v", resourceName, priorIdentityValues, newIdentityValues)
229+
}
230+
}
240231
}
232+
233+
return nil
241234
}
242235

236+
// !kind.plannable()
237+
243238
var importState *terraform.State
239+
240+
err = runProviderCommand(ctx, t, func() error {
241+
return importWd.Import(ctx, resourceName, importId)
242+
}, importWd, providers)
243+
if err != nil {
244+
return err
245+
}
246+
244247
err = runProviderCommand(ctx, t, func() error {
245248
_, importState, err = getState(ctx, t, importWd)
246249
if err != nil {
@@ -410,6 +413,26 @@ func appendImportBlock(config string, resourceName string, importID string) stri
410413
resourceName, importID)
411414
}
412415

416+
func appendImportBlockWithIdentity(config string, resourceName string, identityValues map[string]any) string {
417+
configBuilder := config
418+
configBuilder += fmt.Sprintf(``+"\n"+
419+
`import {`+"\n"+
420+
` to = %s`+"\n"+
421+
` identity = {`+"\n",
422+
resourceName)
423+
424+
// ` id = %q`+"\n"+
425+
for k, v := range identityValues {
426+
configBuilder += fmt.Sprintf(` %q = %q`+"\n", k, v)
427+
}
428+
429+
configBuilder +=
430+
` }` + "\n" +
431+
`}` + "\n"
432+
433+
return configBuilder
434+
}
435+
413436
func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error {
414437
t.Helper()
415438

@@ -437,6 +460,33 @@ func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestS
437460
return nil
438461
}
439462

463+
func resourcesFromState(state *tfjson.State) []*tfjson.StateResource {
464+
stateValues := state.Values
465+
if stateValues == nil || stateValues.RootModule == nil {
466+
return []*tfjson.StateResource{}
467+
}
468+
469+
return stateValues.RootModule.Resources
470+
}
471+
472+
func identityValuesFromState(state *tfjson.State, resourceName string) map[string]any {
473+
var resourceUnderTest *tfjson.StateResource
474+
resources := resourcesFromState(state)
475+
476+
for _, resource := range resources {
477+
if resource.Address == resourceName {
478+
resourceUnderTest = resource
479+
break
480+
}
481+
}
482+
483+
if resourceUnderTest == nil || len(resourceUnderTest.IdentityValues) == 0 {
484+
return map[string]any{}
485+
}
486+
487+
return resourceUnderTest.IdentityValues
488+
}
489+
440490
func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) {
441491
t.Helper()
442492

internal/testing/testsdk/providerserver/providerserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ func (s ProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.
454454
return resp, nil
455455
}
456456

457-
// There is only one imported resource, so this should always be safe
457+
// There is only one imported resource, so this should always be safe // to confirm
458458
resp.ImportedResources[0].Identity = &tfprotov6.ResourceIdentityData{
459459
IdentityData: identity,
460460
}

0 commit comments

Comments
 (0)