diff --git a/internal/fromproto5/upgraderesourceidentity.go b/internal/fromproto5/upgraderesourceidentity.go new file mode 100644 index 000000000..4b279d057 --- /dev/null +++ b/internal/fromproto5/upgraderesourceidentity.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// UpgradeResourceIdentityRequest returns the *fwserver.UpgradeResourceIdentityRequest +// equivalent of a *tfprotov5.UpgradeResourceIdentityRequest. +func UpgradeResourceIdentityRequest(ctx context.Context, proto5 *tfprotov5.UpgradeResourceIdentityRequest, resource resource.Resource, identitySchema fwschema.Schema) (*fwserver.UpgradeResourceIdentityRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if identitySchema == nil { + diags.AddError( + "Unable to Create Empty Identity", + "An unexpected error was encountered when creating the empty Identity. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.UpgradeResourceIdentityRequest{ + RawState: (*tfprotov6.RawState)(proto5.RawIdentity), + IdentitySchema: identitySchema, + Resource: resource, + Version: proto5.Version, + } + + return fw, diags +} diff --git a/internal/fromproto5/upgraderesourceidentity_test.go b/internal/fromproto5/upgraderesourceidentity_test.go new file mode 100644 index 000000000..e7257812d --- /dev/null +++ b/internal/fromproto5/upgraderesourceidentity_test.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestUpgradeResourceIdentityRequest(t *testing.T) { + t.Parallel() + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.UpgradeResourceIdentityRequest + identitySchema fwschema.Schema + resource resource.Resource + expected *fwserver.UpgradeResourceIdentityRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "rawstate": { + input: &tfprotov5.UpgradeResourceIdentityRequest{ + RawIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_attribute": "test-value", + }), + }, + identitySchema: testIdentitySchema, + expected: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewTfprotov6RawState(t, map[string]interface{}{ + "test_attribute": "test-value", + }), + IdentitySchema: testIdentitySchema, + }, + }, + "resourceschema": { + input: &tfprotov5.UpgradeResourceIdentityRequest{}, + identitySchema: testIdentitySchema, + expected: &fwserver.UpgradeResourceIdentityRequest{ + IdentitySchema: testIdentitySchema, + }, + }, + "identityschema-missing": { + input: &tfprotov5.UpgradeResourceIdentityRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Create Empty Identity", + "An unexpected error was encountered when creating the empty Identity. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "version": { + input: &tfprotov5.UpgradeResourceIdentityRequest{ + Version: 123, + }, + identitySchema: testIdentitySchema, + expected: &fwserver.UpgradeResourceIdentityRequest{ + IdentitySchema: testIdentitySchema, + Version: 123, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.UpgradeResourceIdentityRequest(context.Background(), testCase.input, testCase.resource, testCase.identitySchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/upgraderesourceidentity.go b/internal/fromproto6/upgraderesourceidentity.go new file mode 100644 index 000000000..849eb3b28 --- /dev/null +++ b/internal/fromproto6/upgraderesourceidentity.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// UpgradeResourceIdentityRequest returns the *fwserver.UpgradeResourceIdentityRequest +// equivalent of a *tfprotov6.UpgradeResourceIdentityRequest. +func UpgradeResourceIdentityRequest(ctx context.Context, proto6 *tfprotov6.UpgradeResourceIdentityRequest, resource resource.Resource, identitySchema fwschema.Schema) (*fwserver.UpgradeResourceIdentityRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if identitySchema == nil { + diags.AddError( + "Unable to Create Empty Identity", + "An unexpected error was encountered when creating the empty Identity. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.UpgradeResourceIdentityRequest{ + RawState: proto6.RawIdentity, + IdentitySchema: identitySchema, + Resource: resource, + Version: proto6.Version, + } + + return fw, diags +} diff --git a/internal/fromproto6/upgraderesourceidentity_test.go b/internal/fromproto6/upgraderesourceidentity_test.go new file mode 100644 index 000000000..6cf766e6d --- /dev/null +++ b/internal/fromproto6/upgraderesourceidentity_test.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestUpgradeResourceIdentityRequest(t *testing.T) { + t.Parallel() + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.UpgradeResourceIdentityRequest + identitySchema fwschema.Schema + resource resource.Resource + expected *fwserver.UpgradeResourceIdentityRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "rawIdentity": { + input: &tfprotov6.UpgradeResourceIdentityRequest{ + RawIdentity: testNewRawState(t, map[string]interface{}{ + "test_attribute": "test-value", + }), + }, + identitySchema: testIdentitySchema, + expected: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "test_attribute": "test-value", + }), + IdentitySchema: testIdentitySchema, + }, + }, + "resourceschema": { + input: &tfprotov6.UpgradeResourceIdentityRequest{}, + identitySchema: testIdentitySchema, + expected: &fwserver.UpgradeResourceIdentityRequest{ + IdentitySchema: testIdentitySchema, + }, + }, + "resourceschema-missing": { + input: &tfprotov6.UpgradeResourceIdentityRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Create Empty Identity", + "An unexpected error was encountered when creating the empty Identity. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Missing schema.", + ), + }, + }, + "version": { + input: &tfprotov6.UpgradeResourceIdentityRequest{ + Version: 123, + }, + identitySchema: testIdentitySchema, + expected: &fwserver.UpgradeResourceIdentityRequest{ + IdentitySchema: testIdentitySchema, + Version: 123, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.UpgradeResourceIdentityRequest(context.Background(), testCase.input, testCase.resource, testCase.identitySchema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_upgraderesourceidentity.go b/internal/fwserver/server_upgraderesourceidentity.go new file mode 100644 index 000000000..a67f55793 --- /dev/null +++ b/internal/fwserver/server_upgraderesourceidentity.go @@ -0,0 +1,192 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// UpgradeResourceIdentityRequest is the framework server request for the +// UpgradeResourceIdentity RPC. +type UpgradeResourceIdentityRequest struct { + Resource resource.Resource + IdentitySchema fwschema.Schema + // TypeName is the type of resource that Terraform needs to upgrade the + // identity state for. + TypeName string + + // Version is the version of the identity state the resource currently has. + Version int64 + + // Using a terraform-plugin-go type is not ideal for the framework as almost + // all terraform-plugin-go types have framework abstractions, but if there + // is ever a time where it makes sense to re-evaluate this decision, such as + // a major version bump, it could be changed then. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/340 + RawState *tfprotov6.RawState +} + +// UpgradeResourceIdentityResponse is the framework server response for the +// UpgradeResourceIdentity RPC. +type UpgradeResourceIdentityResponse struct { + UpgradedIdentity *tfsdk.ResourceIdentity + Diagnostics diag.Diagnostics +} + +// UpgradeResourceIdentity implements the framework server UpgradeResourceIdentity RPC. +func (s *Server) UpgradeResourceIdentity(ctx context.Context, req *UpgradeResourceIdentityRequest, resp *UpgradeResourceIdentityResponse) { + if req == nil { + return + } + + // No UpgradedIdentity to return. This could return an error diagnostic about + // the odd scenario, but seems best to allow Terraform CLI to handle the + // situation itself in case it might be expected behavior. + if req.RawState == nil { + return + } + + // Define options to be used when unmarshalling raw state. + // IgnoreUndefinedAttributes will silently skip over fields in the JSON + // that do not have a matching entry in the schema. + unmarshalOpts := tfprotov6.UnmarshalOpts{ + ValueFromJSONOpts: tftypes.ValueFromJSONOpts{ + IgnoreUndefinedAttributes: true, + }, + } + + if req.Version == req.IdentitySchema.GetVersion() { + resp.Diagnostics.AddError( + "Unexpected Identity Upgrade Request", + "Terraform Core invoked UpgradeResourceIdentity even though the stored identity schema version matches the current version. "+ + "This likely indicates a bug in the Terraform provider framework or Terraform Core. "+ + "Please report this issue to the provider developer.", + ) + return + } + + if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure") + + configureReq := resource.ConfigureRequest{ + ProviderData: s.ResourceConfigureData, + } + configureResp := resource.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Resource Configure") + resourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined Resource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + resourceWithUpgradeResourceIdentity, ok := req.Resource.(resource.ResourceWithUpgradeIdentity) + + if !ok { + resp.Diagnostics.AddError( + "Unable to Upgrade Resource Identity", + "This resource was implemented without an UpgradeResourceIdentity() 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 + } + + logging.FrameworkTrace(ctx, "Resource implements ResourceWithUpgradeIdentity") + + logging.FrameworkTrace(ctx, "Calling provider defined Resource UpgradeIdentity") + resourceIdentityUpgraders := resourceWithUpgradeResourceIdentity.UpgradeIdentity(ctx) + logging.FrameworkTrace(ctx, "Called provider defined Resource UpgradeIdentity") + + // Panic prevention + if resourceIdentityUpgraders == nil { + resourceIdentityUpgraders = make(map[int64]resource.IdentityUpgrader, 0) + } + + resourceIdentityUpgrader, ok := resourceIdentityUpgraders[req.Version] + + if !ok { + resp.Diagnostics.AddError( + "Unable to Upgrade Resource Identity", + "This resource was implemented with an UpgradeResourceIdentity() 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 + } + + UpgradeResourceIdentityRequest := resource.UpgradeIdentityRequest{ + RawIdentity: req.RawState, + } + + if resourceIdentityUpgrader.PriorSchema != nil { + logging.FrameworkTrace(ctx, "Initializing populated UpgradeIdentityRequest Identity from provider defined prior schema and request RawState") + + priorSchemaType := resourceIdentityUpgrader.PriorSchema.Type().TerraformType(ctx) + + rawIdentityValue, err := req.RawState.UnmarshalWithOpts(priorSchemaType, unmarshalOpts) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Previously Saved Identity for UpgradeResourceIdentity", + fmt.Sprintf("There was an error reading the saved resource Identity 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 + } + + UpgradeResourceIdentityRequest.Identity = &tfsdk.ResourceIdentity{ + Raw: rawIdentityValue, // from the output of req.RawState.UnmarshalWithOpts + Schema: *resourceIdentityUpgrader.PriorSchema, + } + + } + + UpgradeResourceIdentityResponse := resource.UpgradeIdentityResponse{ + Identity: &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + // Raw is intentionally not set. + }, + } + + // To simplify provider logic, this could perform a best effort attempt + // to populate the response Identity by looping through all Attribute/Block + // by calling the equivalent of SetAttribute(GetAttribute()) and skipping + // any errors. + + logging.FrameworkTrace(ctx, "Calling provider defined IdentityUpgrader") + resourceIdentityUpgrader.IdentityUpgrader(ctx, UpgradeResourceIdentityRequest, &UpgradeResourceIdentityResponse) + logging.FrameworkTrace(ctx, "Called provider defined IdentityUpgrader") + + resp.Diagnostics.Append(UpgradeResourceIdentityResponse.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + + if UpgradeResourceIdentityResponse.Identity.Raw.Type() == nil || UpgradeResourceIdentityResponse.Identity.Raw.IsNull() { + resp.Diagnostics.AddError( + "Missing Upgraded Resource Identity", + fmt.Sprintf("After attempting a resource Identity upgrade to version %d, the provider did not return any Identity data. ", req.Version)+ + "Preventing the unexpected loss of resource Identity data. "+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + ) + return + } + + resp.UpgradedIdentity = UpgradeResourceIdentityResponse.Identity +} diff --git a/internal/fwserver/server_upgraderesourceidentity_test.go b/internal/fwserver/server_upgraderesourceidentity_test.go new file mode 100644 index 000000000..48c15c85d --- /dev/null +++ b/internal/fwserver/server_upgraderesourceidentity_test.go @@ -0,0 +1,545 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "encoding/json" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestServerUpgradeResourceIdentity(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + Version: 1, + } + + schemaIdentityType := testIdentitySchema.Type().TerraformType(ctx) + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.UpgradeResourceIdentityRequest + expectedResponse *fwserver.UpgradeResourceIdentityResponse + }{ + "empty-provider": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{}, + }, + "resource-configure-data": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + ResourceConfigureData: "test-provider-configure-value", + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithConfigureAndUpgradeResourceIdentity{ + ConfigureMethod: func(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + PriorSchema: &identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + }, + }, + IdentityUpgrader: func(ctx context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + // In practice, the Configure method would save the + // provider data to the Resource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + + rawStateValue, err := req.RawIdentity.Unmarshal(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Previously Saved Identity for UpgradeResourceIdentity", + fmt.Sprintf("There was an error reading the saved resource Identity using the prior resource schema defined for version %d upgrade.\n\n", req.Identity.Schema.GetVersion())+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + rawValues := make(map[string]tftypes.Value) + err = rawStateValue.As(&rawValues) + if err != nil { + resp.Diagnostics.AddError( + "Unable to convert raw state value into prior identity struct", + fmt.Sprintf("There was an error converting the raw state value into the prior resource identity struct for version %d upgrade.\n\n", req.Identity.Schema.GetVersion())+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + + priorIdentityId := rawValues["id"] + var id string + if priorIdentityId.Type().Is(tftypes.String) { + err := priorIdentityId.As(&id) + if err != nil { + resp.Diagnostics.AddError( + "Unable to convert raw state id value into string", + fmt.Sprintf("There was an error converting the raw state id value into string for version %d upgrade.\n\n", req.Identity.Schema.GetVersion())+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + } + + upgradedIdentityData := struct { + Id string `tfsdk:"id"` + }{ + Id: id, + } + + resp.Diagnostics.Append(resp.Identity.Set(ctx, upgradedIdentityData)...) + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + }, + }, + }, + "RawState-missing": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + IdentitySchema: testIdentitySchema, + Resource: &testprovider.Resource{}, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{}, + }, + "RawState-Unmarshal-and-ResourceIdentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(ctx context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + RawStateValue, err := req.RawIdentity.Unmarshal(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior Identity", + err.Error(), + ) + return + } + + var RawState map[string]tftypes.Value + + if err := RawStateValue.As(&RawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Convert Prior Identity", + err.Error(), + ) + return + } + + ResourceIdentity := &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "id": RawState["id"], + }), + Schema: testIdentitySchema, + } + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Convert Upgraded Identity", + err.Error(), + ) + return + } + + resp.Identity = ResourceIdentity + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + }, + }, + }, + "RawState-JSON-and-ResourceIdentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(ctx context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + var RawState struct { + Id string `json:"id"` + } + + if err := json.Unmarshal(req.RawIdentity.JSON, &RawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior Identity", + err.Error(), + ) + return + } + + ResourceIdentity := tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, RawState.Id), + }), + Schema: testIdentitySchema, + } + + resp.Identity = &ResourceIdentity + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + }, + }, + }, + "ResourceType-UpgradeResourceIdentity-not-implemented": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.Resource{}, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Upgrade Resource Identity", + "This resource was implemented without an UpgradeResourceIdentity() 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.", + ), + }, + }, + }, + "ResourceType-UpgradeResourceIdentity-empty": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return nil + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Upgrade Resource Identity", + "This resource was implemented with an UpgradeResourceIdentity() 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.", + ), + }, + }, + }, + "PriorSchema-incorrect": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "optional_for_import_attribute": true, + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + PriorSchema: &identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "optional_for_import_attribute": identityschema.Int64Attribute{ // Purposefully incorrect + OptionalForImport: true, + }, + }, + }, + IdentityUpgrader: func(ctx context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + // Expect error before reaching this logic. + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Read Previously Saved Identity for UpgradeResourceIdentity", + "There was an error reading the saved resource Identity using the prior resource schema defined for version 0 upgrade.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "AttributeName(\"optional_for_import_attribute\"): unsupported type bool sent as tftypes.Number", + ), + }, + }, + }, + "PriorSchema-and-Identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + PriorSchema: &testIdentitySchema, + IdentityUpgrader: func(ctx context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + rawStateValue, err := req.RawIdentity.Unmarshal(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read Previously Saved Identity for UpgradeResourceIdentity", + fmt.Sprintf("There was an error reading the saved resource Identity using the prior resource schema defined for version %d upgrade.\n\n", req.Identity.Schema.GetVersion())+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + rawValues := make(map[string]tftypes.Value) + err = rawStateValue.As(&rawValues) + if err != nil { + resp.Diagnostics.AddError( + "Unable to convert raw state value into prior identity struct", + fmt.Sprintf("There was an error converting the raw state value into the prior resource identity struct for version %d upgrade.\n\n", req.Identity.Schema.GetVersion())+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + + priorIdentityId := rawValues["id"] + var id string + if priorIdentityId.Type().Is(tftypes.String) { + err := priorIdentityId.As(&id) + if err != nil { + resp.Diagnostics.AddError( + "Unable to convert raw state id value into string", + fmt.Sprintf("There was an error converting the raw state id value into string for version %d upgrade.\n\n", req.Identity.Schema.GetVersion())+ + "Please report this to the provider developer:\n\n"+err.Error(), + ) + return + } + } + + upgradedIdentityData := struct { + Id string `tfsdk:"id"` + }{ + Id: id, + } + + resp.Diagnostics.Append(resp.Identity.Set(ctx, upgradedIdentityData)...) + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + }, + }, + }, + "UpgradedIdentity-missing": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(ctx context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + // Purposfully not setting resp.ResourceIdentity or resp.UpgradedIdentity + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Upgraded Resource Identity", + "After attempting a resource Identity upgrade to version 0, the provider did not return any Identity data. "+ + "Preventing the unexpected loss of resource Identity data. "+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + ), + }, + }, + }, + "Version-not-implemented": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceIdentityRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + }), + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{}, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return nil + }, + }, + Version: 999, + }, + expectedResponse: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Upgrade Resource Identity", + "This resource was implemented with an UpgradeResourceIdentity() 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.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.UpgradeResourceIdentityResponse{} + testCase.server.UpgradeResourceIdentity(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_upgraderesourceidentity.go b/internal/proto5server/server_upgraderesourceidentity.go index f5b1979f1..a0dcfb463 100644 --- a/internal/proto5server/server_upgraderesourceidentity.go +++ b/internal/proto5server/server_upgraderesourceidentity.go @@ -5,11 +5,50 @@ package proto5server import ( "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // UpgradeResourceIdentity satisfies the tfprotov5.ProviderServer interface. func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto5Req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) { - panic("unimplemented") // TODO:ResourceIdentity: implement + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.UpgradeResourceIdentityResponse{} + + if proto5Req == nil { + return toproto5.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + resource, diags := s.FrameworkServer.Resource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.UpgradeResourceIdentityRequest(ctx, proto5Req, resource, identitySchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + s.FrameworkServer.UpgradeResourceIdentity(ctx, fwReq, fwResp) + + return toproto5.UpgradeResourceIdentityResponse(ctx, fwResp), nil } diff --git a/internal/proto5server/server_upgraderesourceidentity_test.go b/internal/proto5server/server_upgraderesourceidentity_test.go new file mode 100644 index 000000000..70849ccc2 --- /dev/null +++ b/internal/proto5server/server_upgraderesourceidentity_test.go @@ -0,0 +1,297 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerUpgradeResourceIdentity(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "optional_attribute": schema.StringAttribute{ + Optional: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + Version: 1, // Must be above 0 + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + Version: 1, // Must be above 0 + } + + ctx := context.Background() + testIdentityType := testIdentitySchema.Type().TerraformType(ctx) + + testCases := map[string]struct { + server *Server + request *tfprotov5.UpgradeResourceIdentityRequest + expectedResponse *tfprotov5.UpgradeResourceIdentityResponse + expectedError error + }{ + "nil": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + }, + request: nil, + expectedResponse: &tfprotov5.UpgradeResourceIdentityResponse{}, + }, + "request-RawIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(_ context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + expectedSourceIdentity := testNewTfprotov6RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }) + + if diff := cmp.Diff(req.RawIdentity, expectedSourceIdentity); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentity difference", diff) + } + + // Prevent Missing Upgraded Resource Identity error + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + } + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.UpgradeResourceIdentityRequest{ + RawIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TypeName: "test_resource", + Version: 0, + }, + expectedResponse: &tfprotov5.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + }, + }, + }, + "request-TypeName-missing": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + }, + request: &tfprotov5.UpgradeResourceIdentityRequest{}, + expectedResponse: &tfprotov5.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Resource Type Not Found", + Detail: "No resource type named \"\" was found in the provider.", + }, + }, + }, + }, + "request-TypeName-unknown": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + }, + request: &tfprotov5.UpgradeResourceIdentityRequest{ + TypeName: "unknown", + }, + expectedResponse: &tfprotov5.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Resource Type Not Found", + Detail: "No resource type named \"unknown\" was found in the provider.", + }, + }, + }, + }, + "response-Diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(_ context.Context, _ resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.UpgradeResourceIdentityRequest{ + RawIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_resource", + Version: 0, + }, + expectedResponse: &tfprotov5.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "response-UpgradedIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(_ context.Context, _ resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + } + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.UpgradeResourceIdentityRequest{ + RawIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TypeName: "test_resource", + Version: 0, + }, + expectedResponse: &tfprotov5.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.UpgradeResourceIdentity(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_upgraderesourceidentity.go b/internal/proto6server/server_upgraderesourceidentity.go index 042efce8e..7484eb49f 100644 --- a/internal/proto6server/server_upgraderesourceidentity.go +++ b/internal/proto6server/server_upgraderesourceidentity.go @@ -5,11 +5,50 @@ package proto6server import ( "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // UpgradeResourceIdentity satisfies the tfprotov6.ProviderServer interface. func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto6Req *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { - panic("unimplemented") // TODO:ResourceIdentity: implement + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.UpgradeResourceIdentityResponse{} + + if proto6Req == nil { + return toproto6.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + resource, diags := s.FrameworkServer.Resource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.UpgradeResourceIdentityRequest(ctx, proto6Req, resource, identitySchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.UpgradeResourceIdentityResponse(ctx, fwResp), nil + } + + s.FrameworkServer.UpgradeResourceIdentity(ctx, fwReq, fwResp) + + return toproto6.UpgradeResourceIdentityResponse(ctx, fwResp), nil } diff --git a/internal/proto6server/server_upgraderesourceidentity_test.go b/internal/proto6server/server_upgraderesourceidentity_test.go new file mode 100644 index 000000000..092f8b2da --- /dev/null +++ b/internal/proto6server/server_upgraderesourceidentity_test.go @@ -0,0 +1,297 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerUpgradeResourceIdentity(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "optional_attribute": schema.StringAttribute{ + Optional: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + Version: 1, // Must be above 0 + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + Version: 1, // Must be above 0 + } + + ctx := context.Background() + testIdentityType := testIdentitySchema.Type().TerraformType(ctx) + + testCases := map[string]struct { + server *Server + request *tfprotov6.UpgradeResourceIdentityRequest + expectedResponse *tfprotov6.UpgradeResourceIdentityResponse + expectedError error + }{ + "nil": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + }, + request: nil, + expectedResponse: &tfprotov6.UpgradeResourceIdentityResponse{}, + }, + "request-RawIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(_ context.Context, req resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + expectedSourceIdentity := testNewRawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }) + + if diff := cmp.Diff(req.RawIdentity, expectedSourceIdentity); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentity difference", diff) + } + + // Prevent Missing Upgraded Resource Identity error + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + } + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.UpgradeResourceIdentityRequest{ + RawIdentity: testNewRawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TypeName: "test_resource", + Version: 0, + }, + expectedResponse: &tfprotov6.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + }, + }, + }, + "request-TypeName-missing": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + }, + request: &tfprotov6.UpgradeResourceIdentityRequest{}, + expectedResponse: &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource Type Not Found", + Detail: "No resource type named \"\" was found in the provider.", + }, + }, + }, + }, + "request-TypeName-unknown": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + }, + request: &tfprotov6.UpgradeResourceIdentityRequest{ + TypeName: "unknown", + }, + expectedResponse: &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource Type Not Found", + Detail: "No resource type named \"unknown\" was found in the provider.", + }, + }, + }, + }, + "response-Diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(_ context.Context, _ resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.UpgradeResourceIdentityRequest{ + RawIdentity: testNewRawState(t, map[string]interface{}{ + "test_id": "test-id-value", + "required_attribute": true, + }), + TypeName: "test_resource", + Version: 0, + }, + expectedResponse: &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "response-UpgradedIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithUpgradeResourceIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + UpgradeResourceIdentityMethod: func(ctx context.Context) map[int64]resource.IdentityUpgrader { + return map[int64]resource.IdentityUpgrader{ + 0: { + IdentityUpgrader: func(_ context.Context, _ resource.UpgradeIdentityRequest, resp *resource.UpgradeIdentityResponse) { + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + Schema: testIdentitySchema, + } + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.UpgradeResourceIdentityRequest{ + RawIdentity: testNewRawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TypeName: "test_resource", + Version: 0, + }, + expectedResponse: &tfprotov6.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + }), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.UpgradeResourceIdentity(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/testing/testprovider/resourcewithconfigureandupgradeidentity.go b/internal/testing/testprovider/resourcewithconfigureandupgradeidentity.go new file mode 100644 index 000000000..7234dda47 --- /dev/null +++ b/internal/testing/testprovider/resourcewithconfigureandupgradeidentity.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithConfigureAndUpgradeResourceIdentity{} +var _ resource.ResourceWithConfigure = &ResourceWithConfigureAndUpgradeResourceIdentity{} +var _ resource.ResourceWithUpgradeIdentity = &ResourceWithConfigureAndUpgradeResourceIdentity{} + +// Declarative resource.ResourceWithConfigureAndUpgradeResourceIdentity for unit testing. +type ResourceWithConfigureAndUpgradeResourceIdentity struct { + *Resource + + // ResourceWithConfigureAndUpgradeResourceIdentity interface methods + ConfigureMethod func(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) + + // ResourceWithUpgradeResourceIdentity interface methods + UpgradeResourceIdentityMethod func(context.Context) map[int64]resource.IdentityUpgrader +} + +// Configure satisfies the resource.ResourceWithConfigureAndUpgradeResourceIdentity interface. +func (r *ResourceWithConfigureAndUpgradeResourceIdentity) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} + +// UpgradeResourceIdentity satisfies the resource.ResourceWithUpgradeResourceIdentity interface. +func (r *ResourceWithConfigureAndUpgradeResourceIdentity) UpgradeIdentity(ctx context.Context) map[int64]resource.IdentityUpgrader { + if r.UpgradeResourceIdentityMethod == nil { + return nil + } + + return r.UpgradeResourceIdentityMethod(ctx) +} diff --git a/internal/testing/testprovider/resourcewithupgradeidentity.go b/internal/testing/testprovider/resourcewithupgradeidentity.go new file mode 100644 index 000000000..48af0f92b --- /dev/null +++ b/internal/testing/testprovider/resourcewithupgradeidentity.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithUpgradeResourceIdentity{} +var _ resource.ResourceWithUpgradeIdentity = &ResourceWithUpgradeResourceIdentity{} + +// Declarative resource.ResourceWithUpgradeResourceIdentity for unit testing. +type ResourceWithUpgradeResourceIdentity struct { + *Resource + + // ResourceWithUpgradeResourceIdentity interface methods + UpgradeResourceIdentityMethod func(context.Context) map[int64]resource.IdentityUpgrader + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) +} + +// UpgradeResourceIdentity satisfies the resource.ResourceWithUpgradeResourceIdentity interface. +func (p *ResourceWithUpgradeResourceIdentity) UpgradeIdentity(ctx context.Context) map[int64]resource.IdentityUpgrader { + if p.UpgradeResourceIdentityMethod == nil { + return nil + } + + return p.UpgradeResourceIdentityMethod(ctx) +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithUpgradeResourceIdentity) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} diff --git a/internal/toproto5/upgraderesourceidentity.go b/internal/toproto5/upgraderesourceidentity.go new file mode 100644 index 000000000..57ea1d9a8 --- /dev/null +++ b/internal/toproto5/upgraderesourceidentity.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// UpgradeResourceIdentityResponse returns the *tfprotov5.UpgradeResourceIdentityResponse +// equivalent of a *fwserver.UpgradeResourceIdentityResponse. +func UpgradeResourceIdentityResponse(ctx context.Context, fw *fwserver.UpgradeResourceIdentityResponse) *tfprotov5.UpgradeResourceIdentityResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.UpgradeResourceIdentityResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + upgradedIdentity, diags := ResourceIdentity(ctx, fw.UpgradedIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.UpgradedIdentity = upgradedIdentity + + return proto5 +} diff --git a/internal/toproto5/upgraderesourceidentity_test.go b/internal/toproto5/upgraderesourceidentity_test.go new file mode 100644 index 000000000..619303bd0 --- /dev/null +++ b/internal/toproto5/upgraderesourceidentity_test.go @@ -0,0 +1,149 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestUpgradeResourceIdentityResponse(t *testing.T) { + t.Parallel() + + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testCases := map[string]struct { + input *fwserver.UpgradeResourceIdentityResponse + expected *tfprotov5.UpgradeResourceIdentityResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.UpgradeResourceIdentityResponse{}, + expected: &tfprotov5.UpgradeResourceIdentityResponse{}, + }, + "diagnostics": { + input: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "diagnostics-invalid-upgradedIdentity": { + input: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + UpgradedIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, + "upgradedIdentity": { + input: &fwserver.UpgradeResourceIdentityResponse{ + UpgradedIdentity: testIdentity, + }, + expected: &tfprotov5.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.UpgradeResourceIdentityResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/upgraderesourceidentity.go b/internal/toproto6/upgraderesourceidentity.go new file mode 100644 index 000000000..2748a4172 --- /dev/null +++ b/internal/toproto6/upgraderesourceidentity.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// UpgradeResourceIdentityResponse returns the *tfprotov6.UpgradeResourceIdentityResponse +// equivalent of a *fwserver.UpgradeResourceIdentityResponse. +func UpgradeResourceIdentityResponse(ctx context.Context, fw *fwserver.UpgradeResourceIdentityResponse) *tfprotov6.UpgradeResourceIdentityResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + upgradedIdentity, diags := ResourceIdentity(ctx, fw.UpgradedIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.UpgradedIdentity = upgradedIdentity + + return proto6 +} diff --git a/internal/toproto6/upgraderesourceidentity_test.go b/internal/toproto6/upgraderesourceidentity_test.go new file mode 100644 index 000000000..191d51832 --- /dev/null +++ b/internal/toproto6/upgraderesourceidentity_test.go @@ -0,0 +1,149 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestUpgradeResourceIdentityResponse(t *testing.T) { + t.Parallel() + + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testCases := map[string]struct { + input *fwserver.UpgradeResourceIdentityResponse + expected *tfprotov6.UpgradeResourceIdentityResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.UpgradeResourceIdentityResponse{}, + expected: &tfprotov6.UpgradeResourceIdentityResponse{}, + }, + "diagnostics": { + input: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "diagnostics-invalid-upgradedIdentity": { + input: &fwserver.UpgradeResourceIdentityResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + UpgradedIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.UpgradeResourceIdentityResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, + "upgradedIdentity": { + input: &fwserver.UpgradeResourceIdentityResponse{ + UpgradedIdentity: testIdentity, + }, + expected: &tfprotov6.UpgradeResourceIdentityResponse{ + UpgradedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.UpgradeResourceIdentityResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identity_upgrader.go b/resource/identity_upgrader.go new file mode 100644 index 000000000..cee02d283 --- /dev/null +++ b/resource/identity_upgrader.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" +) + +// Implementation handler for an UpgradeIdentity operation. +// +// This is used to encapsulate all upgrade logic from a prior identity to the +// current version when a Resource implements the +// ResourceWithUpgradeIdentity interface. +type IdentityUpgrader struct { + // Schema information for the prior identity version. While not required, + // setting this will populate the UpgradeIdentityRequest type Identity + // field similar to other Resource data types. This allows for easier data + // handling such as calling Get() or GetAttribute(). + // + // If not set, prior identity data is available in the + // UpgradeIdentityRequest type RawIdentity field. + PriorSchema *identityschema.Schema + + // Provider defined logic for upgrading a resource identity from the prior + // identity version to the current schema version. + // + // The context.Context parameter contains framework-defined loggers and + // supports request cancellation. + // + // The UpgradeIdentityRequest parameter contains the prior identity data. + // If PriorSchema was set, the Identity field will be available. Otherwise, + // the RawIdentity must be used. + // + // The UpgradeIdentityResponse parameter should contain the upgraded + // identity data and can be used to signal any logic warnings or errors. + IdentityUpgrader func(context.Context, UpgradeIdentityRequest, *UpgradeIdentityResponse) +} diff --git a/resource/resource.go b/resource/resource.go index 2cf334a77..b15bbb80b 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -223,3 +223,18 @@ type ResourceWithIdentity interface { // IdentitySchema should return the identity schema for this resource. IdentitySchema(context.Context, IdentitySchemaRequest, *IdentitySchemaResponse) } + +type ResourceWithUpgradeIdentity interface { + Resource + + // A mapping of the prior identity version to current identity upgrade + // implementation. Only the specified identity upgrader for the prior identity + // version is called, rather than each version in between, so it must + // encapsulate all logic to convert the prior identity to the current identity 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 identity version not be implemented. + UpgradeIdentity(context.Context) map[int64]IdentityUpgrader +} diff --git a/resource/upgrade_identity.go b/resource/upgrade_identity.go new file mode 100644 index 000000000..ade327cca --- /dev/null +++ b/resource/upgrade_identity.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// Request information for the provider logic to update a resource identity +// from a prior resource identity version to the current identity version. +type UpgradeIdentityRequest struct { + // Previous state of the resource identity in JSON format + // (Terraform CLI 0.12 and later) This data is always available, + // regardless of whether the wrapping IdentityUpgrader type + // PriorSchema field was present. + // + // This is advanced functionality for providers wanting to skip the full + // redeclaration of older identity schemas and instead use lower level handlers + // to transform data. A typical implementation for working with this data will + // call the Unmarshal() method. + RawIdentity *tfprotov6.RawState + + // Previous identity of the resource if the wrapping IdentityUpgrader + // type PriorSchema field was present. When available, this allows for + // easier data handling such as calling Get() or GetAttribute(). + Identity *tfsdk.ResourceIdentity +} + +// Response information for the provider logic to update a resource identity +// from a prior resource identity version to the current identity version. +type UpgradeIdentityResponse struct { + // Upgraded identity of the resource, which should match the current identity + //schema version. + // + // This field allows for easier data handling such as calling Set() or + // SetAttribute(). + // + // All data must be populated to prevent data loss during the upgrade + // operation. No prior identity data is copied automatically. + Identity *tfsdk.ResourceIdentity + + // Diagnostics report errors or warnings related to upgrading the resource + // identity state. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +}