Skip to content

Commit bfd5be7

Browse files
authored
helper/resource: plannable import re-work to support resources with dependencies (#476)
* Error on use of ImportStatePersist with plannable import * Error on use of ImportStateVerify with plannable import * Rename test functions for consistency and for running by pattern * Import block works for a resource with a dependency * Simplify no-op action check * Add prerelease changelog entries * Add a catch-up prerelease changelog entry * Refactor test provider functions * Fix 'No changes. Your infrastructure matches the configuration.' false negatives * Log (or do not log) consistently * Fix format string syntax
1 parent 8122107 commit bfd5be7

14 files changed

+358
-110
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BREAKING CHANGES
2+
body: 'importstate: `ImportStatePersist` and `ImportStateVerify` are not supported for plannable import (`ImportBlockWith*`) and will return an error'
3+
time: 2025-04-04T17:06:56.900935-04:00
4+
custom:
5+
Issue: "476"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BREAKING CHANGES
2+
body: 'importstate: renamed `ImportStateWithId` to `ImportStateWithID` and renamed `ImportCommandWithId` to `ImportCommandWithID`.'
3+
time: 2025-04-04T17:10:48.525611-04:00
4+
custom:
5+
Issue: "465"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: 'importstate: plannable import (`ImportBlockWith*`) fixed for a resource with a dependency'
3+
time: 2025-04-04T17:07:34.428542-04:00
4+
custom:
5+
Issue: "476"

helper/resource/importstate/examplecloud_test.go

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package importstate_test
55

66
import (
7+
"context"
8+
79
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
810
"github.com/hashicorp/terraform-plugin-go/tftypes"
911
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
@@ -29,11 +31,7 @@ func examplecloudDataSource() testprovider.DataSource {
2931
Schema: &tfprotov6.Schema{
3032
Block: &tfprotov6.SchemaBlock{
3133
Attributes: []*tfprotov6.SchemaAttribute{
32-
{
33-
Name: "id",
34-
Type: tftypes.String,
35-
Computed: true,
36-
},
34+
ComputedStringAttribute("id"),
3735
},
3836
},
3937
},
@@ -95,21 +93,95 @@ func examplecloudResource() testprovider.Resource {
9593
Schema: &tfprotov6.Schema{
9694
Block: &tfprotov6.SchemaBlock{
9795
Attributes: []*tfprotov6.SchemaAttribute{
98-
{
99-
Name: "id",
100-
Type: tftypes.String,
101-
Computed: true,
102-
},
103-
{
104-
Name: "location",
105-
Type: tftypes.String,
106-
Required: true,
107-
},
108-
{
109-
Name: "name",
110-
Type: tftypes.String,
111-
Required: true,
112-
},
96+
ComputedStringAttribute("id"),
97+
RequiredStringAttribute("location"),
98+
RequiredStringAttribute("name"),
99+
},
100+
},
101+
},
102+
},
103+
}
104+
}
105+
106+
// examplecloudZone is a test resource that mimics a DNS zone resource.
107+
func examplecloudZone() testprovider.Resource {
108+
value := tftypes.NewValue(
109+
tftypes.Object{
110+
AttributeTypes: map[string]tftypes.Type{
111+
"id": tftypes.String,
112+
"name": tftypes.String,
113+
},
114+
},
115+
map[string]tftypes.Value{
116+
"id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
117+
"name": tftypes.NewValue(tftypes.String, "example.net"),
118+
},
119+
)
120+
121+
return testprovider.Resource{
122+
CreateResponse: &resource.CreateResponse{
123+
NewState: value,
124+
},
125+
ReadResponse: &resource.ReadResponse{
126+
NewState: value,
127+
},
128+
ImportStateResponse: &resource.ImportStateResponse{
129+
State: value,
130+
},
131+
SchemaResponse: &resource.SchemaResponse{
132+
Schema: &tfprotov6.Schema{
133+
Block: &tfprotov6.SchemaBlock{
134+
Attributes: []*tfprotov6.SchemaAttribute{
135+
ComputedStringAttribute("id"),
136+
RequiredStringAttribute("name"),
137+
},
138+
},
139+
},
140+
},
141+
}
142+
}
143+
144+
// examplecloudZoneRecord is a test resource that mimics a DNS zone record resource.
145+
// It models a resource dependency; specifically, it depends on a DNS zone ID and will
146+
// plan a replacement if the zone ID changes.
147+
func examplecloudZoneRecord() testprovider.Resource {
148+
value := tftypes.NewValue(
149+
tftypes.Object{
150+
AttributeTypes: map[string]tftypes.Type{
151+
"id": tftypes.String,
152+
"zone_id": tftypes.String,
153+
"name": tftypes.String,
154+
},
155+
},
156+
map[string]tftypes.Value{
157+
"id": tftypes.NewValue(tftypes.String, "f00911be-e188-433d-9ccd-d0393a9f5d05"),
158+
"zone_id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
159+
"name": tftypes.NewValue(tftypes.String, "www"),
160+
},
161+
)
162+
163+
return testprovider.Resource{
164+
CreateResponse: &resource.CreateResponse{
165+
NewState: value,
166+
},
167+
PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) {
168+
resp.RequiresReplace = []*tftypes.AttributePath{
169+
tftypes.NewAttributePath().WithAttributeName("zone_id"),
170+
}
171+
},
172+
ReadResponse: &resource.ReadResponse{
173+
NewState: value,
174+
},
175+
ImportStateResponse: &resource.ImportStateResponse{
176+
State: value,
177+
},
178+
SchemaResponse: &resource.SchemaResponse{
179+
Schema: &tfprotov6.Schema{
180+
Block: &tfprotov6.SchemaBlock{
181+
Attributes: []*tfprotov6.SchemaAttribute{
182+
ComputedStringAttribute("id"),
183+
RequiredStringAttribute("zone_id"),
184+
RequiredStringAttribute("name"),
113185
},
114186
},
115187
},

helper/resource/importstate/import_block_as_first_step_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
1919
)
2020

21-
func Test_ImportBlock_AsFirstStep(t *testing.T) {
21+
func TestImportBlock_AsFirstStep(t *testing.T) {
2222
t.Parallel()
2323

2424
r.UnitTest(t, r.TestCase{
@@ -38,12 +38,10 @@ func Test_ImportBlock_AsFirstStep(t *testing.T) {
3838
ImportStateId: "examplecloud_container.test",
3939
ImportState: true,
4040
ImportStateKind: r.ImportBlockWithID,
41-
// ImportStateVerify: true,
4241
Config: `resource "examplecloud_container" "test" {
4342
name = "somevalue"
4443
location = "westeurope"
4544
}`,
46-
ImportStatePersist: true,
4745
ImportPlanChecks: r.ImportPlanChecks{
4846
PreApply: []plancheck.PlanCheck{
4947
plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionNoop),
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
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+
16+
func TestImportBlockForResourceWithADependency(t *testing.T) {
17+
t.Parallel()
18+
19+
config := `
20+
resource "examplecloud_zone" "zone" {
21+
name = "example.net"
22+
}
23+
24+
resource "examplecloud_zone_record" "record" {
25+
zone_id = examplecloud_zone.zone.id
26+
name = "www"
27+
}
28+
`
29+
r.UnitTest(t, r.TestCase{
30+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
31+
tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later
32+
},
33+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
34+
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
35+
Resources: map[string]testprovider.Resource{
36+
"examplecloud_zone": examplecloudZone(),
37+
"examplecloud_zone_record": examplecloudZoneRecord(),
38+
},
39+
}),
40+
},
41+
Steps: []r.TestStep{
42+
{
43+
Config: config,
44+
},
45+
{
46+
ImportState: true,
47+
ImportStateKind: r.ImportBlockWithID,
48+
ResourceName: "examplecloud_zone_record.record",
49+
},
50+
},
51+
})
52+
}

helper/resource/importstate/import_block_in_config_directory_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
1717
)
1818

19-
func Test_ImportBlock_InConfigDirectory(t *testing.T) {
19+
func TestImportBlock_InConfigDirectory(t *testing.T) {
2020
t.Parallel()
2121

2222
r.UnitTest(t, r.TestCase{
@@ -35,11 +35,10 @@ func Test_ImportBlock_InConfigDirectory(t *testing.T) {
3535
ConfigDirectory: config.StaticDirectory(`testdata/1`),
3636
},
3737
{
38-
ResourceName: "examplecloud_container.test",
39-
ImportState: true,
40-
ImportStateKind: r.ImportBlockWithID,
41-
ImportStateVerify: true,
42-
ConfigDirectory: config.StaticDirectory(`testdata/2`),
38+
ResourceName: "examplecloud_container.test",
39+
ImportState: true,
40+
ImportStateKind: r.ImportBlockWithID,
41+
ConfigDirectory: config.StaticDirectory(`testdata/2`),
4342
},
4443
},
4544
})

helper/resource/importstate/import_block_in_config_file_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
1717
)
1818

19-
func Test_ImportBlock_InConfigFile(t *testing.T) {
19+
func TestImportBlock_InConfigFile(t *testing.T) {
2020
t.Parallel()
2121

2222
r.UnitTest(t, r.TestCase{
@@ -35,11 +35,10 @@ func Test_ImportBlock_InConfigFile(t *testing.T) {
3535
ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`),
3636
},
3737
{
38-
ResourceName: "examplecloud_container.test",
39-
ImportState: true,
40-
ImportStateKind: r.ImportBlockWithID,
41-
ImportStateVerify: true,
42-
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container_import.tf`),
38+
ResourceName: "examplecloud_container.test",
39+
ImportState: true,
40+
ImportStateKind: r.ImportBlockWithID,
41+
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container_import.tf`),
4342
},
4443
},
4544
})

helper/resource/importstate/import_block_with_id_test.go

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
2222
)
2323

24-
func Test_TestStep_ImportBlockId(t *testing.T) {
24+
func TestImportBlock_WithID(t *testing.T) {
2525
t.Parallel()
2626

2727
r.UnitTest(t, r.TestCase{
@@ -44,16 +44,15 @@ func Test_TestStep_ImportBlockId(t *testing.T) {
4444
}`,
4545
},
4646
{
47-
ResourceName: "examplecloud_container.test",
48-
ImportState: true,
49-
ImportStateKind: r.ImportBlockWithID,
50-
ImportStateVerify: true,
47+
ResourceName: "examplecloud_container.test",
48+
ImportState: true,
49+
ImportStateKind: r.ImportBlockWithID,
5150
},
5251
},
5352
})
5453
}
5554

56-
func TestTest_TestStep_ImportBlockId_ExpectError(t *testing.T) {
55+
func TestImportBlock_WithID_ExpectError(t *testing.T) {
5756
t.Parallel()
5857

5958
r.UnitTest(t, r.TestCase{
@@ -81,17 +80,16 @@ func TestTest_TestStep_ImportBlockId_ExpectError(t *testing.T) {
8180
location = "eastus"
8281
name = "somevalue"
8382
}`,
84-
ResourceName: "examplecloud_container.test",
85-
ImportState: true,
86-
ImportStateKind: r.ImportBlockWithID,
87-
ImportStateVerify: true,
88-
ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op resource action, got "update" action with plan(.?)`),
83+
ResourceName: "examplecloud_container.test",
84+
ImportState: true,
85+
ImportStateKind: r.ImportBlockWithID,
86+
ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op import operation, got.*\["update"\] action with plan(.?)`),
8987
},
9088
},
9189
})
9290
}
9391

94-
func TestTest_TestStep_ImportBlockId_FailWhenPlannableImportIsNotSupported(t *testing.T) {
92+
func TestImportBlock_WithID_FailWhenNotSupported(t *testing.T) {
9593
t.Parallel()
9694

9795
r.UnitTest(t, r.TestCase{
@@ -130,7 +128,7 @@ func TestTest_TestStep_ImportBlockId_FailWhenPlannableImportIsNotSupported(t *te
130128
})
131129
}
132130

133-
func TestTest_TestStep_ImportBlockId_SkipDataSourceState(t *testing.T) {
131+
func TestImportBlock_WithID_SkipsDataSources(t *testing.T) {
134132
t.Parallel()
135133

136134
r.UnitTest(t, r.TestCase{
@@ -174,9 +172,7 @@ func TestTest_TestStep_ImportBlockId_SkipDataSourceState(t *testing.T) {
174172
})
175173
}
176174

177-
// These tests currently pass but only because the `getState` function which is used on the imported resource
178-
// to do the state comparison doesn't return an error if there is no state in the working directory
179-
func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore_Real_Example(t *testing.T) {
175+
func TestImportBlock_WithID_WithBlankOptionalAttribute_GeneratesCorrectPlan(t *testing.T) {
180176
/*
181177
This test tries to imitate a real world example of behaviour we often see in the AzureRM provider which requires
182178
the use of `ImportStateVerifyIgnore` when testing the import of a resource using the import command.
@@ -290,17 +286,15 @@ func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore_Real_Example(t *tes
290286
resource "examplecloud_container" "test" {
291287
name = "somename"
292288
}`,
293-
ResourceName: "examplecloud_container.test",
294-
ImportState: true,
295-
ImportStateKind: r.ImportBlockWithID,
296-
ImportStateVerify: true,
297-
ImportStateVerifyIgnore: []string{"password"},
289+
ResourceName: "examplecloud_container.test",
290+
ImportState: true,
291+
ImportStateKind: r.ImportBlockWithID,
298292
},
299293
},
300294
})
301295
}
302296

303-
func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore(t *testing.T) {
297+
func TestImportBlock_WithID_WithBlankComputedAttribute_GeneratesCorrectPlan(t *testing.T) {
304298
t.Parallel()
305299

306300
r.UnitTest(t, r.TestCase{
@@ -375,11 +369,9 @@ func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore(t *testing.T) {
375369
Config: `resource "examplecloud_container" "test" {}`,
376370
},
377371
{
378-
ResourceName: "examplecloud_container.test",
379-
ImportState: true,
380-
ImportStateKind: r.ImportBlockWithID,
381-
ImportStateVerify: true,
382-
ImportStateVerifyIgnore: []string{"password"},
372+
ResourceName: "examplecloud_container.test",
373+
ImportState: true,
374+
ImportStateKind: r.ImportBlockWithID,
383375
},
384376
},
385377
})

helper/resource/importstate/import_command_as_first_step_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
1818
)
1919

20-
func Test_ImportCommand_AsFirstStep(t *testing.T) {
20+
func TestImportCommand_AsFirstStep(t *testing.T) {
2121
t.Parallel()
2222

2323
r.UnitTest(t, r.TestCase{

0 commit comments

Comments
 (0)