terraform: remove deprecated or unused Eval() bits

This commit is contained in:
Kristin Laemmert 2020-09-29 14:47:20 -04:00
parent 184893d1e4
commit 0ac53ae3ed
19 changed files with 62 additions and 575 deletions

View File

@ -1,62 +0,0 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/tfdiags"
)
// EvalNode is the interface that must be implemented by graph nodes to
// evaluate/execute.
type EvalNode interface {
// Eval evaluates this node with the given context. The second parameter
// are the argument values. These will match in order and 1-1 with the
// results of the Args() return value.
Eval(EvalContext) (interface{}, error)
}
// GraphNodeEvalable is the interface that graph nodes must implement
// to enable valuation.
type GraphNodeEvalable interface {
EvalTree() EvalNode
}
// EvalEarlyExitError is a special error return value that can be returned
// by eval nodes that does an early exit.
type EvalEarlyExitError struct{}
func (EvalEarlyExitError) Error() string { return "early exit" }
// Eval evaluates the given EvalNode with the given context, properly
// evaluating all args in the correct order.
func Eval(n EvalNode, ctx EvalContext) (interface{}, error) {
// Call the lower level eval which doesn't understand early exit,
// and if we early exit, it isn't an error.
result, err := EvalRaw(n, ctx)
if err != nil {
if _, ok := err.(EvalEarlyExitError); ok {
return nil, nil
}
}
return result, err
}
// EvalRaw is like Eval except that it returns all errors, even if they
// signal something normal such as EvalEarlyExitError.
func EvalRaw(n EvalNode, ctx EvalContext) (interface{}, error) {
log.Printf("[TRACE] eval: %T", n)
output, err := n.Eval(ctx)
if err != nil {
switch err.(type) {
case EvalEarlyExitError:
log.Printf("[TRACE] eval: %T, early exit err: %s", n, err)
case tfdiags.NonFatalError:
log.Printf("[WARN] eval: %T, non-fatal err: %s", n, err)
default:
log.Printf("[ERROR] eval: %T, err: %s", n, err)
}
}
return output, err
}

View File

@ -1,47 +0,0 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/tfdiags"
)
// EvalPreventDestroy is an EvalNode implementation that returns an
// error if a resource has PreventDestroy configured and the diff
// would destroy the resource.
type EvalCheckPreventDestroy struct {
Addr addrs.ResourceInstance
Config *configs.Resource
Change **plans.ResourceInstanceChange
}
func (n *EvalCheckPreventDestroy) Eval(ctx EvalContext) (interface{}, error) {
if n.Change == nil || *n.Change == nil || n.Config == nil || n.Config.Managed == nil {
return nil, nil
}
change := *n.Change
preventDestroy := n.Config.Managed.PreventDestroy
if (change.Action == plans.Delete || change.Action.IsReplace()) && preventDestroy {
var diags tfdiags.Diagnostics
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Instance cannot be destroyed",
Detail: fmt.Sprintf(
"Resource %s has lifecycle.prevent_destroy set, but the plan calls for this resource to be destroyed. To avoid this error and continue with the plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan using the -target flag.",
n.Addr.Absolute(ctx.Path()).String(),
),
Subject: &n.Config.DeclRange,
})
return nil, diags.Err()
}
return nil, nil
}

View File

@ -789,45 +789,6 @@ func (n *EvalReduceDiff) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
// EvalReadDiff is an EvalNode implementation that retrieves the planned
// change for a particular resource instance object.
type EvalReadDiff struct {
Addr addrs.ResourceInstance
ProviderSchema **ProviderSchema
Change **plans.ResourceInstanceChange
}
func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
providerSchema := *n.ProviderSchema
changes := ctx.Changes()
addr := n.Addr.Absolute(ctx.Path())
schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
if schema == nil {
// Should be caught during validation, so we don't bother with a pretty error here
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
}
gen := states.CurrentGen
csrc := changes.GetResourceInstanceChange(addr, gen)
if csrc == nil {
log.Printf("[TRACE] EvalReadDiff: No planned change recorded for %s", addr)
return nil, nil
}
change, err := csrc.Decode(schema.ImpliedType())
if err != nil {
return nil, fmt.Errorf("failed to decode planned changes for %s: %s", addr, err)
}
if n.Change != nil {
*n.Change = change
}
log.Printf("[TRACE] EvalReadDiff: Read %s change from plan for %s", change.Action, addr)
return nil, nil
}
// EvalWriteDiff is an EvalNode implementation that saves a planned change
// for an instance object into the set of global planned changes.
type EvalWriteDiff struct {

View File

@ -1,20 +1,7 @@
package terraform
// EvalReturnError is an EvalNode implementation that returns an
// error if it is present.
//
// This is useful for scenarios where an error has been captured by
// another EvalNode (like EvalApply) for special EvalTree-based error
// handling, and that handling has completed, so the error should be
// returned normally.
type EvalReturnError struct {
Error *error
}
// EvalEarlyExitError is a special error return value that can be returned
// by eval nodes that does an early exit.
type EvalEarlyExitError struct{}
func (n *EvalReturnError) Eval(ctx EvalContext) (interface{}, error) {
if n.Error == nil {
return nil, nil
}
return nil, *n.Error
}
func (EvalEarlyExitError) Error() string { return "early exit" }

View File

@ -1,25 +0,0 @@
package terraform
// EvalNodeFilterFunc is the callback used to replace a node with
// another to node. To not do the replacement, just return the input node.
type EvalNodeFilterFunc func(EvalNode) EvalNode
// EvalNodeFilterable is an interface that can be implemented by
// EvalNodes to allow filtering of sub-elements. Note that this isn't
// a common thing to implement and you probably don't need it.
type EvalNodeFilterable interface {
EvalNode
Filter(EvalNodeFilterFunc)
}
// EvalFilter runs the filter on the given node and returns the
// final filtered value. This should be called rather than checking
// the EvalNode directly since this will properly handle EvalNodeFilterables.
func EvalFilter(node EvalNode, fn EvalNodeFilterFunc) EvalNode {
if f, ok := node.(EvalNodeFilterable); ok {
f.Filter(fn)
return node
}
return fn(node)
}

View File

@ -1,49 +0,0 @@
package terraform
// EvalNodeOpFilterable is an interface that EvalNodes can implement
// to be filterable by the operation that is being run on Terraform.
type EvalNodeOpFilterable interface {
IncludeInOp(walkOperation) bool
}
// EvalNodeFilterOp returns a filter function that filters nodes that
// include themselves in specific operations.
func EvalNodeFilterOp(op walkOperation) EvalNodeFilterFunc {
return func(n EvalNode) EvalNode {
include := true
if of, ok := n.(EvalNodeOpFilterable); ok {
include = of.IncludeInOp(op)
}
if include {
return n
}
return EvalNoop{}
}
}
// EvalOpFilter is an EvalNode implementation that is a proxy to
// another node but filters based on the operation.
type EvalOpFilter struct {
// Ops is the list of operations to include this node in.
Ops []walkOperation
// Node is the node to execute
Node EvalNode
}
// TODO: test
func (n *EvalOpFilter) Eval(ctx EvalContext) (interface{}, error) {
return EvalRaw(n.Node, ctx)
}
// EvalNodeOpFilterable impl.
func (n *EvalOpFilter) IncludeInOp(op walkOperation) bool {
for _, v := range n.Ops {
if v == op {
return true
}
}
return false
}

View File

@ -1,26 +0,0 @@
package terraform
// EvalIf is an EvalNode that is a conditional.
type EvalIf struct {
If func(EvalContext) (bool, error)
Then EvalNode
Else EvalNode
}
// TODO: test
func (n *EvalIf) Eval(ctx EvalContext) (interface{}, error) {
yes, err := n.If(ctx)
if err != nil {
return nil, err
}
if yes {
return EvalRaw(n.Then, ctx)
} else {
if n.Else != nil {
return EvalRaw(n.Else, ctx)
}
}
return nil, nil
}

View File

@ -1,8 +0,0 @@
package terraform
// EvalNoop is an EvalNode that does nothing.
type EvalNoop struct{}
func (EvalNoop) Eval(EvalContext) (interface{}, error) {
return nil, nil
}

View File

@ -60,42 +60,3 @@ func GetProvider(ctx EvalContext, addr addrs.AbsProviderConfig) (providers.Inter
schema := ctx.ProviderSchema(addr)
return provider, schema, nil
}
// EvalGetProvider is an EvalNode implementation that retrieves an already
// initialized provider instance for the given name.
//
// Unlike most eval nodes, this takes an _absolute_ provider configuration,
// because providers can be passed into and inherited between modules.
// Resource nodes must therefore know the absolute path of the provider they
// will use, which is usually accomplished by implementing
// interface GraphNodeProviderConsumer.
type EvalGetProvider struct {
Addr addrs.AbsProviderConfig
Output *providers.Interface
// If non-nil, Schema will be updated after eval to refer to the
// schema of the provider.
Schema **ProviderSchema
}
func (n *EvalGetProvider) Eval(ctx EvalContext) (interface{}, error) {
if n.Addr.Provider.Type == "" {
// Should never happen
panic("EvalGetProvider used with uninitialized provider configuration address")
}
result := ctx.Provider(n.Addr)
if result == nil {
return nil, fmt.Errorf("provider %s not initialized", n.Addr)
}
if n.Output != nil {
*n.Output = result
}
if n.Schema != nil {
*n.Schema = ctx.ProviderSchema(n.Addr)
}
return nil, nil
}

View File

@ -9,7 +9,6 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
)
func TestBuildProviderConfig(t *testing.T) {
@ -54,30 +53,3 @@ func TestBuildProviderConfig(t *testing.T) {
t.Fatalf("incorrect merged config\ngot: %#v\nwant: %#v", got, want)
}
}
func TestEvalGetProvider_impl(t *testing.T) {
var _ EvalNode = new(EvalGetProvider)
}
func TestEvalGetProvider(t *testing.T) {
var actual providers.Interface
n := &EvalGetProvider{
Addr: addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("foo")),
Output: &actual,
}
provider := &MockProvider{}
ctx := &MockEvalContext{ProviderProvider: provider}
if _, err := n.Eval(ctx); err != nil {
t.Fatalf("err: %s", err)
}
if actual != provider {
t.Fatalf("bad: %#v", actual)
}
if !ctx.ProviderCalled {
t.Fatal("should be called")
}
if ctx.ProviderAddr.String() != `provider["registry.terraform.io/hashicorp/foo"]` {
t.Fatalf("wrong provider address %s", ctx.ProviderAddr)
}
}

View File

@ -1,42 +0,0 @@
package terraform
import (
"github.com/hashicorp/terraform/tfdiags"
)
// EvalSequence is an EvalNode that evaluates in sequence.
type EvalSequence struct {
Nodes []EvalNode
}
func (n *EvalSequence) Eval(ctx EvalContext) (interface{}, error) {
var diags tfdiags.Diagnostics
for _, n := range n.Nodes {
if n == nil {
continue
}
if _, err := EvalRaw(n, ctx); err != nil {
if _, isEarlyExit := err.(EvalEarlyExitError); isEarlyExit {
// In this path we abort early, losing any non-error
// diagnostics we saw earlier.
return nil, err
}
diags = diags.Append(err)
if diags.HasErrors() {
// Halt if we get some errors, but warnings are okay.
break
}
}
}
return nil, diags.ErrWithWarnings()
}
// EvalNodeFilterable impl.
func (n *EvalSequence) Filter(fn EvalNodeFilterFunc) {
for i, node := range n.Nodes {
n.Nodes[i] = fn(node)
}
}

View File

@ -1,9 +0,0 @@
package terraform
import (
"testing"
)
func TestEvalSequence_impl(t *testing.T) {
var _ EvalNodeFilterable = new(EvalSequence)
}

View File

@ -149,35 +149,7 @@ func (n *EvalReadStateDeposed) Eval(ctx EvalContext) (interface{}, error) {
return obj, nil
}
// EvalUpdateStateHook is an EvalNode implementation that calls the
// PostStateUpdate hook with the current state.
type EvalUpdateStateHook struct{}
func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) {
// In principle we could grab the lock here just long enough to take a
// deep copy and then pass that to our hooks below, but we'll instead
// hold the hook for the duration to avoid the potential confusing
// situation of us racing to call PostStateUpdate concurrently with
// different state snapshots.
stateSync := ctx.State()
state := stateSync.Lock().DeepCopy()
defer stateSync.Unlock()
// Call the hook
err := ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostStateUpdate(state)
})
if err != nil {
return nil, err
}
return nil, nil
}
// UpdateStateHook calls the PostStateUpdate hook with the current state.
//
// TODO: UpdateStateHook will eventually replace EvalUpdateStateHook, at which
// point EvalUpdateStateHook can be removed and this comment updated.
func UpdateStateHook(ctx EvalContext) error {
// In principle we could grab the lock here just long enough to take a
// deep copy and then pass that to our hooks below, but we'll instead

View File

@ -12,29 +12,6 @@ import (
"github.com/hashicorp/terraform/states"
)
func TestEvalUpdateStateHook(t *testing.T) {
mockHook := new(MockHook)
state := states.NewState()
state.Module(addrs.RootModuleInstance).SetLocalValue("foo", cty.StringVal("hello"))
ctx := new(MockEvalContext)
ctx.HookHook = mockHook
ctx.StateState = state.SyncWrapper()
node := &EvalUpdateStateHook{}
if _, err := node.Eval(ctx); err != nil {
t.Fatalf("err: %s", err)
}
if !mockHook.PostStateUpdateCalled {
t.Fatal("should call PostStateUpdate")
}
if mockHook.PostStateUpdateState.LocalValue(addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) != cty.StringVal("hello") {
t.Fatalf("wrong state passed to hook: %s", spew.Sdump(mockHook.PostStateUpdateState))
}
}
func TestEvalReadState(t *testing.T) {
var output *states.ResourceInstanceObject
mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{
@ -50,7 +27,7 @@ func TestEvalReadState(t *testing.T) {
cases := map[string]struct {
Resources map[string]*ResourceState
Node EvalNode
Node *EvalReadState
ExpectedInstanceId string
}{
"ReadState gets primary instance state": {
@ -74,6 +51,59 @@ func TestEvalReadState(t *testing.T) {
},
ExpectedInstanceId: "i-abc123",
},
}
for k, c := range cases {
t.Run(k, func(t *testing.T) {
ctx := new(MockEvalContext)
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: c.Resources,
},
},
})
ctx.StateState = state.SyncWrapper()
ctx.PathPath = addrs.RootModuleInstance
result, err := c.Node.Eval(ctx)
if err != nil {
t.Fatalf("[%s] Got err: %#v", k, err)
}
expected := c.ExpectedInstanceId
if !(result != nil && instanceObjectIdForTests(result.(*states.ResourceInstanceObject)) == expected) {
t.Fatalf("[%s] Expected return with ID %#v, got: %#v", k, expected, result)
}
if !(output != nil && output.Value.GetAttr("id") == cty.StringVal(expected)) {
t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, output)
}
output = nil
})
}
}
func TestEvalReadStateDeposed(t *testing.T) {
var output *states.ResourceInstanceObject
mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Optional: true,
},
},
})
providerSchema := mockProvider.GetSchemaReturn
provider := providers.Interface(mockProvider)
cases := map[string]struct {
Resources map[string]*ResourceState
Node *EvalReadStateDeposed
ExpectedInstanceId string
}{
"ReadStateDeposed gets deposed instance": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
@ -97,7 +127,6 @@ func TestEvalReadState(t *testing.T) {
ExpectedInstanceId: "i-abc123",
},
}
for k, c := range cases {
t.Run(k, func(t *testing.T) {
ctx := new(MockEvalContext)
@ -225,9 +254,6 @@ aws_instance.foo: (1 deposed)
`)
}
// Same test as TestEvalUpdateStateHook, similar function, slightly different
// signature. The EvalUpdateStateHook test and function will be removed when the
// EvalNode Removal is complete.
func TestUpdateStateHook(t *testing.T) {
mockHook := new(MockHook)

View File

@ -1,40 +0,0 @@
package terraform
import (
"testing"
)
func TestMockEvalContext_impl(t *testing.T) {
var _ EvalContext = new(MockEvalContext)
}
func TestEval(t *testing.T) {
var result int
n := &testEvalAdd{
Items: []int{10, 32},
Result: &result,
}
if _, err := Eval(n, nil); err != nil {
t.Fatalf("err: %s", err)
}
if result != 42 {
t.Fatalf("bad: %#v", result)
}
}
type testEvalAdd struct {
Items []int
Result *int
}
func (n *testEvalAdd) Eval(ctx EvalContext) (interface{}, error) {
result := 0
for _, item := range n.Items {
result += item
}
*n.Result = result
return nil, nil
}

View File

@ -1,7 +1,6 @@
package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/tfdiags"
@ -57,38 +56,12 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics {
// If the node is exec-able, then execute it.
if ev, ok := v.(GraphNodeExecutable); ok {
// A node must not be both Evalable and Executable. This will be
// removed when GraphNodeEvalable is fully removed.
if _, ok := v.(GraphNodeEvalable); ok {
panic(fmt.Sprintf(
"%T implements both GraphNodeEvalable and GraphNodeExecutable", v,
))
}
diags = diags.Append(walker.Execute(vertexCtx, ev))
if diags.HasErrors() {
return
}
}
// If the node is eval-able, then evaluate it.
if ev, ok := v.(GraphNodeEvalable); ok {
tree := ev.EvalTree()
if tree == nil {
panic(fmt.Sprintf("%q (%T): nil eval tree", dag.VertexName(v), v))
}
// Allow the walker to change our tree if needed. Eval,
// then callback with the output.
log.Printf("[TRACE] vertex %q: evaluating", dag.VertexName(v))
tree = walker.EnterEvalTree(v, tree)
output, err := Eval(tree, vertexCtx)
diags = diags.Append(walker.ExitEvalTree(v, output, err))
if diags.HasErrors() {
return
}
}
// If the node is dynamically expanded, then expand it
if ev, ok := v.(GraphNodeDynamicExpandable); ok {
log.Printf("[TRACE] vertex %q: expanding dynamic subgraph", dag.VertexName(v))

View File

@ -2,7 +2,6 @@ package terraform
import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/tfdiags"
)
@ -12,8 +11,6 @@ type GraphWalker interface {
EvalContext() EvalContext
EnterPath(addrs.ModuleInstance) EvalContext
ExitPath(addrs.ModuleInstance)
EnterEvalTree(dag.Vertex, EvalNode) EvalNode
ExitEvalTree(dag.Vertex, interface{}, error) tfdiags.Diagnostics
Execute(EvalContext, GraphNodeExecutable) tfdiags.Diagnostics
}
@ -22,11 +19,7 @@ type GraphWalker interface {
// implementing all the required functions.
type NullGraphWalker struct{}
func (NullGraphWalker) EvalContext() EvalContext { return new(MockEvalContext) }
func (NullGraphWalker) EnterPath(addrs.ModuleInstance) EvalContext { return new(MockEvalContext) }
func (NullGraphWalker) ExitPath(addrs.ModuleInstance) {}
func (NullGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { return n }
func (NullGraphWalker) ExitEvalTree(dag.Vertex, interface{}, error) tfdiags.Diagnostics {
return nil
}
func (NullGraphWalker) EvalContext() EvalContext { return new(MockEvalContext) }
func (NullGraphWalker) EnterPath(addrs.ModuleInstance) EvalContext { return new(MockEvalContext) }
func (NullGraphWalker) ExitPath(addrs.ModuleInstance) {}
func (NullGraphWalker) Execute(EvalContext, GraphNodeExecutable) tfdiags.Diagnostics { return nil }

View File

@ -2,14 +2,12 @@ package terraform
import (
"context"
"log"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/instances"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/providers"
@ -106,49 +104,6 @@ func (w *ContextGraphWalker) EvalContext() EvalContext {
return ctx
}
func (w *ContextGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode {
log.Printf("[TRACE] [%s] Entering eval tree: %s", w.Operation, dag.VertexName(v))
// Acquire a lock on the semaphore
w.Context.parallelSem.Acquire()
// We want to filter the evaluation tree to only include operations
// that belong in this operation.
return EvalFilter(n, EvalNodeFilterOp(w.Operation))
}
func (w *ContextGraphWalker) ExitEvalTree(v dag.Vertex, output interface{}, err error) tfdiags.Diagnostics {
log.Printf("[TRACE] [%s] Exiting eval tree: %s", w.Operation, dag.VertexName(v))
// Release the semaphore
w.Context.parallelSem.Release()
if err == nil {
return nil
}
// Acquire the lock because anything is going to require a lock.
w.errorLock.Lock()
defer w.errorLock.Unlock()
// If the error is non-fatal then we'll accumulate its diagnostics in our
// non-fatal list, rather than returning it directly, so that the graph
// walk can continue.
if nferr, ok := err.(tfdiags.NonFatalError); ok {
log.Printf("[WARN] %s: %s", dag.VertexName(v), nferr)
w.NonFatalDiagnostics = w.NonFatalDiagnostics.Append(nferr.Diagnostics)
return nil
}
// Otherwise, we'll let our usual diagnostics machinery figure out how to
// unpack this as one or more diagnostic messages and return that. If we
// get down here then the returned diagnostics will contain at least one
// error, causing the graph walk to halt.
var diags tfdiags.Diagnostics
diags = diags.Append(err)
return diags
}
func (w *ContextGraphWalker) init() {
w.contexts = make(map[string]*BuiltinEvalContext)
w.providerCache = make(map[string]providers.Interface)

View File

@ -143,12 +143,7 @@ func (n *NodeDestroyResourceInstance) Execute(ctx EvalContext, op walkOperation)
return err
}
evalReadDiff := &EvalReadDiff{
Addr: addr.Resource,
ProviderSchema: &providerSchema,
Change: &changeApply,
}
_, err = evalReadDiff.Eval(ctx)
changeApply, err = n.ReadDiff(ctx, providerSchema)
if err != nil {
return err
}