Skip to content

Commit 5e962cb

Browse files
committed
tfsdk: Allow Plan and State SetAttribute to create attribute paths
Reference: #148
1 parent d44c3f9 commit 5e962cb

File tree

6 files changed

+4440
-161
lines changed

6 files changed

+4440
-161
lines changed

.changelog/pending.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
tfsdk: `(Plan).SetAttribute()` and `(State).SetAttribute()` will now create missing attribute paths instead of silently failing to update.
3+
```

tfsdk/plan.go

+228-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tfsdk
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
"github.com/hashicorp/terraform-plugin-framework/attr"
@@ -101,6 +102,15 @@ func (p *Plan) Set(ctx context.Context, val interface{}) diag.Diagnostics {
101102
}
102103

103104
// SetAttribute sets the attribute at `path` using the supplied Go value.
105+
//
106+
// The attribute path and value must be valid with the current schema. If the
107+
// attribute path already has a value, it will be overwritten. If the attribute
108+
// path does not have a value, it will be added, including any parent attribute
109+
// paths as necessary.
110+
//
111+
// Lists can only have the first element added if empty and can only add the
112+
// next element according to the current length, otherwise this will return an
113+
// error.
104114
func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, val interface{}) diag.Diagnostics {
105115
var diags diag.Diagnostics
106116

@@ -133,25 +143,26 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va
133143
return diags
134144
}
135145

136-
transformFunc := func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) {
137-
if p.Equal(path) {
138-
tfVal := tftypes.NewValue(attrType.TerraformType(ctx), newTfVal)
146+
tfVal := tftypes.NewValue(attrType.TerraformType(ctx), newTfVal)
139147

140-
if attrTypeWithValidate, ok := attrType.(attr.TypeWithValidate); ok {
141-
diags.Append(attrTypeWithValidate.Validate(ctx, tfVal, path)...)
142-
143-
if diags.HasError() {
144-
return v, nil
145-
}
146-
}
148+
if attrTypeWithValidate, ok := attrType.(attr.TypeWithValidate); ok {
149+
diags.Append(attrTypeWithValidate.Validate(ctx, tfVal, path)...)
147150

148-
return tfVal, nil
151+
if diags.HasError() {
152+
return diags
149153
}
150-
return v, nil
154+
}
155+
156+
transformFunc, transformFuncDiags := p.setAttributeTransformFunc(ctx, path, tfVal)
157+
diags.Append(transformFuncDiags...)
158+
159+
if diags.HasError() {
160+
return diags
151161
}
152162

153163
p.Raw, err = tftypes.Transform(p.Raw, transformFunc)
154164
if err != nil {
165+
err = fmt.Errorf("Cannot transform plan: %w", err)
155166
diags.AddAttributeError(
156167
path,
157168
"Plan Write Error",
@@ -163,6 +174,211 @@ func (p *Plan) SetAttribute(ctx context.Context, path *tftypes.AttributePath, va
163174
return diags
164175
}
165176

177+
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) {
178+
var diags diag.Diagnostics
179+
180+
_, remaining, err := tftypes.WalkAttributePath(p.Raw, path)
181+
182+
if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) {
183+
err = fmt.Errorf("Cannot walk attribute path in plan: %w", err)
184+
diags.AddAttributeError(
185+
path,
186+
"Plan Write Error",
187+
"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(),
188+
)
189+
return nil, diags
190+
}
191+
192+
if len(remaining.Steps()) == 0 {
193+
// Overwrite existing value
194+
return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) {
195+
if p.Equal(path) {
196+
return tfVal, nil
197+
}
198+
return v, nil
199+
}, diags
200+
}
201+
202+
var parentTfVal tftypes.Value
203+
parentPath := path.WithoutLastStep()
204+
parentAttrType, err := p.Schema.AttributeTypeAtPath(parentPath)
205+
206+
if err != nil {
207+
err = fmt.Errorf("error getting parent attribute type in schema: %w", err)
208+
diags.AddAttributeError(
209+
parentPath,
210+
"Plan Write Error",
211+
"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(),
212+
)
213+
return nil, diags
214+
}
215+
216+
parentTfType := parentAttrType.TerraformType(ctx)
217+
parentValue, err := p.terraformValueAtPath(parentPath)
218+
219+
if err != nil && !errors.Is(err, tftypes.ErrInvalidStep) {
220+
diags.AddAttributeError(
221+
parentPath,
222+
"Plan Read Error",
223+
"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(),
224+
)
225+
return nil, diags
226+
}
227+
228+
// Check if parent needs to get created
229+
if parentValue.Equal(tftypes.NewValue(tftypes.Object{}, nil)) {
230+
// NewValue will panic if required attributes are missing in the
231+
// tftypes.Object.
232+
vals := map[string]tftypes.Value{}
233+
for name, t := range parentTfType.(tftypes.Object).AttributeTypes {
234+
vals[name] = tftypes.NewValue(t, nil)
235+
}
236+
parentValue = tftypes.NewValue(parentTfType, vals)
237+
} else if parentValue.Equal(tftypes.Value{}) {
238+
parentValue = tftypes.NewValue(parentTfType, nil)
239+
}
240+
241+
switch step := remaining.Steps()[len(remaining.Steps())-1].(type) {
242+
case tftypes.AttributeName:
243+
// Add to Object
244+
if !parentValue.Type().Is(tftypes.Object{}) {
245+
diags.AddAttributeError(
246+
parentPath,
247+
"Plan Write Error",
248+
"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"+
249+
fmt.Sprintf("Cannot add attribute into parent type: %s", parentValue.Type()),
250+
)
251+
return nil, diags
252+
}
253+
254+
var parentAttrs map[string]tftypes.Value
255+
err = parentValue.Copy().As(&parentAttrs)
256+
257+
if err != nil {
258+
diags.AddAttributeError(
259+
parentPath,
260+
"Plan Write Error",
261+
"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"+
262+
fmt.Sprintf("Unable to extract object elements from parent value: %s", err),
263+
)
264+
return nil, diags
265+
}
266+
267+
parentAttrs[string(step)] = tfVal
268+
parentTfVal = tftypes.NewValue(parentTfType, parentAttrs)
269+
case tftypes.ElementKeyInt:
270+
// Add new List element
271+
if !parentValue.Type().Is(tftypes.List{}) {
272+
diags.AddAttributeError(
273+
parentPath,
274+
"Plan Write Error",
275+
"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"+
276+
fmt.Sprintf("Cannot add list element into parent type: %s", parentValue.Type()),
277+
)
278+
return nil, diags
279+
}
280+
281+
var parentElems []tftypes.Value
282+
err = parentValue.Copy().As(&parentElems)
283+
284+
if err != nil {
285+
diags.AddAttributeError(
286+
parentPath,
287+
"Plan Write Error",
288+
"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"+
289+
fmt.Sprintf("Unable to extract list elements from parent value: %s", err),
290+
)
291+
return nil, diags
292+
}
293+
294+
if int(step) > len(parentElems) {
295+
diags.AddAttributeError(
296+
parentPath,
297+
"Plan Write Error",
298+
"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"+
299+
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)),
300+
)
301+
return nil, diags
302+
}
303+
304+
parentElems = append(parentElems, tfVal)
305+
parentTfVal = tftypes.NewValue(parentTfType, parentElems)
306+
case tftypes.ElementKeyString:
307+
// Add new Map element
308+
if !parentValue.Type().Is(tftypes.Map{}) {
309+
diags.AddAttributeError(
310+
parentPath,
311+
"Plan Write Error",
312+
"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"+
313+
fmt.Sprintf("Cannot add map value into parent type: %s", parentValue.Type()),
314+
)
315+
return nil, diags
316+
}
317+
318+
var parentElems map[string]tftypes.Value
319+
err = parentValue.Copy().As(&parentElems)
320+
321+
if err != nil {
322+
diags.AddAttributeError(
323+
parentPath,
324+
"Plan Write Error",
325+
"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"+
326+
fmt.Sprintf("Unable to extract map elements from parent value: %s", err),
327+
)
328+
return nil, diags
329+
}
330+
331+
parentElems[string(step)] = tfVal
332+
parentTfVal = tftypes.NewValue(parentTfType, parentElems)
333+
case tftypes.ElementKeyValue:
334+
// Add new Set element
335+
if !parentValue.Type().Is(tftypes.Set{}) {
336+
diags.AddAttributeError(
337+
parentPath,
338+
"Plan Write Error",
339+
"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"+
340+
fmt.Sprintf("Cannot add set element into parent type: %s", parentValue.Type()),
341+
)
342+
return nil, diags
343+
}
344+
345+
var parentElems []tftypes.Value
346+
err = parentValue.Copy().As(&parentElems)
347+
348+
if err != nil {
349+
diags.AddAttributeError(
350+
parentPath,
351+
"Plan Write Error",
352+
"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"+
353+
fmt.Sprintf("Unable to extract set elements from parent value: %s", err),
354+
)
355+
return nil, diags
356+
}
357+
358+
parentElems = append(parentElems, tfVal)
359+
parentTfVal = tftypes.NewValue(parentTfType, parentElems)
360+
}
361+
362+
if attrTypeWithValidate, ok := parentAttrType.(attr.TypeWithValidate); ok {
363+
diags.Append(attrTypeWithValidate.Validate(ctx, parentTfVal, parentPath)...)
364+
365+
if diags.HasError() {
366+
return nil, diags
367+
}
368+
}
369+
370+
if len(remaining.Steps()) > 1 {
371+
return p.setAttributeTransformFunc(ctx, parentPath, parentTfVal)
372+
}
373+
374+
return func(p *tftypes.AttributePath, v tftypes.Value) (tftypes.Value, error) {
375+
if p.Equal(parentPath) {
376+
return parentTfVal, nil
377+
}
378+
return v, nil
379+
}, diags
380+
}
381+
166382
func (p Plan) terraformValueAtPath(path *tftypes.AttributePath) (tftypes.Value, error) {
167383
rawValue, remaining, err := tftypes.WalkAttributePath(p.Raw, path)
168384
if err != nil {

0 commit comments

Comments
 (0)