opentofu/terraform/eval_read_data_plan.go
James Bardin f433228906 hide empty plans for misbehaving data resource
If a data source is storing a value that doesn't comply precisely with
the schema, it will now show up as a perpetual diff during plan.

Since we can easily detect if there is no resulting change from the
stored value, rather than presenting a planned read each time, we can
change the plan to a NoOp and log the incongruity as a warning.
2020-06-18 19:21:19 -04:00

194 lines
6.3 KiB
Go

package terraform
import (
"fmt"
"log"
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/objchange"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags"
)
// evalReadDataPlan is an EvalNode implementation that deals with the main part
// of the data resource lifecycle: either actually reading from the data source
// or generating a plan to do so.
type evalReadDataPlan struct {
evalReadData
}
func (n *evalReadDataPlan) Eval(ctx EvalContext) (interface{}, error) {
absAddr := n.Addr.Absolute(ctx.Path())
var diags tfdiags.Diagnostics
var configVal cty.Value
if n.ProviderSchema == nil || *n.ProviderSchema == nil {
return nil, fmt.Errorf("provider schema not available for %s", n.Addr)
}
config := *n.Config
providerSchema := *n.ProviderSchema
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
if schema == nil {
// Should be caught during validation, so we don't bother with a pretty error here
return nil, fmt.Errorf("provider %q does not support data source %q", n.ProviderAddr.Provider.String(), n.Addr.Resource.Type)
}
objTy := schema.ImpliedType()
priorVal := cty.NullVal(objTy)
if n.State != nil && *n.State != nil {
priorVal = (*n.State).Value
}
forEach, _ := evaluateForEachExpression(config.ForEach, ctx)
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
var configDiags tfdiags.Diagnostics
configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, diags.ErrWithWarnings()
}
configKnown := configVal.IsWhollyKnown()
// If our configuration contains any unknown values, or we depend on any
// unknown values then we must defer the read to the apply phase by
// producing a "Read" change for this resource, and a placeholder value for
// it in the state.
if n.forcePlanRead(ctx) || !configKnown {
if configKnown {
log.Printf("[TRACE] evalReadDataPlan: %s configuration is fully known, but we're forcing a read plan to be created", absAddr)
} else {
log.Printf("[TRACE] evalReadDataPlan: %s configuration not fully known yet, so deferring to apply phase", absAddr)
}
proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal)
if err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
}); err != nil {
diags = diags.Append(err)
return nil, diags.ErrWithWarnings()
}
// Apply detects that the data source will need to be read by the After
// value containing unknowns from PlanDataResourceObject.
*n.OutputChange = &plans.ResourceInstanceChange{
Addr: absAddr,
ProviderAddr: n.ProviderAddr,
Change: plans.Change{
Action: plans.Read,
Before: priorVal,
After: proposedNewVal,
},
}
*n.State = &states.ResourceInstanceObject{
Value: cty.NullVal(objTy),
Status: states.ObjectPlanned,
}
if err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostDiff(absAddr, states.CurrentGen, plans.Read, priorVal, proposedNewVal)
}); err != nil {
diags = diags.Append(err)
}
return nil, diags.ErrWithWarnings()
}
var proposedVal cty.Value
// If we have a stored state we may not need to re-read the data source.
// Check the config against the state to see if there are any difference.
if !priorVal.IsNull() {
// Applying the configuration to the prior state lets us see if there
// are any differences.
proposedVal = objchange.ProposedNewObject(schema, priorVal, configVal)
if proposedVal.Equals(priorVal).True() {
log.Printf("[TRACE] evalReadDataPlan: %s no change detected, using existing state", absAddr)
// state looks up to date, and must have been read during refresh
return nil, diags.ErrWithWarnings()
}
log.Printf("[TRACE] evalReadDataPlan: %s configuration changed, planning data source", absAddr)
}
newVal, readDiags := n.readDataSource(ctx, configVal)
diags = diags.Append(readDiags)
if diags.HasErrors() {
return nil, diags.ErrWithWarnings()
}
// if we have a prior value, we can check for any irregularities in the response
if !priorVal.IsNull() {
if errs := objchange.AssertObjectCompatible(schema, proposedVal, newVal); len(errs) > 0 {
// Resources have the LegacyTypeSystem field to signal when they are
// using an SDK which may not produce precise values. While data
// sources are read-only, they can still return a value which is not
// compatible with the config+schema. Since we can't detect the legacy
// type system, we can only warn about this for now.
var buf strings.Builder
fmt.Fprintf(&buf, "[WARN] Provider %q produced an unexpected new value for %s."+
n.ProviderAddr.Provider.String(), absAddr)
for _, err := range errs {
fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err))
}
log.Print(buf.String())
}
}
action := plans.Read
if priorVal.Equals(newVal).True() {
action = plans.NoOp
}
// The returned value from ReadDataSource must be non-nil and known,
// which we store in the change. Apply will use the fact that the After
// value is wholly kown to save the state directly, rather than reading the
// data source again.
*n.OutputChange = &plans.ResourceInstanceChange{
Addr: absAddr,
ProviderAddr: n.ProviderAddr,
Change: plans.Change{
Action: action,
Before: priorVal,
After: newVal,
},
}
*n.State = &states.ResourceInstanceObject{
Value: newVal,
Status: states.ObjectPlanned,
}
if err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostDiff(absAddr, states.CurrentGen, plans.Update, priorVal, newVal)
}); err != nil {
return nil, err
}
return nil, diags.ErrWithWarnings()
}
// forcePlanRead determines if we need to override the usual behavior of
// immediately reading from the data source where possible, instead forcing us
// to generate a plan.
func (n *evalReadDataPlan) forcePlanRead(ctx EvalContext) bool {
// Check and see if any depends_on dependencies have
// changes, since they won't show up as changes in the
// configuration.
changes := ctx.Changes()
for _, d := range n.dependsOn {
for _, change := range changes.GetChangesForConfigResource(d) {
if change != nil && change.Action != plans.NoOp {
return true
}
}
}
return false
}