Skip to content

Commit 18deced

Browse files
authored
helper/resource: Remove data source and resource id attribute requirement (#164)
Reference: #84 The resource instance state identifier was a Terraform versions 0.11 and earlier concept which helped core and the then SDK determine if the resource should be removed and as an identifier value in the human readable output. This concept unfortunately carried over to the testing logic when the testing logic was mostly changed to use the public, machine-readable JSON interface with Terraform, rather than reusing prior internal logic from Terraform. Using the `id` attribute value for this identifier was the default implementation and therefore those older versions of Terraform required the attribute. This is no longer necessary after Terraform versions 0.12 and later. This testing logic only supports Terraform version 0.12.26 and later. The remaining resource instance identifier and `id` attribute usage in the testing logic was: - When the `TestStep` type `ImportStateVerify` field is enabled, the instance state identifier between prior resource state and import resource state are compared to find the correct resource. - When the `TestStep` type `ImportStateIdFunc` and `ImportStateId` fields are empty as the import ID for calling `terraform import` command. - Other various `terraform` package references For the first case, a new `TestStep` type `ImportStateVerifyIdentiferAttribute` field is added, which defaults to the prior `id` attribute. This preserves the existing behavior while enabling developers to choose a different identifier attribute if omitted from the resource so they can still use the `ImportStateVerify` functionality. For the second case, developers can use the `ImportStateId*` fields to choose/create the correct import identifier value. For the final cases, no action is needed as the `terraform` package is not intended for external usage and any usage of this identifer is mainly for equality and printing debugging information which is not accessed by the testing logic. These functions will be marked with Go documentation deprecation comments in a subsequent change.
1 parent ba04167 commit 18deced

File tree

8 files changed

+392
-6
lines changed

8 files changed

+392
-6
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: ENHANCEMENTS
2+
body: 'helper/resource: Added `TestStep` type `ImportStateVerifyIdentifierAttribute`
3+
field, which can override the default `id` attribute used for matching prior
4+
resource state with imported resource state'
5+
time: 2023-08-20T19:53:00.488517-04:00
6+
custom:
7+
Issue: "84"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'helper/resource: Removed data resource and managed resource `id` attribute
3+
requirement'
4+
time: 2023-08-20T19:56:24.356163-04:00
5+
custom:
6+
Issue: "84"

helper/resource/state_shim.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,36 @@ func shimResourceState(res *tfjson.StateResource) (*terraform.ResourceState, err
193193
}
194194
attributes := sf.Flatmap()
195195

196-
if _, ok := attributes["id"]; !ok {
197-
return nil, fmt.Errorf("no %q found in attributes", "id")
196+
// The instance state identifier was a Terraform versions 0.11 and earlier
197+
// concept which helped core and the then SDK determine if the resource
198+
// should be removed and as an identifier value in the human readable
199+
// output. This concept unfortunately carried over to the testing logic when
200+
// the testing logic was mostly changed to use the public, machine-readable
201+
// JSON interface with Terraform, rather than reusing prior internal logic
202+
// from Terraform. Using the "id" attribute value for this identifier was
203+
// the default implementation and therefore those older versions of
204+
// Terraform required the attribute. This is no longer necessary after
205+
// Terraform versions 0.12 and later.
206+
//
207+
// If the "id" attribute is not found, set the instance state identifier to
208+
// a synthetic value that can hopefully lead someone encountering the value
209+
// to these comments. The prior logic used to raise an error if the
210+
// attribute was not present, but this value should now only be present in
211+
// legacy logic of this Go module, such as unintentionally exported logic in
212+
// the terraform package, and not encountered during normal testing usage.
213+
//
214+
// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84
215+
instanceStateID, ok := attributes["id"]
216+
217+
if !ok {
218+
instanceStateID = "id-attribute-not-set"
198219
}
199220

200221
return &terraform.ResourceState{
201222
Provider: res.ProviderName,
202223
Type: res.Type,
203224
Primary: &terraform.InstanceState{
204-
ID: attributes["id"],
225+
ID: instanceStateID,
205226
Attributes: attributes,
206227
Meta: map[string]interface{}{
207228
"schema_version": int(res.SchemaVersion),

helper/resource/testcase_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package resource
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
12+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource"
13+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver"
14+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource"
15+
)
16+
17+
// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84
18+
func TestTestCase_NoDataSourceIdRequirement(t *testing.T) {
19+
t.Parallel()
20+
21+
UnitTest(t, TestCase{
22+
Steps: []TestStep{
23+
{
24+
Check: ComposeAggregateTestCheckFunc(
25+
TestCheckNoResourceAttr("data.test_datasource.test", "id"),
26+
TestCheckResourceAttr("data.test_datasource.test", "not_id", "test"),
27+
),
28+
Config: `data "test_datasource" "test" {}`,
29+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
30+
"test": providerserver.NewProviderServer(testprovider.Provider{
31+
DataSources: map[string]testprovider.DataSource{
32+
"test_datasource": {
33+
ReadResponse: &datasource.ReadResponse{
34+
State: tftypes.NewValue(
35+
tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"not_id": tftypes.String,
38+
},
39+
},
40+
map[string]tftypes.Value{
41+
"not_id": tftypes.NewValue(tftypes.String, "test"),
42+
},
43+
),
44+
},
45+
SchemaResponse: &datasource.SchemaResponse{
46+
Schema: &tfprotov6.Schema{
47+
Block: &tfprotov6.SchemaBlock{
48+
Attributes: []*tfprotov6.SchemaAttribute{
49+
{
50+
Name: "not_id",
51+
Type: tftypes.String,
52+
Computed: true,
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
}),
61+
},
62+
},
63+
},
64+
})
65+
}
66+
67+
// Reference: https://github.com/hashicorp/terraform-plugin-testing/issues/84
68+
func TestTestCase_NoResourceIdRequirement(t *testing.T) {
69+
t.Parallel()
70+
71+
UnitTest(t, TestCase{
72+
Steps: []TestStep{
73+
{
74+
Check: ComposeAggregateTestCheckFunc(
75+
TestCheckNoResourceAttr("test_resource.test", "id"),
76+
TestCheckResourceAttr("test_resource.test", "not_id", "test"),
77+
),
78+
Config: `resource "test_resource" "test" {}`,
79+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
80+
"test": providerserver.NewProviderServer(testprovider.Provider{
81+
Resources: map[string]testprovider.Resource{
82+
"test_resource": {
83+
CreateResponse: &resource.CreateResponse{
84+
NewState: tftypes.NewValue(
85+
tftypes.Object{
86+
AttributeTypes: map[string]tftypes.Type{
87+
"not_id": tftypes.String,
88+
},
89+
},
90+
map[string]tftypes.Value{
91+
"not_id": tftypes.NewValue(tftypes.String, "test"),
92+
},
93+
),
94+
},
95+
SchemaResponse: &resource.SchemaResponse{
96+
Schema: &tfprotov6.Schema{
97+
Block: &tfprotov6.SchemaBlock{
98+
Attributes: []*tfprotov6.SchemaAttribute{
99+
{
100+
Name: "not_id",
101+
Type: tftypes.String,
102+
Computed: true,
103+
},
104+
},
105+
},
106+
},
107+
},
108+
},
109+
},
110+
}),
111+
},
112+
},
113+
},
114+
})
115+
}

helper/resource/testing.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,10 +604,24 @@ type TestStep struct {
604604
// IDs returned by the Import. Note that this checks for strict equality
605605
// and does not respect DiffSuppressFunc or CustomizeDiff.
606606
//
607+
// By default, the prior resource state and import resource state are
608+
// matched by the "id" attribute. If the "id" attribute is not implemented
609+
// or another attribute more uniquely identifies the resource, set the
610+
// ImportStateVerifyIdentifierAttribute field to adjust the attribute for
611+
// matching.
612+
//
613+
// If certain attributes cannot be correctly imported, set the
614+
// ImportStateVerifyIgnore field.
615+
ImportStateVerify bool
616+
617+
// ImportStateVerifyIdentifierAttribute is the resource attribute for
618+
// matching the prior resource state and import resource state during import
619+
// verification. By default, the "id" attribute is used.
620+
ImportStateVerifyIdentifierAttribute string
621+
607622
// ImportStateVerifyIgnore is a list of prefixes of fields that should
608623
// not be verified to be equal. These can be set to ephemeral fields or
609624
// fields that can't be refreshed and don't matter.
610-
ImportStateVerify bool
611625
ImportStateVerifyIgnore []string
612626

613627
// ImportStatePersist, if true, will update the persisted state with the

helper/resource/testing_new_import_state.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,20 +182,41 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
182182
}
183183
}
184184

185+
identifierAttribute := step.ImportStateVerifyIdentifierAttribute
186+
187+
if identifierAttribute == "" {
188+
identifierAttribute = "id"
189+
}
190+
185191
for _, r := range newResources {
192+
rIdentifier, ok := r.Primary.Attributes[identifierAttribute]
193+
194+
if !ok {
195+
t.Fatalf("ImportStateVerify: New resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute)
196+
}
197+
186198
// Find the existing resource
187199
var oldR *terraform.ResourceState
188200
for _, r2 := range oldResources {
201+
if r2.Primary == nil || r2.Type != r.Type || r2.Provider != r.Provider {
202+
continue
203+
}
204+
205+
r2Identifier, ok := r2.Primary.Attributes[identifierAttribute]
206+
207+
if !ok {
208+
t.Fatalf("ImportStateVerify: Old resource missing identifier attribute %q, ensure attribute value is properly set or use ImportStateVerifyIdentifierAttribute to choose different attribute", identifierAttribute)
209+
}
189210

190-
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type && r2.Provider == r.Provider {
211+
if r2Identifier == rIdentifier {
191212
oldR = r2
192213
break
193214
}
194215
}
195216
if oldR == nil || oldR.Primary == nil {
196217
t.Fatalf(
197218
"Failed state verification, resource with ID %s not found",
198-
r.Primary.ID)
219+
rIdentifier)
199220
}
200221

201222
// don't add empty flatmapped containers, so we can more easily

0 commit comments

Comments
 (0)