core: Track changes to outputs in the plan along with the state

Our state models cannot store unknown values (since state only deals with
knowns) and so following the lead of recent similar changes for resource
instances we'll treat the planned changeset as a sort of overlay on the
state, preferring values stored there if present, and then write in basic
planned output changes to the plan when we evaluate them.

We're abusing the plan model a little here: its current design is intended
to lay the groundwork for a future release where output values have a
full lifecycle similar to resource instances where we can properly track
changes during the plan phase, but the rest of Terraform isn't yet ready
for that and so we'll just retain an approximation of the planned action
by only using Create and Destroy actions.

A future release should change this so that output changes can be tracked
accurately using an approach similar to that of resource instances.
This commit is contained in:
Martin Atkins 2018-09-11 09:37:34 -07:00
parent d53c3d5c1b
commit db9718faef
2 changed files with 64 additions and 1 deletions

View File

@ -1,12 +1,14 @@
package terraform
import (
"fmt"
"log"
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plans"
)
// EvalDeleteOutput is an EvalNode implementation that deletes an output
@ -38,6 +40,8 @@ type EvalWriteOutput struct {
// TODO: test
func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
addr := n.Addr.Absolute(ctx.Path())
// This has to run before we have a state lock, since evaluation also
// reads the state
val, diags := ctx.EvaluateExpr(n.Expr, cty.DynamicPseudoType, nil)
@ -48,7 +52,7 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
addr := n.Addr.Absolute(ctx.Path())
changes := ctx.Changes() // may be nil, if we're not working on a changeset
// handling the interpolation error
if diags.HasErrors() {
@ -68,5 +72,48 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
} else {
state.RemoveOutputValue(addr)
}
if changes != nil {
// For the moment we are not properly tracking changes to output
// values, and just marking them always as "Create" or "Destroy"
// actions. A future release will rework the output lifecycle so we
// can track their changes properly, in a similar way to how we work
// with resource instances.
var change *plans.OutputChange
if !val.IsNull() {
change = &plans.OutputChange{
Addr: addr,
Sensitive: n.Sensitive,
Change: plans.Change{
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: val,
},
}
} else {
change = &plans.OutputChange{
Addr: addr,
Sensitive: n.Sensitive,
Change: plans.Change{
// This is just a weird placeholder delete action since
// we don't have an actual prior value to indicate.
// FIXME: Generate real planned changes for output values
// that include the old values.
Action: plans.Delete,
Before: cty.NullVal(cty.DynamicPseudoType),
After: cty.NullVal(cty.DynamicPseudoType),
},
}
}
cs, err := change.Encode()
if err != nil {
// Should never happen, since we just constructed this right above
panic(fmt.Sprintf("planned change for %s could not be encoded: %s", addr, err))
}
changes.AppendOutputChange(cs)
}
return nil, nil
}

View File

@ -354,6 +354,22 @@ func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutpu
return cty.DynamicVal, diags
}
// If a pending change is present in our current changeset then its value
// takes priority over what's in state. (It will usually be the same but
// will differ if the new value is unknown during planning.)
if changeSrc := d.Evaluator.Changes.GetOutputChange(absAddr); changeSrc != nil {
change, err := changeSrc.Decode()
if err != nil {
// This should happen only if someone has tampered with a plan
// file, so we won't bother with a pretty error for it.
diags = diags.Append(fmt.Errorf("planned change for %s could not be decoded: %s", absAddr, err))
return cty.DynamicVal, diags
}
// We care only about the "after" value, which is the value this output
// will take on after the plan is applied.
return change.After, diags
}
os := d.Evaluator.State.OutputValue(absAddr)
if os == nil {
// Not evaluated yet?