diff --git a/.changelog/292.txt b/.changelog/292.txt new file mode 100644 index 000000000..93949e442 --- /dev/null +++ b/.changelog/292.txt @@ -0,0 +1,3 @@ +```release-note:feature +tfsdk: Added optional `ResourceWithUpgradeState` interface, which allows for provider defined logic when the `UpgradeResourceState` RPC is called +``` diff --git a/README.md b/README.md index 7e39a0894..fc9cb7863 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ terraform-plugin-framework is a module for building Terraform providers. It is b terraform-plugin-framework is still in **technical preview**. We are committed to moving forward with the module, but cannot guarantee any of its interfaces will not change as long as it is in version 0. We're waiting for more feedback, usage, and maturity before we're comfortable committing to APIs with the same years-long support timelines that [terraform-plugin-sdk](https://github.com/hashicorp/terraform-plugin-sdk) brings. We do not expect practitioner experiences to break or change as a result of these changes, only the abstractions surfaced to provider developers. -terraform-plugin-framework is also not at full feature parity with terraform-plugin-sdk yet. Notably, it doesn't offer support for [upgrading resource state](https://github.com/hashicorp/terraform-plugin-framework/issues/42) or [using timeouts](https://github.com/hashicorp/terraform-plugin-framework/issues/62). We plan to add these features soon. See [Which SDK Should I Use?](https://terraform.io/docs/plugin/which-sdk.html) on terraform.io for more information. +terraform-plugin-framework is also not at full feature parity with terraform-plugin-sdk yet. Notably, it doesn't offer support for [using timeouts](https://github.com/hashicorp/terraform-plugin-framework/issues/62). We plan to add these features soon. See [Which SDK Should I Use?](https://terraform.io/docs/plugin/which-sdk.html) on terraform.io for more information. We believe terraform-plugin-framework is still a suitable and reliable module to build Terraform providers on, and encourage community members that can afford occasional breaking changes to build with it. terraform-plugin-framework will eventually become a new major version of terraform-plugin-sdk, at which point its interfaces will be stable, but we need real-world use and feedback before we can be comfortable making those commitments. When that happens, this repository will be archived. diff --git a/tfsdk/resource_upgrade_state.go b/tfsdk/resource_upgrade_state.go new file mode 100644 index 000000000..614ae9b03 --- /dev/null +++ b/tfsdk/resource_upgrade_state.go @@ -0,0 +1,121 @@ +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// Optional interface on top of Resource that enables provider control over +// the UpgradeResourceState RPC. This RPC is automatically called by Terraform +// when the current Schema type Version field is greater than the stored state. +// Terraform does not store previous Schema information, so any breaking +// changes to state data types must be handled by providers. +// +// Terraform CLI can execute the UpgradeResourceState RPC even when the prior +// state version matches the current schema version. The framework will +// automatically intercept this request and attempt to respond with the +// existing state. In this situation the framework will not execute any +// provider defined logic, so declaring it for this version is extraneous. +type ResourceWithUpgradeState interface { + // A mapping of prior state version to current schema version state upgrade + // implementations. Only the specified state upgrader for the prior state + // version is called, rather than each version in between, so it must + // encapsulate all logic to convert the prior state to the current schema + // version. + // + // Version keys begin at 0, which is the default schema version when + // undefined. The framework will return an error diagnostic should the + // requested state version not be implemented. + UpgradeState(context.Context) map[int64]ResourceStateUpgrader +} + +// Implementation handler for a UpgradeResourceState operation. +// +// This is used to encapsulate all upgrade logic from a prior state to the +// current schema version when a Resource implements the +// ResourceWithUpgradeState interface. +type ResourceStateUpgrader struct { + // Schema information for the prior state version. While not required, + // setting this will populate the UpgradeResourceStateRequest type State + // field similar to other Resource data types. This allows for easier data + // handling such as calling Get() or GetAttribute(). + // + // If not set, prior state data is available in the + // UpgradeResourceStateRequest type RawState field. + PriorSchema *Schema + + // Provider defined logic for upgrading a resource state from the prior + // state version to the current schema version. + // + // The context.Context parameter contains framework-defined loggers and + // supports request cancellation. + // + // The UpgradeResourceStateRequest parameter contains the prior state data. + // If PriorSchema was set, the State field will be available. Otherwise, + // the RawState must be used. + // + // The UpgradeResourceStateResponse parameter should contain the upgraded + // state data and can be used to signal any logic warnings or errors. + StateUpgrader func(context.Context, UpgradeResourceStateRequest, *UpgradeResourceStateResponse) +} + +// Request information for the provider logic to update a resource state +// from a prior state version to the current schema version. An instance of +// this is supplied as a parameter to the StateUpgrader function defined in a +// ResourceStateUpgrader, which ultimately comes from a Resource's +// UpgradeState method. +type UpgradeResourceStateRequest struct { + // Previous state of the resource in JSON (Terraform CLI 0.12 and later) + // or flatmap format, depending on which version of Terraform CLI last + // wrote the resource state. This data is always available, regardless + // whether the wrapping ResourceStateUpgrader type PriorSchema field was + // present. + // + // This is advanced functionality for providers wanting to skip the full + // redeclaration of older schemas and instead use lower level handlers to + // transform data. A typical implementation for working with this data will + // call the Unmarshal() method. + RawState *tfprotov6.RawState + + // Previous state of the resource if the wrapping ResourceStateUpgrader + // type PriorSchema field was present. When available, this allows for + // easier data handling such as calling Get() or GetAttribute(). + State *State +} + +// Response information for the provider logic to update a resource state +// from a prior state version to the current schema version. An instance of +// this is supplied as a parameter to the StateUpgrader function defined in a +// ResourceStateUpgrader, which ultimately came from a Resource's +// UpgradeState method. +type UpgradeResourceStateResponse struct { + // Diagnostics report errors or warnings related to upgrading the resource + // state. An empty slice indicates a successful operation with no warnings + // or errors generated. + Diagnostics diag.Diagnostics + + // Upgraded state of the resource, which should match the current schema + // version. If set, this will override State. + // + // This field is intended only for advanced provider functionality, such as + // skipping the full redeclaration of older schemas or using lower level + // handlers to transform data. Call tfprotov6.NewDynamicValue() to set this + // value. + // + // All data must be populated to prevent data loss during the upgrade + // operation. No prior state data is copied automatically. + DynamicValue *tfprotov6.DynamicValue + + // Upgraded state of the resource, which should match the current schema + // version. If DynamicValue is set, it will override this value. + // + // This field allows for easier data handling such as calling Set() or + // SetAttribute(). It is generally recommended over working with the lower + // level types and functionality required for DynamicValue. + // + // All data must be populated to prevent data loss during the upgrade + // operation. No prior state data is copied automatically. + State State +} diff --git a/tfsdk/serve.go b/tfsdk/serve.go index 13afbd5ed..5a0f94c43 100644 --- a/tfsdk/serve.go +++ b/tfsdk/serve.go @@ -557,16 +557,6 @@ func (s *server) upgradeResourceState(ctx context.Context, req *tfprotov6.Upgrad return } - // This implementation assumes the current schema is the only valid schema - // for the given resource and will return an error if any mismatched prior - // state is given. This matches prior behavior of the framework, but is now - // more explicit in error handling, rather than just passing through any - // potentially errant prior state, which should have resulted in a similar - // error further in the resource lifecycle. - // - // TODO: Implement resource state upgrades, rather than just using the - // current resource schema. - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/42 resourceSchema, diags := resourceType.GetSchema(ctx) resp.Diagnostics.Append(diags...) @@ -575,32 +565,167 @@ func (s *server) upgradeResourceState(ctx context.Context, req *tfprotov6.Upgrad return } - resourceSchemaType := resourceSchema.TerraformType(ctx) + // Terraform CLI can call UpgradeResourceState even if the stored state + // version matches the current schema. Presumably this is to account for + // the previous terraform-plugin-sdk implementation, which handled some + // state fixups on behalf of Terraform CLI. When this happens, we do not + // want to return errors for a missing ResourceWithUpgradeState + // implementation or an undefined version within an existing + // ResourceWithUpgradeState implementation as that would be confusing + // detail for provider developers. Instead, the framework will attempt to + // roundtrip the prior RawState to a State matching the current Schema. + // + // TODO: To prevent provider developers from accidentially implementing + // ResourceWithUpgradeState with a version matching the current schema + // version which would never get called, the framework can introduce a + // unit test helper. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/113 + if req.Version == resourceSchema.Version { + logging.FrameworkTrace(ctx, "UpgradeResourceState request version matches current Schema version, using framework defined passthrough implementation") - rawStateValue, err := req.RawState.Unmarshal(resourceSchemaType) + resourceSchemaType := resourceSchema.TerraformType(ctx) - if err != nil { + rawStateValue, err := req.RawState.Unmarshal(resourceSchemaType) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Previously Saved State for UpgradeResourceState", + "There was an error reading the saved resource state using the current resource schema.\n\n"+ + "If this resource state was last refreshed with Terraform CLI 0.11 and earlier, it must be refreshed or applied with an older provider version first. "+ + "If you manually modified the resource state, you will need to manually modify it to match the current resource schema. "+ + "Otherwise, please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + + // NewDynamicValue will ensure the Msgpack field is set for Terraform CLI + // 0.12 through 0.14 compatibility when using terraform-plugin-mux tf6to5server. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/262 + upgradedStateValue, err := tfprotov6.NewDynamicValue(resourceSchemaType, rawStateValue) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Convert Previously Saved State for UpgradeResourceState", + "There was an error converting the saved resource state using the current resource schema. "+ + "This is always an issue in the Terraform Provider SDK used to implement the resource and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + + resp.UpgradedState = &upgradedStateValue + + return + } + + resource, diags := resourceType.NewResource(ctx, s.p) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resourceWithUpgradeState, ok := resource.(ResourceWithUpgradeState) + + if !ok { + resp.Diagnostics.AddError( + "Unable to Upgrade Resource State", + "This resource was implemented without an UpgradeState() method, "+ + fmt.Sprintf("however Terraform was expecting an implementation for version %d upgrade.\n\n", req.Version)+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + ) + return + } + + resourceStateUpgraders := resourceWithUpgradeState.UpgradeState(ctx) + + // Panic prevention + if resourceStateUpgraders == nil { + resourceStateUpgraders = make(map[int64]ResourceStateUpgrader, 0) + } + + resourceStateUpgrader, ok := resourceStateUpgraders[req.Version] + + if !ok { + resp.Diagnostics.AddError( + "Unable to Upgrade Resource State", + "This resource was implemented with an UpgradeState() method, "+ + fmt.Sprintf("however Terraform was expecting an implementation for version %d upgrade.\n\n", req.Version)+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + ) + return + } + + upgradeResourceStateRequest := UpgradeResourceStateRequest{ + RawState: req.RawState, + } + + if resourceStateUpgrader.PriorSchema != nil { + logging.FrameworkTrace(ctx, "Initializing populated UpgradeResourceStateRequest state from provider defined prior schema and request RawState") + + priorSchemaType := resourceStateUpgrader.PriorSchema.TerraformType(ctx) + + rawStateValue, err := req.RawState.Unmarshal(priorSchemaType) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Previously Saved State for UpgradeResourceState", + fmt.Sprintf("There was an error reading the saved resource state using the prior resource schema defined for version %d upgrade.\n\n", req.Version)+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + + upgradeResourceStateRequest.State = &State{ + Raw: rawStateValue, + Schema: *resourceStateUpgrader.PriorSchema, + } + } + + upgradeResourceStateResponse := UpgradeResourceStateResponse{ + State: State{ + Schema: resourceSchema, + }, + } + + // To simplify provider logic, this could perform a best effort attempt + // to populate the response State by looping through all Attribute/Block + // by calling the equivalent of SetAttribute(GetAttribute()) and skipping + // any errors. + + logging.FrameworkDebug(ctx, "Calling provider defined StateUpgrader") + resourceStateUpgrader.StateUpgrader(ctx, upgradeResourceStateRequest, &upgradeResourceStateResponse) + logging.FrameworkDebug(ctx, "Called provider defined StateUpgrader") + + resp.Diagnostics.Append(upgradeResourceStateResponse.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + + if upgradeResourceStateResponse.DynamicValue != nil { + logging.FrameworkTrace(ctx, "UpgradeResourceStateResponse DynamicValue set, overriding State") + resp.UpgradedState = upgradeResourceStateResponse.DynamicValue + return + } + + if upgradeResourceStateResponse.State.Raw.Type() == nil || upgradeResourceStateResponse.State.Raw.IsNull() { resp.Diagnostics.AddError( - "Unable to Read Previously Saved State for UpgradeResourceState", - "There was an error reading the saved resource state using the current resource schema. "+ - "This resource was implemented in a Terraform Provider SDK that does not support upgrading resource state yet.\n\n"+ - "If the resource previously implemented different resource state versions, the provider developers will need to revert back to the previous implementation. "+ - "If this resource state was last refreshed with Terraform CLI 0.11 and earlier, it must be refreshed or applied with an older provider version first. "+ - "If you manually modified the resource state, you will need to manually modify it to match the current resource schema. "+ - "Otherwise, please report this to the provider developer:\n\n"+err.Error(), + "Missing Upgraded Resource State", + fmt.Sprintf("After attempting a resource state upgrade to version %d, the provider did not return any state data. ", req.Version)+ + "Preventing the unexpected loss of resource state data. "+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", ) return } - // NewDynamicValue will ensure the Msgpack field is set for Terraform CLI - // 0.12 through 0.14 compatibility when using terraform-plugin-mux tf6to5server. - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/262 - upgradedStateValue, err := tfprotov6.NewDynamicValue(resourceSchemaType, rawStateValue) + upgradedStateValue, err := tfprotov6.NewDynamicValue(upgradeResourceStateResponse.State.Schema.TerraformType(ctx), upgradeResourceStateResponse.State.Raw) if err != nil { resp.Diagnostics.AddError( - "Unable to Convert Previously Saved State for UpgradeResourceState", - "There was an error converting the saved resource state using the current resource schema. "+ + "Unable to Convert Upgraded Resource State", + fmt.Sprintf("An unexpected error was encountered when converting the state returned for version %d upgrade to a usable type. ", req.Version)+ "This is always an issue in the Terraform Provider SDK used to implement the resource and should be reported to the provider developers.\n\n"+ "Please report this to the provider developer:\n\n"+err.Error(), ) diff --git a/tfsdk/serve_provider_test.go b/tfsdk/serve_provider_test.go index 403c7cbcc..907c17d88 100644 --- a/tfsdk/serve_provider_test.go +++ b/tfsdk/serve_provider_test.go @@ -24,9 +24,7 @@ type testServeProvider struct { validateResourceConfigImpl func(context.Context, ValidateResourceConfigRequest, *ValidateResourceConfigResponse) // upgrade resource state - // TODO: Implement with UpgradeResourceState support - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/42 - // upgradeResourceStateCalledResourceType string + upgradeResourceStateCalledResourceType string // read resource request readResourceCurrentStateValue tftypes.Value @@ -635,14 +633,16 @@ var testServeProviderProviderType = tftypes.Object{ func (t *testServeProvider) GetResources(_ context.Context) (map[string]ResourceType, diag.Diagnostics) { return map[string]ResourceType{ - "test_one": testServeResourceTypeOne{}, - "test_two": testServeResourceTypeTwo{}, - "test_three": testServeResourceTypeThree{}, - "test_attribute_plan_modifiers": testServeResourceTypeAttributePlanModifiers{}, - "test_config_validators": testServeResourceTypeConfigValidators{}, - "test_import_state": testServeResourceTypeImportState{}, - "test_upgrade_state": testServeResourceTypeUpgradeState{}, - "test_validate_config": testServeResourceTypeValidateConfig{}, + "test_one": testServeResourceTypeOne{}, + "test_two": testServeResourceTypeTwo{}, + "test_three": testServeResourceTypeThree{}, + "test_attribute_plan_modifiers": testServeResourceTypeAttributePlanModifiers{}, + "test_config_validators": testServeResourceTypeConfigValidators{}, + "test_import_state": testServeResourceTypeImportState{}, + "test_upgrade_state": testServeResourceTypeUpgradeState{}, + "test_upgrade_state_empty": testServeResourceTypeUpgradeStateEmpty{}, + "test_upgrade_state_not_implemented": testServeResourceTypeUpgradeStateNotImplemented{}, + "test_validate_config": testServeResourceTypeValidateConfig{}, }, nil } diff --git a/tfsdk/serve_resource_upgrade_state_empty_test.go b/tfsdk/serve_resource_upgrade_state_empty_test.go new file mode 100644 index 000000000..993536e6b --- /dev/null +++ b/tfsdk/serve_resource_upgrade_state_empty_test.go @@ -0,0 +1,96 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ ResourceWithUpgradeState = testServeResourceUpgradeStateEmpty{} + +type testServeResourceTypeUpgradeStateEmpty struct{} + +func (t testServeResourceTypeUpgradeStateEmpty) GetSchema(_ context.Context) (Schema, diag.Diagnostics) { + return Schema{ + Attributes: map[string]Attribute{ + "id": { + Type: types.StringType, + Computed: true, + }, + "optional_attribute": { + Type: types.StringType, + Optional: true, + }, + "required_attribute": { + Type: types.StringType, + Required: true, + }, + }, + Version: 1, // Something above 0 + }, nil +} + +func (t testServeResourceTypeUpgradeStateEmpty) NewResource(_ context.Context, p Provider) (Resource, diag.Diagnostics) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeResourceUpgradeStateEmpty{ + provider: provider, + }, nil +} + +var testServeResourceTypeUpgradeStateEmptySchema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Computed: true, + Type: tftypes.String, + }, + { + Name: "optional_attribute", + Optional: true, + Type: tftypes.String, + }, + { + Name: "required_attribute", + Required: true, + Type: tftypes.String, + }, + }, + }, + Version: 1, +} + +type testServeResourceUpgradeStateEmpty struct { + provider *testServeProvider +} + +func (r testServeResourceUpgradeStateEmpty) Create(ctx context.Context, req CreateResourceRequest, resp *CreateResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateEmpty) Read(ctx context.Context, req ReadResourceRequest, resp *ReadResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateEmpty) Update(ctx context.Context, req UpdateResourceRequest, resp *UpdateResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateEmpty) Delete(ctx context.Context, req DeleteResourceRequest, resp *DeleteResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateEmpty) ImportState(ctx context.Context, req ImportResourceStateRequest, resp *ImportResourceStateResponse) { + ResourceImportStateNotImplemented(ctx, "intentionally not implemented", resp) +} + +func (r testServeResourceUpgradeStateEmpty) UpgradeState(ctx context.Context) map[int64]ResourceStateUpgrader { + return nil +} diff --git a/tfsdk/serve_resource_upgrade_state_not_implemented_test.go b/tfsdk/serve_resource_upgrade_state_not_implemented_test.go new file mode 100644 index 000000000..6fbe34a0d --- /dev/null +++ b/tfsdk/serve_resource_upgrade_state_not_implemented_test.go @@ -0,0 +1,90 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type testServeResourceTypeUpgradeStateNotImplemented struct{} + +func (t testServeResourceTypeUpgradeStateNotImplemented) GetSchema(_ context.Context) (Schema, diag.Diagnostics) { + return Schema{ + Attributes: map[string]Attribute{ + "id": { + Type: types.StringType, + Computed: true, + }, + "optional_attribute": { + Type: types.StringType, + Optional: true, + }, + "required_attribute": { + Type: types.StringType, + Required: true, + }, + }, + Version: 1, // Something above 0 + }, nil +} + +func (t testServeResourceTypeUpgradeStateNotImplemented) NewResource(_ context.Context, p Provider) (Resource, diag.Diagnostics) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeResourceUpgradeStateNotImplemented{ + provider: provider, + }, nil +} + +var testServeResourceTypeUpgradeStateNotImplementedSchema = &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Computed: true, + Type: tftypes.String, + }, + { + Name: "optional_attribute", + Optional: true, + Type: tftypes.String, + }, + { + Name: "required_attribute", + Required: true, + Type: tftypes.String, + }, + }, + }, + Version: 1, +} + +type testServeResourceUpgradeStateNotImplemented struct { + provider *testServeProvider +} + +func (r testServeResourceUpgradeStateNotImplemented) Create(ctx context.Context, req CreateResourceRequest, resp *CreateResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateNotImplemented) Read(ctx context.Context, req ReadResourceRequest, resp *ReadResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateNotImplemented) Update(ctx context.Context, req UpdateResourceRequest, resp *UpdateResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateNotImplemented) Delete(ctx context.Context, req DeleteResourceRequest, resp *DeleteResourceResponse) { + // Intentionally blank. Not expected to be called during testing. +} +func (r testServeResourceUpgradeStateNotImplemented) ImportState(ctx context.Context, req ImportResourceStateRequest, resp *ImportResourceStateResponse) { + ResourceImportStateNotImplemented(ctx, "intentionally not implemented", resp) +} diff --git a/tfsdk/serve_resource_upgrade_state_test.go b/tfsdk/serve_resource_upgrade_state_test.go index 56fc8f37b..8dc233fb8 100644 --- a/tfsdk/serve_resource_upgrade_state_test.go +++ b/tfsdk/serve_resource_upgrade_state_test.go @@ -2,6 +2,7 @@ package tfsdk import ( "context" + "encoding/json" "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -10,10 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) -// This resource is a placeholder for UpgradeResourceState testing, -// so it is decoupled from other test resources. -// TODO: Implement UpgradeResourceState support, when added -// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/42 +var _ ResourceWithUpgradeState = testServeResourceUpgradeState{} + type testServeResourceTypeUpgradeState struct{} func (t testServeResourceTypeUpgradeState) GetSchema(_ context.Context) (Schema, diag.Diagnostics) { @@ -23,15 +22,16 @@ func (t testServeResourceTypeUpgradeState) GetSchema(_ context.Context) (Schema, Type: types.StringType, Computed: true, }, - "optional_string": { + "optional_attribute": { Type: types.StringType, Optional: true, }, - "required_string": { + "required_attribute": { Type: types.StringType, Required: true, }, }, + Version: 5, }, nil } @@ -58,32 +58,54 @@ var testServeResourceTypeUpgradeStateSchema = &tfprotov6.Schema{ Type: tftypes.String, }, { - Name: "optional_string", + Name: "optional_attribute", Optional: true, Type: tftypes.String, }, { - Name: "required_string", + Name: "required_attribute", Required: true, Type: tftypes.String, }, }, }, + Version: 5, } -// var testServeResourceTypeUpgradeStateTftype = tftypes.Object{ -// AttributeTypes: map[string]tftypes.Type{ -// "id": tftypes.String, -// "optional_string": tftypes.String, -// "required_string": tftypes.String, -// }, -// } +var ( + testServeResourceTypeUpgradeStateTftypeV0 = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "optional_attribute": tftypes.Bool, + "required_attribute": tftypes.Bool, + }, + } + testServeResourceTypeUpgradeStateTftype = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "optional_attribute": tftypes.String, + "required_attribute": tftypes.String, + }, + } +) -// type testServeResourceUpgradeStateData struct { -// Id string `tfsdk:"id"` -// OptionalString *string `tfsdk:"optional_string"` -// RequiredString string `tfsdk:"required_string"` -// } +type testServeResourceUpgradeStateDataV1 struct { + Id string `json:"id"` + OptionalAttribute *bool `json:"optional_attribute,omitempty"` + RequiredAttribute bool `json:"required_attribute"` +} + +type testServeResourceUpgradeStateDataV2 struct { + Id string `tfsdk:"id"` + OptionalAttribute *bool `tfsdk:"optional_attribute"` + RequiredAttribute bool `tfsdk:"required_attribute"` +} + +type testServeResourceUpgradeStateData struct { + Id string `tfsdk:"id"` + OptionalAttribute *string `tfsdk:"optional_attribute"` + RequiredAttribute string `tfsdk:"required_attribute"` +} type testServeResourceUpgradeState struct { provider *testServeProvider @@ -104,3 +126,184 @@ func (r testServeResourceUpgradeState) Delete(ctx context.Context, req DeleteRes func (r testServeResourceUpgradeState) ImportState(ctx context.Context, req ImportResourceStateRequest, resp *ImportResourceStateResponse) { ResourceImportStateNotImplemented(ctx, "intentionally not implemented", resp) } + +func (r testServeResourceUpgradeState) UpgradeState(ctx context.Context) map[int64]ResourceStateUpgrader { + r.provider.upgradeResourceStateCalledResourceType = "test_upgrade_state" + return map[int64]ResourceStateUpgrader{ + 0: { // Successful state upgrade using RawState.Unmarshal() and DynamicValue + StateUpgrader: func(ctx context.Context, req UpgradeResourceStateRequest, resp *UpgradeResourceStateResponse) { + rawStateValue, err := req.RawState.Unmarshal(testServeResourceTypeUpgradeStateTftypeV0) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior State", + err.Error(), + ) + return + } + + var rawState map[string]tftypes.Value + + if err := rawStateValue.As(&rawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Convert Prior State", + err.Error(), + ) + return + } + + var optionalAttributeString *string + + if !rawState["optional_attribute"].IsNull() { + var optionalAttribute bool + + if err := rawState["optional_attribute"].As(&optionalAttribute); err != nil { + resp.Diagnostics.AddAttributeError( + tftypes.NewAttributePath().WithAttributeName("optional_attribute"), + "Unable to Convert Prior State", + err.Error(), + ) + return + } + + v := fmt.Sprintf("%t", optionalAttribute) + optionalAttributeString = &v + } + + var requiredAttribute bool + + if err := rawState["required_attribute"].As(&requiredAttribute); err != nil { + resp.Diagnostics.AddAttributeError( + tftypes.NewAttributePath().WithAttributeName("required_attribute"), + "Unable to Convert Prior State", + err.Error(), + ) + return + } + + dynamicValue, err := tfprotov6.NewDynamicValue( + testServeResourceTypeUpgradeStateTftype, + tftypes.NewValue(testServeResourceTypeUpgradeStateTftype, map[string]tftypes.Value{ + "id": rawState["id"], + "optional_attribute": tftypes.NewValue(tftypes.String, optionalAttributeString), + "required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", requiredAttribute)), + }), + ) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Convert Upgraded State", + err.Error(), + ) + return + } + + resp.DynamicValue = &dynamicValue + }, + }, + 1: { // Successful state upgrade using RawState.JSON and DynamicValue + StateUpgrader: func(ctx context.Context, req UpgradeResourceStateRequest, resp *UpgradeResourceStateResponse) { + var rawState testServeResourceUpgradeStateDataV1 + + if err := json.Unmarshal(req.RawState.JSON, &rawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior State", + err.Error(), + ) + return + } + + var optionalAttribute *string + + if rawState.OptionalAttribute != nil { + v := fmt.Sprintf("%t", *rawState.OptionalAttribute) + optionalAttribute = &v + } + + dynamicValue, err := tfprotov6.NewDynamicValue( + testServeResourceTypeUpgradeStateTftype, + tftypes.NewValue(testServeResourceTypeUpgradeStateTftype, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, rawState.Id), + "optional_attribute": tftypes.NewValue(tftypes.String, optionalAttribute), + "required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", rawState.RequiredAttribute)), + }), + ) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Upgraded State", + err.Error(), + ) + return + } + + resp.DynamicValue = &dynamicValue + }, + }, + 2: { // Successful state upgrade with PriorSchema and State + PriorSchema: &Schema{ + Attributes: map[string]Attribute{ + "id": { + Type: types.StringType, + Computed: true, + }, + "optional_attribute": { + Type: types.BoolType, + Optional: true, + }, + "required_attribute": { + Type: types.BoolType, + Required: true, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req UpgradeResourceStateRequest, resp *UpgradeResourceStateResponse) { + var priorStateData testServeResourceUpgradeStateDataV2 + + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := testServeResourceUpgradeStateData{ + Id: priorStateData.Id, + RequiredAttribute: fmt.Sprintf("%t", priorStateData.RequiredAttribute), + } + + if priorStateData.OptionalAttribute != nil { + v := fmt.Sprintf("%t", *priorStateData.OptionalAttribute) + upgradedStateData.OptionalAttribute = &v + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...) + }, + }, + 3: { // Incorrect PriorSchema + PriorSchema: &Schema{ + Attributes: map[string]Attribute{ + "id": { + Type: types.StringType, + Computed: true, + }, + "optional_attribute": { + Type: types.Int64Type, // Purposefully incorrect + Optional: true, + }, + "required_attribute": { + Type: types.Int64Type, // Purposefully incorrect + Required: true, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req UpgradeResourceStateRequest, resp *UpgradeResourceStateResponse) { + // Expect error before reaching this logic. + }, + }, + 4: { // Missing upgraded resource data + StateUpgrader: func(ctx context.Context, req UpgradeResourceStateRequest, resp *UpgradeResourceStateResponse) { + // Purposfully not setting resp.DynamicValue or resp.State + }, + }, + } +} diff --git a/tfsdk/serve_test.go b/tfsdk/serve_test.go index b5764a6b4..32da7cc24 100644 --- a/tfsdk/serve_test.go +++ b/tfsdk/serve_test.go @@ -308,14 +308,16 @@ func TestServerGetProviderSchema(t *testing.T) { expected := &tfprotov6.GetProviderSchemaResponse{ Provider: testServeProviderProviderSchema, ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeResourceTypeOneSchema, - "test_two": testServeResourceTypeTwoSchema, - "test_three": testServeResourceTypeThreeSchema, - "test_attribute_plan_modifiers": testServeResourceTypeAttributePlanModifiersSchema, - "test_config_validators": testServeResourceTypeConfigValidatorsSchema, - "test_import_state": testServeResourceTypeImportStateSchema, - "test_upgrade_state": testServeResourceTypeUpgradeStateSchema, - "test_validate_config": testServeResourceTypeValidateConfigSchema, + "test_one": testServeResourceTypeOneSchema, + "test_two": testServeResourceTypeTwoSchema, + "test_three": testServeResourceTypeThreeSchema, + "test_attribute_plan_modifiers": testServeResourceTypeAttributePlanModifiersSchema, + "test_config_validators": testServeResourceTypeConfigValidatorsSchema, + "test_import_state": testServeResourceTypeImportStateSchema, + "test_upgrade_state": testServeResourceTypeUpgradeStateSchema, + "test_upgrade_state_empty": testServeResourceTypeUpgradeStateEmptySchema, + "test_upgrade_state_not_implemented": testServeResourceTypeUpgradeStateNotImplementedSchema, + "test_validate_config": testServeResourceTypeValidateConfigSchema, }, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_one": testServeDataSourceTypeOneSchema, @@ -344,14 +346,16 @@ func TestServerGetProviderSchemaWithProviderMeta(t *testing.T) { expected := &tfprotov6.GetProviderSchemaResponse{ Provider: testServeProviderProviderSchema, ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeResourceTypeOneSchema, - "test_two": testServeResourceTypeTwoSchema, - "test_three": testServeResourceTypeThreeSchema, - "test_attribute_plan_modifiers": testServeResourceTypeAttributePlanModifiersSchema, - "test_config_validators": testServeResourceTypeConfigValidatorsSchema, - "test_import_state": testServeResourceTypeImportStateSchema, - "test_upgrade_state": testServeResourceTypeUpgradeStateSchema, - "test_validate_config": testServeResourceTypeValidateConfigSchema, + "test_one": testServeResourceTypeOneSchema, + "test_two": testServeResourceTypeTwoSchema, + "test_three": testServeResourceTypeThreeSchema, + "test_attribute_plan_modifiers": testServeResourceTypeAttributePlanModifiersSchema, + "test_config_validators": testServeResourceTypeConfigValidatorsSchema, + "test_import_state": testServeResourceTypeImportStateSchema, + "test_upgrade_state": testServeResourceTypeUpgradeStateSchema, + "test_upgrade_state_empty": testServeResourceTypeUpgradeStateEmptySchema, + "test_upgrade_state_not_implemented": testServeResourceTypeUpgradeStateNotImplementedSchema, + "test_validate_config": testServeResourceTypeValidateConfigSchema, }, DataSourceSchemas: map[string]*tfprotov6.Schema{ "test_one": testServeDataSourceTypeOneSchema, @@ -1446,29 +1450,6 @@ func TestServerUpgradeResourceState(t *testing.T) { schema, _ := testServeResourceTypeUpgradeState{}.GetSchema(ctx) schemaType := schema.TerraformType(ctx) - validRawStateJSON, err := json.Marshal(map[string]string{ - "id": "test-id-value", - "required_string": "test-required-value", - }) - - if err != nil { - t.Fatalf("unable to create RawState JSON: %s", err) - } - - validRawState := tfprotov6.RawState{ - JSON: validRawStateJSON, - } - - validUpgradedState, err := tfprotov6.NewDynamicValue(schemaType, tftypes.NewValue(schemaType, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "test-id-value"), - "optional_string": tftypes.NewValue(tftypes.String, nil), - "required_string": tftypes.NewValue(tftypes.String, "test-required-value"), - })) - - if err != nil { - t.Fatalf("unable to create UpgradedState: %s", err) - } - testCases := map[string]struct { request *tfprotov6.UpgradeResourceStateRequest expectedResponse *tfprotov6.UpgradeResourceStateResponse @@ -1478,112 +1459,253 @@ func TestServerUpgradeResourceState(t *testing.T) { request: nil, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{}, }, - "RawState-Flatmap": { + "RawState-missing": { request: &tfprotov6.UpgradeResourceStateRequest{ - TypeName: "test_upgrade_state", - RawState: &tfprotov6.RawState{ - Flatmap: map[string]string{ - "flatmap": "is not supported", + TypeName: "test_upgrade_state_not_implemented", + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{}, + }, + "TypeName-missing": { + request: &tfprotov6.UpgradeResourceStateRequest{}, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource not found", + Detail: "No resource named \"\" is configured on the provider", }, }, }, + }, + "TypeName-UpgradeState-not-implemented": { + request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_upgrade_state_not_implemented", + Version: 0, + }, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Unable to Read Previously Saved State for UpgradeResourceState", - Detail: "There was an error reading the saved resource state using the current resource schema. " + - "This resource was implemented in a Terraform Provider SDK that does not support upgrading resource state yet.\n\n" + - "If the resource previously implemented different resource state versions, the provider developers will need to revert back to the previous implementation. " + - "If this resource state was last refreshed with Terraform CLI 0.11 and earlier, it must be refreshed or applied with an older provider version first. " + - "If you manually modified the resource state, you will need to manually modify it to match the current resource schema. " + - "Otherwise, please report this to the provider developer:\n\n" + - "flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled", + Summary: "Unable to Upgrade Resource State", + Detail: "This resource was implemented without an UpgradeState() method, " + + "however Terraform was expecting an implementation for version 0 upgrade.\n\n" + + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", }, }, }, }, - "RawState-JSON-passthrough": { + "TypeName-UpgradeState-empty": { request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_upgrade_state_empty", + Version: 0, + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Upgrade Resource State", + Detail: "This resource was implemented with an UpgradeState() method, " + + "however Terraform was expecting an implementation for version 0 upgrade.\n\n" + + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + }, + }, + }, + }, + "TypeName-unknown": { + request: &tfprotov6.UpgradeResourceStateRequest{ + TypeName: "unknown", + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource not found", + Detail: "No resource named \"unknown\" is configured on the provider", + }, + }, + }, + }, + "Version-0": { + request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), TypeName: "test_upgrade_state", - RawState: &validRawState, + Version: 0, }, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ - UpgradedState: &validUpgradedState, + UpgradedState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), }, }, - "RawState-JSON-mismatch": { + "Version-1": { request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), TypeName: "test_upgrade_state", - RawState: &tfprotov6.RawState{ - JSON: []byte(`{"nonexistent_attribute":"value"}`), - }, + Version: 1, + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + UpgradedState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + }, + }, + "Version-2": { + request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_upgrade_state", + Version: 2, + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + UpgradedState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + }, + }, + "Version-3": { + request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_upgrade_state", + Version: 3, }, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unable to Read Previously Saved State for UpgradeResourceState", - Detail: "There was an error reading the saved resource state using the current resource schema. " + - "This resource was implemented in a Terraform Provider SDK that does not support upgrading resource state yet.\n\n" + - "If the resource previously implemented different resource state versions, the provider developers will need to revert back to the previous implementation. " + - "If this resource state was last refreshed with Terraform CLI 0.11 and earlier, it must be refreshed or applied with an older provider version first. " + - "If you manually modified the resource state, you will need to manually modify it to match the current resource schema. " + - "Otherwise, please report this to the provider developer:\n\n" + - "ElementKeyValue(tftypes.String): unsupported attribute \"nonexistent_attribute\"", + Detail: "There was an error reading the saved resource state using the prior resource schema defined for version 3 upgrade.\n\n" + + "Please report this to the provider developer:\n\n" + + "AttributeName(\"required_attribute\"): unsupported type bool sent as tftypes.Number", }, }, }, }, - "RawState-empty": { + "Version-4": { request: &tfprotov6.UpgradeResourceStateRequest{ - RawState: &tfprotov6.RawState{}, + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), TypeName: "test_upgrade_state", + Version: 4, + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Missing Upgraded Resource State", + Detail: "After attempting a resource state upgrade to version 4, the provider did not return any state data. " + + "Preventing the unexpected loss of resource state data. " + + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + }, + }, + }, + }, + "Version-current-flatmap": { + request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: &tfprotov6.RawState{ + Flatmap: map[string]string{ + "flatmap": "is not supported", + }, + }, + TypeName: "test_upgrade_state_not_implemented", // Framework should allow non-ResourceWithUpgradeState + Version: 1, // Must match current Schema version to trigger framework implementation }, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, Summary: "Unable to Read Previously Saved State for UpgradeResourceState", - Detail: "There was an error reading the saved resource state using the current resource schema. " + - "This resource was implemented in a Terraform Provider SDK that does not support upgrading resource state yet.\n\n" + - "If the resource previously implemented different resource state versions, the provider developers will need to revert back to the previous implementation. " + + Detail: "There was an error reading the saved resource state using the current resource schema.\n\n" + "If this resource state was last refreshed with Terraform CLI 0.11 and earlier, it must be refreshed or applied with an older provider version first. " + "If you manually modified the resource state, you will need to manually modify it to match the current resource schema. " + "Otherwise, please report this to the provider developer:\n\n" + - "RawState had no JSON or flatmap data set", + "flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled", }, }, }, }, - "RawState-missing": { + "Version-current-json-match": { request: &tfprotov6.UpgradeResourceStateRequest{ - TypeName: "test_upgrade_state", + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": "true", + }), + TypeName: "test_upgrade_state_not_implemented", // Framework should allow non-ResourceWithUpgradeState + Version: 1, // Must match current Schema version to trigger framework implementation + }, + expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ + UpgradedState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), }, - expectedResponse: &tfprotov6.UpgradeResourceStateResponse{}, }, - "TypeName-missing": { - request: &tfprotov6.UpgradeResourceStateRequest{}, + "Version-current-json-mismatch": { + request: &tfprotov6.UpgradeResourceStateRequest{ + RawState: &tfprotov6.RawState{ + JSON: []byte(`{"nonexistent_attribute":"value"}`), + }, + TypeName: "test_upgrade_state_not_implemented", // Framework should allow non-ResourceWithUpgradeState + Version: 1, // Must match current Schema version to trigger framework implementation + }, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Resource not found", - Detail: "No resource named \"\" is configured on the provider", + Summary: "Unable to Read Previously Saved State for UpgradeResourceState", + Detail: "There was an error reading the saved resource state using the current resource schema.\n\n" + + "If this resource state was last refreshed with Terraform CLI 0.11 and earlier, it must be refreshed or applied with an older provider version first. " + + "If you manually modified the resource state, you will need to manually modify it to match the current resource schema. " + + "Otherwise, please report this to the provider developer:\n\n" + + "ElementKeyValue(tftypes.String): unsupported attribute \"nonexistent_attribute\"", }, }, }, }, - "TypeName-unknown": { + "Version-not-implemented": { request: &tfprotov6.UpgradeResourceStateRequest{ - TypeName: "unknown", + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_upgrade_state", + Version: 999, }, expectedResponse: &tfprotov6.UpgradeResourceStateResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Resource not found", - Detail: "No resource named \"unknown\" is configured on the provider", + Summary: "Unable to Upgrade Resource State", + Detail: "This resource was implemented with an UpgradeState() method, " + + "however Terraform was expecting an implementation for version 999 upgrade.\n\n" + + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", }, }, }, @@ -1618,12 +1740,10 @@ func TestServerUpgradeResourceState(t *testing.T) { t.Fatalf("got no error, expected: %s", testCase.expectedError) } - // TODO: Implement with UpgradeResourceState support - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/42 - // if testCase.request != nil && testProvider.upgradeResourceStateCalledResourceType != testCase.request.TypeName { - // t.Errorf("expected to call resource %q, called: %s", testCase.request.TypeName, testProvider.upgradeResourceStateCalledResourceType) - // return - // } + if testCase.request != nil && testCase.request.TypeName == "test_upgrade_state" && testProvider.upgradeResourceStateCalledResourceType != testCase.request.TypeName { + t.Errorf("expected to call resource %q, called: %s", testCase.request.TypeName, testProvider.upgradeResourceStateCalledResourceType) + return + } if diff := cmp.Diff(got, testCase.expectedResponse); diff != "" { t.Errorf("unexpected difference in response: %s", diff) @@ -1632,6 +1752,32 @@ func TestServerUpgradeResourceState(t *testing.T) { } } +func testNewDynamicValue(t *testing.T, schemaType tftypes.Type, schemaValue map[string]tftypes.Value) *tfprotov6.DynamicValue { + t.Helper() + + dynamicValue, err := tfprotov6.NewDynamicValue(schemaType, tftypes.NewValue(schemaType, schemaValue)) + + if err != nil { + t.Fatalf("unable to create DynamicValue: %s", err) + } + + return &dynamicValue +} + +func testNewRawState(t *testing.T, jsonMap map[string]interface{}) *tfprotov6.RawState { + t.Helper() + + rawStateJSON, err := json.Marshal(jsonMap) + + if err != nil { + t.Fatalf("unable to create RawState JSON: %s", err) + } + + return &tfprotov6.RawState{ + JSON: rawStateJSON, + } +} + func TestServerReadResource(t *testing.T) { t.Parallel()