mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
9b7bec31b4
Signed-off-by: Nathan Baulch <nathan.baulch@gmail.com>
192 lines
7.2 KiB
Go
192 lines
7.2 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package refactoring
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
type MoveStatement struct {
|
|
From, To *addrs.MoveEndpointInModule
|
|
DeclRange tfdiags.SourceRange
|
|
|
|
// Implied is true for statements produced by ImpliedMoveStatements, and
|
|
// false for statements produced by FindMoveStatements.
|
|
//
|
|
// An "implied" statement is one that has no explicit "moved" block in
|
|
// the configuration and was instead generated automatically based on a
|
|
// comparison between current configuration and previous run state.
|
|
// For implied statements, the DeclRange field contains the source location
|
|
// of something in the source code that implied the statement, in which
|
|
// case it would probably be confusing to show that source range to the
|
|
// user, e.g. in an error message, without clearly mentioning that it's
|
|
// related to an implied move statement.
|
|
Implied bool
|
|
}
|
|
|
|
// FindMoveStatements recurses through the modules of the given configuration
|
|
// and returns a flat set of all "moved" blocks defined within, in a
|
|
// deterministic but undefined order.
|
|
func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
|
|
return findMoveStatements(rootCfg, nil)
|
|
}
|
|
|
|
func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
|
|
modAddr := cfg.Path
|
|
for _, mc := range cfg.Module.Moved {
|
|
fromAddr, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
|
|
if fromAddr == nil || toAddr == nil {
|
|
// Invalid combination should've been caught during original
|
|
// configuration decoding, in the configs package.
|
|
panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange))
|
|
}
|
|
|
|
into = append(into, MoveStatement{
|
|
From: fromAddr,
|
|
To: toAddr,
|
|
DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
|
|
Implied: false,
|
|
})
|
|
}
|
|
|
|
for _, childCfg := range cfg.Children {
|
|
into = findMoveStatements(childCfg, into)
|
|
}
|
|
|
|
return into
|
|
}
|
|
|
|
// ImpliedMoveStatements compares addresses in the given state with addresses
|
|
// in the given configuration and potentially returns additional MoveStatement
|
|
// objects representing moves we infer automatically, even though they aren't
|
|
// explicitly recorded in the configuration.
|
|
//
|
|
// We do this primarily for backward compatibility with behaviors of Terraform
|
|
// versions prior to introducing explicit "moved" blocks. Specifically, this
|
|
// function aims to achieve the same result as the "NodeCountBoundary"
|
|
// heuristic from Terraform v1.0 and earlier, where adding or removing the
|
|
// "count" meta-argument from an already-created resource can automatically
|
|
// preserve the zeroth or the NoKey instance, depending on the direction of
|
|
// the change. We do this only for resources that aren't mentioned already
|
|
// in at least one explicit move statement.
|
|
//
|
|
// As with the previous-version heuristics it replaces, this is a best effort
|
|
// and doesn't handle all situations. An explicit move statement is always
|
|
// preferred, but our goal here is to match exactly the same cases that the
|
|
// old heuristic would've matched, to retain compatibility for existing modules.
|
|
//
|
|
// We should think very hard before adding any _new_ implication rules for
|
|
// moved statements.
|
|
func ImpliedMoveStatements(rootCfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement) []MoveStatement {
|
|
return impliedMoveStatements(rootCfg, prevRunState, explicitStmts, nil)
|
|
}
|
|
|
|
func impliedMoveStatements(cfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement, into []MoveStatement) []MoveStatement {
|
|
modAddr := cfg.Path
|
|
|
|
// There can be potentially many instances of the module, so we need
|
|
// to consider each of them separately.
|
|
for _, modState := range prevRunState.ModuleInstances(modAddr) {
|
|
// What we're looking for here is either a no-key resource instance
|
|
// where the configuration has count set or a zero-key resource
|
|
// instance where the configuration _doesn't_ have count set.
|
|
// If so, we'll generate a statement replacing no-key with zero-key or
|
|
// vice-versa.
|
|
for _, rState := range modState.Resources {
|
|
rAddr := rState.Addr
|
|
rCfg := cfg.Module.ResourceByAddr(rAddr.Resource)
|
|
if rCfg == nil {
|
|
// If there's no configuration at all then there can't be any
|
|
// automatic move fixup to do.
|
|
continue
|
|
}
|
|
approxSrcRange := tfdiags.SourceRangeFromHCL(rCfg.DeclRange)
|
|
|
|
// NOTE: We're intentionally not checking to see whether the
|
|
// "to" addresses in our implied statements already have
|
|
// instances recorded in state, because ApplyMoves should
|
|
// deal with such conflicts in a deterministic way for both
|
|
// explicit and implicit moves, and we'd rather have that
|
|
// handled all in one place.
|
|
|
|
var fromKey, toKey addrs.InstanceKey
|
|
|
|
switch {
|
|
case rCfg.Count != nil:
|
|
// If we have a count expression then we'll use _that_ as
|
|
// a slightly-more-precise approximate source range.
|
|
approxSrcRange = tfdiags.SourceRangeFromHCL(rCfg.Count.Range())
|
|
|
|
if riState := rState.Instances[addrs.NoKey]; riState != nil {
|
|
fromKey = addrs.NoKey
|
|
toKey = addrs.IntKey(0)
|
|
}
|
|
case rCfg.Count == nil && rCfg.ForEach == nil: // no repetition at all
|
|
if riState := rState.Instances[addrs.IntKey(0)]; riState != nil {
|
|
fromKey = addrs.IntKey(0)
|
|
toKey = addrs.NoKey
|
|
}
|
|
}
|
|
|
|
if fromKey != toKey {
|
|
// We mustn't generate an implied statement if the user already
|
|
// wrote an explicit statement referring to this resource,
|
|
// because they may wish to select an instance key other than
|
|
// zero as the one to retain.
|
|
if !haveMoveStatementForResource(rAddr, explicitStmts) {
|
|
into = append(into, MoveStatement{
|
|
From: addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(fromKey), approxSrcRange),
|
|
To: addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(toKey), approxSrcRange),
|
|
DeclRange: approxSrcRange,
|
|
Implied: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, childCfg := range cfg.Children {
|
|
into = impliedMoveStatements(childCfg, prevRunState, explicitStmts, into)
|
|
}
|
|
|
|
return into
|
|
}
|
|
|
|
func (s *MoveStatement) ObjectKind() addrs.MoveEndpointKind {
|
|
// addrs.UnifyMoveEndpoints guarantees that both of our addresses have
|
|
// the same kind, so we can just arbitrary use From and assume To will
|
|
// match it.
|
|
return s.From.ObjectKind()
|
|
}
|
|
|
|
// Name is used internally for displaying the statement graph
|
|
func (s *MoveStatement) Name() string {
|
|
return fmt.Sprintf("%s->%s", s.From, s.To)
|
|
}
|
|
|
|
func haveMoveStatementForResource(addr addrs.AbsResource, stmts []MoveStatement) bool {
|
|
// This is not a particularly optimal way to answer this question,
|
|
// particularly since our caller calls this function in a loop already,
|
|
// but we expect the total number of explicit statements to be small
|
|
// in any reasonable OpenTofu configuration and so a more complicated
|
|
// approach wouldn't be justified here.
|
|
|
|
for _, stmt := range stmts {
|
|
if stmt.From.SelectsResource(addr) {
|
|
return true
|
|
}
|
|
if stmt.To.SelectsResource(addr) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|