Use the EvalContext to lookup trigger changes

The EvalContext is the only place with all the information to be able to
complete the evaluation of the replace_triggered_by expressions. These
need to be evaluated into a reference, which is then looked up in the
pending changes which the context has access too. On top of needing the
plan changes, we also need access to all providers and schemas to decode
the changes if we need to traverse the resource values for individual
attributes.
This commit is contained in:
James Bardin 2022-04-12 15:54:09 -04:00
parent 8b4c89bdaf
commit 4d43d6f699
3 changed files with 116 additions and 1 deletions

View File

@ -117,6 +117,11 @@ type EvalContext interface {
// evaluating. Set this to nil if the "self" object should not be available.
EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics)
// EvaluateReplaceTriggeredBy takes the raw reference expression from the
// config, and returns the evaluated *addrs.Reference along with a boolean
// indicating if that reference forces replacement.
EvaluateReplaceTriggeredBy(expr hcl.Expression, repData instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics)
// EvaluationScope returns a scope that can be used to evaluate reference
// addresses in this context.
EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope

View File

@ -282,7 +282,113 @@ func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Ty
return scope.EvalExpr(expr, wantType)
}
func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope {
func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, repData instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics) {
// get the reference to lookup changes in the plan
ref, diags := evalReplaceTriggeredByExpr(expr, repData)
if diags.HasErrors() {
return nil, false, diags
}
var changes []*plans.ResourceInstanceChangeSrc
// store the address once we get it for validation
var resourceAddr addrs.Resource
// The reference is either a resource or resource instance
switch sub := ref.Subject.(type) {
case addrs.Resource:
resourceAddr = sub
rc := sub.InModule(ctx.Path().Module())
changes = ctx.Changes().GetChangesForConfigResource(rc)
// FIXME: Needs to be restricted to the same module!
// The other caller actually needs the same condition, so we can
// change this method to cover both use cases.
case addrs.ResourceInstance:
resourceAddr = sub.ContainingResource()
rc := sub.Absolute(ctx.Path())
change := ctx.Changes().GetResourceInstanceChange(rc, states.CurrentGen)
if change != nil {
// we'll generate an error below if there was no change
changes = append(changes, change)
}
}
// Do some validation to make sure we are expecting a change at all
cfg := ctx.Evaluator.Config.Descendent(ctx.Path().Module())
resCfg := cfg.Module.ResourceByAddr(resourceAddr)
if resCfg == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to undeclared resource`,
Detail: fmt.Sprintf(`A resource %s has not been declared in %s`, ref.Subject, moduleDisplayAddr(ctx.Path())),
Subject: expr.Range().Ptr(),
})
return nil, false, diags
}
if len(changes) == 0 {
// If the resource is valid there should always be at least one change.
diags = diags.Append(fmt.Errorf("no change found for %s in %s", ref.Subject, moduleDisplayAddr(ctx.Path())))
return nil, false, diags
}
// If we don't have a traversal beyond the resource, then we can just look
// for any change.
if len(ref.Remaining) == 0 {
for _, c := range changes {
if c.ChangeSrc.Action != plans.NoOp {
return ref, true, diags
}
}
// no change triggered
return nil, false, diags
}
// This must be an instances to have a remaining traversal, which means a
// single change.
change := changes[0]
// Since we have a traversal after the resource reference, we will need to
// decode the changes, which means we need a schema.
providerAddr := change.ProviderAddr
schema, err := ctx.ProviderSchema(providerAddr)
if err != nil {
diags = diags.Append(err)
return nil, false, diags
}
resAddr := change.Addr.ContainingResource().Resource
resSchema, _ := schema.SchemaForResourceType(resAddr.Mode, resAddr.Type)
ty := resSchema.ImpliedType()
before, err := change.ChangeSrc.Before.Decode(ty)
if err != nil {
diags = diags.Append(err)
return nil, false, diags
}
after, err := change.ChangeSrc.After.Decode(ty)
if err != nil {
diags = diags.Append(err)
return nil, false, diags
}
path := traversalToPath(ref.Remaining)
attrBefore, _ := path.Apply(before)
attrAfter, _ := path.Apply(after)
if attrBefore == cty.NilVal || attrAfter == cty.NilVal {
replace := attrBefore != attrAfter
return ref, replace, diags
}
replace := !attrBefore.RawEquals(attrAfter)
return ref, replace, diags
}
func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData instances.RepetitionData) *lang.Scope {
if !ctx.pathSet {
panic("context path not set")
}

View File

@ -261,6 +261,10 @@ func (c *MockEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, s
return c.EvaluateExprResult, c.EvaluateExprDiags
}
func (c *MockEvalContext) EvaluateReplaceTriggeredBy(hcl.Expression, instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics) {
return nil, false, nil
}
// installSimpleEval is a helper to install a simple mock implementation of
// both EvaluateBlock and EvaluateExpr into the receiver.
//