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:
parent
e191a57093
commit
069f379e75
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue