Skip to content

Commit 86b2acb

Browse files
authored
resource: Initial MoveResourceState RPC support (#917)
This change adds initial support for the `MoveResourceState` RPC to the framework, including: * Adding the framework shared server implementation for the new RPC * Adding the protocol version 5 and 6 server implementations for the new RPC * Adding the type conversion logic for the framework types to/from the protocol types * Exposing a new `resource.ResourceWithMoveState` interface that providers can implement to support the new RPC * Adding a website documentation page for the new functionality A state move using the new RPC occurs when a Terraform 1.8 and later configuration includes a `moved` configuration block such as the following: ```terraform moved { from = "examplecloud_source.XXX" to = "examplecloud_target.XXX" } ``` There are no restrictions on the source resource types, but target resource types must opt into support to prevent data loss. Target resources can support moves from multiple, differing source resources, so the framework implementation is exposed as an ordered list to developers. The framework implementation for the new RPC is as follows: * If no state move support is defined for the resource, the framework returns an error diagnostic. * If state move support is defined for the resource, each provider-defined state move implementation is called until one responds with error diagnostics or state data. * If all provider-defined state move implementations return without error diagnostics and state data, the framework returns an error diagnostic. The protocol server unit testing shows the end-to-end handling of the new RPC, including the provider-defined resource implementations, the type conversion logic, and the framework shared server implementation. The exposed `resource.ResourceWithMoveState` implementation is intentionally similar to the existing `resource.ResourceWithUpgradeState` handling, both in internal details and the exposed API. This is to provide a smoother experience for provider developers familiar with the other functionality and maintainers of the framework. This initial implementation exposes a lot of the request/response handling as-is, however there is likely potential for introducing native helper functionality for developers, such as implementing one to simplify "aliasing"/"renaming" an existing `resource.Resource` within the same provider codebase.
1 parent f471850 commit 86b2acb

25 files changed

+4024
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'resource: Added the `ResourceWithMoveState` interface, which enables state
3+
moves across resource types with Terraform 1.8 and later'
4+
time: 2024-02-01T17:34:28.190047-05:00
5+
custom:
6+
Issue: "917"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fromproto5
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
11+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
15+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
16+
)
17+
18+
// MoveResourceStateRequest returns the *fwserver.MoveResourceStateRequest
19+
// equivalent of a *tfprotov5.MoveResourceStateRequest.
20+
func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) {
21+
if proto5 == nil {
22+
return nil, nil
23+
}
24+
25+
var diags diag.Diagnostics
26+
27+
// Panic prevention here to simplify the calling implementations.
28+
// This should not happen, but just in case.
29+
if resourceSchema == nil {
30+
diags.AddError(
31+
"Framework Implementation Error",
32+
"An unexpected issue was encountered when converting the MoveResourceState RPC request information from the protocol type to the framework type. "+
33+
"The resource schema was missing. "+
34+
"This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.",
35+
)
36+
37+
return nil, diags
38+
}
39+
40+
fw := &fwserver.MoveResourceStateRequest{
41+
SourceProviderAddress: proto5.SourceProviderAddress,
42+
SourceRawState: (*tfprotov6.RawState)(proto5.SourceState),
43+
SourceSchemaVersion: proto5.SourceSchemaVersion,
44+
SourceTypeName: proto5.SourceTypeName,
45+
TargetResource: resource,
46+
TargetResourceSchema: resourceSchema,
47+
TargetTypeName: proto5.TargetTypeName,
48+
}
49+
50+
sourcePrivate, sourcePrivateDiags := privatestate.NewData(ctx, proto5.SourcePrivate)
51+
52+
diags.Append(sourcePrivateDiags...)
53+
54+
fw.SourcePrivate = sourcePrivate
55+
56+
return fw, diags
57+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fromproto5_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/fromproto5"
13+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
14+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
15+
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
16+
"github.com/hashicorp/terraform-plugin-framework/resource"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
18+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
19+
)
20+
21+
func TestMoveResourceStateRequest(t *testing.T) {
22+
t.Parallel()
23+
24+
testFwSchema := schema.Schema{
25+
Attributes: map[string]schema.Attribute{
26+
"test_attribute": schema.StringAttribute{
27+
Required: true,
28+
},
29+
},
30+
}
31+
32+
testCases := map[string]struct {
33+
input *tfprotov5.MoveResourceStateRequest
34+
resourceSchema fwschema.Schema
35+
resource resource.Resource
36+
expected *fwserver.MoveResourceStateRequest
37+
expectedDiagnostics diag.Diagnostics
38+
}{
39+
"nil": {
40+
input: nil,
41+
expected: nil,
42+
},
43+
"SourcePrivate": {
44+
input: &tfprotov5.MoveResourceStateRequest{
45+
SourcePrivate: privatestate.MustMarshalToJson(map[string][]byte{
46+
".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`),
47+
"providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`),
48+
}),
49+
},
50+
resourceSchema: testFwSchema,
51+
expected: &fwserver.MoveResourceStateRequest{
52+
SourcePrivate: &privatestate.Data{
53+
Framework: map[string][]byte{
54+
".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`),
55+
},
56+
Provider: privatestate.MustProviderData(context.Background(), privatestate.MustMarshalToJson(map[string][]byte{
57+
"providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`),
58+
})),
59+
},
60+
TargetResourceSchema: testFwSchema,
61+
},
62+
},
63+
"SourcePrivate-malformed-json": {
64+
input: &tfprotov5.MoveResourceStateRequest{
65+
SourcePrivate: []byte(`{`),
66+
},
67+
resourceSchema: testFwSchema,
68+
expected: &fwserver.MoveResourceStateRequest{
69+
TargetResourceSchema: testFwSchema,
70+
},
71+
expectedDiagnostics: diag.Diagnostics{
72+
diag.NewErrorDiagnostic(
73+
"Error Decoding Private State",
74+
"An error was encountered when decoding private state: unexpected end of JSON input.\n\n"+
75+
"This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.",
76+
),
77+
},
78+
},
79+
"SourcePrivate-empty-json": {
80+
input: &tfprotov5.MoveResourceStateRequest{
81+
SourcePrivate: []byte("{}"),
82+
},
83+
resourceSchema: testFwSchema,
84+
expected: &fwserver.MoveResourceStateRequest{
85+
SourcePrivate: &privatestate.Data{
86+
Framework: map[string][]byte{},
87+
Provider: privatestate.EmptyProviderData(context.Background()),
88+
},
89+
TargetResourceSchema: testFwSchema,
90+
},
91+
},
92+
"SourceProviderAddress": {
93+
input: &tfprotov5.MoveResourceStateRequest{
94+
SourceProviderAddress: "example.com/namespace/type",
95+
},
96+
resourceSchema: testFwSchema,
97+
expected: &fwserver.MoveResourceStateRequest{
98+
SourceProviderAddress: "example.com/namespace/type",
99+
TargetResourceSchema: testFwSchema,
100+
},
101+
},
102+
"SourceRawState": {
103+
input: &tfprotov5.MoveResourceStateRequest{
104+
SourceState: testNewTfprotov5RawState(t, map[string]interface{}{
105+
"test_attribute": "test-value",
106+
}),
107+
},
108+
resourceSchema: testFwSchema,
109+
expected: &fwserver.MoveResourceStateRequest{
110+
SourceRawState: testNewTfprotov6RawState(t, map[string]interface{}{
111+
"test_attribute": "test-value",
112+
}),
113+
TargetResourceSchema: testFwSchema,
114+
},
115+
},
116+
"SourceSchemaVersion": {
117+
input: &tfprotov5.MoveResourceStateRequest{
118+
SourceSchemaVersion: 123,
119+
},
120+
resourceSchema: testFwSchema,
121+
expected: &fwserver.MoveResourceStateRequest{
122+
SourceSchemaVersion: 123,
123+
TargetResourceSchema: testFwSchema,
124+
},
125+
},
126+
"SourceTypeName": {
127+
input: &tfprotov5.MoveResourceStateRequest{
128+
SourceTypeName: "examplecloud_thing",
129+
},
130+
resourceSchema: testFwSchema,
131+
expected: &fwserver.MoveResourceStateRequest{
132+
SourceTypeName: "examplecloud_thing",
133+
TargetResourceSchema: testFwSchema,
134+
},
135+
},
136+
"TargetResourceSchema": {
137+
input: &tfprotov5.MoveResourceStateRequest{},
138+
resourceSchema: testFwSchema,
139+
expected: &fwserver.MoveResourceStateRequest{
140+
TargetResourceSchema: testFwSchema,
141+
},
142+
},
143+
"TargetResourceSchema-missing": {
144+
input: &tfprotov5.MoveResourceStateRequest{},
145+
expected: nil,
146+
expectedDiagnostics: diag.Diagnostics{
147+
diag.NewErrorDiagnostic(
148+
"Framework Implementation Error",
149+
"An unexpected issue was encountered when converting the MoveResourceState RPC request information from the protocol type to the framework type. "+
150+
"The resource schema was missing. "+
151+
"This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.",
152+
),
153+
},
154+
},
155+
"TargetTypeName": {
156+
input: &tfprotov5.MoveResourceStateRequest{
157+
TargetTypeName: "examplecloud_thing",
158+
},
159+
resourceSchema: testFwSchema,
160+
expected: &fwserver.MoveResourceStateRequest{
161+
TargetResourceSchema: testFwSchema,
162+
TargetTypeName: "examplecloud_thing",
163+
},
164+
},
165+
}
166+
167+
for name, testCase := range testCases {
168+
name, testCase := name, testCase
169+
170+
t.Run(name, func(t *testing.T) {
171+
t.Parallel()
172+
173+
got, diags := fromproto5.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema)
174+
175+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
176+
t.Errorf("unexpected difference: %s", diff)
177+
}
178+
179+
if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" {
180+
t.Errorf("unexpected diagnostics difference: %s", diff)
181+
}
182+
})
183+
}
184+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fromproto6
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
11+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
15+
)
16+
17+
// MoveResourceStateRequest returns the *fwserver.MoveResourceStateRequest
18+
// equivalent of a *tfprotov6.MoveResourceStateRequest.
19+
func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) {
20+
if proto6 == nil {
21+
return nil, nil
22+
}
23+
24+
var diags diag.Diagnostics
25+
26+
// Panic prevention here to simplify the calling implementations.
27+
// This should not happen, but just in case.
28+
if resourceSchema == nil {
29+
diags.AddError(
30+
"Framework Implementation Error",
31+
"An unexpected issue was encountered when converting the MoveResourceState RPC request information from the protocol type to the framework type. "+
32+
"The resource schema was missing. "+
33+
"This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.",
34+
)
35+
36+
return nil, diags
37+
}
38+
39+
fw := &fwserver.MoveResourceStateRequest{
40+
SourceProviderAddress: proto6.SourceProviderAddress,
41+
SourceRawState: proto6.SourceState,
42+
SourceSchemaVersion: proto6.SourceSchemaVersion,
43+
SourceTypeName: proto6.SourceTypeName,
44+
TargetResource: resource,
45+
TargetResourceSchema: resourceSchema,
46+
TargetTypeName: proto6.TargetTypeName,
47+
}
48+
49+
sourcePrivate, sourcePrivateDiags := privatestate.NewData(ctx, proto6.SourcePrivate)
50+
51+
diags.Append(sourcePrivateDiags...)
52+
53+
fw.SourcePrivate = sourcePrivate
54+
55+
return fw, diags
56+
}

0 commit comments

Comments
 (0)