Skip to content
This repository was archived by the owner on Jul 5, 2023. It is now read-only.

Commit 0d079da

Browse files
authored
plugin/framework: Initial State Upgrade information (#2255)
Reference: hashicorp/terraform-plugin-framework#42 Reference: hashicorp/terraform-plugin-framework#292
1 parent f67a9d2 commit 0d079da

File tree

4 files changed

+315
-1
lines changed

4 files changed

+315
-1
lines changed

content/plugin/framework/resources/index.mdx

+6
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ The [`tfsdk.ResourceImportStatePassthroughID` function](https://pkg.go.dev/githu
145145

146146
Refer to [Resource Import](/plugin/framework/resources/import) for more details and code examples.
147147

148+
### UpgradeState
149+
150+
`UpgradeState` is an optional method that implements resource [state](/language/state) upgrades when there are breaking changes to the [schema](/plugin/framework/schemas).
151+
152+
Refer to [State Upgrades](/plugin/framework/resources/state-upgrade) for more details and code examples.
153+
148154
## Add Resource to Provider
149155

150156
To make new resources available to practitioners, add them to the `GetResources` method on the [provider](/plugin/framework/providers).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
---
2+
page_title: 'Plugin Development - Framework: State Upgrade'
3+
description: >-
4+
How to upgrade state data after breaking schema changes using the provider
5+
development framework.
6+
---
7+
8+
# State Upgrade
9+
10+
A resource schema captures the structure and types of the resource [state](/language/state). Any state data that does not conform to the resource schema will generate errors or may not be persisted properly. Over time, it may be necessary for resources to make breaking changes to their schemas, such as changing an attribute type. Terraform supports versioning of these resource schemas and the current version is saved into the Terraform state. When the provider advertises a newer schema version, Terraform will call back to the provider to attempt to upgrade from the saved schema version to the one advertised. This operation is performed prior to planning, but with a configured provider.
11+
12+
-> Some versions of Terraform CLI will also request state upgrades even when the current schema version matches the state version. The framework will automatically handle this request.
13+
14+
## State Upgrade Process
15+
16+
1. When generating a plan, Terraform CLI will request the current resource schema, which contains a version.
17+
1. If Terraform CLI detects that an existing state with its saved version does not match the current version, Terraform CLI will request a state upgrade from the provider with the prior state version and expecting the state to match the current version.
18+
1. The framework will check the resource to see if it defines state upgrade support:
19+
* If no state upgrade support is defined, an error diagnostic is returned.
20+
* If state upgrade support is defined, but not for the requested prior state version, an error diagnostic is returned.
21+
* If state upgrade support is defined and has an implementation for the requested prior state version, the provider defined implementation is executed.
22+
23+
## Implementing State Upgrade Support
24+
25+
Ensure the [`tfsdk.Schema` type `Version` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#Schema.Version) for the [`tfsdk.ResourceType`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ResourceType) is greater than `0`, then implement the [`tfsdk.ResourceWithStateUpgrade` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ResourceWithStateUpgrade) for the [`tfsdk.Resource`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#Resource). Conventionally the version is incremented by `1` for each state upgrade.
26+
27+
This example shows a `ResourceType` which incremented the `Schema` type `Version` field:
28+
29+
```go
30+
// Other ResourceType methods are omitted in this example
31+
var _ tfsdk.ResourceType = exampleResourceType{}
32+
33+
type exampleResourceType struct{/* ... */}
34+
35+
func (t exampleResourceType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
36+
return tfsdk.Schema{
37+
// ... other fields ...
38+
39+
// This example conventionally declares that the resource has prior
40+
// state versions of 0 and 1, while the current version is 2.
41+
Version: 2,
42+
}
43+
}
44+
```
45+
46+
This example shows a `Resource` with the necessary `StateUpgrade` method to implement the `ResourceWithStateUpgrade` interface:
47+
48+
```go
49+
// Other Resource methods are omitted in this example
50+
var _ tfsdk.Resource = exampleResource{}
51+
var _ tfsdk.ResourceWithUpgradeState = exampleResource{}
52+
53+
type exampleResource struct{/* ... */}
54+
55+
func (r exampleResource) UpgradeState(ctx context.Context) map[int64]tfsdk.ResourceStateUpgrader {
56+
return map[int64]tfsdk.ResourceStateUpgrader{
57+
// State upgrade implementation from 0 (prior state version) to 2 (Schema.Version)
58+
0: {
59+
// Optionally, the PriorSchema field can be defined.
60+
StateUpgrader: func(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { /* ... */ },
61+
},
62+
// State upgrade implementation from 1 (prior state version) to 2 (Schema.Version)
63+
1: {
64+
// Optionally, the PriorSchema field can be defined.
65+
StateUpgrader: func(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { /* ... */ },
66+
},
67+
}
68+
}
69+
```
70+
71+
Each [`tfsdk.ResourceStateUpgrader`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ResourceStateUpgrader) implementation is expected to wholly upgrade the resource state from the prior version to the current version. The framework does not iterate through intermediate version implementations as incrementing versions by 1 is only conventional and not required.
72+
73+
All state data must be populated in the [`tfsdk.UpgradeResourceStateResponse`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateResponse). The framework does not copy any prior state data from the [`tfsdk.UpgradeResourceStateRequest`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateRequest).
74+
75+
There are two approaches to implementing the provider logic for state upgrades in `ResourceStateUpgrader`. The recommended approach is defining the prior schema matching the resource state, which allows for prior state access similar to other parts of the framework. The second, more advanced, approach is accessing the prior resource state using lower level data handlers.
76+
77+
### ResourceStateUpgrader With PriorSchema
78+
79+
Implement the [`ResourceStateUpgrader` type `PriorSchema` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#ResourceStateUpgrader.PriorSchema) to enable the framework to populate the [`tfsdk.UpgradeResourceStateRequest` type `State` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateRequest.State) for the provider defined state upgrade logic. Access the request `State` using methods such as [`Get()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.Get) or [`GetAttribute()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.GetAttribute). Write the [`tfsdk.UpgradeResourceStateResponse` type `State` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateResponse.State) using methods such as [`Set()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.Set) or [`SetAttribute()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.SetAttribute).
80+
81+
This example shows a resource that changes the type for two attributes, using the `PriorSchema` approach:
82+
83+
```go
84+
// Other ResourceType methods are omitted in this example
85+
var _ tfsdk.ResourceType = exampleResourceType{}
86+
// Other Resource methods are omitted in this example
87+
var _ tfsdk.Resource = exampleResource{}
88+
var _ tfsdk.ResourceWithUpgradeState = exampleResource{}
89+
90+
type exampleResourceType struct{/* ... */}
91+
type exampleResource struct{/* ... */}
92+
93+
type exampleResourceDataV0 struct {
94+
Id string `tfsdk:"id"`
95+
OptionalAttribute *bool `tfsdk:"optional_attribute"`
96+
RequiredAttribute bool `tfsdk:"required_attribute"`
97+
}
98+
99+
type exampleResourceDataV1 struct {
100+
Id string `tfsdk:"id"`
101+
OptionalAttribute *string `tfsdk:"optional_attribute"`
102+
RequiredAttribute string `tfsdk:"required_attribute"`
103+
}
104+
105+
func (t exampleResourceType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
106+
return tfsdk.Schema{
107+
Attributes: map[string]Attribute{
108+
"id": {
109+
Type: types.StringType,
110+
Computed: true,
111+
},
112+
"optional_attribute": {
113+
Type: types.StringType, // As compared to prior types.BoolType below
114+
Optional: true,
115+
},
116+
"required_attribute": {
117+
Type: types.StringType, // As compared to prior types.BoolType below
118+
Required: true,
119+
},
120+
},
121+
// The resource has a prior state version of 0, which had the attribute
122+
// types of types.BoolType as shown below.
123+
Version: 1,
124+
}
125+
}
126+
127+
func (r exampleResource) UpgradeState(ctx context.Context) map[int64]tfsdk.ResourceStateUpgrader {
128+
return map[int64]tfsdk.ResourceStateUpgrader{
129+
// State upgrade implementation from 0 (prior state version) to 1 (Schema.Version)
130+
0: {
131+
PriorSchema: &tfsdk.Schema{
132+
Attributes: map[string]Attribute{
133+
"id": {
134+
Type: types.StringType,
135+
Computed: true,
136+
},
137+
"optional_attribute": {
138+
Type: types.BoolType, // As compared to current types.StringType above
139+
Optional: true,
140+
},
141+
"required_attribute": {
142+
Type: types.BoolType, // As compared to current types.StringType above
143+
Required: true,
144+
},
145+
},
146+
},
147+
StateUpgrader: func(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) {
148+
var priorStateData exampleResourceDataV0
149+
150+
resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)
151+
152+
if resp.Diagnostics.HasError() {
153+
return
154+
}
155+
156+
upgradedStateData := exampleResourceDataV1{
157+
Id: priorStateData.Id,
158+
RequiredAttribute: fmt.Sprintf("%t", priorStateData.RequiredAttribute),
159+
}
160+
161+
if priorStateData.OptionalAttribute != nil {
162+
v := fmt.Sprintf("%t", *priorStateData.OptionalAttribute)
163+
upgradedStateData.OptionalAttribute = &v
164+
}
165+
166+
resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...)
167+
},
168+
},
169+
}
170+
}
171+
```
172+
173+
### ResourceStateUpgrader Without PriorSchema
174+
175+
Read prior state data from the [`tfsdk.UpgradeResourceStateRequest` type `RawState` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateRequest.RawState). Write the [`tfsdk.UpgradeResourceStateResponse` type `State` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateResponse.State) using methods such as [`Set()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.Set) or [`SetAttribute()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State.SetAttribute), or for more advanced use cases, write the [`tfsdk.UpgradeResourceStateResponse` type `DynamicValue` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#UpgradeResourceStateResponse.DynamicValue).
176+
177+
This example shows a resource that changes the type for two attributes, using the `RawState` approach for the request and `DynamicValue` approach for the response:
178+
179+
```go
180+
// Other ResourceType methods are omitted in this example
181+
var _ tfsdk.ResourceType = exampleResourceType{}
182+
// Other Resource methods are omitted in this example
183+
var _ tfsdk.Resource = exampleResource{}
184+
var _ tfsdk.ResourceWithUpgradeState = exampleResource{}
185+
186+
var exampleResourceTftypesDataV0 = tftypes.Object{
187+
AttributeTypes: map[string]tftypes.Type{
188+
"id": tftypes.String,
189+
"optional_attribute": tftypes.Bool,
190+
"required_attribute": tftypes.Bool,
191+
},
192+
}
193+
194+
var exampleResourceTftypesDataV1 = tftypes.Object{
195+
AttributeTypes: map[string]tftypes.Type{
196+
"id": tftypes.String,
197+
"optional_attribute": tftypes.String,
198+
"required_attribute": tftypes.String,
199+
},
200+
}
201+
202+
type exampleResourceType struct{/* ... */}
203+
type exampleResource struct{/* ... */}
204+
205+
func (t exampleResourceType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
206+
return tfsdk.Schema{
207+
Attributes: map[string]Attribute{
208+
"id": {
209+
Type: types.StringType,
210+
Computed: true,
211+
},
212+
"optional_attribute": {
213+
Type: types.StringType, // As compared to prior types.BoolType below
214+
Optional: true,
215+
},
216+
"required_attribute": {
217+
Type: types.StringType, // As compared to prior types.BoolType below
218+
Required: true,
219+
},
220+
},
221+
// The resource has a prior state version of 0, which had the attribute
222+
// types of types.BoolType as shown below.
223+
Version: 1,
224+
}
225+
}
226+
227+
func (r exampleResource) UpgradeState(ctx context.Context) map[int64]tfsdk.ResourceStateUpgrader {
228+
return map[int64]tfsdk.ResourceStateUpgrader{
229+
// State upgrade implementation from 0 (prior state version) to 1 (Schema.Version)
230+
0: {
231+
StateUpgrader: func(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) {
232+
// Refer also to the RawState type JSON field which can be used
233+
// with json.Unmarshal()
234+
rawStateValue, err := req.RawState.Unmarshal(exampleResourceTftypesDataV0)
235+
236+
if err != nil {
237+
resp.Diagnostics.AddError(
238+
"Unable to Unmarshal Prior State",
239+
err.Error(),
240+
)
241+
return
242+
}
243+
244+
var rawState map[string]tftypes.Value
245+
246+
if err := rawStateValue.As(&rawState); err != nil {
247+
resp.Diagnostics.AddError(
248+
"Unable to Convert Prior State",
249+
err.Error(),
250+
)
251+
return
252+
}
253+
254+
var optionalAttributeString *string
255+
256+
if !rawState["optional_attribute"].IsNull() {
257+
var optionalAttribute bool
258+
259+
if err := rawState["optional_attribute"].As(&optionalAttribute); err != nil {
260+
resp.Diagnostics.AddAttributeError(
261+
tftypes.NewAttributePath().WithAttributeName("optional_attribute"),
262+
"Unable to Convert Prior State",
263+
err.Error(),
264+
)
265+
return
266+
}
267+
268+
v := fmt.Sprintf("%t", optionalAttribute)
269+
optionalAttributeString = &v
270+
}
271+
272+
var requiredAttribute bool
273+
274+
if err := rawState["required_attribute"].As(&requiredAttribute); err != nil {
275+
resp.Diagnostics.AddAttributeError(
276+
tftypes.NewAttributePath().WithAttributeName("required_attribute"),
277+
"Unable to Convert Prior State",
278+
err.Error(),
279+
)
280+
return
281+
}
282+
283+
dynamicValue, err := tfprotov6.NewDynamicValue(
284+
exampleResourceTftypesDataV1,
285+
tftypes.NewValue(exampleResourceTftypesDataV1, map[string]tftypes.Value{
286+
"id": rawState["id"],
287+
"optional_attribute": tftypes.NewValue(tftypes.String, optionalAttributeString),
288+
"required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", requiredAttribute)),
289+
}),
290+
)
291+
292+
if err != nil {
293+
resp.Diagnostics.AddError(
294+
"Unable to Convert Upgraded State",
295+
err.Error(),
296+
)
297+
return
298+
}
299+
300+
resp.DynamicValue = &dynamicValue
301+
},
302+
},
303+
}
304+
}
305+
```

content/plugin/which-sdk.mdx

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ The framework is still under development, and so it may not yet support some
4848
features that are available in SDKv2. These include the ability to:
4949

5050
* Use timeouts.
51-
* Define resource state upgraders.
5251

5352
These features are on our roadmap to implement and support, but if you need
5453
them _today_, SDKv2 may be a better choice.

data/plugin-nav-data.json

+4
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@
181181
{
182182
"title": "Plan Modification",
183183
"path": "framework/resources/plan-modification"
184+
},
185+
{
186+
"title": "State Upgrade",
187+
"path": "framework/resources/state-upgrade"
184188
}
185189
]
186190
},

0 commit comments

Comments
 (0)