From a1b57c136c0b92b20d304ca9014b6cc73a1ac4ef Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Tue, 10 Nov 2020 02:20:17 -0800 Subject: [PATCH] Add the ability to unmarshal RawState. The tfprotov5.RawState type exposes state through a []byte containing JSON when upgrading resource state with the UpgradeResourceState RPC. We had not surfaced any JSON parsing or decoding in the exported API, so providers would need to parse the JSON themselves. To solve this, we use the same Unmarshal interface we use for parsing JSON from DynamicValues, letting users receive a tftypes.Value from a RawState without needing to worry about encoding... mostly. There is one hitch. The Flatmap syntax doesn't have a canonical translation (yet? As far as I know) to Terraform's type system. So we sidestep that by returning an error, for the moment. In the future, we could conceivably institute a mapping, though I'm not convinced it's a good idea to, given the number of assumptions that would need to be made. It is a larger undertaking to do that in a predictable, reliable way. Provider developers should provide their own mapping of Flatmap to tftypes.Value. The Flatmap property should only ever be populated in one specific scenario, as best I can tell: 1. The user _was_ using a version of Terraform below 0.12, and that is the last version of Terraform to write the state file. 2. The user upgrades to 0.12+ and obtains a new release of the provider 3. The new release of the provider is built on plugin-go At that point, the plugin-go-based provider would be the first version of the provider to write the state after 0.12, meaning it would be its responsibility to upgrade the state between the flatmap syntax and Terraform's type system. It is unknown how likely that scenario is, but I want to document it here to avoid any confusion. --- tfprotov5/state.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tfprotov5/state.go b/tfprotov5/state.go index 4c0bc454f..d66ed5c5d 100644 --- a/tfprotov5/state.go +++ b/tfprotov5/state.go @@ -1,5 +1,25 @@ package tfprotov5 +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5/tftypes" +) + +// ErrUnknownRawStateType is returned when a RawState has no Flatmap or JSON +// bytes set. This should never be returned during the normal operation of a +// provider, and indicates one of the following: +// +// 1. terraform-plugin-go is out of sync with the protocol and should be +// updated. +// +// 2. terrafrom-plugin-go has a bug. +// +// 3. The `RawState` was generated or modified by something other than +// terraform-plugin-go and is no longer a valid value. +var ErrUnknownRawStateType = errors.New("RawState had no JSON or flatmap data set") + // RawState is the raw, undecoded state for providers to upgrade. It is // undecoded as Terraform, for whatever reason, doesn't have the previous // schema available to it, and so cannot decode the state itself and pushes @@ -13,3 +33,47 @@ type RawState struct { JSON []byte Flatmap map[string]string } + +// Unmarshal returns a `tftypes.Value` that represents the information +// contained in the RawState in an easy-to-interact-with way. It is the +// main purpose of the RawState type, and is how provider developers should +// obtain state values from the UpgradeResourceState RPC call. +// +// Pass in the type you want the `Value` to be interpreted as. Terraform's type +// system encodes in a lossy manner, meaning the type information is not +// preserved losslessly when going over the wire. Sets, lists, and tuples all +// look the same. Objects and maps all look the same, as well, as do +// user-specified values when DynamicPseudoType is used in the schema. +// Fortunately, the provider should already know the type; it should be the +// type of the schema, or DynamicPseudoType if that's what's in the schema. +// `Unmarshal` will then parse the value as though it belongs to that type, if +// possible, and return a `tftypes.Value` with the appropriate information. If +// the data can't be interpreted as that type, an error will be returned saying +// so. In these cases, double check to make sure the schema is declaring the +// same type being passed into `Unmarshal`. +// +// In the event an ErrUnknownRawStateType is returned, one of three things +// has happened: +// +// 1. terraform-plugin-go is out of date and out of sync with the protocol, and +// an issue should be opened on its repo to get it updated. +// +// 2. terraform-plugin-go has a bug somewhere, and an issue should be opened on +// its repo to get it fixed. +// +// 3. The provider or a dependency has modified the `RawState` in an +// unsupported way, or has created one from scratch, and should treat it as +// opaque and not modify it, only calling `Unmarshal` on `RawState`s received +// from RPC requests. +// +// State files written before Terraform 0.12 that haven't been upgraded yet +// cannot be unmarshaled, and must have their Flatmap property read directly. +func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) { + if s.JSON != nil { + return jsonUnmarshal(s.JSON, typ, tftypes.AttributePath{}) + } + if s.Flatmap != nil { + return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled") + } + return tftypes.Value{}, ErrUnknownRawStateType +}