opentofu/internal/refactoring/move_validate.go
2023-05-02 15:33:06 +00:00

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")
}
}