-
Notifications
You must be signed in to change notification settings - Fork 94
/
server_planresourcechange.go
517 lines (431 loc) · 17.2 KB
/
server_planresourcechange.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package fwserver
import (
"context"
"errors"
"fmt"
"sort"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata"
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
// PlanResourceChangeRequest is the framework server request for the
// PlanResourceChange RPC.
type PlanResourceChangeRequest struct {
ClientCapabilities resource.ModifyPlanClientCapabilities
Config *tfsdk.Config
PriorPrivate *privatestate.Data
PriorState *tfsdk.State
ProposedNewState *tfsdk.Plan
ProviderMeta *tfsdk.Config
ResourceSchema fwschema.Schema
Resource resource.Resource
ResourceBehavior resource.ResourceBehavior
}
// PlanResourceChangeResponse is the framework server response for the
// PlanResourceChange RPC.
type PlanResourceChangeResponse struct {
Deferred *resource.Deferred
Diagnostics diag.Diagnostics
PlannedPrivate *privatestate.Data
PlannedState *tfsdk.State
RequiresReplace path.Paths
}
// PlanResourceChange implements the framework server PlanResourceChange RPC.
func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChangeRequest, resp *PlanResourceChangeResponse) {
if req == nil {
return
}
// Skip ModifyPlan for automatic deferrals with proposed new state as a best effort for PlannedState
// unless ProviderDeferredBehavior.EnablePlanModification is true.
if s.deferred != nil && !req.ResourceBehavior.ProviderDeferred.EnablePlanModification {
logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.",
map[string]interface{}{
logging.KeyDeferredReason: s.deferred.Reason.String(),
},
)
resp.PlannedState = planToState(*req.ProposedNewState)
resp.PlannedPrivate = req.PriorPrivate
resp.Deferred = &resource.Deferred{
Reason: resource.DeferredReason(s.deferred.Reason),
}
return
}
if resourceWithConfigure, ok := req.Resource.(resource.ResourceWithConfigure); ok {
logging.FrameworkTrace(ctx, "Resource implements ResourceWithConfigure")
configureReq := resource.ConfigureRequest{
ProviderData: s.ResourceConfigureData,
}
configureResp := resource.ConfigureResponse{}
logging.FrameworkTrace(ctx, "Calling provider defined Resource Configure")
resourceWithConfigure.Configure(ctx, configureReq, &configureResp)
logging.FrameworkTrace(ctx, "Called provider defined Resource Configure")
resp.Diagnostics.Append(configureResp.Diagnostics...)
if resp.Diagnostics.HasError() {
return
}
}
nullTfValue := tftypes.NewValue(req.ResourceSchema.Type().TerraformType(ctx), nil)
// Prevent potential panics by ensuring incoming Config/Plan/State are null
// instead of nil.
if req.Config == nil {
req.Config = &tfsdk.Config{
Raw: nullTfValue,
Schema: req.ResourceSchema,
}
}
if req.ProposedNewState == nil {
req.ProposedNewState = &tfsdk.Plan{
Raw: nullTfValue,
Schema: req.ResourceSchema,
}
}
if req.PriorState == nil {
req.PriorState = &tfsdk.State{
Raw: nullTfValue,
Schema: req.ResourceSchema,
}
}
// Ensure that resp.PlannedPrivate is never nil.
resp.PlannedPrivate = privatestate.EmptyData(ctx)
if req.PriorPrivate != nil {
// Overwrite resp.PlannedPrivate with req.PriorPrivate providing
// it is not nil.
resp.PlannedPrivate = req.PriorPrivate
// Ensure that resp.PlannedPrivate.Provider is never nil.
if resp.PlannedPrivate.Provider == nil {
resp.PlannedPrivate.Provider = privatestate.EmptyProviderData(ctx)
}
}
resp.PlannedState = planToState(*req.ProposedNewState)
// Set Defaults.
//
// If the planned state is not null (i.e., not a destroy operation) we traverse the schema,
// identifying any attributes which are null within the configuration, and if the attribute
// has a default value specified by the `Default` field on the attribute then the default
// value is assigned.
if !resp.PlannedState.Raw.IsNull() {
data := fwschemadata.Data{
Description: fwschemadata.DataDescriptionState,
Schema: resp.PlannedState.Schema,
TerraformValue: resp.PlannedState.Raw,
}
diags := data.TransformDefaults(ctx, req.Config.Raw)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
resp.PlannedState.Raw = data.TerraformValue
}
// After ensuring there are proposed changes, mark any computed attributes
// that are null in the config as unknown in the plan, so providers have
// the choice to update them.
//
// Later attribute and resource plan modifier passes can override the
// unknown with a known value using any plan modifiers.
//
// We only do this if there's a plan to modify; otherwise, it
// represents a resource being deleted and there's no point.
if !resp.PlannedState.Raw.IsNull() && !resp.PlannedState.Raw.Equal(req.PriorState.Raw) {
// Loop through top level attributes/blocks to individually emit logs
// for value changes. This is helpful for troubleshooting unexpected
// plan outputs and only needs to be done for resource update plans.
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/627
if !req.PriorState.Raw.IsNull() {
var allPaths, changedPaths path.Paths
for attrName := range resp.PlannedState.Schema.GetAttributes() {
allPaths.Append(path.Root(attrName))
}
for blockName := range resp.PlannedState.Schema.GetBlocks() {
allPaths.Append(path.Root(blockName))
}
for _, p := range allPaths {
var plannedState, priorState attr.Value
// This logging is best effort and any errors should not be
// returned to practitioners.
_ = resp.PlannedState.GetAttribute(ctx, p, &plannedState)
_ = req.PriorState.GetAttribute(ctx, p, &priorState)
// Due to ignoring diagnostics, the value may not be populated.
// Prevent the panic and show the path as changed.
if plannedState == nil {
changedPaths.Append(p)
continue
}
if plannedState.Equal(priorState) {
continue
}
changedPaths.Append(p)
}
// Colocate these log entries to not intermix with GetAttribute logging
for _, p := range changedPaths {
logging.FrameworkDebug(ctx,
"Detected value change between proposed new state and prior state",
map[string]any{
logging.KeyAttributePath: p.String(),
},
)
}
}
logging.FrameworkDebug(ctx, "Marking Computed attributes with null configuration values as unknown (known after apply) in the plan to prevent potential Terraform errors")
modifiedPlan, err := tftypes.Transform(resp.PlannedState.Raw, MarkComputedNilsAsUnknown(ctx, req.Config.Raw, req.ResourceSchema))
if err != nil {
resp.Diagnostics.AddError(
"Error modifying plan",
"There was an unexpected error updating the plan. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(),
)
return
}
if !resp.PlannedState.Raw.Equal(modifiedPlan) {
logging.FrameworkTrace(ctx, "At least one Computed null Config value was changed to unknown")
}
resp.PlannedState.Raw = modifiedPlan
}
// Execute any schema-based plan modifiers. This allows overwriting
// any unknown values.
//
// We only do this if there's a plan to modify; otherwise, it
// represents a resource being deleted and there's no point.
if !resp.PlannedState.Raw.IsNull() {
modifySchemaPlanReq := ModifySchemaPlanRequest{
Config: *req.Config,
Plan: stateToPlan(*resp.PlannedState),
State: *req.PriorState,
Private: resp.PlannedPrivate.Provider,
}
if req.ProviderMeta != nil {
modifySchemaPlanReq.ProviderMeta = *req.ProviderMeta
}
modifySchemaPlanResp := ModifySchemaPlanResponse{
Diagnostics: resp.Diagnostics,
Plan: modifySchemaPlanReq.Plan,
Private: modifySchemaPlanReq.Private,
}
SchemaModifyPlan(ctx, req.ResourceSchema, modifySchemaPlanReq, &modifySchemaPlanResp)
resp.Diagnostics = modifySchemaPlanResp.Diagnostics
resp.PlannedState = planToState(modifySchemaPlanResp.Plan)
resp.RequiresReplace = append(resp.RequiresReplace, modifySchemaPlanResp.RequiresReplace...)
resp.PlannedPrivate.Provider = modifySchemaPlanResp.Private
if resp.Diagnostics.HasError() {
return
}
}
// Execute any resource-level ModifyPlan method. This allows
// overwriting any unknown values.
//
// We do this regardless of whether the plan is null or not, because we
// want resources to be able to return diagnostics when planning to
// delete resources, e.g. to inform practitioners that the resource
// _can't_ be deleted in the API and will just be removed from
// Terraform's state
if resourceWithModifyPlan, ok := req.Resource.(resource.ResourceWithModifyPlan); ok {
logging.FrameworkTrace(ctx, "Resource implements ResourceWithModifyPlan")
modifyPlanReq := resource.ModifyPlanRequest{
ClientCapabilities: req.ClientCapabilities,
Config: *req.Config,
Plan: stateToPlan(*resp.PlannedState),
State: *req.PriorState,
Private: resp.PlannedPrivate.Provider,
}
if req.ProviderMeta != nil {
modifyPlanReq.ProviderMeta = *req.ProviderMeta
}
modifyPlanResp := resource.ModifyPlanResponse{
Diagnostics: resp.Diagnostics,
Plan: modifyPlanReq.Plan,
RequiresReplace: path.Paths{},
Private: modifyPlanReq.Private,
}
logging.FrameworkTrace(ctx, "Calling provider defined Resource ModifyPlan")
resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp)
logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan")
resp.Diagnostics = modifyPlanResp.Diagnostics
resp.PlannedState = planToState(modifyPlanResp.Plan)
resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...)
resp.PlannedPrivate.Provider = modifyPlanResp.Private
resp.Deferred = modifyPlanResp.Deferred
// Provider deferred response is present, add the deferred response alongside the provider-modified plan
if s.deferred != nil {
logging.FrameworkDebug(ctx, "Provider has deferred response configured, returning deferred response with modified plan.")
// Only set the response to the provider configured deferred reason if there is no resource configured deferred reason
if resp.Deferred == nil {
resp.Deferred = &resource.Deferred{
Reason: resource.DeferredReason(s.deferred.Reason),
}
} else {
logging.FrameworkDebug(ctx, fmt.Sprintf("Resource has deferred reason configured, "+
"replacing provider deferred reason: %s with resource deferred reason: %s",
s.deferred.Reason.String(), modifyPlanResp.Deferred.Reason.String()))
}
return
}
}
// Ensure deterministic RequiresReplace by sorting and deduplicating
resp.RequiresReplace = NormaliseRequiresReplace(ctx, resp.RequiresReplace)
// If this was a destroy resource plan, ensure the plan remained null.
if req.ProposedNewState.Raw.IsNull() && !resp.PlannedState.Raw.IsNull() {
resp.Diagnostics.AddError(
"Unexpected Planned Resource State on Destroy",
"The Terraform Provider unexpectedly returned resource state data when the resource was planned for destruction. "+
"This is always an issue in the Terraform Provider and should be reported to the provider developers.\n\n"+
"Ensure all resource plan modifiers do not attempt to change resource plan data from being a null value if the request plan is a null value.",
)
}
}
func MarkComputedNilsAsUnknown(ctx context.Context, config tftypes.Value, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) {
return func(path *tftypes.AttributePath, val tftypes.Value) (tftypes.Value, error) {
ctx = logging.FrameworkWithAttributePath(ctx, path.String())
// we are only modifying attributes, not the entire resource
if len(path.Steps()) < 1 {
return val, nil
}
attribute, err := resourceSchema.AttributeAtTerraformPath(ctx, path)
if err != nil {
if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) {
// ignore attributes/elements inside schema.Attributes, they have no schema of their own
logging.FrameworkTrace(ctx, "attribute is a non-schema attribute, not marking unknown")
return val, nil
}
if errors.Is(err, fwschema.ErrPathIsBlock) {
// ignore blocks, they do not have a computed field
logging.FrameworkTrace(ctx, "attribute is a block, not marking unknown")
return val, nil
}
if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) {
// ignore attributes/elements inside schema.DynamicAttribute, they have no schema of their own
logging.FrameworkTrace(ctx, "attribute is inside of a dynamic attribute, not marking unknown")
return val, nil
}
logging.FrameworkError(ctx, "couldn't find attribute in resource schema")
return tftypes.Value{}, fmt.Errorf("couldn't find attribute in resource schema: %w", err)
}
configValIface, _, err := tftypes.WalkAttributePath(config, path)
if err != nil && err != tftypes.ErrInvalidStep {
logging.FrameworkError(ctx,
"Error walking attributes/block path during unknown marking",
map[string]any{
logging.KeyError: err.Error(),
},
)
return val, fmt.Errorf("error walking attribute/block path during unknown marking: %w", err)
}
configVal, ok := configValIface.(tftypes.Value)
if !ok {
return val, fmt.Errorf("unexpected type during unknown marking: %T", configValIface)
}
if !configVal.IsNull() {
logging.FrameworkTrace(ctx, "Attribute/block not null in configuration, not marking unknown")
return val, nil
}
if !attribute.IsComputed() {
logging.FrameworkTrace(ctx, "attribute is not computed in schema, not marking unknown")
return val, nil
}
switch a := attribute.(type) {
case fwschema.AttributeWithBoolDefaultValue:
if a.BoolDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithFloat32DefaultValue:
if a.Float32DefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithFloat64DefaultValue:
if a.Float64DefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithInt32DefaultValue:
if a.Int32DefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithInt64DefaultValue:
if a.Int64DefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithListDefaultValue:
if a.ListDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithMapDefaultValue:
if a.MapDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithNumberDefaultValue:
if a.NumberDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithObjectDefaultValue:
if a.ObjectDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithSetDefaultValue:
if a.SetDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithStringDefaultValue:
if a.StringDefaultValue() != nil {
return val, nil
}
case fwschema.AttributeWithDynamicDefaultValue:
if a.DynamicDefaultValue() != nil {
return val, nil
}
}
// Value type from planned state to create unknown with
newValueType := val.Type()
// If the attribute is dynamic then we can't use the planned state value to create an unknown, as it may be a concrete type.
// This logic explicitly sets the unknown value type to dynamic so the type can be determined during apply.
_, isDynamic := attribute.GetType().(basetypes.DynamicTypable)
if isDynamic {
newValueType = tftypes.DynamicPseudoType
}
logging.FrameworkDebug(ctx, "marking computed attribute that is null in the config as unknown")
return tftypes.NewValue(newValueType, tftypes.UnknownValue), nil
}
}
// NormaliseRequiresReplace sorts and deduplicates the slice of AttributePaths
// used in the RequiresReplace response field.
// Sorting is lexical based on the string representation of each AttributePath.
func NormaliseRequiresReplace(ctx context.Context, rs path.Paths) path.Paths {
if len(rs) < 2 {
return rs
}
sort.Slice(rs, func(i, j int) bool {
return rs[i].String() < rs[j].String()
})
ret := make(path.Paths, len(rs))
ret[0] = rs[0]
// deduplicate
j := 1
for i := 1; i < len(rs); i++ {
if rs[i].Equal(ret[j-1]) {
logging.FrameworkDebug(ctx, "attribute found multiple times in RequiresReplace, removing duplicate", map[string]interface{}{logging.KeyAttributePath: rs[i]})
continue
}
ret[j] = rs[i]
j++
}
return ret[:j]
}
// planToState returns a *tfsdk.State with a copied value from a tfsdk.Plan.
func planToState(plan tfsdk.Plan) *tfsdk.State {
return &tfsdk.State{
Raw: plan.Raw.Copy(),
Schema: plan.Schema,
}
}
// stateToPlan returns a tfsdk.Plan with a copied value from a tfsdk.State.
func stateToPlan(state tfsdk.State) tfsdk.Plan {
return tfsdk.Plan{
Raw: state.Raw.Copy(),
Schema: state.Schema,
}
}