Skip to content

Commit db08a72

Browse files
authored
ResourceIdentity: Add support for import by identity and update pass-through implementations (#1126)
* initial import implementation with some TODOs * add tests for fwserver * add changelog for beta
1 parent f4f3ad9 commit db08a72

17 files changed

+950
-9
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
kind: NOTES
2+
body: This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta1.
3+
Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the
4+
`resource.ResourceWithImportState` interface will automatically pass-through identity data to the `Read` method. The `RequiredForImport` and
5+
`OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block.
6+
time: 2025-04-03T12:12:29.323193-04:00
7+
custom:
8+
Issue: "1126"

internal/fromproto5/importresourcestate.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
// ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest
2020
// equivalent of a *tfprotov5.ImportResourceStateRequest.
21-
func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
21+
func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
2222
if proto5 == nil {
2323
return nil, nil
2424
}
@@ -45,10 +45,17 @@ func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportRes
4545
Schema: resourceSchema,
4646
},
4747
ID: proto5.ID,
48+
IdentitySchema: identitySchema,
4849
Resource: reqResource,
4950
TypeName: proto5.TypeName,
5051
ClientCapabilities: ImportStateClientCapabilities(proto5.ClientCapabilities),
5152
}
5253

54+
identity, identityDiags := ResourceIdentity(ctx, proto5.Identity, identitySchema)
55+
56+
diags.Append(identityDiags...)
57+
58+
fw.Identity = identity
59+
5360
return fw, diags
5461
}

internal/fromproto5/importresourcestate_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
1717
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
1818
"github.com/hashicorp/terraform-plugin-framework/resource"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
1920
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2021
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
2122
)
@@ -31,6 +32,30 @@ func TestImportResourceStateRequest(t *testing.T) {
3132
},
3233
}
3334

35+
testIdentityProto5Type := tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"test_identity_attribute": tftypes.String,
38+
},
39+
}
40+
41+
testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{
42+
"test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"),
43+
})
44+
45+
testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value)
46+
47+
if err != nil {
48+
t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err)
49+
}
50+
51+
testIdentitySchema := identityschema.Schema{
52+
Attributes: map[string]identityschema.Attribute{
53+
"test_identity_attribute": identityschema.StringAttribute{
54+
RequiredForImport: true,
55+
},
56+
},
57+
}
58+
3459
testFwEmptyState := tfsdk.State{
3560
Raw: tftypes.NewValue(testFwSchema.Type().TerraformType(context.Background()), nil),
3661
Schema: testFwSchema,
@@ -39,6 +64,7 @@ func TestImportResourceStateRequest(t *testing.T) {
3964
testCases := map[string]struct {
4065
input *tfprotov5.ImportResourceStateRequest
4166
resourceSchema fwschema.Schema
67+
identitySchema fwschema.Schema
4268
resource resource.Resource
4369
expected *fwserver.ImportResourceStateRequest
4470
expectedDiagnostics diag.Diagnostics
@@ -67,6 +93,42 @@ func TestImportResourceStateRequest(t *testing.T) {
6793
),
6894
},
6995
},
96+
"identity-missing-schema": {
97+
input: &tfprotov5.ImportResourceStateRequest{
98+
Identity: &tfprotov5.ResourceIdentityData{
99+
IdentityData: &testIdentityProto5DynamicValue,
100+
},
101+
},
102+
resourceSchema: testFwSchema,
103+
expected: &fwserver.ImportResourceStateRequest{
104+
EmptyState: testFwEmptyState,
105+
},
106+
expectedDiagnostics: diag.Diagnostics{
107+
diag.NewErrorDiagnostic(
108+
"Unable to Convert Resource Identity",
109+
"An unexpected error was encountered when converting the resource identity from the protocol type. "+
110+
"Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+
111+
"This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.",
112+
),
113+
},
114+
},
115+
"identity": {
116+
input: &tfprotov5.ImportResourceStateRequest{
117+
Identity: &tfprotov5.ResourceIdentityData{
118+
IdentityData: &testIdentityProto5DynamicValue,
119+
},
120+
},
121+
resourceSchema: testFwSchema,
122+
identitySchema: testIdentitySchema,
123+
expected: &fwserver.ImportResourceStateRequest{
124+
EmptyState: testFwEmptyState,
125+
IdentitySchema: testIdentitySchema,
126+
Identity: &tfsdk.ResourceIdentity{
127+
Raw: testIdentityProto5Value,
128+
Schema: testIdentitySchema,
129+
},
130+
},
131+
},
70132
"id": {
71133
input: &tfprotov5.ImportResourceStateRequest{
72134
ID: "test-id",
@@ -122,7 +184,7 @@ func TestImportResourceStateRequest(t *testing.T) {
122184
t.Run(name, func(t *testing.T) {
123185
t.Parallel()
124186

125-
got, diags := fromproto5.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema)
187+
got, diags := fromproto5.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema)
126188

127189
if diff := cmp.Diff(got, testCase.expected); diff != "" {
128190
t.Errorf("unexpected difference: %s", diff)

internal/fromproto6/importresourcestate.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
// ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest
2020
// equivalent of a *tfprotov6.ImportResourceStateRequest.
21-
func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
21+
func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
2222
if proto6 == nil {
2323
return nil, nil
2424
}
@@ -45,10 +45,17 @@ func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportRes
4545
Schema: resourceSchema,
4646
},
4747
ID: proto6.ID,
48+
IdentitySchema: identitySchema,
4849
Resource: reqResource,
4950
TypeName: proto6.TypeName,
5051
ClientCapabilities: ImportStateClientCapabilities(proto6.ClientCapabilities),
5152
}
5253

54+
identity, identityDiags := ResourceIdentity(ctx, proto6.Identity, identitySchema)
55+
56+
diags.Append(identityDiags...)
57+
58+
fw.Identity = identity
59+
5360
return fw, diags
5461
}

internal/fromproto6/importresourcestate_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
1717
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
1818
"github.com/hashicorp/terraform-plugin-framework/resource"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
1920
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2021
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
2122
)
@@ -31,6 +32,30 @@ func TestImportResourceStateRequest(t *testing.T) {
3132
},
3233
}
3334

35+
testIdentityProto6Type := tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"test_identity_attribute": tftypes.String,
38+
},
39+
}
40+
41+
testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{
42+
"test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"),
43+
})
44+
45+
testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value)
46+
47+
if err != nil {
48+
t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err)
49+
}
50+
51+
testIdentitySchema := identityschema.Schema{
52+
Attributes: map[string]identityschema.Attribute{
53+
"test_identity_attribute": identityschema.StringAttribute{
54+
RequiredForImport: true,
55+
},
56+
},
57+
}
58+
3459
testFwEmptyState := tfsdk.State{
3560
Raw: tftypes.NewValue(testFwSchema.Type().TerraformType(context.Background()), nil),
3661
Schema: testFwSchema,
@@ -39,6 +64,7 @@ func TestImportResourceStateRequest(t *testing.T) {
3964
testCases := map[string]struct {
4065
input *tfprotov6.ImportResourceStateRequest
4166
resourceSchema fwschema.Schema
67+
identitySchema fwschema.Schema
4268
resource resource.Resource
4369
expected *fwserver.ImportResourceStateRequest
4470
expectedDiagnostics diag.Diagnostics
@@ -67,6 +93,42 @@ func TestImportResourceStateRequest(t *testing.T) {
6793
),
6894
},
6995
},
96+
"identity-missing-schema": {
97+
input: &tfprotov6.ImportResourceStateRequest{
98+
Identity: &tfprotov6.ResourceIdentityData{
99+
IdentityData: &testIdentityProto6DynamicValue,
100+
},
101+
},
102+
resourceSchema: testFwSchema,
103+
expected: &fwserver.ImportResourceStateRequest{
104+
EmptyState: testFwEmptyState,
105+
},
106+
expectedDiagnostics: diag.Diagnostics{
107+
diag.NewErrorDiagnostic(
108+
"Unable to Convert Resource Identity",
109+
"An unexpected error was encountered when converting the resource identity from the protocol type. "+
110+
"Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+
111+
"This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.",
112+
),
113+
},
114+
},
115+
"identity": {
116+
input: &tfprotov6.ImportResourceStateRequest{
117+
Identity: &tfprotov6.ResourceIdentityData{
118+
IdentityData: &testIdentityProto6DynamicValue,
119+
},
120+
},
121+
resourceSchema: testFwSchema,
122+
identitySchema: testIdentitySchema,
123+
expected: &fwserver.ImportResourceStateRequest{
124+
EmptyState: testFwEmptyState,
125+
IdentitySchema: testIdentitySchema,
126+
Identity: &tfsdk.ResourceIdentity{
127+
Raw: testIdentityProto6Value,
128+
Schema: testIdentitySchema,
129+
},
130+
},
131+
},
70132
"id": {
71133
input: &tfprotov6.ImportResourceStateRequest{
72134
ID: "test-id",
@@ -122,7 +184,7 @@ func TestImportResourceStateRequest(t *testing.T) {
122184
t.Run(name, func(t *testing.T) {
123185
t.Parallel()
124186

125-
got, diags := fromproto6.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema)
187+
got, diags := fromproto6.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema)
126188

127189
if diff := cmp.Diff(got, testCase.expected); diff != "" {
128190
t.Errorf("unexpected difference: %s", diff)

internal/fwserver/server_importresourcestate.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/hashicorp/terraform-plugin-go/tftypes"
1010

1111
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
1213
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
1314
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
1415
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -18,14 +19,29 @@ import (
1819
// ImportedResource represents a resource that was imported.
1920
type ImportedResource struct {
2021
Private *privatestate.Data
22+
Identity *tfsdk.ResourceIdentity
2123
State tfsdk.State
2224
TypeName string
2325
}
2426

2527
// ImportResourceStateRequest is the framework server request for the
2628
// ImportResourceState RPC.
29+
//
30+
// Either ID or Identity will be supplied depending on how the resource is being imported.
2731
type ImportResourceStateRequest struct {
28-
ID string
32+
// ID will come from the import CLI command or an import config block with the "id" attribute assigned.
33+
//
34+
// This ID field is a special string identifier that can be parsed however the provider deems fit.
35+
ID string
36+
37+
// Identity will come from an import config block with the "identity" attribute assigned and will conform
38+
// to the identity schema defined by the resource. (Terraform v1.12+)
39+
//
40+
// All attributes marked as RequiredForImport will be populated (enforced by Terraform core) and OptionalForImport
41+
// attributes may be null, but could have a config value.
42+
Identity *tfsdk.ResourceIdentity
43+
IdentitySchema fwschema.Schema
44+
2945
Resource resource.Resource
3046

3147
// EmptyState is an empty State for the resource schema. This is used to
@@ -132,6 +148,29 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta
132148
Private: privateProviderData,
133149
}
134150

151+
// If the resource supports identity and we are not importing by identity, pre-populate with a null value.
152+
// TODO:ResourceIdentity: Is there any reason a provider WOULD NOT want to populate an identity when it supports one?
153+
if req.Identity == nil && req.IdentitySchema != nil {
154+
nullTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil)
155+
156+
req.Identity = &tfsdk.ResourceIdentity{
157+
Schema: req.IdentitySchema,
158+
Raw: nullTfValue.Copy(),
159+
}
160+
}
161+
162+
if req.Identity != nil {
163+
importReq.Identity = &tfsdk.ResourceIdentity{
164+
Schema: req.Identity.Schema,
165+
Raw: req.Identity.Raw.Copy(),
166+
}
167+
168+
importResp.Identity = &tfsdk.ResourceIdentity{
169+
Schema: req.Identity.Schema,
170+
Raw: req.Identity.Raw.Copy(),
171+
}
172+
}
173+
135174
logging.FrameworkTrace(ctx, "Calling provider defined Resource ImportState")
136175
resourceWithImportState.ImportState(ctx, importReq, &importResp)
137176
logging.FrameworkTrace(ctx, "Called provider defined Resource ImportState")
@@ -154,7 +193,9 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta
154193

155194
importResp.State.Raw = modifiedState
156195

157-
if importResp.State.Raw.Equal(req.EmptyState.Raw) {
196+
// If we are importing by ID, we should ensure that something in the import stub state has been populated,
197+
// otherwise the resource doesn't actually support import, which is a provider issue.
198+
if req.ID != "" && importResp.State.Raw.Equal(req.EmptyState.Raw) {
158199
resp.Diagnostics.AddError(
159200
"Missing Resource Import State",
160201
"An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+
@@ -169,10 +210,21 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta
169210
private.Provider = importResp.Private
170211
}
171212

213+
if importResp.Identity != nil && req.IdentitySchema == nil {
214+
resp.Diagnostics.AddError(
215+
"Unexpected ImportState Response",
216+
"An unexpected error was encountered when creating the import response. New identity data was returned by the provider import operation, but the resource does not indicate identity support.\n\n"+
217+
"This is always a problem with the provider and should be reported to the provider developer.",
218+
)
219+
220+
return
221+
}
222+
172223
resp.Deferred = importResp.Deferred
173224
resp.ImportedResources = []ImportedResource{
174225
{
175226
State: importResp.State,
227+
Identity: importResp.Identity,
176228
TypeName: req.TypeName,
177229
Private: private,
178230
},

0 commit comments

Comments
 (0)