Skip to content

Commit e488dee

Browse files
bbasataaustinvalle
andauthored
helper/resource: add ImportBlockWithResourceIdentity kind (#480)
* 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 * helper/resource: add ImportBlockWithResourceIdentity kind * ImportBlockWithResourceIdentity requires Terraform 1.12.0+ * ImportBlockWithResourceIdentity requires Terraform 1.12.0+ * ImportBlockWithResourceIdentity requires Terraform 1.12.0+ * Add changelog entry * Update .changes/unreleased/NOTES-20250414-095537.yaml Co-authored-by: Austin Valle <[email protected]> * Update helper/resource/testing.go Co-authored-by: Austin Valle <[email protected]> * Nicer version check * Remove incidental test steps * Add type awareness to import block generating * fixup! Add type awareness to import block generating staticcheck * Add more list types to example resource identity schema * Update ImportState tests that have inline config * fixup! Update ImportState tests that have inline config * Use the correct working directory for apply + show * Fix identity values comparison when using ConfigFile * fixup! Fix identity values comparison when using ConfigFile * fixup! Fix identity values comparison when using ConfigFile * Update .changes/unreleased/NOTES-20250414-095537.yaml Co-authored-by: Austin Valle <[email protected]> --------- Co-authored-by: Austin Valle <[email protected]>
1 parent 2fd4c09 commit e488dee

11 files changed

+711
-52
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: NOTES
2+
body: 'This beta pre-release adds support for managed resource identity, which can be used with Terraform v1.12.0-beta2. Acceptance tests can use the `ImportBlockWithResourceIdentity` kind to exercise the import of a managed resource using its resource identity object values instead of using a string identifier.'
3+
time: 2025-04-14T09:55:37.938453-04:00
4+
custom:
5+
Issue: "480"

helper/resource/importstate/examplecloud_test.go

Lines changed: 291 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

@@ -188,3 +231,251 @@ func examplecloudZoneRecord() testprovider.Resource {
188231
},
189232
}
190233
}
234+
235+
func examplecloudResourceWithEveryIdentitySchemaType() testprovider.Resource {
236+
return testprovider.Resource{
237+
CreateResponse: &resource.CreateResponse{
238+
NewState: tftypes.NewValue(
239+
tftypes.Object{
240+
AttributeTypes: map[string]tftypes.Type{
241+
"hostname": tftypes.String,
242+
"cabinet": tftypes.String,
243+
"unit": tftypes.Number,
244+
"active": tftypes.Bool,
245+
"tags": tftypes.List{ElementType: tftypes.String},
246+
"magic_numbers": tftypes.List{ElementType: tftypes.Number},
247+
"beep_boop": tftypes.List{ElementType: tftypes.Bool},
248+
},
249+
},
250+
map[string]tftypes.Value{
251+
"hostname": tftypes.NewValue(tftypes.String, "mail.example.net"),
252+
"cabinet": tftypes.NewValue(tftypes.String, "A1"),
253+
"unit": tftypes.NewValue(tftypes.Number, 14),
254+
"active": tftypes.NewValue(tftypes.Bool, true),
255+
"tags": tftypes.NewValue(
256+
tftypes.List{ElementType: tftypes.String}, []tftypes.Value{
257+
tftypes.NewValue(tftypes.String, "storage"),
258+
tftypes.NewValue(tftypes.String, "fast")}),
259+
"magic_numbers": tftypes.NewValue(
260+
tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{
261+
tftypes.NewValue(tftypes.Number, 5),
262+
tftypes.NewValue(tftypes.Number, 2)}),
263+
"beep_boop": tftypes.NewValue(
264+
tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{
265+
tftypes.NewValue(tftypes.Bool, false),
266+
tftypes.NewValue(tftypes.Bool, true),
267+
}),
268+
},
269+
),
270+
NewIdentity: teststep.Pointer(tftypes.NewValue(
271+
tftypes.Object{
272+
AttributeTypes: map[string]tftypes.Type{
273+
"cabinet": tftypes.String,
274+
"unit": tftypes.Number,
275+
"active": tftypes.Bool,
276+
"tags": tftypes.List{ElementType: tftypes.String},
277+
"magic_numbers": tftypes.List{ElementType: tftypes.Number},
278+
"beep_boop": tftypes.List{ElementType: tftypes.Bool},
279+
},
280+
},
281+
map[string]tftypes.Value{
282+
"cabinet": tftypes.NewValue(tftypes.String, "A1"),
283+
"unit": tftypes.NewValue(tftypes.Number, 14),
284+
"active": tftypes.NewValue(tftypes.Bool, true),
285+
"tags": tftypes.NewValue(
286+
tftypes.List{ElementType: tftypes.String}, []tftypes.Value{
287+
tftypes.NewValue(tftypes.String, "storage"),
288+
tftypes.NewValue(tftypes.String, "fast"),
289+
}),
290+
"magic_numbers": tftypes.NewValue(
291+
tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{
292+
tftypes.NewValue(tftypes.Number, 5),
293+
tftypes.NewValue(tftypes.Number, 2)}),
294+
"beep_boop": tftypes.NewValue(
295+
tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{
296+
tftypes.NewValue(tftypes.Bool, false),
297+
tftypes.NewValue(tftypes.Bool, true),
298+
}),
299+
},
300+
)),
301+
},
302+
ReadResponse: &resource.ReadResponse{
303+
NewState: tftypes.NewValue(
304+
tftypes.Object{
305+
AttributeTypes: map[string]tftypes.Type{
306+
"hostname": tftypes.String,
307+
"cabinet": tftypes.String,
308+
"unit": tftypes.Number,
309+
"active": tftypes.Bool,
310+
"tags": tftypes.List{ElementType: tftypes.String},
311+
"magic_numbers": tftypes.List{ElementType: tftypes.Number},
312+
"beep_boop": tftypes.List{ElementType: tftypes.Bool},
313+
},
314+
},
315+
map[string]tftypes.Value{
316+
"hostname": tftypes.NewValue(tftypes.String, "mail.example.net"),
317+
"cabinet": tftypes.NewValue(tftypes.String, "A1"),
318+
"unit": tftypes.NewValue(tftypes.Number, 14),
319+
"active": tftypes.NewValue(tftypes.Bool, true),
320+
"tags": tftypes.NewValue(
321+
tftypes.List{ElementType: tftypes.String}, []tftypes.Value{
322+
tftypes.NewValue(tftypes.String, "storage"),
323+
tftypes.NewValue(tftypes.String, "fast")}),
324+
"magic_numbers": tftypes.NewValue(
325+
tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{
326+
tftypes.NewValue(tftypes.Number, 5),
327+
tftypes.NewValue(tftypes.Number, 2)}),
328+
"beep_boop": tftypes.NewValue(
329+
tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{
330+
tftypes.NewValue(tftypes.Bool, false),
331+
tftypes.NewValue(tftypes.Bool, true),
332+
}),
333+
},
334+
),
335+
NewIdentity: teststep.Pointer(tftypes.NewValue(
336+
tftypes.Object{
337+
AttributeTypes: map[string]tftypes.Type{
338+
"cabinet": tftypes.String,
339+
"unit": tftypes.Number,
340+
"active": tftypes.Bool,
341+
"tags": tftypes.List{ElementType: tftypes.String},
342+
"magic_numbers": tftypes.List{ElementType: tftypes.Number},
343+
"beep_boop": tftypes.List{ElementType: tftypes.Bool},
344+
},
345+
},
346+
map[string]tftypes.Value{
347+
"cabinet": tftypes.NewValue(tftypes.String, "A1"),
348+
"unit": tftypes.NewValue(tftypes.Number, 14),
349+
"active": tftypes.NewValue(tftypes.Bool, true),
350+
"tags": tftypes.NewValue(
351+
tftypes.List{ElementType: tftypes.String}, []tftypes.Value{
352+
tftypes.NewValue(tftypes.String, "storage"),
353+
tftypes.NewValue(tftypes.String, "fast")}),
354+
"magic_numbers": tftypes.NewValue(
355+
tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{
356+
tftypes.NewValue(tftypes.Number, 5),
357+
tftypes.NewValue(tftypes.Number, 2)}),
358+
"beep_boop": tftypes.NewValue(
359+
tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{
360+
tftypes.NewValue(tftypes.Bool, false),
361+
tftypes.NewValue(tftypes.Bool, true),
362+
}),
363+
},
364+
)),
365+
},
366+
ImportStateResponse: &resource.ImportStateResponse{
367+
State: tftypes.NewValue(
368+
tftypes.Object{
369+
AttributeTypes: map[string]tftypes.Type{
370+
"hostname": tftypes.String,
371+
"cabinet": tftypes.String,
372+
"unit": tftypes.Number,
373+
"active": tftypes.Bool,
374+
"tags": tftypes.List{ElementType: tftypes.String},
375+
"magic_numbers": tftypes.List{ElementType: tftypes.Number},
376+
"beep_boop": tftypes.List{ElementType: tftypes.Bool},
377+
},
378+
},
379+
map[string]tftypes.Value{
380+
"hostname": tftypes.NewValue(tftypes.String, "mail.example.net"),
381+
"cabinet": tftypes.NewValue(tftypes.String, "A1"),
382+
"unit": tftypes.NewValue(tftypes.Number, 14),
383+
"active": tftypes.NewValue(tftypes.Bool, true),
384+
"tags": tftypes.NewValue(
385+
tftypes.List{ElementType: tftypes.String}, []tftypes.Value{
386+
tftypes.NewValue(tftypes.String, "storage"),
387+
tftypes.NewValue(tftypes.String, "fast")}),
388+
"magic_numbers": tftypes.NewValue(
389+
tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{
390+
tftypes.NewValue(tftypes.Number, 5),
391+
tftypes.NewValue(tftypes.Number, 2)}),
392+
"beep_boop": tftypes.NewValue(
393+
tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{
394+
tftypes.NewValue(tftypes.Bool, false),
395+
tftypes.NewValue(tftypes.Bool, true),
396+
}),
397+
},
398+
),
399+
Identity: teststep.Pointer(tftypes.NewValue(
400+
tftypes.Object{
401+
AttributeTypes: map[string]tftypes.Type{
402+
"cabinet": tftypes.String,
403+
"unit": tftypes.Number,
404+
"active": tftypes.Bool,
405+
"tags": tftypes.List{ElementType: tftypes.String},
406+
"magic_numbers": tftypes.List{ElementType: tftypes.Number},
407+
"beep_boop": tftypes.List{ElementType: tftypes.Bool},
408+
},
409+
},
410+
map[string]tftypes.Value{
411+
"cabinet": tftypes.NewValue(tftypes.String, "A1"),
412+
"unit": tftypes.NewValue(tftypes.Number, 14),
413+
"active": tftypes.NewValue(tftypes.Bool, true),
414+
"tags": tftypes.NewValue(
415+
tftypes.List{ElementType: tftypes.String}, []tftypes.Value{
416+
tftypes.NewValue(tftypes.String, "storage"),
417+
tftypes.NewValue(tftypes.String, "fast")}),
418+
"magic_numbers": tftypes.NewValue(
419+
tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{
420+
tftypes.NewValue(tftypes.Number, 5),
421+
tftypes.NewValue(tftypes.Number, 2)}),
422+
"beep_boop": tftypes.NewValue(
423+
tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{
424+
tftypes.NewValue(tftypes.Bool, false),
425+
tftypes.NewValue(tftypes.Bool, true),
426+
}),
427+
},
428+
)),
429+
},
430+
SchemaResponse: &resource.SchemaResponse{
431+
Schema: &tfprotov6.Schema{
432+
Block: &tfprotov6.SchemaBlock{
433+
Attributes: []*tfprotov6.SchemaAttribute{
434+
ComputedStringAttribute("hostname"),
435+
RequiredStringAttribute("cabinet"),
436+
RequiredNumberAttribute("unit"),
437+
RequiredBoolAttribute("active"),
438+
RequiredListAttribute("tags", tftypes.String),
439+
OptionalComputedListAttribute("magic_numbers", tftypes.Number),
440+
},
441+
},
442+
},
443+
},
444+
IdentitySchemaResponse: &resource.IdentitySchemaResponse{
445+
Schema: &tfprotov6.ResourceIdentitySchema{
446+
Version: 1,
447+
IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{
448+
{
449+
Name: "cabinet",
450+
Type: tftypes.String,
451+
RequiredForImport: true,
452+
},
453+
{
454+
Name: "unit",
455+
Type: tftypes.Number,
456+
OptionalForImport: true,
457+
},
458+
{
459+
Name: "active",
460+
Type: tftypes.Bool,
461+
OptionalForImport: true,
462+
},
463+
{
464+
Name: "tags",
465+
Type: tftypes.List{
466+
ElementType: tftypes.String,
467+
},
468+
OptionalForImport: true,
469+
},
470+
{
471+
Name: "magic_numbers",
472+
Type: tftypes.List{
473+
ElementType: tftypes.Number,
474+
},
475+
OptionalForImport: true,
476+
},
477+
},
478+
},
479+
},
480+
}
481+
}

helper/resource/importstate/import_block_as_first_step_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ func TestImportBlock_AsFirstStep(t *testing.T) {
4141
Config: `resource "examplecloud_container" "test" {
4242
name = "somevalue"
4343
location = "westeurope"
44-
}`,
44+
}
45+
46+
import {
47+
to = examplecloud_container.test
48+
id = "westeurope/somevalue"
49+
}
50+
`,
4551
ImportPlanChecks: r.ImportPlanChecks{
4652
PreApply: []plancheck.PlanCheck{
4753
plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionNoop),

helper/resource/importstate/import_block_in_config_file_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,31 @@ func TestImportBlock_InConfigFile(t *testing.T) {
4343
},
4444
})
4545
}
46+
47+
func TestImportBlock_WithResourceIdentity_InConfigFile(t *testing.T) {
48+
t.Parallel()
49+
50+
r.UnitTest(t, r.TestCase{
51+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
52+
tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later
53+
},
54+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
55+
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
56+
Resources: map[string]testprovider.Resource{
57+
"examplecloud_container": examplecloudResource(),
58+
},
59+
}),
60+
},
61+
Steps: []r.TestStep{
62+
{
63+
ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`),
64+
},
65+
{
66+
ResourceName: "examplecloud_container.test",
67+
ImportState: true,
68+
ImportStateKind: r.ImportBlockWithResourceIdentity,
69+
ConfigFile: config.StaticFile(`testdata/examplecloud_container_import_with_identity.tf`),
70+
},
71+
},
72+
})
73+
}

0 commit comments

Comments
 (0)