opentofu/terraform/node_data_refresh.go

222 lines
5.4 KiB
Go
Raw Normal View History

package terraform
import (
"github.com/hashicorp/terraform/dag"
)
// NodeRefreshableDataResource represents a resource that is "plannable":
// it is ready to be planned in order to create a diff.
type NodeRefreshableDataResource struct {
*NodeAbstractCountResource
}
// GraphNodeDynamicExpandable
func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
// Grab the state which we read
state, lock := ctx.State()
lock.RLock()
defer lock.RUnlock()
// Expand the resource count which must be available by now from EvalTree
count, err := n.Config.Count()
if err != nil {
return nil, err
}
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
// Add the config and state since we don't do that via transforms
a.Config = n.Config
a.ResolvedProvider = n.ResolvedProvider
return &NodeRefreshableDataResourceInstance{
NodeAbstractResource: a,
}
}
core: New refresh graph building behaviour Currently, the refresh graph uses the resources from state as a base, with data sources then layered on. Config is not consulted for resources and hence new resources that are added with count (or any new resource from config, for that matter) do not get added to the graph during refresh. This is leading to issues with scale in and scale out when the same value for count is used in both resources, and data sources that may depend on that resource (and possibly vice versa). While the resources exist in config and can be used, the fact that ConfigTransformer for resources is missing means that they don't get added into the graph, leading to "index out of range" errors and what not. Further to that, if we add these new resources to the graph for scale out, considerations need to be taken for scale in as well, which are not being caught 100% by the current implementation of NodeRefreshableDataResource. Scale-in resources should be treated as orphans, which according to the instance-form NodeRefreshableResource node, should be NodeDestroyableDataResource nodes, but this this logic is currently not rolled into NodeRefreshableDataResource. This causes issues on scale-in in the form of race-ish "index out of range" errors again. This commit updates the refresh graph so that StateTransformer is no longer used as the base of the graph. Instead, we add resources from the state and config in a hybrid fashion: * First off, resource nodes are added from config, but only if resources currently exist in state. NodeRefreshableManagedResource is a new expandable resource node that will expand count and add orphans from state. Any count-expanded node that has config but no state is also transformed into a plannable resource, via a new ResourceRefreshPlannableTransformer. * The NodeRefreshableDataResource node type will now add count orphans as NodeDestroyableDataResource nodes. This achieves the same effect as if the data sources were added by StateTransformer, but ensures there are no races in the dependency chain, with the added benefit of directing these nodes straight to the proper NodeDestroyableDataResource node. * Finally, config orphans (nodes that don't exist in config anymore period) are then added, to complete the graph. This should ensure as much as possible that there is a refresh graph that best represents both the current state and config with updated variables and counts.
2017-04-30 01:07:01 -05:00
// We also need a destroyable resource for orphans that are a result of a
// scaled-in count.
concreteResourceDestroyable := func(a *NodeAbstractResource) dag.Vertex {
// Add the config since we don't do that via transforms
a.Config = n.Config
return &NodeDestroyableDataResource{
NodeAbstractResource: a,
core: New refresh graph building behaviour Currently, the refresh graph uses the resources from state as a base, with data sources then layered on. Config is not consulted for resources and hence new resources that are added with count (or any new resource from config, for that matter) do not get added to the graph during refresh. This is leading to issues with scale in and scale out when the same value for count is used in both resources, and data sources that may depend on that resource (and possibly vice versa). While the resources exist in config and can be used, the fact that ConfigTransformer for resources is missing means that they don't get added into the graph, leading to "index out of range" errors and what not. Further to that, if we add these new resources to the graph for scale out, considerations need to be taken for scale in as well, which are not being caught 100% by the current implementation of NodeRefreshableDataResource. Scale-in resources should be treated as orphans, which according to the instance-form NodeRefreshableResource node, should be NodeDestroyableDataResource nodes, but this this logic is currently not rolled into NodeRefreshableDataResource. This causes issues on scale-in in the form of race-ish "index out of range" errors again. This commit updates the refresh graph so that StateTransformer is no longer used as the base of the graph. Instead, we add resources from the state and config in a hybrid fashion: * First off, resource nodes are added from config, but only if resources currently exist in state. NodeRefreshableManagedResource is a new expandable resource node that will expand count and add orphans from state. Any count-expanded node that has config but no state is also transformed into a plannable resource, via a new ResourceRefreshPlannableTransformer. * The NodeRefreshableDataResource node type will now add count orphans as NodeDestroyableDataResource nodes. This achieves the same effect as if the data sources were added by StateTransformer, but ensures there are no races in the dependency chain, with the added benefit of directing these nodes straight to the proper NodeDestroyableDataResource node. * Finally, config orphans (nodes that don't exist in config anymore period) are then added, to complete the graph. This should ensure as much as possible that there is a refresh graph that best represents both the current state and config with updated variables and counts.
2017-04-30 01:07:01 -05:00
}
}
// Start creating the steps
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Count: count,
Addr: n.ResourceAddr(),
},
core: New refresh graph building behaviour Currently, the refresh graph uses the resources from state as a base, with data sources then layered on. Config is not consulted for resources and hence new resources that are added with count (or any new resource from config, for that matter) do not get added to the graph during refresh. This is leading to issues with scale in and scale out when the same value for count is used in both resources, and data sources that may depend on that resource (and possibly vice versa). While the resources exist in config and can be used, the fact that ConfigTransformer for resources is missing means that they don't get added into the graph, leading to "index out of range" errors and what not. Further to that, if we add these new resources to the graph for scale out, considerations need to be taken for scale in as well, which are not being caught 100% by the current implementation of NodeRefreshableDataResource. Scale-in resources should be treated as orphans, which according to the instance-form NodeRefreshableResource node, should be NodeDestroyableDataResource nodes, but this this logic is currently not rolled into NodeRefreshableDataResource. This causes issues on scale-in in the form of race-ish "index out of range" errors again. This commit updates the refresh graph so that StateTransformer is no longer used as the base of the graph. Instead, we add resources from the state and config in a hybrid fashion: * First off, resource nodes are added from config, but only if resources currently exist in state. NodeRefreshableManagedResource is a new expandable resource node that will expand count and add orphans from state. Any count-expanded node that has config but no state is also transformed into a plannable resource, via a new ResourceRefreshPlannableTransformer. * The NodeRefreshableDataResource node type will now add count orphans as NodeDestroyableDataResource nodes. This achieves the same effect as if the data sources were added by StateTransformer, but ensures there are no races in the dependency chain, with the added benefit of directing these nodes straight to the proper NodeDestroyableDataResource node. * Finally, config orphans (nodes that don't exist in config anymore period) are then added, to complete the graph. This should ensure as much as possible that there is a refresh graph that best represents both the current state and config with updated variables and counts.
2017-04-30 01:07:01 -05:00
// Add the count orphans. As these are orphaned refresh nodes, we add them
// directly as NodeDestroyableDataResource.
&OrphanResourceCountTransformer{
Concrete: concreteResourceDestroyable,
Count: count,
Addr: n.ResourceAddr(),
State: state,
},
// Attach the state
&AttachStateTransformer{State: state},
// Targeting
&TargetsTransformer{ParsedTargets: n.Targets},
// Connect references so ordering is correct
&ReferenceTransformer{},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph
b := &BasicGraphBuilder{
Steps: steps,
Validate: true,
Name: "NodeRefreshableDataResource",
}
return b.Build(ctx.Path())
}
// NodeRefreshableDataResourceInstance represents a _single_ resource instance
// that is refreshable.
type NodeRefreshableDataResourceInstance struct {
*NodeAbstractResource
}
// GraphNodeEvalable
func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode {
addr := n.NodeAbstractResource.Addr
// stateId is the ID to put into the state
stateId := addr.stateId()
// Build the instance info. More of this will be populated during eval
info := &InstanceInfo{
Id: stateId,
Type: addr.Type,
}
// Get the state if we have it, if not we build it
rs := n.ResourceState
if rs == nil {
rs = &ResourceState{
Provider: n.ResolvedProvider,
}
}
// If the config isn't empty we update the state
if n.Config != nil {
rs = &ResourceState{
Type: n.Config.Type,
Provider: n.Config.Provider,
Dependencies: n.StateReferences(),
}
}
// Build the resource for eval
resource := &Resource{
Name: addr.Name,
Type: addr.Type,
CountIndex: addr.Index,
}
if resource.CountIndex < 0 {
resource.CountIndex = 0
}
// Declare a bunch of variables that are used for state during
// evaluation. Most of this are written to by-address below.
var config *ResourceConfig
var diff *InstanceDiff
var provider ResourceProvider
var state *InstanceState
return &EvalSequence{
Nodes: []EvalNode{
// Always destroy the existing state first, since we must
// make sure that values from a previous read will not
// get interpolated if we end up needing to defer our
// loading until apply time.
&EvalWriteState{
Name: stateId,
ResourceType: rs.Type,
Provider: n.ResolvedProvider,
Dependencies: rs.Dependencies,
State: &state, // state is nil here
},
&EvalInterpolate{
Config: n.Config.RawConfig.Copy(),
Resource: resource,
Output: &config,
},
// The rest of this pass can proceed only if there are no
// computed values in our config.
// (If there are, we'll deal with this during the plan and
// apply phases.)
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
if config.ComputedKeys != nil && len(config.ComputedKeys) > 0 {
return true, EvalEarlyExitError{}
}
// If the config explicitly has a depends_on for this
// data source, assume the intention is to prevent
// refreshing ahead of that dependency.
if len(n.Config.DependsOn) > 0 {
return true, EvalEarlyExitError{}
}
return true, nil
},
Then: EvalNoop{},
},
// The remainder of this pass is the same as running
// a "plan" pass immediately followed by an "apply" pass,
// populating the state early so it'll be available to
// provider configurations that need this data during
// refresh/plan.
&EvalGetProvider{
Name: n.ResolvedProvider,
Output: &provider,
},
&EvalReadDataDiff{
Info: info,
Config: &config,
Provider: &provider,
Output: &diff,
OutputState: &state,
},
&EvalReadDataApply{
Info: info,
Diff: &diff,
Provider: &provider,
Output: &state,
},
&EvalWriteState{
Name: stateId,
ResourceType: rs.Type,
Provider: n.ResolvedProvider,
Dependencies: rs.Dependencies,
State: &state,
},
&EvalUpdateStateHook{},
},
}
}