mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-11 08:05:33 -06:00
For `terraform destroy`, we currently build up the same graph we do for `plan` and `apply` and we do a walk with a special Diff that says "destroy everything". We have fought the interpolation subsystem time and again through this code path. Beginning in #2775 we gained a new feature to selectively prune out problematic graph nodes. The past chain of destroy fixes I have been involved with (#6557, #6599, #6753) have attempted to massage the "noop" definitions to properly handle the edge cases reported. "Variable is depended on by provider config" is another edge case we add here and try to fix. This dive only makes me more convinced that the whole `terraform destroy` code path needs to be reworked. For now, I went with a "surgical strike" approach to the problem expressed in #7047. I found a couple of issues with the existing Noop and DestroyEdgeInclude logic, especially with regards to flattening, but I'm explicitly ignoring these for now so we can get this particular bug fixed ahead of the 0.7 release. My hope is that we can circle around with a fully specced initiative to refactor `terraform destroy`'s graph to be more state-derived than config-derived. Until then, this fixes #7407
258 lines
6.9 KiB
Go
258 lines
6.9 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/hashicorp/terraform/config"
|
|
"github.com/hashicorp/terraform/config/module"
|
|
"github.com/hashicorp/terraform/dag"
|
|
)
|
|
|
|
// GraphNodeConfigVariable represents a Variable in the config.
|
|
type GraphNodeConfigVariable struct {
|
|
Variable *config.Variable
|
|
|
|
// Value, if non-nil, will be used to set the value of the variable
|
|
// during evaluation. If this is nil, evaluation will do nothing.
|
|
//
|
|
// Module is the name of the module to set the variables on.
|
|
Module string
|
|
Value *config.RawConfig
|
|
|
|
ModuleTree *module.Tree
|
|
ModulePath []string
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariable) Name() string {
|
|
return fmt.Sprintf("var.%s", n.Variable.Name)
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariable) ConfigType() GraphNodeConfigType {
|
|
return GraphNodeConfigTypeVariable
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariable) DependableName() []string {
|
|
return []string{n.Name()}
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariable) DependentOn() []string {
|
|
// If we don't have any value set, we don't depend on anything
|
|
if n.Value == nil {
|
|
return nil
|
|
}
|
|
|
|
// Get what we depend on based on our value
|
|
vars := n.Value.Variables
|
|
result := make([]string, 0, len(vars))
|
|
for _, v := range vars {
|
|
if vn := varNameForVar(v); vn != "" {
|
|
result = append(result, vn)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariable) VariableName() string {
|
|
return n.Variable.Name
|
|
}
|
|
|
|
// GraphNodeDestroyEdgeInclude impl.
|
|
func (n *GraphNodeConfigVariable) DestroyEdgeInclude(v dag.Vertex) bool {
|
|
// Only include this variable in a destroy edge if the source vertex
|
|
// "v" has a count dependency on this variable.
|
|
log.Printf("[DEBUG] DestroyEdgeInclude: Checking: %s", dag.VertexName(v))
|
|
cv, ok := v.(GraphNodeCountDependent)
|
|
if !ok {
|
|
log.Printf("[DEBUG] DestroyEdgeInclude: Not GraphNodeCountDependent: %s", dag.VertexName(v))
|
|
return false
|
|
}
|
|
|
|
for _, d := range cv.CountDependentOn() {
|
|
for _, d2 := range n.DependableName() {
|
|
log.Printf("[DEBUG] DestroyEdgeInclude: d = %s : d2 = %s", d, d2)
|
|
if d == d2 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GraphNodeNoopPrunable
|
|
func (n *GraphNodeConfigVariable) Noop(opts *NoopOpts) bool {
|
|
log.Printf("[DEBUG] Checking variable noop: %s", n.Name())
|
|
// If we have no diff, always keep this in the graph. We have to do
|
|
// this primarily for validation: we want to validate that variable
|
|
// interpolations are valid even if there are no resources that
|
|
// depend on them.
|
|
if opts.Diff == nil || opts.Diff.Empty() {
|
|
log.Printf("[DEBUG] No diff, not a noop")
|
|
return false
|
|
}
|
|
|
|
// We have to find our our module diff since we do funky things with
|
|
// the flat node's implementation of Path() below.
|
|
modDiff := opts.Diff.ModuleByPath(n.ModulePath)
|
|
|
|
// If we're destroying, we have no need of variables unless they are depended
|
|
// on by the count of a resource.
|
|
if modDiff != nil && modDiff.Destroy {
|
|
if n.hasDestroyEdgeInPath(opts, nil) {
|
|
log.Printf("[DEBUG] Variable has destroy edge from %s, not a noop",
|
|
dag.VertexName(opts.Vertex))
|
|
return false
|
|
}
|
|
log.Printf("[DEBUG] Variable has no included destroy edges: noop!")
|
|
return true
|
|
}
|
|
|
|
for _, v := range opts.Graph.UpEdges(opts.Vertex).List() {
|
|
// This is terrible, but I can't think of a better way to do this.
|
|
if dag.VertexName(v) == rootNodeName {
|
|
continue
|
|
}
|
|
|
|
log.Printf("[DEBUG] Found up edge to %s, var is not noop", dag.VertexName(v))
|
|
return false
|
|
}
|
|
|
|
log.Printf("[DEBUG] No up edges, treating variable as a noop")
|
|
return true
|
|
}
|
|
|
|
// hasDestroyEdgeInPath recursively walks for a destroy edge, ensuring that
|
|
// a variable both has no immediate destroy edges or any in its full module
|
|
// path, ensuring that links do not get severed in the middle.
|
|
func (n *GraphNodeConfigVariable) hasDestroyEdgeInPath(opts *NoopOpts, vertex dag.Vertex) bool {
|
|
if vertex == nil {
|
|
vertex = opts.Vertex
|
|
}
|
|
log.Printf("[DEBUG] hasDestroyEdgeInPath: Looking for destroy edge: %s - %T", dag.VertexName(vertex), vertex)
|
|
for _, v := range opts.Graph.UpEdges(vertex).List() {
|
|
if len(opts.Graph.UpEdges(v).List()) > 1 {
|
|
if n.hasDestroyEdgeInPath(opts, v) == true {
|
|
return true
|
|
}
|
|
}
|
|
// Here we borrow the implementation of DestroyEdgeInclude, whose logic
|
|
// and semantics are exactly what we want here.
|
|
if cv, ok := vertex.(*GraphNodeConfigVariableFlat); ok {
|
|
if cv.DestroyEdgeInclude(v) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GraphNodeProxy impl.
|
|
func (n *GraphNodeConfigVariable) Proxy() bool {
|
|
return true
|
|
}
|
|
|
|
// GraphNodeEvalable impl.
|
|
func (n *GraphNodeConfigVariable) EvalTree() EvalNode {
|
|
// If we have no value, do nothing
|
|
if n.Value == nil {
|
|
return &EvalNoop{}
|
|
}
|
|
|
|
// Otherwise, interpolate the value of this variable and set it
|
|
// within the variables mapping.
|
|
var config *ResourceConfig
|
|
variables := make(map[string]interface{})
|
|
return &EvalSequence{
|
|
Nodes: []EvalNode{
|
|
&EvalInterpolate{
|
|
Config: n.Value,
|
|
Output: &config,
|
|
},
|
|
|
|
&EvalVariableBlock{
|
|
Config: &config,
|
|
VariableValues: variables,
|
|
},
|
|
|
|
&EvalTypeCheckVariable{
|
|
Variables: variables,
|
|
ModulePath: n.ModulePath,
|
|
ModuleTree: n.ModuleTree,
|
|
},
|
|
|
|
&EvalSetVariables{
|
|
Module: &n.Module,
|
|
Variables: variables,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// GraphNodeFlattenable impl.
|
|
func (n *GraphNodeConfigVariable) Flatten(p []string) (dag.Vertex, error) {
|
|
return &GraphNodeConfigVariableFlat{
|
|
GraphNodeConfigVariable: n,
|
|
PathValue: p,
|
|
}, nil
|
|
}
|
|
|
|
type GraphNodeConfigVariableFlat struct {
|
|
*GraphNodeConfigVariable
|
|
|
|
PathValue []string
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariableFlat) Name() string {
|
|
return fmt.Sprintf(
|
|
"%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigVariable.Name())
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariableFlat) DependableName() []string {
|
|
return []string{n.Name()}
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariableFlat) DependentOn() []string {
|
|
// We only wrap the dependencies and such if we have a path that is
|
|
// longer than 2 elements (root, child, more). This is because when
|
|
// flattened, variables can point outside the graph.
|
|
prefix := ""
|
|
if len(n.PathValue) > 2 {
|
|
prefix = modulePrefixStr(n.PathValue[:len(n.PathValue)-1])
|
|
}
|
|
|
|
return modulePrefixList(
|
|
n.GraphNodeConfigVariable.DependentOn(),
|
|
prefix)
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariableFlat) Path() []string {
|
|
if len(n.PathValue) > 2 {
|
|
return n.PathValue[:len(n.PathValue)-1]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *GraphNodeConfigVariableFlat) Noop(opts *NoopOpts) bool {
|
|
// First look for provider nodes that depend on this variable downstream
|
|
modDiff := opts.Diff.ModuleByPath(n.ModulePath)
|
|
if modDiff != nil && modDiff.Destroy {
|
|
ds, err := opts.Graph.Descendents(n)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Error looking up descendents of %s: %s", n.Name(), err)
|
|
} else {
|
|
for _, d := range ds.List() {
|
|
if _, ok := d.(GraphNodeProvider); ok {
|
|
log.Printf("[DEBUG] This variable is depended on by a provider, can't be a noop.")
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then fall back to existing impl
|
|
return n.GraphNodeConfigVariable.Noop(opts)
|
|
}
|