terraform: refactor Node*Ouput

This commit refactors NodeApplyableOutput and NodeDestroyableOutput into
the new Execute() pattern, collapsing the functions in eval_output.go
into one place.

I also reverted a recent decision to have Execute take a _pointer_ to a
walkOperation: I was thinking of interfaces, not constant bytes, so all
it did was cause problems.

And finally I removed eval_lang.go, which was unused.
This commit is contained in:
Kristin Laemmert 2020-09-08 14:02:45 -04:00
parent e191a57093
commit 069f379e75
10 changed files with 144 additions and 232 deletions

View File

@ -1,61 +0,0 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
// EvalConfigBlock is an EvalNode implementation that takes a raw
// configuration block and evaluates any expressions within it.
//
// ExpandedConfig is populated with the result of expanding any "dynamic"
// blocks in the given body, which can be useful for extracting correct source
// location information for specific attributes in the result.
type EvalConfigBlock struct {
Config *hcl.Body
Schema *configschema.Block
SelfAddr addrs.Referenceable
Output *cty.Value
ExpandedConfig *hcl.Body
ContinueOnErr bool
}
func (n *EvalConfigBlock) Eval(ctx EvalContext) (interface{}, error) {
val, body, diags := ctx.EvaluateBlock(*n.Config, n.Schema, n.SelfAddr, EvalDataForNoInstanceKey)
if diags.HasErrors() && n.ContinueOnErr {
log.Printf("[WARN] Block evaluation failed: %s", diags.Err())
return nil, EvalEarlyExitError{}
}
if n.Output != nil {
*n.Output = val
}
if n.ExpandedConfig != nil {
*n.ExpandedConfig = body
}
return nil, diags.ErrWithWarnings()
}
// EvalConfigExpr is an EvalNode implementation that takes a raw configuration
// expression and evaluates it.
type EvalConfigExpr struct {
Expr hcl.Expression
SelfAddr addrs.Referenceable
Output *cty.Value
}
func (n *EvalConfigExpr) Eval(ctx EvalContext) (interface{}, error) {
val, diags := ctx.EvaluateExpr(n.Expr, cty.DynamicPseudoType, n.SelfAddr)
if n.Output != nil {
*n.Output = val
}
return nil, diags.ErrWithWarnings()
}

View File

@ -1,138 +0,0 @@
package terraform
import (
"fmt"
"log"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states"
)
// EvalDeleteOutput is an EvalNode implementation that deletes an output
// from the state.
type EvalDeleteOutput struct {
Addr addrs.AbsOutputValue
}
// TODO: test
func (n *EvalDeleteOutput) Eval(ctx EvalContext) (interface{}, error) {
state := ctx.State()
if state == nil {
return nil, nil
}
state.RemoveOutputValue(n.Addr)
return nil, nil
}
// EvalWriteOutput is an EvalNode implementation that writes the output
// for the given name to the current state.
type EvalWriteOutput struct {
Addr addrs.OutputValue
Config *configs.Output
// ContinueOnErr allows interpolation to fail during Input
ContinueOnErr bool
}
// 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.Config.Expr, cty.DynamicPseudoType, nil)
// We'll handle errors below, after we have loaded the module.
// Outputs don't have a separate mode for validation, so validate
// depends_on expressions here too
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
state := ctx.State()
if state == nil {
return nil, nil
}
changes := ctx.Changes() // may be nil, if we're not working on a changeset
// handling the interpolation error
if diags.HasErrors() {
if n.ContinueOnErr || flagWarnOutputErrors {
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr.Name, diags.Err())
// if we're continuing, make sure the output is included, and
// marked as unknown. If the evaluator was able to find a type
// for the value in spite of the error then we'll use it.
n.setValue(addr, state, changes, cty.UnknownVal(val.Type()))
return nil, EvalEarlyExitError{}
}
return nil, diags.Err()
}
n.setValue(addr, state, changes, val)
return nil, nil
}
func (n *EvalWriteOutput) setValue(addr addrs.AbsOutputValue, state *states.SyncState, changes *plans.ChangesSync, val cty.Value) {
if val.IsKnown() && !val.IsNull() {
// The state itself doesn't represent unknown values, so we null them
// out here and then we'll save the real unknown value in the planned
// changeset below, if we have one on this graph walk.
log.Printf("[TRACE] EvalWriteOutput: Saving value for %s in state", addr)
stateVal := cty.UnknownAsNull(val)
state.SetOutputValue(addr, stateVal, n.Config.Sensitive)
} else {
log.Printf("[TRACE] EvalWriteOutput: Removing %s from state (it is now null)", addr)
state.RemoveOutputValue(addr)
}
// If we also have an active changeset then we'll replicate the value in
// there. This is used in preference to the state where present, since it
// *is* able to represent unknowns, while the state cannot.
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.Config.Sensitive,
Change: plans.Change{
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: val,
},
}
} else {
change = &plans.OutputChange{
Addr: addr,
Sensitive: n.Config.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))
}
log.Printf("[TRACE] EvalWriteOutput: Saving %s change for %s in changeset", change.Action, addr)
changes.RemoveOutputChange(addr) // remove any existing planned change, if present
changes.AppendOutputChange(cs) // add the new planned change
}
}

View File

@ -5,5 +5,5 @@ package terraform
// the process of being removed. A given graph node should _not_ implement both
// GraphNodeExecutable and GraphNodeEvalable.
type GraphNodeExecutable interface {
Execute(EvalContext, *walkOperation) error
Execute(EvalContext, walkOperation) error
}

View File

@ -167,7 +167,7 @@ func (w *ContextGraphWalker) Execute(ctx EvalContext, n GraphNodeExecutable) tfd
// Acquire a lock on the semaphore
w.Context.parallelSem.Acquire()
err := n.Execute(ctx, &w.Operation)
err := n.Execute(ctx, w.Operation)
// Release the semaphore
w.Context.parallelSem.Release()

View File

@ -13,7 +13,7 @@ var (
)
// GraphNodeExecutable
func (n *NodeDestroyableDataResourceInstance) Execute(ctx EvalContext, op *walkOperation) error {
func (n *NodeDestroyableDataResourceInstance) Execute(ctx EvalContext, op walkOperation) error {
log.Printf("[TRACE] NodeDestroyableDataResourceInstance: removing state object for %s", n.Addr)
ctx.State().SetResourceInstanceCurrent(n.Addr, nil, n.ResolvedProvider)
return nil

View File

@ -36,7 +36,7 @@ func TestNodeDataDestroyExecute(t *testing.T) {
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
}}
err := node.Execute(ctx, nil)
err := node.Execute(ctx, walkApply)
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}

View File

@ -191,7 +191,7 @@ type NodeRefreshableDataResourceInstance struct {
}
// GraphNodeExecutable
func (n *NodeRefreshableDataResourceInstance) Execute(ctx EvalContext, op *walkOperation) error {
func (n *NodeRefreshableDataResourceInstance) Execute(ctx EvalContext, op walkOperation) error {
addr := n.ResourceInstanceAddr()
// These variables are the state for the eval sequence below, and are
@ -280,7 +280,5 @@ func (n *NodeRefreshableDataResourceInstance) Execute(ctx EvalContext, op *walkO
return err
}
}
return err
}

View File

@ -8,6 +8,9 @@ import (
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states"
"github.com/zclconf/go-cty/cty"
)
// nodeExpandOutput is the placeholder for an output that has not yet had
@ -111,7 +114,7 @@ var (
_ GraphNodeReferenceable = (*NodeApplyableOutput)(nil)
_ GraphNodeReferencer = (*NodeApplyableOutput)(nil)
_ GraphNodeReferenceOutside = (*NodeApplyableOutput)(nil)
_ GraphNodeEvalable = (*NodeApplyableOutput)(nil)
_ GraphNodeExecutable = (*NodeApplyableOutput)(nil)
_ graphNodeTemporaryValue = (*NodeApplyableOutput)(nil)
_ dag.GraphNodeDotter = (*NodeApplyableOutput)(nil)
)
@ -193,18 +196,43 @@ func (n *NodeApplyableOutput) References() []*addrs.Reference {
return referencesForOutput(n.Config)
}
// GraphNodeEvalable
func (n *NodeApplyableOutput) EvalTree() EvalNode {
return &EvalSequence{
Nodes: []EvalNode{
&EvalOpFilter{
Ops: []walkOperation{walkEval, walkRefresh, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy},
Node: &EvalWriteOutput{
Addr: n.Addr.OutputValue,
Config: n.Config,
},
},
},
// GraphNodeExecutable
func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) error {
switch op {
// Everything except walkImport
case walkEval, walkRefresh, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy:
// This has to run before we have a state lock, since evaluation also
// reads the state
val, diags := ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
// We'll handle errors below, after we have loaded the module.
// Outputs don't have a separate mode for validation, so validate
// depends_on expressions here too
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
state := ctx.State()
if state == nil {
return nil
}
changes := ctx.Changes() // may be nil, if we're not working on a changeset
// handling the interpolation error
if diags.HasErrors() {
if flagWarnOutputErrors {
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr, diags.Err())
// if we're continuing, make sure the output is included, and
// marked as unknown. If the evaluator was able to find a type
// for the value in spite of the error then we'll use it.
n.setValue(state, changes, cty.UnknownVal(val.Type()))
return EvalEarlyExitError{}
}
return diags.Err()
}
n.setValue(state, changes, val)
return nil
default:
return nil
}
}
@ -227,7 +255,7 @@ type NodeDestroyableOutput struct {
}
var (
_ GraphNodeEvalable = (*NodeDestroyableOutput)(nil)
_ GraphNodeExecutable = (*NodeDestroyableOutput)(nil)
_ dag.GraphNodeDotter = (*NodeDestroyableOutput)(nil)
)
@ -245,11 +273,14 @@ func (n *NodeDestroyableOutput) temporaryValue() bool {
return !n.Addr.Module.IsRoot()
}
// GraphNodeEvalable
func (n *NodeDestroyableOutput) EvalTree() EvalNode {
return &EvalDeleteOutput{
Addr: n.Addr,
// GraphNodeExecutable
func (n *NodeDestroyableOutput) Execute(ctx EvalContext, op walkOperation) error {
state := ctx.State()
if state == nil {
return nil
}
state.RemoveOutputValue(n.Addr)
return nil
}
// dag.GraphNodeDotter impl.
@ -262,3 +293,64 @@ func (n *NodeDestroyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.Dot
},
}
}
func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.ChangesSync, val cty.Value) {
if val.IsKnown() && !val.IsNull() {
// The state itself doesn't represent unknown values, so we null them
// out here and then we'll save the real unknown value in the planned
// changeset below, if we have one on this graph walk.
log.Printf("[TRACE] EvalWriteOutput: Saving value for %s in state", n.Addr)
stateVal := cty.UnknownAsNull(val)
state.SetOutputValue(n.Addr, stateVal, n.Config.Sensitive)
} else {
log.Printf("[TRACE] EvalWriteOutput: Removing %s from state (it is now null)", n.Addr)
state.RemoveOutputValue(n.Addr)
}
// If we also have an active changeset then we'll replicate the value in
// there. This is used in preference to the state where present, since it
// *is* able to represent unknowns, while the state cannot.
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: n.Addr,
Sensitive: n.Config.Sensitive,
Change: plans.Change{
Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType),
After: val,
},
}
} else {
change = &plans.OutputChange{
Addr: n.Addr,
Sensitive: n.Config.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", n.Addr, err))
}
log.Printf("[TRACE] ExecuteWriteOutput: Saving %s change for %s in changeset", change.Action, n.Addr)
changes.RemoveOutputChange(n.Addr) // remove any existing planned change, if present
changes.AppendOutputChange(cs) // add the new planned change
}
}

View File

@ -3,14 +3,13 @@ package terraform
import (
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/addrs"
"github.com/zclconf/go-cty/cty"
)
func TestEvalWriteMapOutput(t *testing.T) {
func TestNodeApplyableOutputExecute(t *testing.T) {
ctx := new(MockEvalContext)
ctx.StateState = states.NewState().SyncWrapper()
@ -44,16 +43,38 @@ func TestEvalWriteMapOutput(t *testing.T) {
}
for _, tc := range cases {
evalNode := &EvalWriteOutput{
node := &NodeApplyableOutput{
Config: &configs.Output{},
Addr: addrs.OutputValue{Name: tc.name},
Addr: addrs.OutputValue{Name: tc.name}.Absolute(addrs.RootModuleInstance),
}
ctx.EvaluateExprResult = tc.val
t.Run(tc.name, func(t *testing.T) {
_, err := evalNode.Eval(ctx)
err := node.Execute(ctx, walkApply)
if err != nil && !tc.err {
t.Fatal(err)
}
})
}
}
func TestNodeDestroyableOutputExecute(t *testing.T) {
outputAddr := addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)
state := states.NewState()
state.Module(addrs.RootModuleInstance).SetOutputValue("foo", cty.StringVal("bar"), false)
state.OutputValue(outputAddr)
ctx := &MockEvalContext{
StateState: state.SyncWrapper(),
}
node := NodeDestroyableOutput{Addr: outputAddr}
err := node.Execute(ctx, walkApply)
if err != nil {
t.Fatalf("Unexpected error: %s", err.Error())
}
if state.OutputValue(outputAddr) != nil {
t.Fatal("Unexpected outputs in state after removal")
}
}

View File

@ -209,7 +209,7 @@ func (n *NodeRefreshableManagedResourceInstance) DestroyAddr() *addrs.AbsResourc
}
// GraphNodeEvalable
func (n *NodeRefreshableManagedResourceInstance) Execute(ctx EvalContext, op *walkOperation) error {
func (n *NodeRefreshableManagedResourceInstance) Execute(ctx EvalContext, op walkOperation) error {
addr := n.ResourceInstanceAddr()
// Eval info is different depending on what kind of resource this is
@ -238,7 +238,7 @@ func (n *NodeRefreshableManagedResourceInstance) Execute(ctx EvalContext, op *wa
}
}
return dn.Execute(ctx, nil)
return dn.Execute(ctx, op)
default:
panic(fmt.Errorf("unsupported resource mode %s", addr.Resource.Resource.Mode))
}