github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/refactoring/move_validate.go (about)

     1  package refactoring
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  
    10  	"github.com/hashicorp/terraform/internal/addrs"
    11  	"github.com/hashicorp/terraform/internal/configs"
    12  	"github.com/hashicorp/terraform/internal/dag"
    13  	"github.com/hashicorp/terraform/internal/instances"
    14  	"github.com/hashicorp/terraform/internal/tfdiags"
    15  )
    16  
    17  // ValidateMoves tests whether all of the given move statements comply with
    18  // both the single-statement validation rules and the "big picture" rules
    19  // that constrain statements in relation to one another.
    20  //
    21  // The validation rules are primarily in terms of the configuration, but
    22  // ValidateMoves also takes the expander that resulted from creating a plan
    23  // so that it can see which instances are defined for each module and resource,
    24  // to precisely validate move statements involving specific-instance addresses.
    25  //
    26  // Because validation depends on the planning result but move execution must
    27  // happen _before_ planning, we have the unusual situation where sibling
    28  // function ApplyMoves must run before ValidateMoves and must therefore
    29  // tolerate and ignore any invalid statements. The plan walk will then
    30  // construct in incorrect plan (because it'll be starting from the wrong
    31  // prior state) but ValidateMoves will block actually showing that invalid
    32  // plan to the user.
    33  func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics {
    34  	var diags tfdiags.Diagnostics
    35  
    36  	if len(stmts) == 0 {
    37  		return diags
    38  	}
    39  
    40  	g := buildMoveStatementGraph(stmts)
    41  
    42  	// We need to track the absolute versions of our endpoint addresses in
    43  	// order to detect when there are ambiguous moves.
    44  	type AbsMoveEndpoint struct {
    45  		Other     addrs.AbsMoveable
    46  		StmtRange tfdiags.SourceRange
    47  	}
    48  	stmtFrom := addrs.MakeMap[addrs.AbsMoveable, AbsMoveEndpoint]()
    49  	stmtTo := addrs.MakeMap[addrs.AbsMoveable, AbsMoveEndpoint]()
    50  
    51  	for _, stmt := range stmts {
    52  		// Earlier code that constructs MoveStatement values should ensure that
    53  		// both stmt.From and stmt.To always belong to the same statement.
    54  		fromMod, _ := stmt.From.ModuleCallTraversals()
    55  
    56  		for _, fromModInst := range declaredInsts.InstancesForModule(fromMod) {
    57  			absFrom := stmt.From.InModuleInstance(fromModInst)
    58  
    59  			absTo := stmt.To.InModuleInstance(fromModInst)
    60  
    61  			if addrs.Equivalent(absFrom, absTo) {
    62  				diags = diags.Append(&hcl.Diagnostic{
    63  					Severity: hcl.DiagError,
    64  					Summary:  "Redundant move statement",
    65  					Detail: fmt.Sprintf(
    66  						"This statement declares a move from %s to the same address, which is the same as not declaring this move at all.",
    67  						absFrom,
    68  					),
    69  					Subject: stmt.DeclRange.ToHCL().Ptr(),
    70  				})
    71  				continue
    72  			}
    73  
    74  			var noun string
    75  			var shortNoun string
    76  			switch absFrom.(type) {
    77  			case addrs.ModuleInstance:
    78  				noun = "module instance"
    79  				shortNoun = "instance"
    80  			case addrs.AbsModuleCall:
    81  				noun = "module call"
    82  				shortNoun = "call"
    83  			case addrs.AbsResourceInstance:
    84  				noun = "resource instance"
    85  				shortNoun = "instance"
    86  			case addrs.AbsResource:
    87  				noun = "resource"
    88  				shortNoun = "resource"
    89  			default:
    90  				// The above cases should cover all of the AbsMoveable types
    91  				panic("unsupported AbsMoveable address type")
    92  			}
    93  
    94  			// It's invalid to have a move statement whose "from" address
    95  			// refers to something that is still declared in the configuration.
    96  			if moveableObjectExists(absFrom, declaredInsts) {
    97  				conflictRange, hasRange := movableObjectDeclRange(absFrom, rootCfg)
    98  				declaredAt := ""
    99  				if hasRange {
   100  					// NOTE: It'd be pretty weird to _not_ have a range, since
   101  					// we're only in this codepath because the plan phase
   102  					// thought this object existed in the configuration.
   103  					declaredAt = fmt.Sprintf(" at %s", conflictRange.StartString())
   104  				}
   105  
   106  				diags = diags.Append(&hcl.Diagnostic{
   107  					Severity: hcl.DiagError,
   108  					Summary:  "Moved object still exists",
   109  					Detail: fmt.Sprintf(
   110  						"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.",
   111  						absFrom, noun, declaredAt, shortNoun, absTo,
   112  					),
   113  					Subject: stmt.DeclRange.ToHCL().Ptr(),
   114  				})
   115  			}
   116  
   117  			// There can only be one destination for each source address.
   118  			if existing, exists := stmtFrom.GetOk(absFrom); exists {
   119  				if !addrs.Equivalent(existing.Other, absTo) {
   120  					diags = diags.Append(&hcl.Diagnostic{
   121  						Severity: hcl.DiagError,
   122  						Summary:  "Ambiguous move statements",
   123  						Detail: fmt.Sprintf(
   124  							"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.",
   125  							existing.StmtRange.StartString(), absFrom, existing.Other, absTo,
   126  							noun, shortNoun,
   127  						),
   128  						Subject: stmt.DeclRange.ToHCL().Ptr(),
   129  					})
   130  				}
   131  			} else {
   132  				stmtFrom.Put(absFrom, AbsMoveEndpoint{
   133  					Other:     absTo,
   134  					StmtRange: stmt.DeclRange,
   135  				})
   136  			}
   137  
   138  			// There can only be one source for each destination address.
   139  			if existing, exists := stmtTo.GetOk(absTo); exists {
   140  				if !addrs.Equivalent(existing.Other, absFrom) {
   141  					diags = diags.Append(&hcl.Diagnostic{
   142  						Severity: hcl.DiagError,
   143  						Summary:  "Ambiguous move statements",
   144  						Detail: fmt.Sprintf(
   145  							"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.",
   146  							existing.StmtRange.StartString(), existing.Other, absTo, absFrom,
   147  							noun, shortNoun,
   148  						),
   149  						Subject: stmt.DeclRange.ToHCL().Ptr(),
   150  					})
   151  				}
   152  			} else {
   153  				stmtTo.Put(absTo, AbsMoveEndpoint{
   154  					Other:     absFrom,
   155  					StmtRange: stmt.DeclRange,
   156  				})
   157  			}
   158  
   159  			// Resource types must match.
   160  			if resourceTypesDiffer(absFrom, absTo) {
   161  				diags = diags.Append(&hcl.Diagnostic{
   162  					Severity: hcl.DiagError,
   163  					Summary:  "Resource type mismatch",
   164  					Detail: fmt.Sprintf(
   165  						"This statement declares a move from %s to %s, which is a %s of a different type.", absFrom, absTo, noun,
   166  					),
   167  				})
   168  			}
   169  
   170  		}
   171  	}
   172  
   173  	// If we're not already returning other errors then we'll also check for
   174  	// and report cycles.
   175  	//
   176  	// Cycles alone are difficult to report in a helpful way because we don't
   177  	// have enough context to guess the user's intent. However, some particular
   178  	// mistakes that might lead to a cycle can also be caught by other
   179  	// validation rules above where we can make better suggestions, and so
   180  	// we'll use a cycle report only as a last resort.
   181  	if !diags.HasErrors() {
   182  		diags = diags.Append(validateMoveStatementGraph(g))
   183  	}
   184  
   185  	return diags
   186  }
   187  
   188  func validateMoveStatementGraph(g *dag.AcyclicGraph) tfdiags.Diagnostics {
   189  	var diags tfdiags.Diagnostics
   190  	for _, cycle := range g.Cycles() {
   191  		// Reporting cycles is awkward because there isn't any definitive
   192  		// way to decide which of the objects in the cycle is the cause of
   193  		// the problem. Therefore we'll just list them all out and leave
   194  		// the user to figure it out. :(
   195  		stmtStrs := make([]string, 0, len(cycle))
   196  		for _, stmtI := range cycle {
   197  			// move statement graph nodes are pointers to move statements
   198  			stmt := stmtI.(*MoveStatement)
   199  			stmtStrs = append(stmtStrs, fmt.Sprintf(
   200  				"\n  - %s: %s → %s",
   201  				stmt.DeclRange.StartString(),
   202  				stmt.From.String(),
   203  				stmt.To.String(),
   204  			))
   205  		}
   206  		sort.Strings(stmtStrs) // just to make the order deterministic
   207  
   208  		diags = diags.Append(tfdiags.Sourceless(
   209  			tfdiags.Error,
   210  			"Cyclic dependency in move statements",
   211  			fmt.Sprintf(
   212  				"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.",
   213  				strings.Join(stmtStrs, ""),
   214  			),
   215  		))
   216  	}
   217  
   218  	// Look for cycles to self.
   219  	// A user shouldn't be able to create self-references, but we cannot
   220  	// correctly process a graph with them.
   221  	for _, e := range g.Edges() {
   222  		src := e.Source()
   223  		if src == e.Target() {
   224  			diags = diags.Append(tfdiags.Sourceless(
   225  				tfdiags.Error,
   226  				"Self reference in move statements",
   227  				fmt.Sprintf(
   228  					"The move statement %s refers to itself the move dependency graph, which is invalid. This is a bug in Terraform; please report it!",
   229  					src.(*MoveStatement).Name(),
   230  				),
   231  			))
   232  		}
   233  	}
   234  
   235  	return diags
   236  }
   237  
   238  func moveableObjectExists(addr addrs.AbsMoveable, in instances.Set) bool {
   239  	switch addr := addr.(type) {
   240  	case addrs.ModuleInstance:
   241  		return in.HasModuleInstance(addr)
   242  	case addrs.AbsModuleCall:
   243  		return in.HasModuleCall(addr)
   244  	case addrs.AbsResourceInstance:
   245  		return in.HasResourceInstance(addr)
   246  	case addrs.AbsResource:
   247  		return in.HasResource(addr)
   248  	default:
   249  		// The above cases should cover all of the AbsMoveable types
   250  		panic("unsupported AbsMoveable address type")
   251  	}
   252  }
   253  
   254  func resourceTypesDiffer(absFrom, absTo addrs.AbsMoveable) bool {
   255  	switch absFrom := absFrom.(type) {
   256  	case addrs.AbsMoveableResource:
   257  		// addrs.UnifyMoveEndpoints guarantees that both addresses are of the
   258  		// same kind, so at this point we can assume that absTo is also an
   259  		// addrs.AbsResourceInstance or addrs.AbsResource.
   260  		absTo := absTo.(addrs.AbsMoveableResource)
   261  		return absFrom.AffectedAbsResource().Resource.Type != absTo.AffectedAbsResource().Resource.Type
   262  	default:
   263  		return false
   264  	}
   265  }
   266  
   267  func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiags.SourceRange, bool) {
   268  	switch addr := addr.(type) {
   269  	case addrs.ModuleInstance:
   270  		// For a module instance we're actually looking for the call that
   271  		// declared it, which belongs to the parent module.
   272  		// (NOTE: This assumes "addr" can never be the root module instance,
   273  		// because the root module is never moveable.)
   274  		parentAddr, callAddr := addr.Call()
   275  		modCfg := cfg.DescendentForInstance(parentAddr)
   276  		if modCfg == nil {
   277  			return tfdiags.SourceRange{}, false
   278  		}
   279  		call := modCfg.Module.ModuleCalls[callAddr.Name]
   280  		if call == nil {
   281  			return tfdiags.SourceRange{}, false
   282  		}
   283  
   284  		// If the call has either count or for_each set then we'll "blame"
   285  		// that expression, rather than the block as a whole, because it's
   286  		// the expression that decides which instances are available.
   287  		switch {
   288  		case call.ForEach != nil:
   289  			return tfdiags.SourceRangeFromHCL(call.ForEach.Range()), true
   290  		case call.Count != nil:
   291  			return tfdiags.SourceRangeFromHCL(call.Count.Range()), true
   292  		default:
   293  			return tfdiags.SourceRangeFromHCL(call.DeclRange), true
   294  		}
   295  	case addrs.AbsModuleCall:
   296  		modCfg := cfg.DescendentForInstance(addr.Module)
   297  		if modCfg == nil {
   298  			return tfdiags.SourceRange{}, false
   299  		}
   300  		call := modCfg.Module.ModuleCalls[addr.Call.Name]
   301  		if call == nil {
   302  			return tfdiags.SourceRange{}, false
   303  		}
   304  		return tfdiags.SourceRangeFromHCL(call.DeclRange), true
   305  	case addrs.AbsResourceInstance:
   306  		modCfg := cfg.DescendentForInstance(addr.Module)
   307  		if modCfg == nil {
   308  			return tfdiags.SourceRange{}, false
   309  		}
   310  		rc := modCfg.Module.ResourceByAddr(addr.Resource.Resource)
   311  		if rc == nil {
   312  			return tfdiags.SourceRange{}, false
   313  		}
   314  
   315  		// If the resource has either count or for_each set then we'll "blame"
   316  		// that expression, rather than the block as a whole, because it's
   317  		// the expression that decides which instances are available.
   318  		switch {
   319  		case rc.ForEach != nil:
   320  			return tfdiags.SourceRangeFromHCL(rc.ForEach.Range()), true
   321  		case rc.Count != nil:
   322  			return tfdiags.SourceRangeFromHCL(rc.Count.Range()), true
   323  		default:
   324  			return tfdiags.SourceRangeFromHCL(rc.DeclRange), true
   325  		}
   326  	case addrs.AbsResource:
   327  		modCfg := cfg.DescendentForInstance(addr.Module)
   328  		if modCfg == nil {
   329  			return tfdiags.SourceRange{}, false
   330  		}
   331  		rc := modCfg.Module.ResourceByAddr(addr.Resource)
   332  		if rc == nil {
   333  			return tfdiags.SourceRange{}, false
   334  		}
   335  		return tfdiags.SourceRangeFromHCL(rc.DeclRange), true
   336  	default:
   337  		// The above cases should cover all of the AbsMoveable types
   338  		panic("unsupported AbsMoveable address type")
   339  	}
   340  }