diff --git a/terraform/eval_lang.go b/terraform/eval_lang.go deleted file mode 100644 index d3a4f5b44..000000000 --- a/terraform/eval_lang.go +++ /dev/null @@ -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() -} diff --git a/terraform/eval_output.go b/terraform/eval_output.go deleted file mode 100644 index 4fea60f11..000000000 --- a/terraform/eval_output.go +++ /dev/null @@ -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 - } -} diff --git a/terraform/execute.go b/terraform/execute.go index f379752b2..5bf06c4d0 100644 --- a/terraform/execute.go +++ b/terraform/execute.go @@ -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 } diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index a50f26c54..4a2e26841 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -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() diff --git a/terraform/node_data_destroy.go b/terraform/node_data_destroy.go index 8490c6373..731371920 100644 --- a/terraform/node_data_destroy.go +++ b/terraform/node_data_destroy.go @@ -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 diff --git a/terraform/node_data_destroy_test.go b/terraform/node_data_destroy_test.go index 597d31b3d..24fb988dd 100644 --- a/terraform/node_data_destroy_test.go +++ b/terraform/node_data_destroy_test.go @@ -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()) } diff --git a/terraform/node_data_refresh.go b/terraform/node_data_refresh.go index fc89a2f99..f9377bf76 100644 --- a/terraform/node_data_refresh.go +++ b/terraform/node_data_refresh.go @@ -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 - } diff --git a/terraform/node_output.go b/terraform/node_output.go index 4301a755e..51eb7e156 100644 --- a/terraform/node_output.go +++ b/terraform/node_output.go @@ -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 + } +} diff --git a/terraform/eval_output_test.go b/terraform/node_output_test.go similarity index 56% rename from terraform/eval_output_test.go rename to terraform/node_output_test.go index 1a2e738c2..a983c336c 100644 --- a/terraform/eval_output_test.go +++ b/terraform/node_output_test.go @@ -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") + } } diff --git a/terraform/node_resource_refresh.go b/terraform/node_resource_refresh.go index b3ca64f6f..4d1ff74ec 100644 --- a/terraform/node_resource_refresh.go +++ b/terraform/node_resource_refresh.go @@ -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)) }