| package refactoring |
| |
| import ( |
| "fmt" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/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 impied 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 Terraform 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 |
| } |