Skip to content

Commit 34b8271

Browse files
authored
TFECO-9286: add ImportStatePassthroughWithIdentity (#1474)
* add ImportStatePassthroughWithIdentity * remove obsolete comment * add documentation comment and one more test case
1 parent 6aefc6f commit 34b8271

File tree

3 files changed

+236
-2
lines changed

3 files changed

+236
-2
lines changed

helper/schema/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ func (p *Provider) ImportStateWithIdentity(
505505
if err != nil {
506506
return nil, err // this should not happen, as we checked above
507507
}
508-
identityData.raw = identity // is this too hacky / unexpected?
508+
identityData.raw = identity
509509
} else if identity != nil {
510510
return nil, fmt.Errorf("resource %s doesn't support identity import", info.Type)
511511
}

helper/schema/resource_importer.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package schema
66
import (
77
"context"
88
"errors"
9+
"fmt"
910
)
1011

1112
// ResourceImporter defines how a resource is imported in Terraform. This
@@ -83,3 +84,49 @@ func ImportStatePassthrough(d *ResourceData, m interface{}) ([]*ResourceData, er
8384
func ImportStatePassthroughContext(ctx context.Context, d *ResourceData, m interface{}) ([]*ResourceData, error) {
8485
return []*ResourceData{d}, nil
8586
}
87+
88+
// ImportStatePassthroughWithIdentity creates a StateContextFunc that supports both
89+
// identity-based and ID-only resource import scenarios. This function is useful
90+
// when a resource can be imported either by its unique ID or by an identity attribute.
91+
//
92+
// The `idAttributePath` parameter specifies the name of the identity attribute
93+
// to use when importing by identity. Since identity attributes are "flat",
94+
// `idAttributePath` should be a simple attribute name (e.g., "name" or "identifier").
95+
// Note that the identity attribute must be a string, as this function expects
96+
// to set the resource ID using the value of the specified attribute.
97+
//
98+
// If the resource is imported by ID (i.e., `d.Id()` is already set), the function
99+
// simply returns the resource data as-is. Otherwise, it attempts to retrieve the
100+
// identity attribute specified by `idAttributePath` and sets it as the resource ID.
101+
//
102+
// Parameters:
103+
// - idAttributePath: The name of the identity attribute to use for setting the ID.
104+
//
105+
// Returns:
106+
// - A StateContextFunc that handles the import logic.
107+
func ImportStatePassthroughWithIdentity(idAttributePath string) StateContextFunc {
108+
return func(ctx context.Context, d *ResourceData, m interface{}) ([]*ResourceData, error) {
109+
// If we import by id, we just return the resource data as is, no need to change it
110+
if d.Id() != "" {
111+
return []*ResourceData{d}, nil
112+
}
113+
114+
// If we import by identity, we need to set the id based on the idAttributePath
115+
identity, err := d.Identity()
116+
if err != nil {
117+
return nil, fmt.Errorf("error getting identity: %s", err)
118+
}
119+
id, exists := identity.GetOk(idAttributePath)
120+
if !exists {
121+
return nil, fmt.Errorf("expected identity to contain key %s", idAttributePath)
122+
}
123+
idStr, ok := id.(string)
124+
if !ok {
125+
return nil, fmt.Errorf("expected identity key %s to be a string, was: %T", idAttributePath, id)
126+
}
127+
128+
d.SetId(idStr)
129+
130+
return []*ResourceData{d}, nil
131+
}
132+
}

helper/schema/resource_importer_test.go

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
package schema
55

6-
import "testing"
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
10+
)
711

812
func TestInternalValidate(t *testing.T) {
913
r := &ResourceImporter{
@@ -14,3 +18,186 @@ func TestInternalValidate(t *testing.T) {
1418
t.Fatal("ResourceImporter should not allow State and StateContext to be set")
1519
}
1620
}
21+
22+
func TestImportStatePassthroughWithIdentity(t *testing.T) {
23+
// shared among all tests, defined once to keep them shorter
24+
identitySchema := map[string]*Schema{
25+
"email": {
26+
Type: TypeString,
27+
RequiredForImport: true,
28+
},
29+
"region": {
30+
Type: TypeString,
31+
OptionalForImport: true,
32+
},
33+
}
34+
35+
tests := []struct {
36+
name string
37+
idAttributePath string
38+
resourceData *ResourceData
39+
expectedResourceData *ResourceData
40+
expectedError string
41+
}{
42+
{
43+
name: "import from id just sets id",
44+
idAttributePath: "email",
45+
resourceData: &ResourceData{
46+
identitySchema: identitySchema,
47+
state: &terraform.InstanceState{
48+
49+
},
50+
},
51+
expectedResourceData: &ResourceData{
52+
identitySchema: identitySchema,
53+
state: &terraform.InstanceState{
54+
55+
},
56+
},
57+
},
58+
{
59+
name: "import from identity sets id and identity",
60+
idAttributePath: "email",
61+
resourceData: &ResourceData{
62+
identitySchema: identitySchema,
63+
state: &terraform.InstanceState{
64+
Identity: map[string]string{
65+
"email": "[email protected]",
66+
},
67+
},
68+
},
69+
expectedResourceData: &ResourceData{
70+
identitySchema: identitySchema,
71+
state: &terraform.InstanceState{
72+
73+
},
74+
newIdentity: &IdentityData{
75+
schema: identitySchema,
76+
raw: map[string]string{
77+
"email": "[email protected]",
78+
},
79+
},
80+
},
81+
},
82+
{
83+
name: "import from identity sets id and identity (with region set)",
84+
idAttributePath: "email",
85+
resourceData: &ResourceData{
86+
identitySchema: identitySchema,
87+
state: &terraform.InstanceState{
88+
Identity: map[string]string{
89+
"email": "[email protected]",
90+
"region": "eu-west-1",
91+
},
92+
},
93+
},
94+
expectedResourceData: &ResourceData{
95+
identitySchema: identitySchema,
96+
state: &terraform.InstanceState{
97+
98+
},
99+
newIdentity: &IdentityData{
100+
schema: identitySchema,
101+
raw: map[string]string{
102+
"email": "[email protected]",
103+
"region": "eu-west-1",
104+
},
105+
},
106+
},
107+
},
108+
{
109+
name: "import from identity fails without required field",
110+
idAttributePath: "email",
111+
resourceData: &ResourceData{
112+
identitySchema: identitySchema,
113+
state: &terraform.InstanceState{
114+
Identity: map[string]string{
115+
"region": "eu-west-1",
116+
},
117+
},
118+
},
119+
expectedError: "expected identity to contain key email",
120+
},
121+
{
122+
name: "import from identity fails if attribute is not a string",
123+
idAttributePath: "number",
124+
resourceData: &ResourceData{
125+
identitySchema: map[string]*Schema{
126+
"number": {
127+
Type: TypeInt,
128+
RequiredForImport: true,
129+
},
130+
},
131+
state: &terraform.InstanceState{
132+
Identity: map[string]string{
133+
"number": "1",
134+
},
135+
},
136+
},
137+
expectedError: "expected identity key number to be a string, was: int",
138+
},
139+
{
140+
name: "import from identity fails without schema",
141+
idAttributePath: "email",
142+
resourceData: &ResourceData{
143+
state: &terraform.InstanceState{
144+
Identity: map[string]string{
145+
"email": "[email protected]",
146+
},
147+
},
148+
},
149+
expectedError: "error getting identity: Resource does not have Identity schema. Please set one in order to use Identity(). This is always a problem in the provider code.",
150+
},
151+
}
152+
153+
for _, test := range tests {
154+
t.Run(test.name, func(t *testing.T) {
155+
results, err := ImportStatePassthroughWithIdentity(test.idAttributePath)(nil, test.resourceData, nil)
156+
if err != nil {
157+
if test.expectedError == "" {
158+
t.Fatalf("unexpected error: %s", err)
159+
}
160+
if err.Error() != test.expectedError {
161+
t.Fatalf("expected error: %s, got: %s", test.expectedError, err)
162+
}
163+
return // we don't expect any results if there is an error
164+
}
165+
if len(results) != 1 {
166+
t.Fatalf("expected 1 result, got: %d", len(results))
167+
}
168+
// compare id and identity in resource data
169+
if results[0].Id() != test.expectedResourceData.Id() {
170+
t.Fatalf("expected id: %s, got: %s", test.expectedResourceData.Id(), results[0].Id())
171+
}
172+
// compare identity
173+
expectedIdentity, err := test.expectedResourceData.Identity()
174+
if err != nil {
175+
t.Fatalf("unexpected error: %s", err)
176+
}
177+
resultIdentity, err := results[0].Identity()
178+
if err != nil {
179+
t.Fatalf("unexpected error: %s", err)
180+
}
181+
182+
// check whether all result identity attributes exist as expected
183+
for key := range expectedIdentity.schema {
184+
expected := expectedIdentity.getRaw(key)
185+
if expected.Exists {
186+
result := resultIdentity.getRaw(key)
187+
if !result.Exists {
188+
t.Fatalf("expected identity attribute %s to exist", key)
189+
}
190+
if expected.Value != result.Value {
191+
t.Fatalf("expected identity attribute %s to be %s, got: %s", key, expected.Value, result.Value)
192+
}
193+
}
194+
}
195+
// check whether there are no additional attributes in the result identity
196+
for key := range resultIdentity.schema {
197+
if _, ok := expectedIdentity.schema[key]; !ok {
198+
t.Fatalf("unexpected identity attribute %s", key)
199+
}
200+
}
201+
})
202+
}
203+
}

0 commit comments

Comments
 (0)