// 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(cfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement) []MoveStatement { modAddr := cfg.Path into := make([]MoveStatement, 0) // Create implied move statements for module calls. We're typically // looking for module where meta-arguments were changed to see if they // can be moved without an explicit moved block. If there's an existing // explicit move statement for the module, we don't create an implied move statement. for modCallName, modCallCfg := range cfg.Module.ModuleCalls { into = append(into, impliedMoveStatementsForModules(prevRunState, cfg, modCallName, modCallCfg, explicitStmts)...) } // 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. into = append(into, impliedMoveStatementsForModuleResources(cfg, modState, explicitStmts)...) } for _, childCfg := range cfg.Children { into = append(into, ImpliedMoveStatements(childCfg, prevRunState, explicitStmts)...) } return into } // impliedMoveStatementsForModules creates implied move statements for module calls. func impliedMoveStatementsForModules( prevRunState *states.State, parentCfg *configs.Config, modCallName string, modCallCfg *configs.ModuleCall, explicitStmts []MoveStatement, ) []MoveStatement { var into []MoveStatement var toKey addrs.InstanceKey approxSrcRange := tfdiags.SourceRangeFromHCL(modCallCfg.DeclRange) // Use the configuration to determine the instance key to // use for the implied move `To statement. switch { case modCallCfg.Count != nil: // If we have a count expression then we'll use _that_ as // a slightly-more-precise approximate source range. approxSrcRange = tfdiags.SourceRangeFromHCL(modCallCfg.Count.Range()) toKey = addrs.IntKey(0) case modCallCfg.Count == nil && modCallCfg.ForEach == nil: // no repetition at all toKey = addrs.NoKey default: // Other combinations of meta-arguments are not supported. return into } // Get the module address modAddr := parentCfg.Path.Child(modCallName) // Iterate over all module instances, that can be generated by // meta-arguments like count or for_each on the saved state for the module. for _, modState := range prevRunState.ModuleInstances(modAddr) { callerAddr, callAddr := modState.Addr.CallInstance() absCallAddr := addrs.AbsModuleCall{ Module: callerAddr, Call: callAddr.Call, } // Only one instance of the module can be dealt, because moving from single // to repeated can't support multiple instances. The other instances are going // to be removed or created, depending on the direction of the change. fromKey := callAddr.Key if fromKey != addrs.NoKey && fromKey != addrs.IntKey(0) { continue } // We mustn't generate an implied statement if the user already // wrote an explicit statement referring to this module, // because they may wish to select an instance key other than // zero as the one to retain. If the instance key from the state // equals the instance key from the configuration, then we don't // need to generate an implied statement. if fromKey == toKey || haveMoveStatementForModule(modState.Addr, explicitStmts) { continue } fromAddr := absCallAddr.Instance(fromKey) toAddr := absCallAddr.Instance(toKey) into = append(into, MoveStatement{ From: addrs.ImpliedMoveStatementEndpoint(fromAddr, approxSrcRange), To: addrs.ImpliedMoveStatementEndpoint(toAddr, approxSrcRange), DeclRange: approxSrcRange, Implied: true, }) } return into } // impliedMoveStatementsForModuleResources creates implied move statements for module resources. func impliedMoveStatementsForModuleResources(cfg *configs.Config, modState *states.Module, explicitStmts []MoveStatement) []MoveStatement { var into []MoveStatement 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, }) } } } 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 } func haveMoveStatementForModule(addr addrs.ModuleInstance, 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.SelectsModule(addr) { return true } if stmt.To.SelectsModule(addr) { return true } } return false }