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