mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
344 lines
12 KiB
Go
344 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package refactoring
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/dag"
|
|
"github.com/hashicorp/terraform/internal/instances"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// ValidateMoves tests whether all of the given move statements comply with
|
|
// both the single-statement validation rules and the "big picture" rules
|
|
// that constrain statements in relation to one another.
|
|
//
|
|
// The validation rules are primarily in terms of the configuration, but
|
|
// ValidateMoves also takes the expander that resulted from creating a plan
|
|
// so that it can see which instances are defined for each module and resource,
|
|
// to precisely validate move statements involving specific-instance addresses.
|
|
//
|
|
// Because validation depends on the planning result but move execution must
|
|
// happen _before_ planning, we have the unusual situation where sibling
|
|
// function ApplyMoves must run before ValidateMoves and must therefore
|
|
// tolerate and ignore any invalid statements. The plan walk will then
|
|
// construct in incorrect plan (because it'll be starting from the wrong
|
|
// prior state) but ValidateMoves will block actually showing that invalid
|
|
// plan to the user.
|
|
func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
if len(stmts) == 0 {
|
|
return diags
|
|
}
|
|
|
|
g := buildMoveStatementGraph(stmts)
|
|
|
|
// We need to track the absolute versions of our endpoint addresses in
|
|
// order to detect when there are ambiguous moves.
|
|
type AbsMoveEndpoint struct {
|
|
Other addrs.AbsMoveable
|
|
StmtRange tfdiags.SourceRange
|
|
}
|
|
stmtFrom := addrs.MakeMap[addrs.AbsMoveable, AbsMoveEndpoint]()
|
|
stmtTo := addrs.MakeMap[addrs.AbsMoveable, AbsMoveEndpoint]()
|
|
|
|
for _, stmt := range stmts {
|
|
// Earlier code that constructs MoveStatement values should ensure that
|
|
// both stmt.From and stmt.To always belong to the same statement.
|
|
fromMod, _ := stmt.From.ModuleCallTraversals()
|
|
|
|
for _, fromModInst := range declaredInsts.InstancesForModule(fromMod) {
|
|
absFrom := stmt.From.InModuleInstance(fromModInst)
|
|
|
|
absTo := stmt.To.InModuleInstance(fromModInst)
|
|
|
|
if addrs.Equivalent(absFrom, absTo) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Redundant move statement",
|
|
Detail: fmt.Sprintf(
|
|
"This statement declares a move from %s to the same address, which is the same as not declaring this move at all.",
|
|
absFrom,
|
|
),
|
|
Subject: stmt.DeclRange.ToHCL().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
var noun string
|
|
var shortNoun string
|
|
switch absFrom.(type) {
|
|
case addrs.ModuleInstance:
|
|
noun = "module instance"
|
|
shortNoun = "instance"
|
|
case addrs.AbsModuleCall:
|
|
noun = "module call"
|
|
shortNoun = "call"
|
|
case addrs.AbsResourceInstance:
|
|
noun = "resource instance"
|
|
shortNoun = "instance"
|
|
case addrs.AbsResource:
|
|
noun = "resource"
|
|
shortNoun = "resource"
|
|
default:
|
|
// The above cases should cover all of the AbsMoveable types
|
|
panic("unsupported AbsMoveable address type")
|
|
}
|
|
|
|
// It's invalid to have a move statement whose "from" address
|
|
// refers to something that is still declared in the configuration.
|
|
if moveableObjectExists(absFrom, declaredInsts) {
|
|
conflictRange, hasRange := movableObjectDeclRange(absFrom, rootCfg)
|
|
declaredAt := ""
|
|
if hasRange {
|
|
// NOTE: It'd be pretty weird to _not_ have a range, since
|
|
// we're only in this codepath because the plan phase
|
|
// thought this object existed in the configuration.
|
|
declaredAt = fmt.Sprintf(" at %s", conflictRange.StartString())
|
|
}
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Moved object still exists",
|
|
Detail: fmt.Sprintf(
|
|
"This statement declares a move from %s, but that %s is still declared%s.\n\nChange your configuration so that this %s will be declared as %s instead.",
|
|
absFrom, noun, declaredAt, shortNoun, absTo,
|
|
),
|
|
Subject: stmt.DeclRange.ToHCL().Ptr(),
|
|
})
|
|
}
|
|
|
|
// There can only be one destination for each source address.
|
|
if existing, exists := stmtFrom.GetOk(absFrom); exists {
|
|
if !addrs.Equivalent(existing.Other, absTo) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Ambiguous move statements",
|
|
Detail: fmt.Sprintf(
|
|
"A statement at %s declared that %s moved to %s, but this statement instead declares that it moved to %s.\n\nEach %s can move to only one destination %s.",
|
|
existing.StmtRange.StartString(), absFrom, existing.Other, absTo,
|
|
noun, shortNoun,
|
|
),
|
|
Subject: stmt.DeclRange.ToHCL().Ptr(),
|
|
})
|
|
}
|
|
} else {
|
|
stmtFrom.Put(absFrom, AbsMoveEndpoint{
|
|
Other: absTo,
|
|
StmtRange: stmt.DeclRange,
|
|
})
|
|
}
|
|
|
|
// There can only be one source for each destination address.
|
|
if existing, exists := stmtTo.GetOk(absTo); exists {
|
|
if !addrs.Equivalent(existing.Other, absFrom) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Ambiguous move statements",
|
|
Detail: fmt.Sprintf(
|
|
"A statement at %s declared that %s moved to %s, but this statement instead declares that %s moved there.\n\nEach %s can have moved from only one source %s.",
|
|
existing.StmtRange.StartString(), existing.Other, absTo, absFrom,
|
|
noun, shortNoun,
|
|
),
|
|
Subject: stmt.DeclRange.ToHCL().Ptr(),
|
|
})
|
|
}
|
|
} else {
|
|
stmtTo.Put(absTo, AbsMoveEndpoint{
|
|
Other: absFrom,
|
|
StmtRange: stmt.DeclRange,
|
|
})
|
|
}
|
|
|
|
// Resource types must match.
|
|
if resourceTypesDiffer(absFrom, absTo) {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Resource type mismatch",
|
|
Detail: fmt.Sprintf(
|
|
"This statement declares a move from %s to %s, which is a %s of a different type.", absFrom, absTo, noun,
|
|
),
|
|
})
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// If we're not already returning other errors then we'll also check for
|
|
// and report cycles.
|
|
//
|
|
// Cycles alone are difficult to report in a helpful way because we don't
|
|
// have enough context to guess the user's intent. However, some particular
|
|
// mistakes that might lead to a cycle can also be caught by other
|
|
// validation rules above where we can make better suggestions, and so
|
|
// we'll use a cycle report only as a last resort.
|
|
if !diags.HasErrors() {
|
|
diags = diags.Append(validateMoveStatementGraph(g))
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func validateMoveStatementGraph(g *dag.AcyclicGraph) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
for _, cycle := range g.Cycles() {
|
|
// Reporting cycles is awkward because there isn't any definitive
|
|
// way to decide which of the objects in the cycle is the cause of
|
|
// the problem. Therefore we'll just list them all out and leave
|
|
// the user to figure it out. :(
|
|
stmtStrs := make([]string, 0, len(cycle))
|
|
for _, stmtI := range cycle {
|
|
// move statement graph nodes are pointers to move statements
|
|
stmt := stmtI.(*MoveStatement)
|
|
stmtStrs = append(stmtStrs, fmt.Sprintf(
|
|
"\n - %s: %s → %s",
|
|
stmt.DeclRange.StartString(),
|
|
stmt.From.String(),
|
|
stmt.To.String(),
|
|
))
|
|
}
|
|
sort.Strings(stmtStrs) // just to make the order deterministic
|
|
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Cyclic dependency in move statements",
|
|
fmt.Sprintf(
|
|
"The following chained move statements form a cycle, and so there is no final location to move objects to:%s\n\nA chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.",
|
|
strings.Join(stmtStrs, ""),
|
|
),
|
|
))
|
|
}
|
|
|
|
// Look for cycles to self.
|
|
// A user shouldn't be able to create self-references, but we cannot
|
|
// correctly process a graph with them.
|
|
for _, e := range g.Edges() {
|
|
src := e.Source()
|
|
if src == e.Target() {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Self reference in move statements",
|
|
fmt.Sprintf(
|
|
"The move statement %s refers to itself the move dependency graph, which is invalid. This is a bug in Terraform; please report it!",
|
|
src.(*MoveStatement).Name(),
|
|
),
|
|
))
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func moveableObjectExists(addr addrs.AbsMoveable, in instances.Set) bool {
|
|
switch addr := addr.(type) {
|
|
case addrs.ModuleInstance:
|
|
return in.HasModuleInstance(addr)
|
|
case addrs.AbsModuleCall:
|
|
return in.HasModuleCall(addr)
|
|
case addrs.AbsResourceInstance:
|
|
return in.HasResourceInstance(addr)
|
|
case addrs.AbsResource:
|
|
return in.HasResource(addr)
|
|
default:
|
|
// The above cases should cover all of the AbsMoveable types
|
|
panic("unsupported AbsMoveable address type")
|
|
}
|
|
}
|
|
|
|
func resourceTypesDiffer(absFrom, absTo addrs.AbsMoveable) bool {
|
|
switch absFrom := absFrom.(type) {
|
|
case addrs.AbsMoveableResource:
|
|
// addrs.UnifyMoveEndpoints guarantees that both addresses are of the
|
|
// same kind, so at this point we can assume that absTo is also an
|
|
// addrs.AbsResourceInstance or addrs.AbsResource.
|
|
absTo := absTo.(addrs.AbsMoveableResource)
|
|
return absFrom.AffectedAbsResource().Resource.Type != absTo.AffectedAbsResource().Resource.Type
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiags.SourceRange, bool) {
|
|
switch addr := addr.(type) {
|
|
case addrs.ModuleInstance:
|
|
// For a module instance we're actually looking for the call that
|
|
// declared it, which belongs to the parent module.
|
|
// (NOTE: This assumes "addr" can never be the root module instance,
|
|
// because the root module is never moveable.)
|
|
parentAddr, callAddr := addr.Call()
|
|
modCfg := cfg.DescendentForInstance(parentAddr)
|
|
if modCfg == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
call := modCfg.Module.ModuleCalls[callAddr.Name]
|
|
if call == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
|
|
// If the call has either count or for_each set then we'll "blame"
|
|
// that expression, rather than the block as a whole, because it's
|
|
// the expression that decides which instances are available.
|
|
switch {
|
|
case call.ForEach != nil:
|
|
return tfdiags.SourceRangeFromHCL(call.ForEach.Range()), true
|
|
case call.Count != nil:
|
|
return tfdiags.SourceRangeFromHCL(call.Count.Range()), true
|
|
default:
|
|
return tfdiags.SourceRangeFromHCL(call.DeclRange), true
|
|
}
|
|
case addrs.AbsModuleCall:
|
|
modCfg := cfg.DescendentForInstance(addr.Module)
|
|
if modCfg == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
call := modCfg.Module.ModuleCalls[addr.Call.Name]
|
|
if call == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
return tfdiags.SourceRangeFromHCL(call.DeclRange), true
|
|
case addrs.AbsResourceInstance:
|
|
modCfg := cfg.DescendentForInstance(addr.Module)
|
|
if modCfg == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
rc := modCfg.Module.ResourceByAddr(addr.Resource.Resource)
|
|
if rc == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
|
|
// If the resource has either count or for_each set then we'll "blame"
|
|
// that expression, rather than the block as a whole, because it's
|
|
// the expression that decides which instances are available.
|
|
switch {
|
|
case rc.ForEach != nil:
|
|
return tfdiags.SourceRangeFromHCL(rc.ForEach.Range()), true
|
|
case rc.Count != nil:
|
|
return tfdiags.SourceRangeFromHCL(rc.Count.Range()), true
|
|
default:
|
|
return tfdiags.SourceRangeFromHCL(rc.DeclRange), true
|
|
}
|
|
case addrs.AbsResource:
|
|
modCfg := cfg.DescendentForInstance(addr.Module)
|
|
if modCfg == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
rc := modCfg.Module.ResourceByAddr(addr.Resource)
|
|
if rc == nil {
|
|
return tfdiags.SourceRange{}, false
|
|
}
|
|
return tfdiags.SourceRangeFromHCL(rc.DeclRange), true
|
|
default:
|
|
// The above cases should cover all of the AbsMoveable types
|
|
panic("unsupported AbsMoveable address type")
|
|
}
|
|
}
|