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