mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-17 12:12:59 -06:00
b807505d55
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.
164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
package terraform
|
|
|
|
import (
|
|
"log"
|
|
|
|
"github.com/hashicorp/terraform/config"
|
|
"github.com/hashicorp/terraform/config/module"
|
|
"github.com/hashicorp/terraform/dag"
|
|
)
|
|
|
|
// RefreshGraphBuilder implements GraphBuilder and is responsible for building
|
|
// a graph for refreshing (updating the Terraform state).
|
|
//
|
|
// The primary difference between this graph and others:
|
|
//
|
|
// * Based on the state since it represents the only resources that
|
|
// need to be refreshed.
|
|
//
|
|
// * Ignores lifecycle options since no lifecycle events occur here. This
|
|
// simplifies the graph significantly since complex transforms such as
|
|
// create-before-destroy can be completely ignored.
|
|
//
|
|
type RefreshGraphBuilder struct {
|
|
// Module is the root module for the graph to build.
|
|
Module *module.Tree
|
|
|
|
// State is the current state
|
|
State *State
|
|
|
|
// Providers is the list of providers supported.
|
|
Providers []string
|
|
|
|
// Targets are resources to target
|
|
Targets []string
|
|
|
|
// DisableReduce, if true, will not reduce the graph. Great for testing.
|
|
DisableReduce bool
|
|
|
|
// Validate will do structural validation of the graph.
|
|
Validate bool
|
|
}
|
|
|
|
// See GraphBuilder
|
|
func (b *RefreshGraphBuilder) Build(path []string) (*Graph, error) {
|
|
return (&BasicGraphBuilder{
|
|
Steps: b.Steps(),
|
|
Validate: b.Validate,
|
|
Name: "RefreshGraphBuilder",
|
|
}).Build(path)
|
|
}
|
|
|
|
// See GraphBuilder
|
|
func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
|
|
// Custom factory for creating providers.
|
|
concreteProvider := func(a *NodeAbstractProvider) dag.Vertex {
|
|
return &NodeApplyableProvider{
|
|
NodeAbstractProvider: a,
|
|
}
|
|
}
|
|
|
|
concreteManagedResource := func(a *NodeAbstractResource) dag.Vertex {
|
|
return &NodeRefreshableManagedResource{
|
|
NodeAbstractCountResource: &NodeAbstractCountResource{
|
|
NodeAbstractResource: a,
|
|
},
|
|
}
|
|
}
|
|
|
|
concreteManagedResourceInstance := func(a *NodeAbstractResource) dag.Vertex {
|
|
return &NodeRefreshableManagedResourceInstance{
|
|
NodeAbstractResource: a,
|
|
}
|
|
}
|
|
|
|
concreteDataResource := func(a *NodeAbstractResource) dag.Vertex {
|
|
return &NodeRefreshableDataResource{
|
|
NodeAbstractCountResource: &NodeAbstractCountResource{
|
|
NodeAbstractResource: a,
|
|
},
|
|
}
|
|
}
|
|
|
|
steps := []GraphTransformer{
|
|
// Creates all the managed resources that aren't in the state, but only if
|
|
// we have a state already. No resources in state means there's not
|
|
// anything to refresh.
|
|
func() GraphTransformer {
|
|
if b.State.HasResources() {
|
|
return &ConfigTransformer{
|
|
Concrete: concreteManagedResource,
|
|
Module: b.Module,
|
|
Unique: true,
|
|
ModeFilter: true,
|
|
Mode: config.ManagedResourceMode,
|
|
}
|
|
}
|
|
log.Println("[TRACE] No managed resources in state during refresh, skipping managed resource transformer")
|
|
return nil
|
|
}(),
|
|
|
|
// Creates all the data resources that aren't in the state. This will also
|
|
// add any orphans from scaling in as destroy nodes.
|
|
&ConfigTransformer{
|
|
Concrete: concreteDataResource,
|
|
Module: b.Module,
|
|
Unique: true,
|
|
ModeFilter: true,
|
|
Mode: config.DataResourceMode,
|
|
},
|
|
|
|
// Add any fully-orphaned resources from config (ones that have been
|
|
// removed completely, not ones that are just orphaned due to a scaled-in
|
|
// count.
|
|
&OrphanResourceTransformer{
|
|
Concrete: concreteManagedResourceInstance,
|
|
State: b.State,
|
|
Module: b.Module,
|
|
},
|
|
|
|
// Attach the state
|
|
&AttachStateTransformer{State: b.State},
|
|
|
|
// Attach the configuration to any resources
|
|
&AttachResourceConfigTransformer{Module: b.Module},
|
|
|
|
// Add root variables
|
|
&RootVariableTransformer{Module: b.Module},
|
|
|
|
// Create all the providers
|
|
&MissingProviderTransformer{Providers: b.Providers, Concrete: concreteProvider},
|
|
&ProviderTransformer{},
|
|
&DisableProviderTransformer{},
|
|
&ParentProviderTransformer{},
|
|
&AttachProviderConfigTransformer{Module: b.Module},
|
|
|
|
// Add the outputs
|
|
&OutputTransformer{Module: b.Module},
|
|
|
|
// Add module variables
|
|
&ModuleVariableTransformer{Module: b.Module},
|
|
|
|
// Connect so that the references are ready for targeting. We'll
|
|
// have to connect again later for providers and so on.
|
|
&ReferenceTransformer{},
|
|
|
|
// Target
|
|
&TargetsTransformer{Targets: b.Targets},
|
|
|
|
// Close opened plugin connections
|
|
&CloseProviderTransformer{},
|
|
|
|
// Single root
|
|
&RootTransformer{},
|
|
}
|
|
|
|
if !b.DisableReduce {
|
|
// Perform the transitive reduction to make our graph a bit
|
|
// more sane if possible (it usually is possible).
|
|
steps = append(steps, &TransitiveReductionTransformer{})
|
|
}
|
|
|
|
return steps
|
|
}
|