Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tfsdk: Allow Plan and State SetAttribute to create attribute paths #165

Merged
merged 10 commits into from
Sep 24, 2021
3 changes: 3 additions & 0 deletions .changelog/165.txt
Original file line number Diff line number Diff line change
@@ -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.
```
31 changes: 31 additions & 0 deletions internal/testing/types/listwithvalidate.go
Original file line number Diff line number Diff line change
@@ -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)}
}
31 changes: 31 additions & 0 deletions internal/testing/types/mapwithvalidate.go
Original file line number Diff line number Diff line change
@@ -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)}
}
31 changes: 31 additions & 0 deletions internal/testing/types/setwithvalidate.go
Original file line number Diff line number Diff line change
@@ -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)}
}
144 changes: 132 additions & 12 deletions tfsdk/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tfsdk

import (
"context"
"errors"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
Expand Down Expand Up @@ -101,6 +102,13 @@ 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 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

Expand Down Expand Up @@ -133,25 +141,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, nil)
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",
Expand All @@ -163,6 +172,117 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va
return diags
}

// 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) {
paddycarver marked this conversation as resolved.
Show resolved Hide resolved
var diags diag.Diagnostics

_, remaining, err := tftypes.WalkAttributePath(p.Raw, path)

if err != nil {
if errors.Is(err, tftypes.ErrInvalidStep) {
return false, diags
}

diags.AddAttributeError(
path,
"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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a request for a change, just noting here that this shouldn't be necessary, it should always be true when there's no error. I think it's good to check it anyways, just wanted to surface that context on WalkAttributePath.

}

// 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) {
paddycarver marked this conversation as resolved.
Show resolved Hide resolved
exists, pathExistsDiags := p.pathExists(ctx, path)
diags.Append(pathExistsDiags...)

if diags.HasError() {
return nil, diags
}

if exists {
// 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
}
paddycarver marked this conversation as resolved.
Show resolved Hide resolved

parentPath := path.WithoutLastStep()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the root, and has no parent, parentPath will be nil.

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
}

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
}

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 !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, parentValue, childStep, tfVal)
diags.Append(childValueDiags...)

if diags.HasError() {
return nil, diags
}

if attrTypeWithValidate, ok := parentAttrType.(attr.TypeWithValidate); ok {
diags.Append(attrTypeWithValidate.Validate(ctx, parentValue, parentPath)...)

if diags.HasError() {
return nil, diags
}
}

return p.setAttributeTransformFunc(ctx, parentPath, parentValue, diags)
}

func (p Plan) terraformValueAtPath(path *tftypes.AttributePath) (tftypes.Value, error) {
rawValue, remaining, err := tftypes.WalkAttributePath(p.Raw, path)
if err != nil {
Expand Down
Loading