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) {
}
state := *n.State
- if state == nil || state.ID == "" {
+ if state == nil || state.Value.IsNull() {
return nil, EvalEarlyExitError{}
}
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) {
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
}