Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/fwserver: Use existing data instead of path-based lookups du…
…ring plan modification (#468) Reference: #293 This change is a major rewrite of the plan modification logic to move away from globally mutating the schema response to instead localize handling of each attribute/block. The logic still transverses downward in the schema while updated plan modifier values are passed up the call stack to eventually overwrite the top level attribute/block data. The main benefit of this logic is that it cannot lose track of which element to update within a set and may have some nominal performance improvements. The majority of the logic changes are convincing the plan modification process that nested attributes and blocks have specific types for their data, which allows it to safely traverse deeper based on the schema definition. There is just a lot of repeated diagnostics handling. The new `TestAttributeModifyPlan/attribute-set-nested-usestateforunknown` and `TestBlockModifyPlan/block-set-nested-usestateforunknown` unit test cases specifically test a similar issue to the linked one where sets previously could not properly use `resource.UseStateForKnown()` for set nested attributes or set blocks.
- Loading branch information
Showing
8 changed files
with
5,162 additions
and
3,534 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:bug | ||
internal/fwserver: Fixed alignment of set type plan modification | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package fwserver | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/hashicorp/terraform-plugin-framework/attr" | ||
"github.com/hashicorp/terraform-plugin-framework/diag" | ||
"github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
"github.com/hashicorp/terraform-plugin-go/tftypes" | ||
) | ||
|
||
func coerceListValue(schemaPath path.Path, value attr.Value) (types.List, diag.Diagnostics) { | ||
list, ok := value.(types.List) | ||
|
||
if !ok { | ||
return types.List{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationWalkError(schemaPath, value), | ||
} | ||
} | ||
|
||
return list, nil | ||
} | ||
|
||
func coerceMapValue(schemaPath path.Path, value attr.Value) (types.Map, diag.Diagnostics) { | ||
m, ok := value.(types.Map) | ||
|
||
if !ok { | ||
return types.Map{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationWalkError(schemaPath, value), | ||
} | ||
} | ||
|
||
return m, nil | ||
} | ||
|
||
func coerceObjectValue(schemaPath path.Path, value attr.Value) (types.Object, diag.Diagnostics) { | ||
object, ok := value.(types.Object) | ||
|
||
if !ok { | ||
return types.Object{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationWalkError(schemaPath, value), | ||
} | ||
} | ||
|
||
return object, nil | ||
} | ||
|
||
func coerceSetValue(schemaPath path.Path, value attr.Value) (types.Set, diag.Diagnostics) { | ||
set, ok := value.(types.Set) | ||
|
||
if !ok { | ||
return types.Set{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationWalkError(schemaPath, value), | ||
} | ||
} | ||
|
||
return set, nil | ||
} | ||
|
||
func listElemObject(ctx context.Context, schemaPath path.Path, list types.List, index int, description fwschemadata.DataDescription) (types.Object, diag.Diagnostics) { | ||
if list.IsNull() { | ||
return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, nil) | ||
} | ||
|
||
if list.IsUnknown() { | ||
return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, tftypes.UnknownValue) | ||
} | ||
|
||
if index > len(list.Elems) { | ||
return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, nil) | ||
} | ||
|
||
return coerceObjectValue(schemaPath, list.Elems[index]) | ||
} | ||
|
||
func listElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, list types.List, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) { | ||
elemValue, err := list.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(list.ElemType.TerraformType(ctx), tfValue)) | ||
|
||
if err != nil { | ||
return types.Object{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationValueError(ctx, list, description, err), | ||
} | ||
} | ||
|
||
return coerceObjectValue(schemaPath, elemValue) | ||
} | ||
|
||
func mapElemObject(ctx context.Context, schemaPath path.Path, m types.Map, key string, description fwschemadata.DataDescription) (types.Object, diag.Diagnostics) { | ||
if m.IsNull() { | ||
return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, nil) | ||
} | ||
|
||
if m.IsUnknown() { | ||
return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, tftypes.UnknownValue) | ||
} | ||
|
||
elemValue, ok := m.Elems[key] | ||
|
||
if !ok { | ||
return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, nil) | ||
} | ||
|
||
return coerceObjectValue(schemaPath, elemValue) | ||
} | ||
|
||
func mapElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, m types.Map, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) { | ||
elemValue, err := m.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(m.ElemType.TerraformType(ctx), tfValue)) | ||
|
||
if err != nil { | ||
return types.Object{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationValueError(ctx, m, description, err), | ||
} | ||
} | ||
|
||
return coerceObjectValue(schemaPath, elemValue) | ||
} | ||
|
||
func objectAttributeValue(ctx context.Context, object types.Object, attributeName string, description fwschemadata.DataDescription) (attr.Value, diag.Diagnostics) { | ||
if object.IsNull() { | ||
return objectAttributeValueFromTerraformValue(ctx, object, attributeName, description, nil) | ||
} | ||
|
||
if object.IsUnknown() { | ||
return objectAttributeValueFromTerraformValue(ctx, object, attributeName, description, tftypes.UnknownValue) | ||
} | ||
|
||
// A panic here indicates a bug somewhere else in the framework or an | ||
// invalid test case. | ||
return object.Attrs[attributeName], nil | ||
} | ||
|
||
func objectAttributeValueFromTerraformValue(ctx context.Context, object types.Object, attributeName string, description fwschemadata.DataDescription, tfValue any) (attr.Value, diag.Diagnostics) { | ||
// A panic here indicates a bug somewhere else in the framework or an | ||
// invalid test case. | ||
attrType := object.AttrTypes[attributeName] | ||
|
||
elemValue, err := attrType.ValueFromTerraform(ctx, tftypes.NewValue(attrType.TerraformType(ctx), tfValue)) | ||
|
||
if err != nil { | ||
return nil, diag.Diagnostics{ | ||
attributePlanModificationValueError(ctx, object, description, err), | ||
} | ||
} | ||
|
||
return elemValue, nil | ||
} | ||
|
||
func setElemObject(ctx context.Context, schemaPath path.Path, set types.Set, index int, description fwschemadata.DataDescription) (types.Object, diag.Diagnostics) { | ||
if set.IsNull() { | ||
return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, nil) | ||
} | ||
|
||
if set.IsUnknown() { | ||
return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, tftypes.UnknownValue) | ||
} | ||
|
||
if index > len(set.Elems) { | ||
return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, nil) | ||
} | ||
|
||
return coerceObjectValue(schemaPath, set.Elems[index]) | ||
} | ||
|
||
func setElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, set types.Set, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) { | ||
elemValue, err := set.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(set.ElemType.TerraformType(ctx), tfValue)) | ||
|
||
if err != nil { | ||
return types.Object{Null: true}, diag.Diagnostics{ | ||
attributePlanModificationValueError(ctx, set, description, err), | ||
} | ||
} | ||
|
||
return coerceObjectValue(schemaPath, elemValue) | ||
} |
Oops, something went wrong.