From 5e962cba7c1afaa2b3aa1e5cfa29a06d8fda4138 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 16 Sep 2021 17:32:29 -0400 Subject: [PATCH 01/10] tfsdk: Allow Plan and State SetAttribute to create attribute paths Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/148 --- .changelog/pending.txt | 3 + tfsdk/plan.go | 240 ++++- tfsdk/plan_test.go | 2141 +++++++++++++++++++++++++++++++++++++++- tfsdk/schema.go | 4 + tfsdk/state.go | 240 ++++- tfsdk/state_test.go | 1973 +++++++++++++++++++++++++++++++++--- 6 files changed, 4440 insertions(+), 161 deletions(-) create mode 100644 .changelog/pending.txt diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..18aaf5915 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,3 @@ +```release-note:bug +tfsdk: `(Plan).SetAttribute()` and `(State).SetAttribute()` will now create missing attribute paths instead of silently failing to update. +``` diff --git a/tfsdk/plan.go b/tfsdk/plan.go index 4676efa1c..70d0ce472 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -2,6 +2,7 @@ package tfsdk import ( "context" + "errors" "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -101,6 +102,15 @@ func (p *Plan) Set(ctx context.Context, val interface{}) diag.Diagnostics { } // SetAttribute sets the attribute at `path` using the supplied Go value. +// +// The attribute path and value must be valid with the current schema. If the +// attribute path already has a value, it will be overwritten. If the attribute +// path does not have a value, it will be added, including any parent attribute +// paths as necessary. +// +// Lists can only have the first element added if empty and can only add the +// next element according to the current length, otherwise this will return an +// error. func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, val interface{}) diag.Diagnostics { var diags diag.Diagnostics @@ -133,25 +143,26 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va return diags } - transformFunc := func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { - if p.Equal(path) { - tfVal := tftypes.NewValue(attrType.TerraformType(ctx), newTfVal) + tfVal := tftypes.NewValue(attrType.TerraformType(ctx), newTfVal) - if attrTypeWithValidate, ok := attrType.(attr.TypeWithValidate); ok { - diags.Append(attrTypeWithValidate.Validate(ctx, tfVal, path)...) - - if diags.HasError() { - return v, nil - } - } + if attrTypeWithValidate, ok := attrType.(attr.TypeWithValidate); ok { + diags.Append(attrTypeWithValidate.Validate(ctx, tfVal, path)...) - return tfVal, nil + if diags.HasError() { + return diags } - return v, nil + } + + transformFunc, transformFuncDiags := p.setAttributeTransformFunc(ctx, path, tfVal) + diags.Append(transformFuncDiags...) + + if diags.HasError() { + return diags } p.Raw, err = tftypes.Transform(p.Raw, transformFunc) if err != nil { + err = fmt.Errorf("Cannot transform plan: %w", err) diags.AddAttributeError( path, "Plan Write Error", @@ -163,6 +174,211 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va return diags } +func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { + var diags diag.Diagnostics + + _, remaining, err := tftypes.WalkAttributePath(p.Raw, path) + + if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { + err = fmt.Errorf("Cannot walk attribute path in plan: %w", err) + diags.AddAttributeError( + path, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return nil, diags + } + + if len(remaining.Steps()) == 0 { + // Overwrite existing value + return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { + if p.Equal(path) { + return tfVal, nil + } + return v, nil + }, diags + } + + var parentTfVal tftypes.Value + parentPath := path.WithoutLastStep() + parentAttrType, err := p.Schema.AttributeTypeAtPath(parentPath) + + if err != nil { + err = fmt.Errorf("error getting parent attribute type in schema: %w", err) + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return nil, diags + } + + parentTfType := parentAttrType.TerraformType(ctx) + parentValue, err := p.terraformValueAtPath(parentPath) + + if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { + diags.AddAttributeError( + parentPath, + "Plan Read Error", + "An unexpected error was encountered trying to read an attribute from the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return nil, diags + } + + // Check if parent needs to get created + if parentValue.Equal(tftypes.NewValue(tftypes.Object{}, nil)) { + // NewValue will panic if required attributes are missing in the + // tftypes.Object. + vals := map[string]tftypes.Value{} + for name, t := range parentTfType.(tftypes.Object).AttributeTypes { + vals[name] = tftypes.NewValue(t, nil) + } + parentValue = tftypes.NewValue(parentTfType, vals) + } else if parentValue.Equal(tftypes.Value{}) { + parentValue = tftypes.NewValue(parentTfType, nil) + } + + switch step := remaining.Steps()[len(remaining.Steps())-1].(type) { + case tftypes.AttributeName: + // Add to Object + if !parentValue.Type().Is(tftypes.Object{}) { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add attribute into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentAttrs map[string]tftypes.Value + err = parentValue.Copy().As(&parentAttrs) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract object elements from parent value: %s", err), + ) + return nil, diags + } + + parentAttrs[string(step)] = tfVal + parentTfVal = tftypes.NewValue(parentTfType, parentAttrs) + case tftypes.ElementKeyInt: + // Add new List element + if !parentValue.Type().Is(tftypes.List{}) { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add list element into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentElems []tftypes.Value + err = parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract list elements from parent value: %s", err), + ) + return nil, diags + } + + if int(step) > len(parentElems) { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add list element %d as list currently has %d length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", int(step)+1, len(parentElems)), + ) + return nil, diags + } + + parentElems = append(parentElems, tfVal) + parentTfVal = tftypes.NewValue(parentTfType, parentElems) + case tftypes.ElementKeyString: + // Add new Map element + if !parentValue.Type().Is(tftypes.Map{}) { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add map value into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentElems map[string]tftypes.Value + err = parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract map elements from parent value: %s", err), + ) + return nil, diags + } + + parentElems[string(step)] = tfVal + parentTfVal = tftypes.NewValue(parentTfType, parentElems) + case tftypes.ElementKeyValue: + // Add new Set element + if !parentValue.Type().Is(tftypes.Set{}) { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add set element into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentElems []tftypes.Value + err = parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract set elements from parent value: %s", err), + ) + return nil, diags + } + + parentElems = append(parentElems, tfVal) + parentTfVal = tftypes.NewValue(parentTfType, parentElems) + } + + if attrTypeWithValidate, ok := parentAttrType.(attr.TypeWithValidate); ok { + diags.Append(attrTypeWithValidate.Validate(ctx, parentTfVal, parentPath)...) + + if diags.HasError() { + return nil, diags + } + } + + if len(remaining.Steps()) > 1 { + return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal) + } + + return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { + if p.Equal(parentPath) { + return parentTfVal, nil + } + return v, nil + }, diags +} + func (p Plan) terraformValueAtPath(path *tftypes.AttributePath) (tftypes.Value, error) { rawValue, remaining, err := tftypes.WalkAttributePath(p.Raw, path) if err != nil { diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index c1c52c377..15e28c361 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -389,32 +389,2157 @@ func TestPlanSetAttribute(t *testing.T) { } testCases := map[string]testCase{ - "basic": { + "add-Bool": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, false), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-List": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-List-Element-append": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-List-Element-append-length-error": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(2), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("disks"), + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "add-List-Element-first": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-List-Element-first-length-error": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("disks"), + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "add-Map": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: map[string]string{ + "newkey": "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "newkey": tftypes.NewValue(tftypes.String, "newvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Map-Element-append": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, "key1value"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key2"), + val: "key2value", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, "key1value"), + "key2": tftypes.NewValue(tftypes.String, "key2value"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Map-Element-first": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Number": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.NumberType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: 1, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Object": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), + val: struct { + Interface string `tfsdk:"interface"` + }{ + Interface: "NVME", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Set": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Set-Element-append": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Set-Element-first": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-String": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Bool": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, false), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-List": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-List-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Map": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "originalkey": tftypes.NewValue(tftypes.String, "originalvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: map[string]string{ + "newkey": "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "newkey": tftypes.NewValue(tftypes.String, "newvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Map-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Number": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.NumberType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: 2, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 2), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Object": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "SCSI"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), + val: struct { + Interface string `tfsdk:"interface"` + }{ + Interface: "NVME", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Object-Attribute": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "SCSI"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface"), + val: "NVME", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Set": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Set-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-String": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, + "test": tftypes.String, + "other": tftypes.String, }, }, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "originalname"), + "test": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), Schema: Schema{ Attributes: map[string]Attribute{ - "name": { + "test": { + Type: types.StringType, + Required: true, + }, + "other": { Type: types.StringType, Required: true, }, }, }, }, - path: tftypes.NewAttributePath().WithAttributeName("name"), - val: "newname", + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: "newvalue", expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, + "test": tftypes.String, + "other": tftypes.String, }, }, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "newname"), + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "write-Bool": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, false), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-List": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-List-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-List-Element-length-error": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("disks"), + "Plan Write Error", + "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "write-Map": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: map[string]string{ + "newkey": "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "newkey": tftypes.NewValue(tftypes.String, "newvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Map-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Number": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.NumberType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: 1, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Object": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), + val: struct { + Interface string `tfsdk:"interface"` + }{ + Interface: "NVME", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Set": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Set-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-String": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, nil), }), }, "AttrTypeWithValidateError": { diff --git a/tfsdk/schema.go b/tfsdk/schema.go index a499a7622..ffb2ec105 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -84,6 +84,10 @@ func (s Schema) AttributeTypeAtPath(path *tftypes.AttributePath) (attr.Type, err return n.AttributeType(), nil } + if s, ok := rawType.(Schema); ok { + return s.AttributeType(), nil + } + a, ok := rawType.(Attribute) if !ok { return nil, fmt.Errorf("got unexpected type %T", rawType) diff --git a/tfsdk/state.go b/tfsdk/state.go index 7060a66a8..86085adeb 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -2,6 +2,7 @@ package tfsdk import ( "context" + "errors" "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -110,6 +111,15 @@ func (s *State) Set(ctx context.Context, val interface{}) diag.Diagnostics { } // SetAttribute sets the attribute at `path` using the supplied Go value. +// +// The attribute path and value must be valid with the current schema. If the +// attribute path already has a value, it will be overwritten. If the attribute +// path does not have a value, it will be added, including any parent attribute +// paths as necessary. +// +// Lists can only have the first element added if empty and can only add the +// next element according to the current length, otherwise this will return an +// error. func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, val interface{}) diag.Diagnostics { var diags diag.Diagnostics @@ -142,25 +152,26 @@ func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, v return diags } - transformFunc := func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { - if p.Equal(path) { - tfVal := tftypes.NewValue(attrType.TerraformType(ctx), newTfVal) + tfVal := tftypes.NewValue(attrType.TerraformType(ctx), newTfVal) - if attrTypeWithValidate, ok := attrType.(attr.TypeWithValidate); ok { - diags.Append(attrTypeWithValidate.Validate(ctx, tfVal, path)...) - - if diags.HasError() { - return v, nil - } - } + if attrTypeWithValidate, ok := attrType.(attr.TypeWithValidate); ok { + diags.Append(attrTypeWithValidate.Validate(ctx, tfVal, path)...) - return tfVal, nil + if diags.HasError() { + return diags } - return v, nil + } + + transformFunc, transformFuncDiags := s.setAttributeTransformFunc(ctx, path, tfVal) + diags.Append(transformFuncDiags...) + + if diags.HasError() { + return diags } s.Raw, err = tftypes.Transform(s.Raw, transformFunc) if err != nil { + err = fmt.Errorf("Cannot transform state: %w", err) diags.AddAttributeError( path, "State Write Error", @@ -172,6 +183,211 @@ func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, v return diags } +func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { + var diags diag.Diagnostics + + _, remaining, err := tftypes.WalkAttributePath(s.Raw, path) + + if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { + err = fmt.Errorf("Cannot walk attribute path in state: %w", err) + diags.AddAttributeError( + path, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return nil, diags + } + + if len(remaining.Steps()) == 0 { + // Overwrite existing value + return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { + if p.Equal(path) { + return tfVal, nil + } + return v, nil + }, diags + } + + var parentTfVal tftypes.Value + parentPath := path.WithoutLastStep() + parentAttrType, err := s.Schema.AttributeTypeAtPath(parentPath) + + if err != nil { + err = fmt.Errorf("error getting parent attribute type in schema: %w", err) + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return nil, diags + } + + parentTfType := parentAttrType.TerraformType(ctx) + parentValue, err := s.terraformValueAtPath(parentPath) + + if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { + diags.AddAttributeError( + parentPath, + "State Read Error", + "An unexpected error was encountered trying to read an attribute from the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return nil, diags + } + + // Check if parent needs to get created + if parentValue.Equal(tftypes.NewValue(tftypes.Object{}, nil)) { + // NewValue will panic if required attributes are missing in the + // tftypes.Object. + vals := map[string]tftypes.Value{} + for name, t := range parentTfType.(tftypes.Object).AttributeTypes { + vals[name] = tftypes.NewValue(t, nil) + } + parentValue = tftypes.NewValue(parentTfType, vals) + } else if parentValue.Equal(tftypes.Value{}) { + parentValue = tftypes.NewValue(parentTfType, nil) + } + + switch step := remaining.Steps()[len(remaining.Steps())-1].(type) { + case tftypes.AttributeName: + // Add to Object + if !parentValue.Type().Is(tftypes.Object{}) { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add attribute into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentAttrs map[string]tftypes.Value + err = parentValue.Copy().As(&parentAttrs) + + if err != nil { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract object elements from parent value: %s", err), + ) + return nil, diags + } + + parentAttrs[string(step)] = tfVal + parentTfVal = tftypes.NewValue(parentTfType, parentAttrs) + case tftypes.ElementKeyInt: + // Add new List element + if !parentValue.Type().Is(tftypes.List{}) { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add list element into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentElems []tftypes.Value + err = parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract list elements from parent value: %s", err), + ) + return nil, diags + } + + if int(step) > len(parentElems) { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add list element %d as list currently has %d length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", int(step)+1, len(parentElems)), + ) + return nil, diags + } + + parentElems = append(parentElems, tfVal) + parentTfVal = tftypes.NewValue(parentTfType, parentElems) + case tftypes.ElementKeyString: + // Add new Map element + if !parentValue.Type().Is(tftypes.Map{}) { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add map value into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentElems map[string]tftypes.Value + err = parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract map elements from parent value: %s", err), + ) + return nil, diags + } + + parentElems[string(step)] = tfVal + parentTfVal = tftypes.NewValue(parentTfType, parentElems) + case tftypes.ElementKeyValue: + // Add new Set element + if !parentValue.Type().Is(tftypes.Set{}) { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add set element into parent type: %s", parentValue.Type()), + ) + return nil, diags + } + + var parentElems []tftypes.Value + err = parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract set elements from parent value: %s", err), + ) + return nil, diags + } + + parentElems = append(parentElems, tfVal) + parentTfVal = tftypes.NewValue(parentTfType, parentElems) + } + + if attrTypeWithValidate, ok := parentAttrType.(attr.TypeWithValidate); ok { + diags.Append(attrTypeWithValidate.Validate(ctx, parentTfVal, parentPath)...) + + if diags.HasError() { + return nil, diags + } + } + + if len(remaining.Steps()) > 1 { + return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal) + } + + return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { + if p.Equal(parentPath) { + return parentTfVal, nil + } + return v, nil + }, diags +} + // RemoveResource removes the entire resource from state. func (s *State) RemoveResource(ctx context.Context) { s.Raw = tftypes.NewValue(s.Schema.TerraformType(ctx), nil) diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index 7a14b73fd..f0386aac4 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -1789,21 +1789,19 @@ func TestStateSetAttribute(t *testing.T) { } testCases := map[string]testCase{ - "basic": { + "add-Bool": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "originalname"), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), Schema: Schema{ Attributes: map[string]Attribute{ - "name": { - Type: types.StringType, + "test": { + Type: types.BoolType, Required: true, }, "other": { @@ -1813,33 +1811,25 @@ func TestStateSetAttribute(t *testing.T) { }, }, }, - path: tftypes.NewAttributePath().WithAttributeName("name"), - val: "newname", + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: false, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "name": tftypes.String, + "test": tftypes.Bool, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "newname"), + "test": tftypes.NewValue(tftypes.Bool, false), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "list": { + "add-List": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.List{ElementType: tftypes.String}, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "red"), - tftypes.NewValue(tftypes.String, "blue"), - tftypes.NewValue(tftypes.String, "green"), - }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), Schema: Schema{ @@ -1874,7 +1864,7 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "list-element": { + "add-List-Element-append": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -1906,15 +1896,6 @@ func TestStateSetAttribute(t *testing.T) { "id": tftypes.NewValue(tftypes.String, "disk0"), "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), }), - tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "delete_with_instance": tftypes.Bool, - }, - }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "disk1"), - "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), - }), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -1992,36 +1973,56 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "object-attribute": { + "add-List-Element-append-length-error": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "scratch_disk": tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, }, }, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "scratch_disk": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, }, - }, map[string]tftypes.Value{ - "interface": tftypes.NewValue(tftypes.String, "SCSI"), + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), Schema: Schema{ Attributes: map[string]Attribute{ - "scratch_disk": { - Type: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "interface": types.StringType, + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, }, - }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), Optional: true, + Computed: true, }, "other": { Type: types.StringType, @@ -2030,52 +2031,96 @@ func TestStateSetAttribute(t *testing.T) { }, }, }, - path: tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface"), - val: "NVME", + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(2), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "scratch_disk": tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, }, }, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "scratch_disk": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, }, - }, map[string]tftypes.Value{ - "interface": tftypes.NewValue(tftypes.String, "NVME"), + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("disks"), + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, }, - "set": { + "add-List-Element-first": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.Set{ElementType: tftypes.String}, + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "red"), - tftypes.NewValue(tftypes.String, "blue"), - tftypes.NewValue(tftypes.String, "green"), - }), + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, nil), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), Schema: Schema{ Attributes: map[string]Attribute{ - "tags": { - Type: types.SetType{ - ElemType: types.StringType, - }, - Required: true, + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, }, "other": { Type: types.StringType, @@ -2084,28 +2129,53 @@ func TestStateSetAttribute(t *testing.T) { }, }, }, - path: tftypes.NewAttributePath().WithAttributeName("tags"), - val: []string{"one", "two"}, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.Set{ElementType: tftypes.String}, + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, "other": tftypes.String, }, }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "set-element": { + "add-List-Element-first-length-error": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "disks": tftypes.Set{ + "disks": tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -2116,39 +2186,20 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.String, }, }, map[string]tftypes.Value{ - "disks": tftypes.NewValue(tftypes.Set{ + "disks": tftypes.NewValue(tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, "delete_with_instance": tftypes.Bool, }, }, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "delete_with_instance": tftypes.Bool, - }, - }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "disk0"), - "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), - }), - tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "delete_with_instance": tftypes.Bool, - }, - }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "disk1"), - "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), - }), - }), + }, nil), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), Schema: Schema{ Attributes: map[string]Attribute{ "disks": { - Attributes: SetNestedAttributes(map[string]Attribute{ + Attributes: ListNestedAttributes(map[string]Attribute{ "id": { Type: types.StringType, Required: true, @@ -2157,7 +2208,7 @@ func TestStateSetAttribute(t *testing.T) { Type: types.BoolType, Optional: true, }, - }, SetNestedAttributesOptions{}), + }, ListNestedAttributesOptions{}), Optional: true, Computed: true, }, @@ -2168,15 +2219,7 @@ func TestStateSetAttribute(t *testing.T) { }, }, }, - path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "delete_with_instance": tftypes.Bool, - }, - }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "disk1"), - "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), - })), + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), val: struct { ID string `tfsdk:"id"` DeleteWithInstance bool `tfsdk:"delete_with_instance"` @@ -2186,7 +2229,7 @@ func TestStateSetAttribute(t *testing.T) { }, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "disks": tftypes.Set{ + "disks": tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, @@ -2197,35 +2240,1707 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.String, }, }, map[string]tftypes.Value{ - "disks": tftypes.NewValue(tftypes.Set{ + "disks": tftypes.NewValue(tftypes.List{ ElementType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "id": tftypes.String, "delete_with_instance": tftypes.Bool, }, }, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "delete_with_instance": tftypes.Bool, - }, - }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "disk0"), - "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), - }), - tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "id": tftypes.String, - "delete_with_instance": tftypes.Bool, - }, - }, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "mynewdisk"), - "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), - }), - }), + }, nil), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("disks"), + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "add-Map": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: map[string]string{ + "newkey": "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "newkey": tftypes.NewValue(tftypes.String, "newvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Map-Element-append": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, "key1value"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key2"), + val: "key2value", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key1": tftypes.NewValue(tftypes.String, "key1value"), + "key2": tftypes.NewValue(tftypes.String, "key2value"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Map-Element-first": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Number": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.NumberType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: 1, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Object": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), + val: struct { + Interface string `tfsdk:"interface"` + }{ + Interface: "NVME", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Set": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Set-Element-append": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-Set-Element-first": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "add-String": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Bool": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, true), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, false), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-List": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-List-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Map": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "originalkey": tftypes.NewValue(tftypes.String, "originalvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: map[string]string{ + "newkey": "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "newkey": tftypes.NewValue(tftypes.String, "newvalue"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Map-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Number": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.NumberType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: 2, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 2), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Object": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "SCSI"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), + val: struct { + Interface string `tfsdk:"interface"` + }{ + Interface: "NVME", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Object-Attribute": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "SCSI"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface"), + val: "NVME", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Set": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "red"), + tftypes.NewValue(tftypes.String, "blue"), + tftypes.NewValue(tftypes.String, "green"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-Set-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "overwrite-String": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "write-Bool": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Bool, false), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-List": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-List-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-List-Element-length-error": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(1), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("disks"), + "State Write Error", + "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "write-Map": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: map[string]string{ + "newkey": "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "newkey": tftypes.NewValue(tftypes.String, "newvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Map-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Number": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.NumberType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: 1, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Number, 1), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Object": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "scratch_disk": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "interface": types.StringType, + }, + }, + Optional: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), + val: struct { + Interface string `tfsdk:"interface"` + }{ + Interface: "NVME", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "scratch_disk": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, map[string]tftypes.Value{ + "interface": tftypes.NewValue(tftypes.String, "NVME"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Set": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags"), + val: []string{"one", "two"}, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-Set-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + }, + "write-String": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, nil), + }), }, "AttrTypeWithValidateError": { state: State{ From 5e022d5bfbc77a9e95a4f4608c42cb016497c38a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 16 Sep 2021 17:33:38 -0400 Subject: [PATCH 02/10] Update CHANGELOG for #165 --- .changelog/{pending.txt => 165.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 165.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/165.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/165.txt From 9b9efe6a96fcea62e3f157bd2e0ac5f79903c087 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 16 Sep 2021 17:36:50 -0400 Subject: [PATCH 03/10] tfsdk: Update TestServerImportResourceState to match fixed SetAttribute --- tfsdk/serve_import_test.go | 49 +++++++++++++++----------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/tfsdk/serve_import_test.go b/tfsdk/serve_import_test.go index 4b376cffa..c6d7ad879 100644 --- a/tfsdk/serve_import_test.go +++ b/tfsdk/serve_import_test.go @@ -71,39 +71,28 @@ func TestServerImportResourceState(t *testing.T) { }, resp: &tfprotov6.ImportResourceStateResponse{ - // TODO: SetAttribute should error or import should do the right thing here as - // this seems like a potentially common implementation. - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/148 - Diagnostics: []*tfprotov6.Diagnostic{ + ImportedResources: []*tfprotov6.ImportedResource{ { - Summary: "Missing Resource Import State", - Severity: tfprotov6.DiagnosticSeverityError, - Detail: "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n" + - "Resource ImportState method returned no State in response. If import is intentionally not supported, call the ResourceImportStateNotImplemented() function or return an error.", + State: func() *tfprotov6.DynamicValue { + val, err := tfprotov6.NewDynamicValue( + testServeResourceTypeImportStateTftype, + tftypes.NewValue( + testServeResourceTypeImportStateTftype, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test"), + "optional_string": tftypes.NewValue(tftypes.String, nil), + "required_string": tftypes.NewValue(tftypes.String, nil), + }, + ), + ) + if err != nil { + panic(err) + } + return &val + }(), + TypeName: "test_import_state", }, }, - // ImportedResources: []*tfprotov6.ImportedResource{ - // { - // State: func() *tfprotov6.DynamicValue { - // val, err := tfprotov6.NewDynamicValue( - // testServeResourceTypeImportStateTftype, - // tftypes.NewValue( - // testServeResourceTypeImportStateTftype, - // map[string]tftypes.Value{ - // "id": tftypes.NewValue(tftypes.String, "test"), - // "optional_string": tftypes.NewValue(tftypes.String, nil), - // "required_string": tftypes.NewValue(tftypes.String, ""), - // }, - // ), - // ) - // if err != nil { - // panic(err) - // } - // return &val - // }(), - // TypeName: "test_import_state", - // }, - // }, }, }, "ResourceImportStateNotImplemented": { From c753938425ef8f79fb33d9aa14b207ca6401b51e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 17 Sep 2021 10:15:46 -0400 Subject: [PATCH 04/10] tfsdk: Rearrange SetAttribute tail recursion for clarity --- tfsdk/plan.go | 16 ++++++++-------- tfsdk/state.go | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tfsdk/plan.go b/tfsdk/plan.go index 70d0ce472..830c09c2b 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -367,16 +367,16 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri } } - if len(remaining.Steps()) > 1 { - return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal) + if len(remaining.Steps()) == 1 { + return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { + if p.Equal(parentPath) { + return parentTfVal, nil + } + return v, nil + }, diags } - return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { - if p.Equal(parentPath) { - return parentTfVal, nil - } - return v, nil - }, diags + return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal) } func (p Plan) terraformValueAtPath(path *tftypes.AttributePath) (tftypes.Value, error) { diff --git a/tfsdk/state.go b/tfsdk/state.go index 86085adeb..c4767d182 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -376,16 +376,16 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr } } - if len(remaining.Steps()) > 1 { - return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal) + if len(remaining.Steps()) == 1 { + return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { + if p.Equal(parentPath) { + return parentTfVal, nil + } + return v, nil + }, diags } - return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { - if p.Equal(parentPath) { - return parentTfVal, nil - } - return v, nil - }, diags + return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal) } // RemoveResource removes the entire resource from state. From a291be5722b544b8b250ef6554c0af69d59c06fd Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 17 Sep 2021 11:08:39 -0400 Subject: [PATCH 05/10] tfsdk: Ensure warning diagnostics at any level of path creation are returned in SetAttribute handling Previously: ``` --- FAIL: TestPlanSetAttribute (0.01s) --- FAIL: TestPlanSetAttribute/write-List-AttrTypeWithValidateWarning-Element (0.00s) plan_test.go:2949: unexpected diagnostics (+wanted, -got): diag.Diagnostics( - nil, + { + diag.AttributeWarningDiagnostic{ + WarningDiagnostic: diag.WarningDiagnostic{detail: "This is a warning.", summary: "Warning Diagnostic"}, + path: s`AttributeName("test")`, + }, + }, ) --- FAIL: TestPlanSetAttribute/write-Map-AttrTypeWithValidateWarning-Element (0.00s) plan_test.go:2949: unexpected diagnostics (+wanted, -got): diag.Diagnostics( - nil, + { + diag.AttributeWarningDiagnostic{ + WarningDiagnostic: diag.WarningDiagnostic{detail: "This is a warning.", summary: "Warning Diagnostic"}, + path: s`AttributeName("test")`, + }, + }, ) --- FAIL: TestPlanSetAttribute/write-Set-AttrTypeWithValidateWarning-Element (0.00s) plan_test.go:2949: unexpected diagnostics (+wanted, -got): diag.Diagnostics( - nil, + { + diag.AttributeWarningDiagnostic{ + WarningDiagnostic: diag.WarningDiagnostic{detail: "This is a warning.", summary: "Warning Diagnostic"}, + path: s`AttributeName("test")`, + }, + }, ) --- FAIL: TestStateSetAttribute (0.01s) --- FAIL: TestStateSetAttribute/write-Map-AttrTypeWithValidateWarning-Element (0.00s) state_test.go:4349: unexpected diagnostics (+wanted, -got): diag.Diagnostics( - nil, + { + diag.AttributeWarningDiagnostic{ + WarningDiagnostic: diag.WarningDiagnostic{detail: "This is a warning.", summary: "Warning Diagnostic"}, + path: s`AttributeName("test")`, + }, + }, ) --- FAIL: TestStateSetAttribute/write-List-AttrTypeWithValidateWarning-Element (0.00s) state_test.go:4349: unexpected diagnostics (+wanted, -got): diag.Diagnostics( - nil, + { + diag.AttributeWarningDiagnostic{ + WarningDiagnostic: diag.WarningDiagnostic{detail: "This is a warning.", summary: "Warning Diagnostic"}, + path: s`AttributeName("test")`, + }, + }, ) --- FAIL: TestStateSetAttribute/write-Set-AttrTypeWithValidateWarning-Element (0.00s) state_test.go:4349: unexpected diagnostics (+wanted, -got): diag.Diagnostics( - nil, + { + diag.AttributeWarningDiagnostic{ + WarningDiagnostic: diag.WarningDiagnostic{detail: "This is a warning.", summary: "Warning Diagnostic"}, + path: s`AttributeName("test")`, + }, + }, ) FAIL ``` --- internal/testing/types/listwithvalidate.go | 31 ++ internal/testing/types/mapwithvalidate.go | 31 ++ internal/testing/types/setwithvalidate.go | 31 ++ tfsdk/plan.go | 8 +- tfsdk/plan_test.go | 334 +++++++++++++++++++++ tfsdk/state.go | 8 +- tfsdk/state_test.go | 334 +++++++++++++++++++++ 7 files changed, 767 insertions(+), 10 deletions(-) create mode 100644 internal/testing/types/listwithvalidate.go create mode 100644 internal/testing/types/mapwithvalidate.go create mode 100644 internal/testing/types/setwithvalidate.go diff --git a/internal/testing/types/listwithvalidate.go b/internal/testing/types/listwithvalidate.go new file mode 100644 index 000000000..8e1120544 --- /dev/null +++ b/internal/testing/types/listwithvalidate.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ attr.TypeWithValidate = ListTypeWithValidateError{} + _ attr.TypeWithValidate = ListTypeWithValidateWarning{} +) + +type ListTypeWithValidateError struct { + types.ListType +} + +type ListTypeWithValidateWarning struct { + types.ListType +} + +func (t ListTypeWithValidateError) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + return diag.Diagnostics{TestErrorDiagnostic(path)} +} + +func (t ListTypeWithValidateWarning) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + return diag.Diagnostics{TestWarningDiagnostic(path)} +} diff --git a/internal/testing/types/mapwithvalidate.go b/internal/testing/types/mapwithvalidate.go new file mode 100644 index 000000000..3900aebc8 --- /dev/null +++ b/internal/testing/types/mapwithvalidate.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ attr.TypeWithValidate = MapTypeWithValidateError{} + _ attr.TypeWithValidate = MapTypeWithValidateWarning{} +) + +type MapTypeWithValidateError struct { + types.MapType +} + +type MapTypeWithValidateWarning struct { + types.MapType +} + +func (t MapTypeWithValidateError) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + return diag.Diagnostics{TestErrorDiagnostic(path)} +} + +func (t MapTypeWithValidateWarning) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + return diag.Diagnostics{TestWarningDiagnostic(path)} +} diff --git a/internal/testing/types/setwithvalidate.go b/internal/testing/types/setwithvalidate.go new file mode 100644 index 000000000..c3be6c79f --- /dev/null +++ b/internal/testing/types/setwithvalidate.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var ( + _ attr.TypeWithValidate = SetTypeWithValidateError{} + _ attr.TypeWithValidate = SetTypeWithValidateWarning{} +) + +type SetTypeWithValidateError struct { + types.SetType +} + +type SetTypeWithValidateWarning struct { + types.SetType +} + +func (t SetTypeWithValidateError) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + return diag.Diagnostics{TestErrorDiagnostic(path)} +} + +func (t SetTypeWithValidateWarning) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics { + return diag.Diagnostics{TestWarningDiagnostic(path)} +} diff --git a/tfsdk/plan.go b/tfsdk/plan.go index 830c09c2b..9940211f3 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -153,7 +153,7 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va } } - transformFunc, transformFuncDiags := p.setAttributeTransformFunc(ctx, path, tfVal) + transformFunc, transformFuncDiags := p.setAttributeTransformFunc(ctx, path, tfVal, nil) diags.Append(transformFuncDiags...) if diags.HasError() { @@ -174,9 +174,7 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va return diags } -func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { - var diags diag.Diagnostics - +func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { _, remaining, err := tftypes.WalkAttributePath(p.Raw, path) if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { @@ -376,7 +374,7 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri }, diags } - return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal) + return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal, diags) } func (p Plan) terraformValueAtPath(path *tftypes.AttributePath) (tftypes.Value, error) { diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index 15e28c361..0985c7f66 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -2123,6 +2123,50 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-List-AttrTypeWithValidateWarning-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.ListTypeWithValidateWarning{ + ListType: types.ListType{ + ElemType: types.StringType, + }, + }, + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + val: "testvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, + }, "write-List-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2241,6 +2285,79 @@ func TestPlanSetAttribute(t *testing.T) { ), }, }, + "write-List-Element-AttrTypeWithValidateWarning": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0).WithAttributeName("id")), + }, + }, "write-Map": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2281,6 +2398,49 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-Map-AttrTypeWithValidateWarning-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.MapTypeWithValidateWarning{ + MapType: types.MapType{ + ElemType: types.StringType, + }, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, + }, "write-Map-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2319,6 +2479,47 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-Map-Element-AttrTypeWithValidateWarning": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: testtypes.StringTypeWithValidateWarning{}, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key")), + }, + }, "write-Number": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2512,6 +2713,139 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-Set-AttrTypeWithValidateWarning-Element": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.SetTypeWithValidateWarning{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "testvalue")), + val: "testvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, + }, + "write-Set-Element-AttrTypeWithValidateWarning": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })).WithAttributeName("id")), + }, + }, "write-String": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ diff --git a/tfsdk/state.go b/tfsdk/state.go index c4767d182..2fe76bf0f 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -162,7 +162,7 @@ func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, v } } - transformFunc, transformFuncDiags := s.setAttributeTransformFunc(ctx, path, tfVal) + transformFunc, transformFuncDiags := s.setAttributeTransformFunc(ctx, path, tfVal, nil) diags.Append(transformFuncDiags...) if diags.HasError() { @@ -183,9 +183,7 @@ func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, v return diags } -func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { - var diags diag.Diagnostics - +func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { _, remaining, err := tftypes.WalkAttributePath(s.Raw, path) if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { @@ -385,7 +383,7 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr }, diags } - return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal) + return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal, diags) } // RemoveResource removes the entire resource from state. diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index f0386aac4..0ec298203 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -3523,6 +3523,50 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-List-AttrTypeWithValidateWarning-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.ListTypeWithValidateWarning{ + ListType: types.ListType{ + ElemType: types.StringType, + }, + }, + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + val: "testvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, + }, "write-List-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -3641,6 +3685,79 @@ func TestStateSetAttribute(t *testing.T) { ), }, }, + "write-List-Element-AttrTypeWithValidateWarning": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: ListNestedAttributes(map[string]Attribute{ + "id": { + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0).WithAttributeName("id")), + }, + }, "write-Map": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -3719,6 +3836,90 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-Map-AttrTypeWithValidateWarning-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.MapTypeWithValidateWarning{ + MapType: types.MapType{ + ElemType: types.StringType, + }, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, + }, + "write-Map-Element-AttrTypeWithValidateWarning": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: testtypes.StringTypeWithValidateWarning{}, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + val: "keyvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "keyvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key")), + }, + }, "write-Number": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -3912,6 +4113,139 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, + "write-Set-AttrTypeWithValidateWarning-Element": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: testtypes.SetTypeWithValidateWarning{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "testvalue")), + val: "testvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, + }, + "write-Set-Element-AttrTypeWithValidateWarning": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Attributes: SetNestedAttributes(map[string]Attribute{ + "id": { + Type: testtypes.StringTypeWithValidateWarning{}, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedAttributesOptions{}), + Optional: true, + Computed: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })), + val: struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + ID: "mynewdisk", + DeleteWithInstance: true, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + }), + "other": tftypes.NewValue(tftypes.String, nil), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "mynewdisk"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + })).WithAttributeName("id")), + }, + }, "write-String": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ From 50e3ddfd1a9e6a4e6ca8ab836444796fc9e46fe7 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 21 Sep 2021 09:21:07 -0400 Subject: [PATCH 06/10] tfsdk: Add Plan and State pathExists methods --- tfsdk/plan.go | 42 +++-- tfsdk/plan_test.go | 432 ++++++++++++++++++++++++++++++++++++++++++++ tfsdk/state.go | 42 +++-- tfsdk/state_test.go | 432 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 916 insertions(+), 32 deletions(-) diff --git a/tfsdk/plan.go b/tfsdk/plan.go index 9940211f3..d007ebc8d 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -174,20 +174,39 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va return diags } -func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { +// pathExists walks the current state and returns true if the path can be reached. +// The value at the path may be null or unknown. +func (p Plan) pathExists(ctx context.Context, path *tftypes.AttributePath) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + _, remaining, err := tftypes.WalkAttributePath(p.Raw, path) - if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { - err = fmt.Errorf("Cannot walk attribute path in plan: %w", err) + if err != nil { + if errors.Is(err, tftypes.ErrInvalidStep) { + return false, diags + } + diags.AddAttributeError( path, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + "Plan Read Error", + "An unexpected error was encountered trying to read an attribute from the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot walk attribute path in plan: %s", err), ) + return false, diags + } + + return len(remaining.Steps()) == 0, diags +} + +func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { + exists, pathExistsDiags := p.pathExists(ctx, path) + diags.Append(pathExistsDiags...) + + if diags.HasError() { return nil, diags } - if len(remaining.Steps()) == 0 { + if exists { // Overwrite existing value return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { if p.Equal(path) { @@ -236,7 +255,7 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri parentValue = tftypes.NewValue(parentTfType, nil) } - switch step := remaining.Steps()[len(remaining.Steps())-1].(type) { + switch step := path.Steps()[len(path.Steps())-1].(type) { case tftypes.AttributeName: // Add to Object if !parentValue.Type().Is(tftypes.Object{}) { @@ -365,15 +384,6 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri } } - if len(remaining.Steps()) == 1 { - return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { - if p.Equal(parentPath) { - return parentTfVal, nil - } - return v, nil - }, diags - } - return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal, diags) } diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index 0985c7f66..cb2ec4872 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -244,6 +244,438 @@ func TestPlanGetAttribute(t *testing.T) { } } +func TestPlanPathExists(t *testing.T) { + t.Parallel() + + type testCase struct { + plan Plan + path *tftypes.AttributePath + expected bool + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + "empty-path": { + plan: Plan{}, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: false, + }, + "empty-root": { + plan: Plan{}, + path: tftypes.NewAttributePath(), + expected: true, + }, + "root": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath(), + expected: true, + }, + "WithAttributeName": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: true, + }, + "WithAttributeName-mismatch": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("other"), + expected: false, + }, + "WithAttributeName.WithAttributeName": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, map[string]tftypes.Value{ + "nested": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested": types.StringType, + }, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("nested"), + expected: true, + }, + "WithAttributeName.WithAttributeName-mismatch-child": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, map[string]tftypes.Value{ + "nested": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested": types.StringType, + }, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("other"), + expected: false, + }, + "WithAttributeName.WithAttributeName-mismatch-parent": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("other"), + expected: false, + }, + "WithAttributeName.WithElementKeyInt": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: true, + }, + "WithAttributeName.WithElementKeyInt-mismatch-child": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(1), + expected: false, + }, + "WithAttributeName.WithElementKeyInt-mismatch-parent": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: false, + }, + "WithAttributeName.WithElementKeyString": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + expected: true, + }, + "WithAttributeName.WithElementKeyString-mismatch-child": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("other"), + expected: false, + }, + "WithAttributeName.WithElementKeyString-mismatch-parent": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("other"), + expected: false, + }, + "WithAttributeName.WithElementKeyValue": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "testvalue")), + expected: true, + }, + "WithAttributeName.WithElementKeyValue-mismatch-child": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "othervalue")), + expected: false, + }, + "WithAttributeName.WithElementKeyValue-mismatch-parent": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "othervalue")), + expected: false, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.plan.pathExists(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + func TestPlanSet(t *testing.T) { t.Parallel() diff --git a/tfsdk/state.go b/tfsdk/state.go index 2fe76bf0f..274111f5b 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -183,20 +183,39 @@ func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, v return diags } -func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { +// pathExists walks the current state and returns true if the path can be reached. +// The value at the path may be null or unknown. +func (s State) pathExists(ctx context.Context, path *tftypes.AttributePath) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + _, remaining, err := tftypes.WalkAttributePath(s.Raw, path) - if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { - err = fmt.Errorf("Cannot walk attribute path in state: %w", err) + if err != nil { + if errors.Is(err, tftypes.ErrInvalidStep) { + return false, diags + } + diags.AddAttributeError( path, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + "State Read Error", + "An unexpected error was encountered trying to read an attribute from the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot walk attribute path in state: %s", err), ) + return false, diags + } + + return len(remaining.Steps()) == 0, diags +} + +func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { + exists, pathExistsDiags := s.pathExists(ctx, path) + diags.Append(pathExistsDiags...) + + if diags.HasError() { return nil, diags } - if len(remaining.Steps()) == 0 { + if exists { // Overwrite existing value return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { if p.Equal(path) { @@ -245,7 +264,7 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr parentValue = tftypes.NewValue(parentTfType, nil) } - switch step := remaining.Steps()[len(remaining.Steps())-1].(type) { + switch step := path.Steps()[len(path.Steps())-1].(type) { case tftypes.AttributeName: // Add to Object if !parentValue.Type().Is(tftypes.Object{}) { @@ -374,15 +393,6 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr } } - if len(remaining.Steps()) == 1 { - return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) { - if p.Equal(parentPath) { - return parentTfVal, nil - } - return v, nil - }, diags - } - return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal, diags) } diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index 0ec298203..a2eb45bee 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -1295,6 +1295,438 @@ func TestStateGetAttribute(t *testing.T) { } } +func TestStatePathExists(t *testing.T) { + t.Parallel() + + type testCase struct { + state State + path *tftypes.AttributePath + expected bool + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + "empty-path": { + state: State{}, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: false, + }, + "empty-root": { + state: State{}, + path: tftypes.NewAttributePath(), + expected: true, + }, + "root": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath(), + expected: true, + }, + "WithAttributeName": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: true, + }, + "WithAttributeName-mismatch": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("other"), + expected: false, + }, + "WithAttributeName.WithAttributeName": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, map[string]tftypes.Value{ + "nested": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested": types.StringType, + }, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("nested"), + expected: true, + }, + "WithAttributeName.WithAttributeName-mismatch-child": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested": tftypes.String, + }, + }, map[string]tftypes.Value{ + "nested": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested": types.StringType, + }, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("other"), + expected: false, + }, + "WithAttributeName.WithAttributeName-mismatch-parent": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("other"), + expected: false, + }, + "WithAttributeName.WithElementKeyInt": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: true, + }, + "WithAttributeName.WithElementKeyInt-mismatch-child": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(1), + expected: false, + }, + "WithAttributeName.WithElementKeyInt-mismatch-parent": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: false, + }, + "WithAttributeName.WithElementKeyString": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("key"), + expected: true, + }, + "WithAttributeName.WithElementKeyString-mismatch-child": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.MapType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("other"), + expected: false, + }, + "WithAttributeName.WithElementKeyString-mismatch-parent": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("other"), + expected: false, + }, + "WithAttributeName.WithElementKeyValue": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "testvalue")), + expected: true, + }, + "WithAttributeName.WithElementKeyValue-mismatch-child": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "testvalue"), + }), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "othervalue")), + expected: false, + }, + "WithAttributeName.WithElementKeyValue-mismatch-parent": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "othervalue")), + expected: false, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.state.pathExists(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + func TestStateSet(t *testing.T) { t.Parallel() From e21869a44fbcae6b4df03ffea35ace6b16be3407 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 21 Sep 2021 09:37:23 -0400 Subject: [PATCH 07/10] tfsdk: Ensure some form of error is thrown trying to incorrectly write root path --- tfsdk/plan_test.go | 27 +++++++++++++++++++++++++++ tfsdk/state_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index cb2ec4872..cd5a1572a 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -2488,6 +2488,33 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, + "write-root": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath(), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath(), + "Value Conversion Error", + "An unexpected error was encountered trying to convert the Terraform value. This is always an error in the provider. Please report the following to the provider developer:\n\nexpected tftypes.Object[\"test\":tftypes.Bool], got tftypes.Bool", + ), + }, + }, "write-Bool": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index a2eb45bee..eaead483e 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -3888,6 +3888,33 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, + "write-root": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Type: types.BoolType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath(), + val: false, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, nil), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath(), + "Value Conversion Error", + "An unexpected error was encountered trying to convert the Terraform value. This is always an error in the provider. Please report the following to the provider developer:\n\nexpected tftypes.Object[\"test\":tftypes.Bool], got tftypes.Bool", + ), + }, + }, "write-Bool": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ From e2355fe1642e3a48207b042e06e048c6e904bb9a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 21 Sep 2021 14:36:16 -0400 Subject: [PATCH 08/10] tfsdk: Refactor setAttributeTransformFunc into more unit testable functionality --- tfsdk/plan.go | 156 ++------- tfsdk/plan_test.go | 18 +- tfsdk/state.go | 154 ++------- tfsdk/state_test.go | 18 +- tfsdk/tftypes_transform.go | 8 + tfsdk/tftypes_value.go | 221 +++++++++++++ tfsdk/tftypes_value_test.go | 632 ++++++++++++++++++++++++++++++++++++ 7 files changed, 918 insertions(+), 289 deletions(-) create mode 100644 tfsdk/tftypes_transform.go create mode 100644 tfsdk/tftypes_value.go create mode 100644 tfsdk/tftypes_value_test.go diff --git a/tfsdk/plan.go b/tfsdk/plan.go index d007ebc8d..28d82da63 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -108,9 +108,7 @@ func (p *Plan) Set(ctx context.Context, val interface{}) diag.Diagnostics { // path does not have a value, it will be added, including any parent attribute // paths as necessary. // -// Lists can only have the first element added if empty and can only add the -// next element according to the current length, otherwise this will return an -// error. +// Lists can only have the next element added according to the current length. func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, val interface{}) diag.Diagnostics { var diags diag.Diagnostics @@ -198,7 +196,11 @@ func (p Plan) pathExists(ctx context.Context, path *tftypes.AttributePath) (bool return len(remaining.Steps()) == 0, diags } -func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { +// setAttributeTransformFunc recursively creates a value based on the current +// Plan values along the path. If the value at the path does not yet exist, +// this will perform recursion to add the child value to a parent value, +// creating the parent value if necessary. +func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (transformFunc, diag.Diagnostics) { exists, pathExistsDiags := p.pathExists(ctx, path) diags.Append(pathExistsDiags...) @@ -216,7 +218,6 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri }, diags } - var parentTfVal tftypes.Value parentPath := path.WithoutLastStep() parentAttrType, err := p.Schema.AttributeTypeAtPath(parentPath) @@ -242,149 +243,32 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri return nil, diags } - // Check if parent needs to get created - if parentValue.Equal(tftypes.NewValue(tftypes.Object{}, nil)) { - // NewValue will panic if required attributes are missing in the - // tftypes.Object. - vals := map[string]tftypes.Value{} - for name, t := range parentTfType.(tftypes.Object).AttributeTypes { - vals[name] = tftypes.NewValue(t, nil) - } - parentValue = tftypes.NewValue(parentTfType, vals) - } else if parentValue.Equal(tftypes.Value{}) { - parentValue = tftypes.NewValue(parentTfType, nil) - } - - switch step := path.Steps()[len(path.Steps())-1].(type) { - case tftypes.AttributeName: - // Add to Object - if !parentValue.Type().Is(tftypes.Object{}) { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add attribute into parent type: %s", parentValue.Type()), - ) - return nil, diags - } - - var parentAttrs map[string]tftypes.Value - err = parentValue.Copy().As(&parentAttrs) - - if err != nil { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract object elements from parent value: %s", err), - ) - return nil, diags - } - - parentAttrs[string(step)] = tfVal - parentTfVal = tftypes.NewValue(parentTfType, parentAttrs) - case tftypes.ElementKeyInt: - // Add new List element - if !parentValue.Type().Is(tftypes.List{}) { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add list element into parent type: %s", parentValue.Type()), - ) - return nil, diags - } - - var parentElems []tftypes.Value - err = parentValue.Copy().As(&parentElems) - - if err != nil { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract list elements from parent value: %s", err), - ) - return nil, diags - } - - if int(step) > len(parentElems) { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add list element %d as list currently has %d length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", int(step)+1, len(parentElems)), - ) - return nil, diags - } - - parentElems = append(parentElems, tfVal) - parentTfVal = tftypes.NewValue(parentTfType, parentElems) - case tftypes.ElementKeyString: - // Add new Map element - if !parentValue.Type().Is(tftypes.Map{}) { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add map value into parent type: %s", parentValue.Type()), - ) - return nil, diags - } - - var parentElems map[string]tftypes.Value - err = parentValue.Copy().As(&parentElems) - - if err != nil { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract map elements from parent value: %s", err), - ) - return nil, diags - } - - parentElems[string(step)] = tfVal - parentTfVal = tftypes.NewValue(parentTfType, parentElems) - case tftypes.ElementKeyValue: - // Add new Set element - if !parentValue.Type().Is(tftypes.Set{}) { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add set element into parent type: %s", parentValue.Type()), - ) - return nil, diags - } + var parentValueDiags diag.Diagnostics + parentValue, parentValueDiags = createParentValue(ctx, parentPath, parentTfType, parentValue) + diags.Append(parentValueDiags...) - var parentElems []tftypes.Value - err = parentValue.Copy().As(&parentElems) + if diags.HasError() { + return nil, diags + } - if err != nil { - diags.AddAttributeError( - parentPath, - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract set elements from parent value: %s", err), - ) - return nil, diags - } + var childValueDiags diag.Diagnostics + childStep := path.Steps()[len(path.Steps())-1] + parentValue, childValueDiags = upsertChildValue(ctx, parentPath, parentTfType, parentValue, childStep, tfVal) + diags.Append(childValueDiags...) - parentElems = append(parentElems, tfVal) - parentTfVal = tftypes.NewValue(parentTfType, parentElems) + if diags.HasError() { + return nil, diags } if attrTypeWithValidate, ok := parentAttrType.(attr.TypeWithValidate); ok { - diags.Append(attrTypeWithValidate.Validate(ctx, parentTfVal, parentPath)...) + diags.Append(attrTypeWithValidate.Validate(ctx, parentValue, parentPath)...) if diags.HasError() { return nil, diags } } - return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal, diags) + return p.setAttributeTransformFunc(ctx, parentPath, parentValue, diags) } func (p Plan) terraformValueAtPath(path *tftypes.AttributePath) (tftypes.Value, error) { diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index cd5a1572a..70829041b 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -1107,9 +1107,9 @@ func TestPlanSetAttribute(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("disks"), - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", ), }, }, @@ -1285,9 +1285,9 @@ func TestPlanSetAttribute(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("disks"), - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", ), }, }, @@ -2738,9 +2738,9 @@ func TestPlanSetAttribute(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("disks"), - "Plan Write Error", - "An unexpected error was encountered trying to write an attribute to the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", ), }, }, diff --git a/tfsdk/state.go b/tfsdk/state.go index 274111f5b..6e24094ad 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -117,9 +117,7 @@ func (s *State) Set(ctx context.Context, val interface{}) diag.Diagnostics { // path does not have a value, it will be added, including any parent attribute // paths as necessary. // -// Lists can only have the first element added if empty and can only add the -// next element according to the current length, otherwise this will return an -// error. +// Lists can only have the next element added according to the current length. func (s *State) SetAttribute(ctx context.Context, path *tftypes.AttributePath, val interface{}) diag.Diagnostics { var diags diag.Diagnostics @@ -207,6 +205,10 @@ func (s State) pathExists(ctx context.Context, path *tftypes.AttributePath) (boo return len(remaining.Steps()) == 0, diags } +// setAttributeTransformFunc recursively creates a value based on the current +// Plan values along the path. If the value at the path does not yet exist, +// this will perform recursion to add the child value to a parent value, +// creating the parent value if necessary. func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.AttributePath, tfVal tftypes.Value, diags diag.Diagnostics) (func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error), diag.Diagnostics) { exists, pathExistsDiags := s.pathExists(ctx, path) diags.Append(pathExistsDiags...) @@ -225,7 +227,6 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr }, diags } - var parentTfVal tftypes.Value parentPath := path.WithoutLastStep() parentAttrType, err := s.Schema.AttributeTypeAtPath(parentPath) @@ -251,149 +252,32 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr return nil, diags } - // Check if parent needs to get created - if parentValue.Equal(tftypes.NewValue(tftypes.Object{}, nil)) { - // NewValue will panic if required attributes are missing in the - // tftypes.Object. - vals := map[string]tftypes.Value{} - for name, t := range parentTfType.(tftypes.Object).AttributeTypes { - vals[name] = tftypes.NewValue(t, nil) - } - parentValue = tftypes.NewValue(parentTfType, vals) - } else if parentValue.Equal(tftypes.Value{}) { - parentValue = tftypes.NewValue(parentTfType, nil) - } - - switch step := path.Steps()[len(path.Steps())-1].(type) { - case tftypes.AttributeName: - // Add to Object - if !parentValue.Type().Is(tftypes.Object{}) { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add attribute into parent type: %s", parentValue.Type()), - ) - return nil, diags - } - - var parentAttrs map[string]tftypes.Value - err = parentValue.Copy().As(&parentAttrs) - - if err != nil { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract object elements from parent value: %s", err), - ) - return nil, diags - } - - parentAttrs[string(step)] = tfVal - parentTfVal = tftypes.NewValue(parentTfType, parentAttrs) - case tftypes.ElementKeyInt: - // Add new List element - if !parentValue.Type().Is(tftypes.List{}) { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add list element into parent type: %s", parentValue.Type()), - ) - return nil, diags - } - - var parentElems []tftypes.Value - err = parentValue.Copy().As(&parentElems) - - if err != nil { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract list elements from parent value: %s", err), - ) - return nil, diags - } - - if int(step) > len(parentElems) { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add list element %d as list currently has %d length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", int(step)+1, len(parentElems)), - ) - return nil, diags - } - - parentElems = append(parentElems, tfVal) - parentTfVal = tftypes.NewValue(parentTfType, parentElems) - case tftypes.ElementKeyString: - // Add new Map element - if !parentValue.Type().Is(tftypes.Map{}) { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add map value into parent type: %s", parentValue.Type()), - ) - return nil, diags - } - - var parentElems map[string]tftypes.Value - err = parentValue.Copy().As(&parentElems) - - if err != nil { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract map elements from parent value: %s", err), - ) - return nil, diags - } - - parentElems[string(step)] = tfVal - parentTfVal = tftypes.NewValue(parentTfType, parentElems) - case tftypes.ElementKeyValue: - // Add new Set element - if !parentValue.Type().Is(tftypes.Set{}) { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add set element into parent type: %s", parentValue.Type()), - ) - return nil, diags - } + var parentValueDiags diag.Diagnostics + parentValue, parentValueDiags = createParentValue(ctx, parentPath, parentTfType, parentValue) + diags.Append(parentValueDiags...) - var parentElems []tftypes.Value - err = parentValue.Copy().As(&parentElems) + if diags.HasError() { + return nil, diags + } - if err != nil { - diags.AddAttributeError( - parentPath, - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unable to extract set elements from parent value: %s", err), - ) - return nil, diags - } + var childValueDiags diag.Diagnostics + childStep := path.Steps()[len(path.Steps())-1] + parentValue, childValueDiags = upsertChildValue(ctx, parentPath, parentTfType, parentValue, childStep, tfVal) + diags.Append(childValueDiags...) - parentElems = append(parentElems, tfVal) - parentTfVal = tftypes.NewValue(parentTfType, parentElems) + if diags.HasError() { + return nil, diags } if attrTypeWithValidate, ok := parentAttrType.(attr.TypeWithValidate); ok { - diags.Append(attrTypeWithValidate.Validate(ctx, parentTfVal, parentPath)...) + diags.Append(attrTypeWithValidate.Validate(ctx, parentValue, parentPath)...) if diags.HasError() { return nil, diags } } - return s.setAttributeTransformFunc(ctx, parentPath, parentTfVal, diags) + return s.setAttributeTransformFunc(ctx, parentPath, parentValue, diags) } // RemoveResource removes the entire resource from state. diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index eaead483e..729eb024b 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -2507,9 +2507,9 @@ func TestStateSetAttribute(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("disks"), - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", ), }, }, @@ -2685,9 +2685,9 @@ func TestStateSetAttribute(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("disks"), - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", ), }, }, @@ -4138,9 +4138,9 @@ func TestStateSetAttribute(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("disks"), - "State Write Error", - "An unexpected error was encountered trying to write an attribute to the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, SetAttribute can only add the next element to a list. Add empty elements into the list prior to this call, if appropriate.", + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", ), }, }, diff --git a/tfsdk/tftypes_transform.go b/tfsdk/tftypes_transform.go new file mode 100644 index 000000000..4263bb24b --- /dev/null +++ b/tfsdk/tftypes_transform.go @@ -0,0 +1,8 @@ +package tfsdk + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// transformFunc is the signature expected for tftypes.Transform functions. +type transformFunc func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) diff --git a/tfsdk/tftypes_value.go b/tfsdk/tftypes_value.go new file mode 100644 index 000000000..535927d84 --- /dev/null +++ b/tfsdk/tftypes_value.go @@ -0,0 +1,221 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// createParentValue ensures that the given parent value can have children +// values upserted. If the parent value is known and not null, it is returned +// without modification. A null Object or Tuple is converted to known with null +// children. An unknown Object or Tuple is converted to known with unknown +// children. List, Map, and Set are created with empty elements. +// +// The parentType parameter ensures that the value will match the current +// schema, not what is currently stored in the parentValue. +func createParentValue(ctx context.Context, parentPath *tftypes.AttributePath, parentType tftypes.Type, parentValue tftypes.Value) (tftypes.Value, diag.Diagnostics) { + var diags diag.Diagnostics + + if !parentValue.IsNull() && parentValue.IsKnown() { + return parentValue, diags + } + + var childValue interface{} + + if !parentValue.IsKnown() { + childValue = tftypes.UnknownValue + } + + switch parentType := parentType.(type) { + case tftypes.List: + parentValue = tftypes.NewValue(parentType, []tftypes.Value{}) + case tftypes.Set: + parentValue = tftypes.NewValue(parentType, []tftypes.Value{}) + case tftypes.Map: + parentValue = tftypes.NewValue(parentType, map[string]tftypes.Value{}) + case tftypes.Object: + vals := map[string]tftypes.Value{} + + for name, t := range parentType.AttributeTypes { + vals[name] = tftypes.NewValue(t, childValue) + } + + parentValue = tftypes.NewValue(parentType, vals) + case tftypes.Tuple: + vals := []tftypes.Value{} + + for _, elementType := range parentType.ElementTypes { + vals = append(vals, tftypes.NewValue(elementType, childValue)) + } + + parentValue = tftypes.NewValue(parentType, vals) + default: + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unknown parent type %T to create value.", parentValue.Type()), + ) + return parentValue, diags + } + + return parentValue, diags +} + +// upsertChildValue will upsert a child value into a parent value. If the +// path step already has a value, it will be overwritten. Otherwise, the child +// value will be added. +// +// Lists can only have the next element added according to the current length. +// +// The parentType parameter ensures that the value will match the current +// schema, not what is currently stored in the parentValue. +func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, parentType tftypes.Type, parentValue tftypes.Value, childStep tftypes.AttributePathStep, childValue tftypes.Value) (tftypes.Value, diag.Diagnostics) { + var diags diag.Diagnostics + + // TODO: Add Tuple support + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/54 + switch childStep := childStep.(type) { + case tftypes.AttributeName: + // Set in Object + if !parentType.Is(tftypes.Object{}) { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add attribute into parent type: %s", parentType), + ) + return parentValue, diags + } + + var parentAttrs map[string]tftypes.Value + err := parentValue.Copy().As(&parentAttrs) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract object elements from parent value: %s", err), + ) + return parentValue, diags + } + + parentAttrs[string(childStep)] = childValue + parentValue = tftypes.NewValue(parentType, parentAttrs) + case tftypes.ElementKeyInt: + // Upsert List element, except past length + 1 + if !parentType.Is(tftypes.List{}) { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add list element into parent type: %s", parentType), + ) + return parentValue, diags + } + + var parentElems []tftypes.Value + err := parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract list elements from parent value: %s", err), + ) + return parentValue, diags + } + + if int(childStep) > len(parentElems) { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add list element %d as list currently has %d length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", int(childStep)+1, len(parentElems)), + ) + return parentValue, diags + } + + if int(childStep) == len(parentElems) { + parentElems = append(parentElems, childValue) + } else { + parentElems[int(childStep)] = childValue + } + + parentValue = tftypes.NewValue(parentType, parentElems) + case tftypes.ElementKeyString: + // Upsert Map element + if !parentType.Is(tftypes.Map{}) { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add map value into parent type: %s", parentType), + ) + return parentValue, diags + } + + var parentElems map[string]tftypes.Value + err := parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract map elements from parent value: %s", err), + ) + return parentValue, diags + } + + parentElems[string(childStep)] = childValue + parentValue = tftypes.NewValue(parentType, parentElems) + case tftypes.ElementKeyValue: + // Upsert Set element + if !parentType.Is(tftypes.Set{}) { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Cannot add set element into parent type: %s", parentType), + ) + return parentValue, diags + } + + var parentElems []tftypes.Value + err := parentValue.Copy().As(&parentElems) + + if err != nil { + diags.AddAttributeError( + parentPath, + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + fmt.Sprintf("Unable to extract set elements from parent value: %s", err), + ) + return parentValue, diags + } + + // Prevent duplicates + var found bool + + for _, parentElem := range parentElems { + if parentElem.Equal(childValue) { + found = true + break + } + } + + if !found { + parentElems = append(parentElems, childValue) + } + + parentValue = tftypes.NewValue(parentType, parentElems) + } + + return parentValue, diags +} diff --git a/tfsdk/tftypes_value_test.go b/tfsdk/tftypes_value_test.go new file mode 100644 index 000000000..7998ca817 --- /dev/null +++ b/tfsdk/tftypes_value_test.go @@ -0,0 +1,632 @@ +package tfsdk + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestCreateParentValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parentType tftypes.Type + parentValue tftypes.Value + expected tftypes.Value + expectedDiags diag.Diagnostics + }{ + "Bool-null": { + parentType: tftypes.Bool, + parentValue: tftypes.NewValue(tftypes.Bool, nil), + expected: tftypes.NewValue(tftypes.Bool, nil), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test"), + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Unknown parent type tftypes.primitive to create value.", + ), + }, + }, + "List-null": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, nil), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + }, + "List-unknown": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, tftypes.UnknownValue), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + }, + "List-value": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + }, + "Map-null": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, nil), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{}), + }, + "Map-unknown": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, tftypes.UnknownValue), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{}), + }, + "Map-value": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "keyone": tftypes.NewValue(tftypes.String, "valueone"), + "keytwo": tftypes.NewValue(tftypes.String, "valuetwo"), + }), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "keyone": tftypes.NewValue(tftypes.String, "valueone"), + "keytwo": tftypes.NewValue(tftypes.String, "valuetwo"), + }), + }, + "Object-null": { + parentType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, + parentValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, nil), + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, nil), + "attrtwo": tftypes.NewValue(tftypes.String, nil), + }), + }, + "Object-unknown": { + parentType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, + parentValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, tftypes.UnknownValue), + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "attrtwo": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + "Object-value": { + parentType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, + parentValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, "one"), + "attrtwo": tftypes.NewValue(tftypes.String, "two"), + }), + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, "one"), + "attrtwo": tftypes.NewValue(tftypes.String, "two"), + }), + }, + "Set-null": { + parentType: tftypes.Set{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, nil), + expected: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + }, + "Set-unknown": { + parentType: tftypes.Set{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, tftypes.UnknownValue), + expected: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + }, + "Set-value": { + parentType: tftypes.Set{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + expected: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + }, + "Tuple-null": { + parentType: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, + parentValue: tftypes.NewValue(tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, nil), + expected: tftypes.NewValue(tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, nil), + }), + }, + "Tuple-unknown": { + parentType: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, + parentValue: tftypes.NewValue(tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, tftypes.UnknownValue), + expected: tftypes.NewValue(tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + "Tuple-value": { + parentType: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, + parentValue: tftypes.NewValue(tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + expected: tftypes.NewValue(tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := createParentValue( + context.Background(), + tftypes.NewAttributePath().WithAttributeName("test"), + tc.parentType, + tc.parentValue, + ) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestUpsertChildValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parentType tftypes.Type + parentValue tftypes.Value + childStep tftypes.AttributePathStep + childValue tftypes.Value + expected tftypes.Value + expectedDiags diag.Diagnostics + }{ + "List-empty-write-first": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + childStep: tftypes.ElementKeyInt(0), + childValue: tftypes.NewValue(tftypes.String, "one"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + "List-empty-write-length-error": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + childStep: tftypes.ElementKeyInt(1), + childValue: tftypes.NewValue(tftypes.String, "two"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{}), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test"), + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "List-null-write-first": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, nil), + childStep: tftypes.ElementKeyInt(0), + childValue: tftypes.NewValue(tftypes.String, "one"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + "List-null-write-length-error": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, nil), + childStep: tftypes.ElementKeyInt(1), + childValue: tftypes.NewValue(tftypes.String, "two"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, nil), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test"), + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 2 as list currently has 0 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "List-value-overwrite": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + childStep: tftypes.ElementKeyInt(0), + childValue: tftypes.NewValue(tftypes.String, "new"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "new"), + }), + }, + "List-value-write-next": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + childStep: tftypes.ElementKeyInt(1), + childValue: tftypes.NewValue(tftypes.String, "two"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + }, + "List-value-write-length-error": { + parentType: tftypes.List{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + childStep: tftypes.ElementKeyInt(2), + childValue: tftypes.NewValue(tftypes.String, "three"), + expected: tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test"), + "Value Conversion Error", + "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Cannot add list element 3 as list currently has 1 length. To prevent ambiguity, only the next element can be added to a list. Add empty elements into the list prior to this call, if appropriate.", + ), + }, + }, + "Map-empty": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{}), + childStep: tftypes.ElementKeyString("key"), + childValue: tftypes.NewValue(tftypes.String, "value"), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "value"), + }), + }, + "Map-null": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, nil), + childStep: tftypes.ElementKeyString("key"), + childValue: tftypes.NewValue(tftypes.String, "value"), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "value"), + }), + }, + "Map-value-overwrite": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "oldvalue"), + }), + childStep: tftypes.ElementKeyString("key"), + childValue: tftypes.NewValue(tftypes.String, "newvalue"), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "key": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }, + "Map-value-write": { + parentType: tftypes.Map{ + AttributeType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "keyone": tftypes.NewValue(tftypes.String, "valueone"), + }), + childStep: tftypes.ElementKeyString("keytwo"), + childValue: tftypes.NewValue(tftypes.String, "valuetwo"), + expected: tftypes.NewValue(tftypes.Map{ + AttributeType: tftypes.String, + }, map[string]tftypes.Value{ + "keyone": tftypes.NewValue(tftypes.String, "valueone"), + "keytwo": tftypes.NewValue(tftypes.String, "valuetwo"), + }), + }, + "Object-overwrite": { + parentType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, + parentValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, "oldvalue"), + "attrtwo": tftypes.NewValue(tftypes.String, nil), + }), + childStep: tftypes.AttributeName("attrone"), + childValue: tftypes.NewValue(tftypes.String, "newvalue"), + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, "newvalue"), + "attrtwo": tftypes.NewValue(tftypes.String, nil), + }), + }, + "Object-write": { + parentType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, + parentValue: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, nil), + "attrtwo": tftypes.NewValue(tftypes.String, nil), + }), + childStep: tftypes.AttributeName("attrone"), + childValue: tftypes.NewValue(tftypes.String, "attronevalue"), + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attrone": tftypes.String, + "attrtwo": tftypes.String, + }, + }, map[string]tftypes.Value{ + "attrone": tftypes.NewValue(tftypes.String, "attronevalue"), + "attrtwo": tftypes.NewValue(tftypes.String, nil), + }), + }, + "Set-empty": { + parentType: tftypes.Set{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, nil), + childStep: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "one")), + childValue: tftypes.NewValue(tftypes.String, "one"), + expected: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + "Set-overwrite-value": { + parentType: tftypes.Set{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + childStep: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "one")), + childValue: tftypes.NewValue(tftypes.String, "one"), + expected: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + "Set-write-value": { + parentType: tftypes.Set{ + ElementType: tftypes.String, + }, + parentValue: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + childStep: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "two")), + childValue: tftypes.NewValue(tftypes.String, "two"), + expected: tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + }), + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := upsertChildValue( + context.Background(), + tftypes.NewAttributePath().WithAttributeName("test"), + tc.parentType, + tc.parentValue, + tc.childStep, + tc.childValue, + ) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected result (+wanted, -got): %s", diff) + } + }) + } +} From 3835b4d8d4957c30b4f796019c0fb617a2bdd6c4 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 23 Sep 2021 20:21:57 -0400 Subject: [PATCH 09/10] tfsdk: Make SetAttribute testing appropriate --- tfsdk/plan_test.go | 490 +++++++++++++++++------------------------- tfsdk/state_test.go | 512 +++++++++++++++++--------------------------- 2 files changed, 393 insertions(+), 609 deletions(-) diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index 70829041b..25005b7e7 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -821,81 +821,6 @@ func TestPlanSetAttribute(t *testing.T) { } testCases := map[string]testCase{ - "add-Bool": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.BoolType, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: false, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.Bool, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.Bool, false), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, - "add-List": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "tags": { - Type: types.ListType{ - ElemType: types.StringType, - }, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("tags"), - val: []string{"one", "two"}, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.List{ElementType: tftypes.String}, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "add-List-Element-append": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1291,50 +1216,6 @@ func TestPlanSetAttribute(t *testing.T) { ), }, }, - "add-Map": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.MapType{ - ElemType: types.StringType, - }, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: map[string]string{ - "newkey": "newvalue", - }, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.Map{ - AttributeType: tftypes.String, - }, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.Map{ - AttributeType: tftypes.String, - }, map[string]tftypes.Value{ - "newkey": tftypes.NewValue(tftypes.String, "newvalue"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "add-Map-Element-append": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1434,133 +1315,6 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "add-Number": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.NumberType, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: 1, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.Number, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.Number, 1), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, - "add-Object": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "scratch_disk": { - Type: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "interface": types.StringType, - }, - }, - Optional: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), - val: struct { - Interface string `tfsdk:"interface"` - }{ - Interface: "NVME", - }, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "scratch_disk": tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, - }, - }, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "scratch_disk": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, - }, - }, map[string]tftypes.Value{ - "interface": tftypes.NewValue(tftypes.String, "NVME"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, - "add-Set": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "tags": { - Type: types.SetType{ - ElemType: types.StringType, - }, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("tags"), - val: []string{"one", "two"}, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.Set{ElementType: tftypes.String}, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "add-Set-Element-append": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -1776,40 +1530,6 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "add-String": { - plan: Plan{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.StringType, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: "newvalue", - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.String, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.String, "newvalue"), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "overwrite-Bool": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2027,6 +1747,7 @@ func TestPlanSetAttribute(t *testing.T) { AttributeType: tftypes.String, }, map[string]tftypes.Value{ "originalkey": tftypes.NewValue(tftypes.String, "originalvalue"), + "otherkey": tftypes.NewValue(tftypes.String, "othervalue"), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -2160,6 +1881,7 @@ func TestPlanSetAttribute(t *testing.T) { "scratch_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, "other": tftypes.String, @@ -2168,9 +1890,11 @@ func TestPlanSetAttribute(t *testing.T) { "scratch_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, map[string]tftypes.Value{ "interface": tftypes.NewValue(tftypes.String, "SCSI"), + "other": tftypes.NewValue(tftypes.String, "originalvalue"), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -2180,6 +1904,7 @@ func TestPlanSetAttribute(t *testing.T) { Type: types.ObjectType{ AttrTypes: map[string]attr.Type{ "interface": types.StringType, + "other": types.StringType, }, }, Optional: true, @@ -2194,14 +1919,17 @@ func TestPlanSetAttribute(t *testing.T) { path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), val: struct { Interface string `tfsdk:"interface"` + Other string `tfsdk:"other"` }{ Interface: "NVME", + Other: "newvalue", }, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "scratch_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, "other": tftypes.String, @@ -2210,9 +1938,11 @@ func TestPlanSetAttribute(t *testing.T) { "scratch_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, map[string]tftypes.Value{ "interface": tftypes.NewValue(tftypes.String, "NVME"), + "other": tftypes.NewValue(tftypes.String, "newvalue"), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -2452,6 +2182,60 @@ func TestPlanSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, + "overwrite-Set-Element-duplicate": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + tftypes.NewValue(tftypes.String, "three"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags").WithElementKeyValue(tftypes.NewValue(tftypes.String, "three")), + val: "three", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + tftypes.NewValue(tftypes.String, "three"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, "overwrite-String": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2491,7 +2275,9 @@ func TestPlanSetAttribute(t *testing.T) { "write-root": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2505,7 +2291,9 @@ func TestPlanSetAttribute(t *testing.T) { path: tftypes.NewAttributePath(), val: false, expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, }, nil), expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( @@ -2518,7 +2306,10 @@ func TestPlanSetAttribute(t *testing.T) { "write-Bool": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2548,7 +2339,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-List": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2585,7 +2381,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-List-AttrTypeWithValidateWarning-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2629,7 +2430,17 @@ func TestPlanSetAttribute(t *testing.T) { "write-List-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2699,7 +2510,17 @@ func TestPlanSetAttribute(t *testing.T) { "write-List-Element-length-error": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2733,7 +2554,17 @@ func TestPlanSetAttribute(t *testing.T) { DeleteWithInstance: true, }, expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( @@ -2820,7 +2651,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-Map": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2860,7 +2696,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-Map-AttrTypeWithValidateWarning-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2903,7 +2744,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-Map-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2941,7 +2787,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-Map-Element-AttrTypeWithValidateWarning": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -2982,7 +2833,10 @@ func TestPlanSetAttribute(t *testing.T) { "write-Number": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3012,7 +2866,14 @@ func TestPlanSetAttribute(t *testing.T) { "write-Object": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3060,7 +2921,10 @@ func TestPlanSetAttribute(t *testing.T) { "write-Set": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3097,7 +2961,17 @@ func TestPlanSetAttribute(t *testing.T) { "write-Set-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3175,7 +3049,12 @@ func TestPlanSetAttribute(t *testing.T) { "write-Set-AttrTypeWithValidateWarning-Element": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3219,7 +3098,17 @@ func TestPlanSetAttribute(t *testing.T) { "write-Set-Element-AttrTypeWithValidateWarning": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3308,7 +3197,10 @@ func TestPlanSetAttribute(t *testing.T) { "write-String": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index 729eb024b..02ec66290 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -2221,81 +2221,6 @@ func TestStateSetAttribute(t *testing.T) { } testCases := map[string]testCase{ - "add-Bool": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.BoolType, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: false, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.Bool, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.Bool, false), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, - "add-List": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "tags": { - Type: types.ListType{ - ElemType: types.StringType, - }, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("tags"), - val: []string{"one", "two"}, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.List{ElementType: tftypes.String}, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "add-List-Element-append": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2691,50 +2616,6 @@ func TestStateSetAttribute(t *testing.T) { ), }, }, - "add-Map": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.MapType{ - ElemType: types.StringType, - }, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: map[string]string{ - "newkey": "newvalue", - }, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.Map{ - AttributeType: tftypes.String, - }, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.Map{ - AttributeType: tftypes.String, - }, map[string]tftypes.Value{ - "newkey": tftypes.NewValue(tftypes.String, "newvalue"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "add-Map-Element-append": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2834,133 +2715,6 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "add-Number": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.NumberType, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: 1, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.Number, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.Number, 1), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, - "add-Object": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "scratch_disk": { - Type: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "interface": types.StringType, - }, - }, - Optional: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), - val: struct { - Interface string `tfsdk:"interface"` - }{ - Interface: "NVME", - }, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "scratch_disk": tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, - }, - }, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "scratch_disk": tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "interface": tftypes.String, - }, - }, map[string]tftypes.Value{ - "interface": tftypes.NewValue(tftypes.String, "NVME"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, - "add-Set": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "tags": { - Type: types.SetType{ - ElemType: types.StringType, - }, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("tags"), - val: []string{"one", "two"}, - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "tags": tftypes.Set{ElementType: tftypes.String}, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "tags": tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "add-Set-Element-append": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -3176,40 +2930,6 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, - "add-String": { - state: State{ - Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - Schema: Schema{ - Attributes: map[string]Attribute{ - "test": { - Type: types.StringType, - Required: true, - }, - "other": { - Type: types.StringType, - Required: true, - }, - }, - }, - }, - path: tftypes.NewAttributePath().WithAttributeName("test"), - val: "newvalue", - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test": tftypes.String, - "other": tftypes.String, - }, - }, map[string]tftypes.Value{ - "test": tftypes.NewValue(tftypes.String, "newvalue"), - "other": tftypes.NewValue(tftypes.String, "should be untouched"), - }), - }, "overwrite-Bool": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -3427,6 +3147,7 @@ func TestStateSetAttribute(t *testing.T) { AttributeType: tftypes.String, }, map[string]tftypes.Value{ "originalkey": tftypes.NewValue(tftypes.String, "originalvalue"), + "otherkey": tftypes.NewValue(tftypes.String, "othervalue"), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -3560,6 +3281,7 @@ func TestStateSetAttribute(t *testing.T) { "scratch_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, "other": tftypes.String, @@ -3568,9 +3290,11 @@ func TestStateSetAttribute(t *testing.T) { "scratch_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, map[string]tftypes.Value{ "interface": tftypes.NewValue(tftypes.String, "SCSI"), + "other": tftypes.NewValue(tftypes.String, "originalvalue"), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -3580,6 +3304,7 @@ func TestStateSetAttribute(t *testing.T) { Type: types.ObjectType{ AttrTypes: map[string]attr.Type{ "interface": types.StringType, + "other": types.StringType, }, }, Optional: true, @@ -3594,14 +3319,17 @@ func TestStateSetAttribute(t *testing.T) { path: tftypes.NewAttributePath().WithAttributeName("scratch_disk"), val: struct { Interface string `tfsdk:"interface"` + Other string `tfsdk:"other"` }{ Interface: "NVME", + Other: "newvalue", }, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "scratch_disk": tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, "other": tftypes.String, @@ -3610,9 +3338,11 @@ func TestStateSetAttribute(t *testing.T) { "scratch_disk": tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "interface": tftypes.String, + "other": tftypes.String, }, }, map[string]tftypes.Value{ "interface": tftypes.NewValue(tftypes.String, "NVME"), + "other": tftypes.NewValue(tftypes.String, "newvalue"), }), "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), @@ -3852,6 +3582,60 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, "should be untouched"), }), }, + "overwrite-Set-Element-duplicate": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + tftypes.NewValue(tftypes.String, "three"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "tags": { + Type: types.SetType{ + ElemType: types.StringType, + }, + Required: true, + }, + "other": { + Type: types.StringType, + Required: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("tags").WithElementKeyValue(tftypes.NewValue(tftypes.String, "three")), + val: "three", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "tags": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + tftypes.NewValue(tftypes.String, "two"), + tftypes.NewValue(tftypes.String, "three"), + }), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, "overwrite-String": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -3891,7 +3675,9 @@ func TestStateSetAttribute(t *testing.T) { "write-root": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3905,7 +3691,9 @@ func TestStateSetAttribute(t *testing.T) { path: tftypes.NewAttributePath(), val: false, expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + }, }, nil), expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( @@ -3918,7 +3706,10 @@ func TestStateSetAttribute(t *testing.T) { "write-Bool": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Bool, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3948,7 +3739,12 @@ func TestStateSetAttribute(t *testing.T) { "write-List": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.List{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -3985,7 +3781,12 @@ func TestStateSetAttribute(t *testing.T) { "write-List-AttrTypeWithValidateWarning-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4029,7 +3830,17 @@ func TestStateSetAttribute(t *testing.T) { "write-List-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4099,7 +3910,17 @@ func TestStateSetAttribute(t *testing.T) { "write-List-Element-length-error": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4133,7 +3954,17 @@ func TestStateSetAttribute(t *testing.T) { DeleteWithInstance: true, }, expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( @@ -4220,7 +4051,12 @@ func TestStateSetAttribute(t *testing.T) { "write-Map": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4257,16 +4093,23 @@ func TestStateSetAttribute(t *testing.T) { "other": tftypes.NewValue(tftypes.String, nil), }), }, - "write-Map-Element": { + "write-Map-AttrTypeWithValidateWarning-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ "test": { - Type: types.MapType{ - ElemType: types.StringType, + Type: testtypes.MapTypeWithValidateWarning{ + MapType: types.MapType{ + ElemType: types.StringType, + }, }, Required: true, }, @@ -4294,19 +4137,25 @@ func TestStateSetAttribute(t *testing.T) { }), "other": tftypes.NewValue(tftypes.String, nil), }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), + }, }, - "write-Map-AttrTypeWithValidateWarning-Element": { + "write-Map-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ "test": { - Type: testtypes.MapTypeWithValidateWarning{ - MapType: types.MapType{ - ElemType: types.StringType, - }, + Type: types.MapType{ + ElemType: types.StringType, }, Required: true, }, @@ -4334,14 +4183,16 @@ func TestStateSetAttribute(t *testing.T) { }), "other": tftypes.NewValue(tftypes.String, nil), }), - expectedDiags: diag.Diagnostics{ - testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test")), - }, }, "write-Map-Element-AttrTypeWithValidateWarning": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + AttributeType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4382,7 +4233,10 @@ func TestStateSetAttribute(t *testing.T) { "write-Number": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Number, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4412,7 +4266,14 @@ func TestStateSetAttribute(t *testing.T) { "write-Object": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "interface": tftypes.String, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4460,7 +4321,10 @@ func TestStateSetAttribute(t *testing.T) { "write-Set": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "tags": tftypes.Set{ElementType: tftypes.String}, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4497,7 +4361,17 @@ func TestStateSetAttribute(t *testing.T) { "write-Set-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4575,7 +4449,12 @@ func TestStateSetAttribute(t *testing.T) { "write-Set-AttrTypeWithValidateWarning-Element": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.String, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4619,7 +4498,17 @@ func TestStateSetAttribute(t *testing.T) { "write-Set-Element-AttrTypeWithValidateWarning": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ @@ -4708,7 +4597,10 @@ func TestStateSetAttribute(t *testing.T) { "write-String": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{}, + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, }, nil), Schema: Schema{ Attributes: map[string]Attribute{ From daf86761c4ac899fe05d53299ae9a74c49cd8adf Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 23 Sep 2021 21:09:31 -0400 Subject: [PATCH 10/10] tfsdk: All the fixes --- tfsdk/plan.go | 26 ++++-- tfsdk/plan_test.go | 3 + tfsdk/state.go | 30 ++++--- tfsdk/tftypes_value.go | 47 ++++------- tfsdk/tftypes_value_test.go | 153 ++++-------------------------------- 5 files changed, 75 insertions(+), 184 deletions(-) diff --git a/tfsdk/plan.go b/tfsdk/plan.go index 28d82da63..a5e6ec804 100644 --- a/tfsdk/plan.go +++ b/tfsdk/plan.go @@ -231,7 +231,6 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri return nil, diags } - parentTfType := parentAttrType.TerraformType(ctx) parentValue, err := p.terraformValueAtPath(parentPath) if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { @@ -243,17 +242,30 @@ func (p Plan) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attri return nil, diags } - var parentValueDiags diag.Diagnostics - parentValue, parentValueDiags = createParentValue(ctx, parentPath, parentTfType, parentValue) - diags.Append(parentValueDiags...) + if parentValue.IsNull() || !parentValue.IsKnown() { + // TODO: This will break when DynamicPsuedoType is introduced. + // tftypes.Type should implement AttributePathStepper, but it currently does not. + // When it does, we should use: tftypes.WalkAttributePath(p.Raw.Type(), parentPath) + // Reference: https://github.com/hashicorp/terraform-plugin-go/issues/110 + parentType := parentAttrType.TerraformType(ctx) + var childValue interface{} - if diags.HasError() { - return nil, diags + if !parentValue.IsKnown() { + childValue = tftypes.UnknownValue + } + + var parentValueDiags diag.Diagnostics + parentValue, parentValueDiags = createParentValue(ctx, parentPath, parentType, childValue) + diags.Append(parentValueDiags...) + + if diags.HasError() { + return nil, diags + } } var childValueDiags diag.Diagnostics childStep := path.Steps()[len(path.Steps())-1] - parentValue, childValueDiags = upsertChildValue(ctx, parentPath, parentTfType, parentValue, childStep, tfVal) + parentValue, childValueDiags = upsertChildValue(ctx, parentPath, parentValue, childStep, tfVal) diags.Append(childValueDiags...) if diags.HasError() { diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index 25005b7e7..d3a726e07 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -3297,6 +3297,9 @@ func TestPlanSetAttribute(t *testing.T) { diags := tc.plan.SetAttribute(context.Background(), tc.path, tc.val) if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + for _, diagnostic := range diags { + t.Log(diagnostic) + } t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) } diff --git a/tfsdk/state.go b/tfsdk/state.go index 6e24094ad..74af7d4a8 100644 --- a/tfsdk/state.go +++ b/tfsdk/state.go @@ -240,29 +240,41 @@ func (s State) setAttributeTransformFunc(ctx context.Context, path *tftypes.Attr return nil, diags } - parentTfType := parentAttrType.TerraformType(ctx) parentValue, err := s.terraformValueAtPath(parentPath) if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) { diags.AddAttributeError( parentPath, - "State Read Error", - "An unexpected error was encountered trying to read an attribute from the state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + "Plan Read Error", + "An unexpected error was encountered trying to read an attribute from the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), ) return nil, diags } - var parentValueDiags diag.Diagnostics - parentValue, parentValueDiags = createParentValue(ctx, parentPath, parentTfType, parentValue) - diags.Append(parentValueDiags...) + if parentValue.IsNull() || !parentValue.IsKnown() { + // TODO: This will break when DynamicPsuedoType is introduced. + // tftypes.Type should implement AttributePathStepper, but it currently does not. + // When it does, we should use: tftypes.WalkAttributePath(s.Raw.Type(), parentPath) + // Reference: https://github.com/hashicorp/terraform-plugin-go/issues/110 + parentType := parentAttrType.TerraformType(ctx) + var childValue interface{} - if diags.HasError() { - return nil, diags + if !parentValue.IsKnown() { + childValue = tftypes.UnknownValue + } + + var parentValueDiags diag.Diagnostics + parentValue, parentValueDiags = createParentValue(ctx, parentPath, parentType, childValue) + diags.Append(parentValueDiags...) + + if diags.HasError() { + return nil, diags + } } var childValueDiags diag.Diagnostics childStep := path.Steps()[len(path.Steps())-1] - parentValue, childValueDiags = upsertChildValue(ctx, parentPath, parentTfType, parentValue, childStep, tfVal) + parentValue, childValueDiags = upsertChildValue(ctx, parentPath, parentValue, childStep, tfVal) diags.Append(childValueDiags...) if diags.HasError() { diff --git a/tfsdk/tftypes_value.go b/tfsdk/tftypes_value.go index 535927d84..8559e79ab 100644 --- a/tfsdk/tftypes_value.go +++ b/tfsdk/tftypes_value.go @@ -13,21 +13,9 @@ import ( // without modification. A null Object or Tuple is converted to known with null // children. An unknown Object or Tuple is converted to known with unknown // children. List, Map, and Set are created with empty elements. -// -// The parentType parameter ensures that the value will match the current -// schema, not what is currently stored in the parentValue. -func createParentValue(ctx context.Context, parentPath *tftypes.AttributePath, parentType tftypes.Type, parentValue tftypes.Value) (tftypes.Value, diag.Diagnostics) { +func createParentValue(ctx context.Context, parentPath *tftypes.AttributePath, parentType tftypes.Type, childValue interface{}) (tftypes.Value, diag.Diagnostics) { var diags diag.Diagnostics - - if !parentValue.IsNull() && parentValue.IsKnown() { - return parentValue, diags - } - - var childValue interface{} - - if !parentValue.IsKnown() { - childValue = tftypes.UnknownValue - } + var parentValue tftypes.Value switch parentType := parentType.(type) { case tftypes.List: @@ -57,7 +45,7 @@ func createParentValue(ctx context.Context, parentPath *tftypes.AttributePath, p parentPath, "Value Conversion Error", "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Unknown parent type %T to create value.", parentValue.Type()), + fmt.Sprintf("Unknown parent type %s to create value.", parentType), ) return parentValue, diags } @@ -70,10 +58,7 @@ func createParentValue(ctx context.Context, parentPath *tftypes.AttributePath, p // value will be added. // // Lists can only have the next element added according to the current length. -// -// The parentType parameter ensures that the value will match the current -// schema, not what is currently stored in the parentValue. -func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, parentType tftypes.Type, parentValue tftypes.Value, childStep tftypes.AttributePathStep, childValue tftypes.Value) (tftypes.Value, diag.Diagnostics) { +func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, parentValue tftypes.Value, childStep tftypes.AttributePathStep, childValue tftypes.Value) (tftypes.Value, diag.Diagnostics) { var diags diag.Diagnostics // TODO: Add Tuple support @@ -81,12 +66,12 @@ func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, pa switch childStep := childStep.(type) { case tftypes.AttributeName: // Set in Object - if !parentType.Is(tftypes.Object{}) { + if !parentValue.Type().Is(tftypes.Object{}) { diags.AddAttributeError( parentPath, "Value Conversion Error", "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add attribute into parent type: %s", parentType), + fmt.Sprintf("Cannot add attribute into parent type: %s", parentValue.Type()), ) return parentValue, diags } @@ -105,15 +90,15 @@ func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, pa } parentAttrs[string(childStep)] = childValue - parentValue = tftypes.NewValue(parentType, parentAttrs) + parentValue = tftypes.NewValue(parentValue.Type(), parentAttrs) case tftypes.ElementKeyInt: // Upsert List element, except past length + 1 - if !parentType.Is(tftypes.List{}) { + if !parentValue.Type().Is(tftypes.List{}) { diags.AddAttributeError( parentPath, "Value Conversion Error", "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add list element into parent type: %s", parentType), + fmt.Sprintf("Cannot add list element into parent type: %s", parentValue.Type()), ) return parentValue, diags } @@ -147,15 +132,15 @@ func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, pa parentElems[int(childStep)] = childValue } - parentValue = tftypes.NewValue(parentType, parentElems) + parentValue = tftypes.NewValue(parentValue.Type(), parentElems) case tftypes.ElementKeyString: // Upsert Map element - if !parentType.Is(tftypes.Map{}) { + if !parentValue.Type().Is(tftypes.Map{}) { diags.AddAttributeError( parentPath, "Value Conversion Error", "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add map value into parent type: %s", parentType), + fmt.Sprintf("Cannot add map value into parent type: %s", parentValue.Type()), ) return parentValue, diags } @@ -174,15 +159,15 @@ func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, pa } parentElems[string(childStep)] = childValue - parentValue = tftypes.NewValue(parentType, parentElems) + parentValue = tftypes.NewValue(parentValue.Type(), parentElems) case tftypes.ElementKeyValue: // Upsert Set element - if !parentType.Is(tftypes.Set{}) { + if !parentValue.Type().Is(tftypes.Set{}) { diags.AddAttributeError( parentPath, "Value Conversion Error", "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - fmt.Sprintf("Cannot add set element into parent type: %s", parentType), + fmt.Sprintf("Cannot add set element into parent type: %s", parentValue.Type()), ) return parentValue, diags } @@ -214,7 +199,7 @@ func upsertChildValue(ctx context.Context, parentPath *tftypes.AttributePath, pa parentElems = append(parentElems, childValue) } - parentValue = tftypes.NewValue(parentType, parentElems) + parentValue = tftypes.NewValue(parentValue.Type(), parentElems) } return parentValue, diags diff --git a/tfsdk/tftypes_value_test.go b/tfsdk/tftypes_value_test.go index 7998ca817..ebe61c25c 100644 --- a/tfsdk/tftypes_value_test.go +++ b/tfsdk/tftypes_value_test.go @@ -14,20 +14,20 @@ func TestCreateParentValue(t *testing.T) { testCases := map[string]struct { parentType tftypes.Type - parentValue tftypes.Value + childValue interface{} expected tftypes.Value expectedDiags diag.Diagnostics }{ "Bool-null": { - parentType: tftypes.Bool, - parentValue: tftypes.NewValue(tftypes.Bool, nil), - expected: tftypes.NewValue(tftypes.Bool, nil), + parentType: tftypes.Bool, + childValue: nil, + expected: tftypes.Value{}, expectedDiags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("test"), "Value Conversion Error", "An unexpected error was encountered trying to create a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Unknown parent type tftypes.primitive to create value.", + "Unknown parent type tftypes.Bool to create value.", ), }, }, @@ -35,9 +35,7 @@ func TestCreateParentValue(t *testing.T) { parentType: tftypes.List{ ElementType: tftypes.String, }, - parentValue: tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, nil), + childValue: nil, expected: tftypes.NewValue(tftypes.List{ ElementType: tftypes.String, }, []tftypes.Value{}), @@ -46,37 +44,16 @@ func TestCreateParentValue(t *testing.T) { parentType: tftypes.List{ ElementType: tftypes.String, }, - parentValue: tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, tftypes.UnknownValue), + childValue: tftypes.UnknownValue, expected: tftypes.NewValue(tftypes.List{ ElementType: tftypes.String, }, []tftypes.Value{}), }, - "List-value": { - parentType: tftypes.List{ - ElementType: tftypes.String, - }, - parentValue: tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - expected: tftypes.NewValue(tftypes.List{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - }, "Map-null": { parentType: tftypes.Map{ AttributeType: tftypes.String, }, - parentValue: tftypes.NewValue(tftypes.Map{ - AttributeType: tftypes.String, - }, nil), + childValue: nil, expected: tftypes.NewValue(tftypes.Map{ AttributeType: tftypes.String, }, map[string]tftypes.Value{}), @@ -85,30 +62,11 @@ func TestCreateParentValue(t *testing.T) { parentType: tftypes.Map{ AttributeType: tftypes.String, }, - parentValue: tftypes.NewValue(tftypes.Map{ - AttributeType: tftypes.String, - }, tftypes.UnknownValue), + childValue: tftypes.UnknownValue, expected: tftypes.NewValue(tftypes.Map{ AttributeType: tftypes.String, }, map[string]tftypes.Value{}), }, - "Map-value": { - parentType: tftypes.Map{ - AttributeType: tftypes.String, - }, - parentValue: tftypes.NewValue(tftypes.Map{ - AttributeType: tftypes.String, - }, map[string]tftypes.Value{ - "keyone": tftypes.NewValue(tftypes.String, "valueone"), - "keytwo": tftypes.NewValue(tftypes.String, "valuetwo"), - }), - expected: tftypes.NewValue(tftypes.Map{ - AttributeType: tftypes.String, - }, map[string]tftypes.Value{ - "keyone": tftypes.NewValue(tftypes.String, "valueone"), - "keytwo": tftypes.NewValue(tftypes.String, "valuetwo"), - }), - }, "Object-null": { parentType: tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -116,12 +74,7 @@ func TestCreateParentValue(t *testing.T) { "attrtwo": tftypes.String, }, }, - parentValue: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "attrone": tftypes.String, - "attrtwo": tftypes.String, - }, - }, nil), + childValue: nil, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "attrone": tftypes.String, @@ -139,12 +92,7 @@ func TestCreateParentValue(t *testing.T) { "attrtwo": tftypes.String, }, }, - parentValue: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "attrone": tftypes.String, - "attrtwo": tftypes.String, - }, - }, tftypes.UnknownValue), + childValue: tftypes.UnknownValue, expected: tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "attrone": tftypes.String, @@ -155,39 +103,11 @@ func TestCreateParentValue(t *testing.T) { "attrtwo": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), }), }, - "Object-value": { - parentType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "attrone": tftypes.String, - "attrtwo": tftypes.String, - }, - }, - parentValue: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "attrone": tftypes.String, - "attrtwo": tftypes.String, - }, - }, map[string]tftypes.Value{ - "attrone": tftypes.NewValue(tftypes.String, "one"), - "attrtwo": tftypes.NewValue(tftypes.String, "two"), - }), - expected: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "attrone": tftypes.String, - "attrtwo": tftypes.String, - }, - }, map[string]tftypes.Value{ - "attrone": tftypes.NewValue(tftypes.String, "one"), - "attrtwo": tftypes.NewValue(tftypes.String, "two"), - }), - }, "Set-null": { parentType: tftypes.Set{ ElementType: tftypes.String, }, - parentValue: tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, nil), + childValue: nil, expected: tftypes.NewValue(tftypes.Set{ ElementType: tftypes.String, }, []tftypes.Value{}), @@ -196,37 +116,16 @@ func TestCreateParentValue(t *testing.T) { parentType: tftypes.Set{ ElementType: tftypes.String, }, - parentValue: tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, tftypes.UnknownValue), + childValue: tftypes.UnknownValue, expected: tftypes.NewValue(tftypes.Set{ ElementType: tftypes.String, }, []tftypes.Value{}), }, - "Set-value": { - parentType: tftypes.Set{ - ElementType: tftypes.String, - }, - parentValue: tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - expected: tftypes.NewValue(tftypes.Set{ - ElementType: tftypes.String, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - }, "Tuple-null": { parentType: tftypes.Tuple{ ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, }, - parentValue: tftypes.NewValue(tftypes.Tuple{ - ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, - }, nil), + childValue: nil, expected: tftypes.NewValue(tftypes.Tuple{ ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, }, []tftypes.Value{ @@ -238,9 +137,7 @@ func TestCreateParentValue(t *testing.T) { parentType: tftypes.Tuple{ ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, }, - parentValue: tftypes.NewValue(tftypes.Tuple{ - ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, - }, tftypes.UnknownValue), + childValue: tftypes.UnknownValue, expected: tftypes.NewValue(tftypes.Tuple{ ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, }, []tftypes.Value{ @@ -248,23 +145,6 @@ func TestCreateParentValue(t *testing.T) { tftypes.NewValue(tftypes.String, tftypes.UnknownValue), }), }, - "Tuple-value": { - parentType: tftypes.Tuple{ - ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, - }, - parentValue: tftypes.NewValue(tftypes.Tuple{ - ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - expected: tftypes.NewValue(tftypes.Tuple{ - ElementTypes: []tftypes.Type{tftypes.String, tftypes.String}, - }, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "one"), - tftypes.NewValue(tftypes.String, "two"), - }), - }, } for name, tc := range testCases { @@ -276,7 +156,7 @@ func TestCreateParentValue(t *testing.T) { context.Background(), tftypes.NewAttributePath().WithAttributeName("test"), tc.parentType, - tc.parentValue, + tc.childValue, ) if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { @@ -614,7 +494,6 @@ func TestUpsertChildValue(t *testing.T) { got, diags := upsertChildValue( context.Background(), tftypes.NewAttributePath().WithAttributeName("test"), - tc.parentType, tc.parentValue, tc.childStep, tc.childValue,