]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blobdiff - vendor/github.com/hashicorp/terraform/terraform/eval_state.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / terraform / eval_state.go
index 11826907ca45a344dc76e50c11bfee6f710ad62a..d506ce3fe1a3b0cb341db728564caf3729d8100f 100644 (file)
@@ -2,91 +2,149 @@ package terraform
 
 import (
        "fmt"
+       "log"
+
+       "github.com/hashicorp/terraform/addrs"
+       "github.com/hashicorp/terraform/configs"
+       "github.com/hashicorp/terraform/providers"
+       "github.com/hashicorp/terraform/states"
+       "github.com/hashicorp/terraform/tfdiags"
 )
 
 // EvalReadState is an EvalNode implementation that reads the
-// primary InstanceState for a specific resource out of the state.
+// current object for a specific instance in the state.
 type EvalReadState struct {
-       Name   string
-       Output **InstanceState
+       // Addr is the address of the instance to read state for.
+       Addr addrs.ResourceInstance
+
+       // ProviderSchema is the schema for the provider given in Provider.
+       ProviderSchema **ProviderSchema
+
+       // Provider is the provider that will subsequently perform actions on
+       // the the state object. This is used to perform any schema upgrades
+       // that might be required to prepare the stored data for use.
+       Provider *providers.Interface
+
+       // Output will be written with a pointer to the retrieved object.
+       Output **states.ResourceInstanceObject
 }
 
 func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) {
-       return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
-               return rs.Primary, nil
-       })
+       if n.Provider == nil || *n.Provider == nil {
+               panic("EvalReadState used with no Provider object")
+       }
+       if n.ProviderSchema == nil || *n.ProviderSchema == nil {
+               panic("EvalReadState used with no ProviderSchema object")
+       }
+
+       absAddr := n.Addr.Absolute(ctx.Path())
+       log.Printf("[TRACE] EvalReadState: reading state for %s", absAddr)
+
+       src := ctx.State().ResourceInstanceObject(absAddr, states.CurrentGen)
+       if src == nil {
+               // Presumably we only have deposed objects, then.
+               log.Printf("[TRACE] EvalReadState: no state present for %s", absAddr)
+               return nil, nil
+       }
+
+       schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // Shouldn't happen since we should've failed long ago if no schema is present
+               return nil, fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", absAddr)
+       }
+       var diags tfdiags.Diagnostics
+       src, diags = UpgradeResourceState(absAddr, *n.Provider, src, schema, currentVersion)
+       if diags.HasErrors() {
+               // Note that we don't have any channel to return warnings here. We'll
+               // accept that for now since warnings during a schema upgrade would
+               // be pretty weird anyway, since this operation is supposed to seem
+               // invisible to the user.
+               return nil, diags.Err()
+       }
+
+       obj, err := src.Decode(schema.ImpliedType())
+       if err != nil {
+               return nil, err
+       }
+
+       if n.Output != nil {
+               *n.Output = obj
+       }
+       return obj, nil
 }
 
 // EvalReadStateDeposed is an EvalNode implementation that reads the
 // deposed InstanceState for a specific resource out of the state
 type EvalReadStateDeposed struct {
-       Name   string
-       Output **InstanceState
-       // Index indicates which instance in the Deposed list to target, or -1 for
-       // the last item.
-       Index int
+       // Addr is the address of the instance to read state for.
+       Addr addrs.ResourceInstance
+
+       // Key identifies which deposed object we will read.
+       Key states.DeposedKey
+
+       // ProviderSchema is the schema for the provider given in Provider.
+       ProviderSchema **ProviderSchema
+
+       // Provider is the provider that will subsequently perform actions on
+       // the the state object. This is used to perform any schema upgrades
+       // that might be required to prepare the stored data for use.
+       Provider *providers.Interface
+
+       // Output will be written with a pointer to the retrieved object.
+       Output **states.ResourceInstanceObject
 }
 
 func (n *EvalReadStateDeposed) Eval(ctx EvalContext) (interface{}, error) {
-       return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
-               // Get the index. If it is negative, then we get the last one
-               idx := n.Index
-               if idx < 0 {
-                       idx = len(rs.Deposed) - 1
-               }
-               if idx >= 0 && idx < len(rs.Deposed) {
-                       return rs.Deposed[idx], nil
-               } else {
-                       return nil, fmt.Errorf("bad deposed index: %d, for resource: %#v", idx, rs)
-               }
-       })
-}
+       if n.Provider == nil || *n.Provider == nil {
+               panic("EvalReadStateDeposed used with no Provider object")
+       }
+       if n.ProviderSchema == nil || *n.ProviderSchema == nil {
+               panic("EvalReadStateDeposed used with no ProviderSchema object")
+       }
 
-// Does the bulk of the work for the various flavors of ReadState eval nodes.
-// Each node just provides a reader function to get from the ResourceState to the
-// InstanceState, and this takes care of all the plumbing.
-func readInstanceFromState(
-       ctx EvalContext,
-       resourceName string,
-       output **InstanceState,
-       readerFn func(*ResourceState) (*InstanceState, error),
-) (*InstanceState, error) {
-       state, lock := ctx.State()
-
-       // Get a read lock so we can access this instance
-       lock.RLock()
-       defer lock.RUnlock()
-
-       // Look for the module state. If we don't have one, then it doesn't matter.
-       mod := state.ModuleByPath(ctx.Path())
-       if mod == nil {
-               return nil, nil
+       key := n.Key
+       if key == states.NotDeposed {
+               return nil, fmt.Errorf("EvalReadStateDeposed used with no instance key; this is a bug in Terraform and should be reported")
        }
+       absAddr := n.Addr.Absolute(ctx.Path())
+       log.Printf("[TRACE] EvalReadStateDeposed: reading state for %s deposed object %s", absAddr, n.Key)
 
-       // Look for the resource state. If we don't have one, then it is okay.
-       rs := mod.Resources[resourceName]
-       if rs == nil {
+       src := ctx.State().ResourceInstanceObject(absAddr, key)
+       if src == nil {
+               // Presumably we only have deposed objects, then.
+               log.Printf("[TRACE] EvalReadStateDeposed: no state present for %s deposed object %s", absAddr, n.Key)
                return nil, nil
        }
 
-       // Use the delegate function to get the instance state from the resource state
-       is, err := readerFn(rs)
+       schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // Shouldn't happen since we should've failed long ago if no schema is present
+               return nil, fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", absAddr)
+       }
+       var diags tfdiags.Diagnostics
+       src, diags = UpgradeResourceState(absAddr, *n.Provider, src, schema, currentVersion)
+       if diags.HasErrors() {
+               // Note that we don't have any channel to return warnings here. We'll
+               // accept that for now since warnings during a schema upgrade would
+               // be pretty weird anyway, since this operation is supposed to seem
+               // invisible to the user.
+               return nil, diags.Err()
+       }
+
+       obj, err := src.Decode(schema.ImpliedType())
        if err != nil {
                return nil, err
        }
-
-       // Write the result to the output pointer
-       if output != nil {
-               *output = is
+       if n.Output != nil {
+               *n.Output = obj
        }
-
-       return is, nil
+       return obj, nil
 }
 
-// EvalRequireState is an EvalNode implementation that early exits
-// if the state doesn't have an ID.
+// EvalRequireState is an EvalNode implementation that exits early if the given
+// object is null.
 type EvalRequireState struct {
-       State **InstanceState
+       State **states.ResourceInstanceObject
 }
 
 func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) {
@@ -95,7 +153,7 @@ func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) {
        }
 
        state := *n.State
-       if state == nil || state.ID == "" {
+       if state == nil || state.Value.IsNull() {
                return nil, EvalEarlyExitError{}
        }
 
@@ -107,12 +165,14 @@ func (n *EvalRequireState) Eval(ctx EvalContext) (interface{}, error) {
 type EvalUpdateStateHook struct{}
 
 func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) {
-       state, lock := ctx.State()
-
-       // Get a full lock. Even calling something like WriteState can modify
-       // (prune) the state, so we need the full lock.
-       lock.Lock()
-       defer lock.Unlock()
+       // 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) {
@@ -125,171 +185,285 @@ func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) {
        return nil, nil
 }
 
-// EvalWriteState is an EvalNode implementation that writes the
-// primary InstanceState for a specific resource into the state.
+// EvalWriteState is an EvalNode implementation that saves the given object
+// as the current object for the selected resource instance.
 type EvalWriteState struct {
-       Name         string
-       ResourceType string
-       Provider     string
-       Dependencies []string
-       State        **InstanceState
+       // Addr is the address of the instance to read state for.
+       Addr addrs.ResourceInstance
+
+       // State is the object state to save.
+       State **states.ResourceInstanceObject
+
+       // ProviderSchema is the schema for the provider given in ProviderAddr.
+       ProviderSchema **ProviderSchema
+
+       // ProviderAddr is the address of the provider configuration that
+       // produced the given object.
+       ProviderAddr addrs.AbsProviderConfig
 }
 
 func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
-       return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Provider, n.Dependencies,
-               func(rs *ResourceState) error {
-                       rs.Primary = *n.State
-                       return nil
-               },
-       )
+       if n.State == nil {
+               // Note that a pointer _to_ nil is valid here, indicating the total
+               // absense of an object as we'd see during destroy.
+               panic("EvalWriteState used with no ResourceInstanceObject")
+       }
+
+       absAddr := n.Addr.Absolute(ctx.Path())
+       state := ctx.State()
+
+       if n.ProviderAddr.ProviderConfig.Type == "" {
+               return nil, fmt.Errorf("failed to write state for %s, missing provider type", absAddr)
+       }
+
+       obj := *n.State
+       if obj == nil || obj.Value.IsNull() {
+               // No need to encode anything: we'll just write it directly.
+               state.SetResourceInstanceCurrent(absAddr, nil, n.ProviderAddr)
+               log.Printf("[TRACE] EvalWriteState: removing state object for %s", absAddr)
+               return nil, nil
+       }
+       if n.ProviderSchema == nil || *n.ProviderSchema == nil {
+               // Should never happen, unless our state object is nil
+               panic("EvalWriteState used with pointer to nil ProviderSchema object")
+       }
+
+       if obj != nil {
+               log.Printf("[TRACE] EvalWriteState: writing current state object for %s", absAddr)
+       } else {
+               log.Printf("[TRACE] EvalWriteState: removing current state object for %s", absAddr)
+       }
+
+       schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // It shouldn't be possible to get this far in any real scenario
+               // without a schema, but we might end up here in contrived tests that
+               // fail to set up their world properly.
+               return nil, fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr)
+       }
+       src, err := obj.Encode(schema.ImpliedType(), currentVersion)
+       if err != nil {
+               return nil, fmt.Errorf("failed to encode %s in state: %s", absAddr, err)
+       }
+
+       state.SetResourceInstanceCurrent(absAddr, src, n.ProviderAddr)
+       return nil, nil
 }
 
 // EvalWriteStateDeposed is an EvalNode implementation that writes
 // an InstanceState out to the Deposed list of a resource in the state.
 type EvalWriteStateDeposed struct {
-       Name         string
-       ResourceType string
-       Provider     string
-       Dependencies []string
-       State        **InstanceState
-       // Index indicates which instance in the Deposed list to target, or -1 to append.
-       Index int
+       // Addr is the address of the instance to read state for.
+       Addr addrs.ResourceInstance
+
+       // Key indicates which deposed object to write to.
+       Key states.DeposedKey
+
+       // State is the object state to save.
+       State **states.ResourceInstanceObject
+
+       // ProviderSchema is the schema for the provider given in ProviderAddr.
+       ProviderSchema **ProviderSchema
+
+       // ProviderAddr is the address of the provider configuration that
+       // produced the given object.
+       ProviderAddr addrs.AbsProviderConfig
 }
 
 func (n *EvalWriteStateDeposed) Eval(ctx EvalContext) (interface{}, error) {
-       return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Provider, n.Dependencies,
-               func(rs *ResourceState) error {
-                       if n.Index == -1 {
-                               rs.Deposed = append(rs.Deposed, *n.State)
-                       } else {
-                               rs.Deposed[n.Index] = *n.State
-                       }
-                       return nil
-               },
-       )
-}
+       if n.State == nil {
+               // Note that a pointer _to_ nil is valid here, indicating the total
+               // absense of an object as we'd see during destroy.
+               panic("EvalWriteStateDeposed used with no ResourceInstanceObject")
+       }
 
-// Pulls together the common tasks of the EvalWriteState nodes.  All the args
-// are passed directly down from the EvalNode along with a `writer` function
-// which is yielded the *ResourceState and is responsible for writing an
-// InstanceState to the proper field in the ResourceState.
-func writeInstanceToState(
-       ctx EvalContext,
-       resourceName string,
-       resourceType string,
-       provider string,
-       dependencies []string,
-       writerFn func(*ResourceState) error,
-) (*InstanceState, error) {
-       state, lock := ctx.State()
-       if state == nil {
-               return nil, fmt.Errorf("cannot write state to nil state")
-       }
-
-       // Get a write lock so we can access this instance
-       lock.Lock()
-       defer lock.Unlock()
-
-       // Look for the module state. If we don't have one, create it.
-       mod := state.ModuleByPath(ctx.Path())
-       if mod == nil {
-               mod = state.AddModule(ctx.Path())
-       }
-
-       // Look for the resource state.
-       rs := mod.Resources[resourceName]
-       if rs == nil {
-               rs = &ResourceState{}
-               rs.init()
-               mod.Resources[resourceName] = rs
-       }
-       rs.Type = resourceType
-       rs.Dependencies = dependencies
-       rs.Provider = provider
-
-       if err := writerFn(rs); err != nil {
-               return nil, err
+       absAddr := n.Addr.Absolute(ctx.Path())
+       key := n.Key
+       state := ctx.State()
+
+       if key == states.NotDeposed {
+               // should never happen
+               return nil, fmt.Errorf("can't save deposed object for %s without a deposed key; this is a bug in Terraform that should be reported", absAddr)
+       }
+
+       obj := *n.State
+       if obj == nil {
+               // No need to encode anything: we'll just write it directly.
+               state.SetResourceInstanceDeposed(absAddr, key, nil, n.ProviderAddr)
+               log.Printf("[TRACE] EvalWriteStateDeposed: removing state object for %s deposed %s", absAddr, key)
+               return nil, nil
+       }
+       if n.ProviderSchema == nil || *n.ProviderSchema == nil {
+               // Should never happen, unless our state object is nil
+               panic("EvalWriteStateDeposed used with no ProviderSchema object")
+       }
+
+       schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // It shouldn't be possible to get this far in any real scenario
+               // without a schema, but we might end up here in contrived tests that
+               // fail to set up their world properly.
+               return nil, fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr)
+       }
+       src, err := obj.Encode(schema.ImpliedType(), currentVersion)
+       if err != nil {
+               return nil, fmt.Errorf("failed to encode %s in state: %s", absAddr, err)
        }
 
+       log.Printf("[TRACE] EvalWriteStateDeposed: writing state object for %s deposed %s", absAddr, key)
+       state.SetResourceInstanceDeposed(absAddr, key, src, n.ProviderAddr)
        return nil, nil
 }
 
-// EvalDeposeState is an EvalNode implementation that takes the primary
-// out of a state and makes it Deposed. This is done at the beginning of
-// create-before-destroy calls so that the create can create while preserving
-// the old state of the to-be-destroyed resource.
+// EvalDeposeState is an EvalNode implementation that moves the current object
+// for the given instance to instead be a deposed object, leaving the instance
+// with no current object.
+// This is used at the beginning of a create-before-destroy replace action so
+// that the create can create while preserving the old state of the
+// to-be-destroyed object.
 type EvalDeposeState struct {
-       Name string
+       Addr addrs.ResourceInstance
+
+       // ForceKey, if a value other than states.NotDeposed, will be used as the
+       // key for the newly-created deposed object that results from this action.
+       // If set to states.NotDeposed (the zero value), a new unique key will be
+       // allocated.
+       ForceKey states.DeposedKey
+
+       // OutputKey, if non-nil, will be written with the deposed object key that
+       // was generated for the object. This can then be passed to
+       // EvalUndeposeState.Key so it knows which deposed instance to forget.
+       OutputKey *states.DeposedKey
 }
 
 // TODO: test
 func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) {
-       state, lock := ctx.State()
-
-       // Get a read lock so we can access this instance
-       lock.RLock()
-       defer lock.RUnlock()
-
-       // Look for the module state. If we don't have one, then it doesn't matter.
-       mod := state.ModuleByPath(ctx.Path())
-       if mod == nil {
-               return nil, nil
-       }
-
-       // Look for the resource state. If we don't have one, then it is okay.
-       rs := mod.Resources[n.Name]
-       if rs == nil {
-               return nil, nil
+       absAddr := n.Addr.Absolute(ctx.Path())
+       state := ctx.State()
+
+       var key states.DeposedKey
+       if n.ForceKey == states.NotDeposed {
+               key = state.DeposeResourceInstanceObject(absAddr)
+       } else {
+               key = n.ForceKey
+               state.DeposeResourceInstanceObjectForceKey(absAddr, key)
        }
+       log.Printf("[TRACE] EvalDeposeState: prior object for %s now deposed with key %s", absAddr, key)
 
-       // If we don't have a primary, we have nothing to depose
-       if rs.Primary == nil {
-               return nil, nil
+       if n.OutputKey != nil {
+               *n.OutputKey = key
        }
 
-       // Depose
-       rs.Deposed = append(rs.Deposed, rs.Primary)
-       rs.Primary = nil
-
        return nil, nil
 }
 
-// EvalUndeposeState is an EvalNode implementation that reads the
-// InstanceState for a specific resource out of the state.
-type EvalUndeposeState struct {
-       Name  string
-       State **InstanceState
+// EvalMaybeRestoreDeposedObject is an EvalNode implementation that will
+// restore a particular deposed object of the specified resource instance
+// to be the "current" object if and only if the instance doesn't currently
+// have a current object.
+//
+// This is intended for use when the create leg of a create before destroy
+// fails with no partial new object: if we didn't take any action, the user
+// would be left in the unfortunate situation of having no current object
+// and the previously-workign object now deposed. This EvalNode causes a
+// better outcome by restoring things to how they were before the replace
+// operation began.
+//
+// The create operation may have produced a partial result even though it
+// failed and it's important that we don't "forget" that state, so in that
+// situation the prior object remains deposed and the partial new object
+// remains the current object, allowing the situation to hopefully be
+// improved in a subsequent run.
+type EvalMaybeRestoreDeposedObject struct {
+       Addr addrs.ResourceInstance
+
+       // Key is a pointer to the deposed object key that should be forgotten
+       // from the state, which must be non-nil.
+       Key *states.DeposedKey
 }
 
 // TODO: test
-func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) {
-       state, lock := ctx.State()
+func (n *EvalMaybeRestoreDeposedObject) Eval(ctx EvalContext) (interface{}, error) {
+       absAddr := n.Addr.Absolute(ctx.Path())
+       dk := *n.Key
+       state := ctx.State()
+
+       restored := state.MaybeRestoreResourceInstanceDeposed(absAddr, dk)
+       if restored {
+               log.Printf("[TRACE] EvalMaybeRestoreDeposedObject: %s deposed object %s was restored as the current object", absAddr, dk)
+       } else {
+               log.Printf("[TRACE] EvalMaybeRestoreDeposedObject: %s deposed object %s remains deposed", absAddr, dk)
+       }
 
-       // Get a read lock so we can access this instance
-       lock.RLock()
-       defer lock.RUnlock()
+       return nil, nil
+}
 
-       // Look for the module state. If we don't have one, then it doesn't matter.
-       mod := state.ModuleByPath(ctx.Path())
-       if mod == nil {
-               return nil, nil
-       }
+// EvalWriteResourceState is an EvalNode implementation that ensures that
+// a suitable resource-level state record is present in the state, if that's
+// required for the "each mode" of that resource.
+//
+// This is important primarily for the situation where count = 0, since this
+// eval is the only change we get to set the resource "each mode" to list
+// in that case, allowing expression evaluation to see it as a zero-element
+// list rather than as not set at all.
+type EvalWriteResourceState struct {
+       Addr         addrs.Resource
+       Config       *configs.Resource
+       ProviderAddr addrs.AbsProviderConfig
+}
 
-       // Look for the resource state. If we don't have one, then it is okay.
-       rs := mod.Resources[n.Name]
-       if rs == nil {
-               return nil, nil
+// TODO: test
+func (n *EvalWriteResourceState) Eval(ctx EvalContext) (interface{}, error) {
+       var diags tfdiags.Diagnostics
+       absAddr := n.Addr.Absolute(ctx.Path())
+       state := ctx.State()
+
+       count, countDiags := evaluateResourceCountExpression(n.Config.Count, ctx)
+       diags = diags.Append(countDiags)
+       if countDiags.HasErrors() {
+               return nil, diags.Err()
        }
 
-       // If we don't have any desposed resource, then we don't have anything to do
-       if len(rs.Deposed) == 0 {
-               return nil, nil
+       // Currently we ony support NoEach and EachList, because for_each support
+       // is not fully wired up across Terraform. Once for_each support is added,
+       // we'll need to handle that here too, setting states.EachMap if the
+       // assigned expression is a map.
+       eachMode := states.NoEach
+       if count >= 0 { // -1 signals "count not set"
+               eachMode = states.EachList
        }
 
-       // Undepose
-       idx := len(rs.Deposed) - 1
-       rs.Primary = rs.Deposed[idx]
-       rs.Deposed[idx] = *n.State
+       // This method takes care of all of the business logic of updating this
+       // while ensuring that any existing instances are preserved, etc.
+       state.SetResourceMeta(absAddr, eachMode, n.ProviderAddr)
+
+       return nil, nil
+}
+
+// EvalForgetResourceState is an EvalNode implementation that prunes out an
+// empty resource-level state for a given resource address, or produces an
+// error if it isn't empty after all.
+//
+// This should be the last action taken for a resource that has been removed
+// from the configuration altogether, to clean up the leftover husk of the
+// resource in the state after other EvalNodes have destroyed and removed
+// all of the instances and instance objects beneath it.
+type EvalForgetResourceState struct {
+       Addr addrs.Resource
+}
+
+func (n *EvalForgetResourceState) Eval(ctx EvalContext) (interface{}, error) {
+       absAddr := n.Addr.Absolute(ctx.Path())
+       state := ctx.State()
+
+       pruned := state.RemoveResourceIfEmpty(absAddr)
+       if !pruned {
+               // If this produces an error, it indicates a bug elsewhere in Terraform
+               // -- probably missing graph nodes, graph edges, or
+               // incorrectly-implemented evaluation steps.
+               return nil, fmt.Errorf("orphan resource %s still has a non-empty state after apply; this is a bug in Terraform", absAddr)
+       }
+       log.Printf("[TRACE] EvalForgetResourceState: Pruned husk of %s from state", absAddr)
 
        return nil, nil
 }