-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Switch upgradeResourceState to use SDKv2's gRPC method #1735
Changes from 5 commits
efb15f3
42810fc
abeedba
e79f389
c9ea19c
1832fb7
c54e9cd
16cc874
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,82 +6,94 @@ import ( | |
"strconv" | ||
|
||
"github.com/hashicorp/go-cty/cty" | ||
ctyjson "github.com/hashicorp/go-cty/cty/json" | ||
"github.com/hashicorp/go-cty/cty/msgpack" | ||
"github.com/hashicorp/terraform-plugin-go/tfprotov5" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" | ||
) | ||
|
||
func upgradeResourceState(ctx context.Context, p *schema.Provider, res *schema.Resource, | ||
func upgradeResourceState(ctx context.Context, typeName string, p *schema.Provider, res *schema.Resource, | ||
instanceState *terraform.InstanceState) (*terraform.InstanceState, error) { | ||
|
||
if instanceState == nil { | ||
return nil, nil | ||
} | ||
|
||
m := instanceState.Attributes | ||
|
||
// Ensure that we have an ID in the attributes. | ||
m["id"] = instanceState.ID | ||
if state := instanceState.RawState.AsValueMap(); !has(state, "id") { | ||
state["id"] = cty.StringVal(instanceState.ID) | ||
instanceState.RawState = cty.ObjectVal(state) | ||
} | ||
|
||
version, hasVersion := 0, false | ||
version, hasVersion := int64(0), false | ||
if versionValue, ok := instanceState.Meta["schema_version"]; ok { | ||
versionString, ok := versionValue.(string) | ||
if !ok { | ||
return nil, fmt.Errorf("unexpected type %T for schema_version", versionValue) | ||
} | ||
v, err := strconv.ParseInt(versionString, 0, 32) | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("failed to parse schema_version: %w", err) | ||
} | ||
version, hasVersion = int(v), true | ||
version, hasVersion = v, true | ||
} | ||
|
||
// First, build a JSON state from the InstanceState. | ||
json, version, err := schema.UpgradeFlatmapState(ctx, version, m, res, p.Meta()) | ||
json, err := ctyjson.Marshal(instanceState.RawState, res.CoreConfigSchema().ImpliedType()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eyes on this line please, I think res.CoreConfigSchema().ImpliedType() comes from the current provider schema but instanceState.RawState may be on prev provider schema in the case of actual upgrade. We could try inferring the type from the value. I think it's about tradeoffs both approaches slightly problematic. If this is exactly how it worked before perhaps can leave that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be able to change that to json, err := ctyjson.Marshal(instanceState.RawState, res.RawState.Type()) I'll add a test where we do a more substantial upgrade to verify that this works as expected. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It didn't work without the change. Good catch! I have added the fix and a test to go with it. |
||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("failed to marshal RawState: %w", err) | ||
} | ||
|
||
// Next, migrate the JSON state up to the current version. | ||
json, err = schema.UpgradeJSONState(ctx, version, json, res, p.Meta()) | ||
// Now, we perform the UpgradeResourceState operation by calling into TF's UpgradeResourceState. | ||
|
||
resp, err := schema.NewGRPCProviderServer(p). | ||
UpgradeResourceState(ctx, &tfprotov5.UpgradeResourceStateRequest{ | ||
TypeName: typeName, | ||
Version: version, | ||
RawState: &tfprotov5.RawState{JSON: json}, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("upgrade resource state GRPC: %w", err) | ||
} | ||
|
||
configBlock := res.CoreConfigSchema() | ||
|
||
// Strip out removed fields. | ||
schema.RemoveAttributes(ctx, json, configBlock.ImpliedType()) | ||
|
||
// now we need to turn the state into the default json representation, so | ||
// that it can be re-decoded using the actual schema. | ||
v, err := schema.JSONMapToStateValue(json, configBlock) | ||
if err != nil { | ||
return nil, err | ||
// Handle returned diagnostics. | ||
var dd diag.Diagnostics | ||
for _, d := range resp.Diagnostics { | ||
if d == nil { | ||
continue | ||
} | ||
rd := recoverDiagnostic(*d) | ||
dd = append(dd, rd) | ||
logDiag(ctx, rd) | ||
} | ||
if err := diagToError(dd); err != nil { | ||
return nil, fmt.Errorf("diag: %w", err) | ||
} | ||
|
||
// Now we need to make sure blocks are represented correctly, which means | ||
// that missing blocks are empty collections, rather than null. | ||
// First we need to CoerceValue to ensure that all object types match. | ||
v, err = configBlock.CoerceValue(v) | ||
// Unmarshal to get back the underlying type. | ||
var rawState cty.Value | ||
if resp.UpgradedState.JSON != nil { | ||
rawState, err = ctyjson.Unmarshal(resp.UpgradedState.JSON, res.CoreConfigSchema().ImpliedType()) | ||
} | ||
if resp.UpgradedState.MsgPack != nil { | ||
rawState, err = msgpack.Unmarshal(resp.UpgradedState.MsgPack, res.CoreConfigSchema().ImpliedType()) | ||
} | ||
if err != nil { | ||
return nil, err | ||
return nil, fmt.Errorf("failed to unmarshal: %w", err) | ||
} | ||
|
||
// Normalize the value and fill in any missing blocks. | ||
v = schema.NormalizeObjectFromLegacySDK(v, configBlock) | ||
|
||
// Convert the value back to an InstanceState. | ||
newState, err := res.ShimInstanceStateFromValue(v) | ||
newState, err := res.ShimInstanceStateFromValue(rawState) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
newState.RawConfig = instanceState.RawConfig | ||
|
||
// Copy the original ID and meta to the new state and stamp in the new version. | ||
newState.ID = instanceState.ID | ||
|
||
// If state upgraders have modified the ID, respect the modification. | ||
if updatedID, ok := findID(v); ok { | ||
if updatedID, ok := findID(rawState); ok { | ||
newState.ID = updatedID | ||
} | ||
|
||
|
@@ -90,7 +102,7 @@ func upgradeResourceState(ctx context.Context, p *schema.Provider, res *schema.R | |
if newState.Meta == nil { | ||
newState.Meta = map[string]interface{}{} | ||
} | ||
newState.Meta["schema_version"] = strconv.Itoa(version) | ||
newState.Meta["schema_version"] = strconv.Itoa(int(version)) | ||
} | ||
return newState, nil | ||
} | ||
|
@@ -108,3 +120,8 @@ func findID(v cty.Value) (string, bool) { | |
} | ||
return id.AsString(), true | ||
} | ||
|
||
func has[K comparable, V any](m map[K]V, k K) bool { | ||
_, ok := m[k] | ||
return ok | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package sdkv2 | ||
|
||
import ( | ||
"context" | ||
// "math/big" | ||
"testing" | ||
|
||
"github.com/hashicorp/go-cty/cty" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestUpgradeResourceState(t *testing.T) { | ||
state := func() *terraform.InstanceState { | ||
n, err := cty.ParseNumberVal("641577219598130723") | ||
require.NoError(t, err) | ||
v := cty.ObjectVal(map[string]cty.Value{"x": n}) | ||
s := terraform.NewInstanceStateShimmedFromValue(v, 0) | ||
s.Meta["schema_version"] = "0" | ||
s.ID = "id" | ||
s.RawState = v | ||
s.Attributes["id"] = s.ID | ||
return s | ||
} | ||
|
||
rSchema := &schema.Resource{ | ||
Schema: map[string]*schema.Schema{ | ||
"x": {Type: schema.TypeInt, Optional: true}, | ||
}, | ||
} | ||
|
||
require.NoError(t, rSchema.InternalValidate(rSchema.Schema, true)) | ||
|
||
t.Logf(`Attributes["x"]: %s`, state().Attributes["x"]) | ||
|
||
const tfToken = "test_token" | ||
|
||
actual, err := upgradeResourceState(context.Background(), tfToken, &schema.Provider{ | ||
ResourcesMap: map[string]*schema.Resource{ | ||
tfToken: { | ||
UseJSONNumber: true, | ||
Schema: map[string]*schema.Schema{ | ||
"x": {Type: schema.TypeInt, Optional: true}, | ||
}, | ||
}, | ||
}, | ||
}, rSchema, state()) | ||
|
||
require.NoError(t, err) | ||
assert.Equal(t, state().Attributes, actual.Attributes) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a check for downgrades here - pretty sure TF doesn't allow downgrades, see #1998 (comment)